By Sagar Shankaran, Founder of CallSphere
Create a smart home AI agent that controls IoT devices, manages lighting and climate scenes, and configures automation rules — a complete home automation assistant built with Python and the OpenAI Agents SDK.
Key takeaways
Smart home devices are powerful individually but painful to orchestrate. Adjusting lights, thermostat, and speakers typically means opening three different apps. A smart home agent provides a single natural language interface: say "movie night" and it dims the lights, sets the thermostat to 72, and queues your playlist. This tutorial builds a complete home automation agent with device control, scene management, and rule-based automation.
mkdir smarthome-agent && cd smarthome-agent
python -m venv venv && source venv/bin/activate
pip install openai-agents pydantic
mkdir -p src
touch src/__init__.py src/devices.py src/scenes.py
touch src/automation.py src/agent.py
We simulate IoT devices with an in-memory registry. Each device has a type, state, and configurable properties.
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
# src/devices.py
from pydantic import BaseModel
class DeviceState(BaseModel):
power: bool = False
brightness: int | None = None # 0-100 for lights
temperature: float | None = None # for thermostat
volume: int | None = None # 0-100 for speakers
color: str | None = None # for color lights
mode: str | None = None # for multi-mode devices
class Device(BaseModel):
id: str
name: str
room: str
device_type: str # light, thermostat, speaker, lock, camera
state: DeviceState
class DeviceRegistry:
def __init__(self):
self.devices: dict[str, Device] = {}
self._init_defaults()
def _init_defaults(self):
defaults = [
Device(id="light_lr", name="Living Room Light",
room="living room", device_type="light",
state=DeviceState(power=True, brightness=80)),
Device(id="light_br", name="Bedroom Light",
room="bedroom", device_type="light",
state=DeviceState(power=False, brightness=50,
color="warm white")),
Device(id="thermo_main", name="Main Thermostat",
room="hallway", device_type="thermostat",
state=DeviceState(power=True, temperature=72.0,
mode="auto")),
Device(id="speaker_lr", name="Living Room Speaker",
room="living room", device_type="speaker",
state=DeviceState(power=False, volume=40)),
Device(id="lock_front", name="Front Door Lock",
room="entrance", device_type="lock",
state=DeviceState(power=True, mode="locked")),
Device(id="light_kt", name="Kitchen Light",
room="kitchen", device_type="light",
state=DeviceState(power=True, brightness=100)),
]
for d in defaults:
self.devices[d.id] = d
def get_device(self, device_id: str) -> Device | None:
return self.devices.get(device_id)
def find_by_room(self, room: str) -> list[Device]:
room_lower = room.lower()
return [
d for d in self.devices.values()
if room_lower in d.room.lower()
]
def find_by_type(self, device_type: str) -> list[Device]:
return [
d for d in self.devices.values()
if d.device_type == device_type
]
def set_property(
self, device_id: str, prop: str, value
) -> str:
device = self.get_device(device_id)
if not device:
return f"Device {device_id} not found"
if not hasattr(device.state, prop):
return f"Property {prop} not valid for this device"
setattr(device.state, prop, value)
return f"Set {device.name} {prop} to {value}"
def get_all_status(self) -> str:
lines = []
for d in self.devices.values():
status = "ON" if d.state.power else "OFF"
extra = []
if d.state.brightness is not None:
extra.append(f"brightness={d.state.brightness}%")
if d.state.temperature is not None:
extra.append(f"temp={d.state.temperature}F")
if d.state.volume is not None:
extra.append(f"vol={d.state.volume}%")
if d.state.mode:
extra.append(f"mode={d.state.mode}")
detail = ", ".join(extra) if extra else ""
lines.append(
f" {d.name} ({d.room}): {status} {detail}"
)
return "\n".join(lines)
registry = DeviceRegistry()
Scenes apply multiple device changes simultaneously.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
# src/scenes.py
from src.devices import registry
SCENES = {
"movie night": [
("light_lr", "brightness", 20),
("light_lr", "color", "warm white"),
("speaker_lr", "power", True),
("speaker_lr", "volume", 60),
("thermo_main", "temperature", 72.0),
],
"good morning": [
("light_br", "power", True),
("light_br", "brightness", 80),
("light_kt", "power", True),
("light_kt", "brightness", 100),
("thermo_main", "temperature", 70.0),
],
"goodnight": [
("light_lr", "power", False),
("light_kt", "power", False),
("light_br", "brightness", 10),
("lock_front", "mode", "locked"),
("thermo_main", "temperature", 68.0),
("speaker_lr", "power", False),
],
"away": [
("light_lr", "power", False),
("light_br", "power", False),
("light_kt", "power", False),
("thermo_main", "temperature", 65.0),
("lock_front", "mode", "locked"),
],
}
def activate_scene(scene_name: str) -> str:
actions = SCENES.get(scene_name.lower())
if not actions:
available = ", ".join(SCENES.keys())
return f"Scene not found. Available: {available}"
results = []
for device_id, prop, value in actions:
result = registry.set_property(device_id, prop, value)
results.append(result)
return f"Scene '{scene_name}' activated:\n" + "\n".join(results)
def list_scenes() -> list[str]:
return list(SCENES.keys())
# src/automation.py
from datetime import datetime
from src.devices import registry
class AutomationRule:
def __init__(
self, name: str, trigger: str, device_id: str,
prop: str, value, active: bool = True,
):
self.name = name
self.trigger = trigger # e.g., "time:22:00", "sunset"
self.device_id = device_id
self.prop = prop
self.value = value
self.active = active
rules: list[AutomationRule] = []
def add_rule(
name: str, trigger: str, device_id: str,
prop: str, value,
) -> str:
rules.append(AutomationRule(name, trigger, device_id, prop, value))
return f"Rule '{name}' created: on {trigger}, set {device_id} {prop}={value}"
def list_rules() -> str:
if not rules:
return "No automation rules configured."
lines = []
for r in rules:
status = "active" if r.active else "paused"
lines.append(
f" - {r.name}: {r.trigger} -> "
f"{r.device_id}.{r.prop}={r.value} [{status}]"
)
return "\n".join(lines)
def evaluate_rules():
now = datetime.now().strftime("%H:%M")
triggered = []
for rule in rules:
if not rule.active:
continue
if rule.trigger == f"time:{now}":
registry.set_property(rule.device_id, rule.prop, rule.value)
triggered.append(rule.name)
return triggered
# src/agent.py
import asyncio
from agents import Agent, Runner, function_tool
from src.devices import registry
from src.scenes import activate_scene, list_scenes
from src.automation import add_rule, list_rules
@function_tool
def get_device_status() -> str:
"""Get status of all smart home devices."""
return registry.get_all_status()
@function_tool
def control_device(
device_id: str, property_name: str, value: str,
) -> str:
"""Control a specific device property."""
typed_value: bool | int | float | str = value
if value.lower() in ("true", "false"):
typed_value = value.lower() == "true"
elif value.replace(".", "").isdigit():
typed_value = float(value) if "." in value else int(value)
return registry.set_property(device_id, property_name, typed_value)
@function_tool
def set_scene(scene_name: str) -> str:
"""Activate a predefined scene."""
return activate_scene(scene_name)
@function_tool
def create_automation(
name: str, trigger: str, device_id: str,
prop: str, value: str,
) -> str:
"""Create a time-based automation rule."""
return add_rule(name, trigger, device_id, prop, value)
@function_tool
def view_automations() -> str:
"""View all automation rules."""
return list_rules()
home_agent = Agent(
name="Smart Home Assistant",
instructions="""You are a smart home control agent.
Help users control devices, activate scenes, and set up automations.
Always confirm actions taken. When the user describes a mood or
activity, suggest an appropriate scene. Device IDs: light_lr,
light_br, light_kt, thermo_main, speaker_lr, lock_front.
Available scenes: movie night, good morning, goodnight, away.""",
tools=[
get_device_status, control_device, set_scene,
create_automation, view_automations,
],
)
async def main():
result = await Runner.run(
home_agent,
"Set up movie night and also create an automation "
"to lock the front door every night at 11pm.",
)
print(result.final_output)
if __name__ == "__main__":
asyncio.run(main())
The agent activates the movie night scene, then creates the nightly lock automation — all from a single natural language command.
Replace the DeviceRegistry with API calls to platforms like Home Assistant, SmartThings, or Philips Hue. Each platform provides REST APIs or Python libraries for device control. The agent tools remain the same — only the underlying registry methods change from in-memory state updates to HTTP requests.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
Yes. Log every command the user gives and the scenes they activate. Over time, analyze patterns — for example, if the user always dims lights at 9pm, proactively suggest creating an automation rule. Store preferences in a JSON file or database that the agent reads on startup.
Add a priority field to AutomationRule and a conflict detection method that checks whether two rules modify the same device property at the same time. When conflicts are detected, the agent alerts the user and asks which rule should take precedence before activating them.
#SmartHome #IoT #AIAgent #Python #HomeAutomation #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
Enterprise CIO Guide perspective on Klarna's AI agent pioneered the resolution-equivalent metric and is now in its third year of production data.
Smolagents lets agents write Python instead of JSON. Why code-as-action reduces tool errors and where the security trade-offs are for production deployments.
SMB Founder Playbook perspective on Klarna's AI agent pioneered the resolution-equivalent metric and is now in its third year of production data.
Healthcare Practice Use Case perspective on Klarna's AI agent pioneered the resolution-equivalent metric and is now in its third year of production data.
Modal turns a Python function into autoscaling serverless compute with optional GPU. Deploy a LiveKit Agent with one command and get pay-per-second billing.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco