Smart Home Controller
SmartHomeHub mediates between devices with zero direct coupling. Commands enable scenes and undo. Composite models room/floor hierarchy. Facade exposes simple routines like goodNight().
Key Abstractions
Interface -- turnOn, turnOff, getStatus. All devices implement this contract.
Concrete devices with device-specific behavior (brightness, temperature, lock state, recording).
Mediator -- devices register here, hub orchestrates all inter-device interactions.
Command -- encapsulates device actions with execute/undo for scenes, scheduling, and replay.
Composite -- rooms contain devices, floors contain rooms. Recursive operations on the hierarchy.
Facade -- goodMorning(), goodNight(), leaveHome() routines that coordinate dozens of device actions.
Observer -- reacts to sensor events like motion detected or temperature threshold crossed.
Class Diagram
How It Works
A smart home controller connects dozens of heterogeneous devices (lights, thermostats, locks, cameras, sensors) and makes them work together without any device knowing about any other device. The SmartHomeHub sits at the center as a Mediator. Every device registers with the hub and communicates only through it. When a motion sensor detects movement, it tells the hub. The hub decides which lights to turn on. The sensor never imports the light class, and the light never imports the sensor class.
User-facing routines like "good night" are sequences of Commands. Each command wraps a single device action (turn on, set temperature) and knows how to undo itself. The Facade composes these commands into named routines. Calling goodNight() executes a dozen commands in order: dim lights, lower thermostat, lock doors, arm cameras. Because every action is a command object, the entire sequence is undoable, replayable, and schedulable.
The physical layout of the home maps to a Composite tree. A house contains floors, floors contain rooms, rooms contain devices. "Turn off the second floor" is a single recursive call that propagates down the tree. No filtering, no room tags, no flat-list scanning.
Observers handle automation rules. A MotionAutomation listener reacts to motion events by turning on lights. A TemperatureAutomation listener reacts to temperature spikes by adjusting the thermostat. Adding a new automation means adding a new listener, not modifying existing device or hub code.
Requirements
Functional
- Register heterogeneous devices (lights, thermostats, locks, cameras) with a central hub
- Execute named routines (good morning, good night, leave home) that coordinate multiple devices
- Undo individual device actions and replay command sequences
- Group devices into rooms and floors with recursive operations (turn off entire floor)
- React to sensor events (motion, temperature) with automated device responses
Non-Functional
- Zero coupling between devices: adding a new device type requires no changes to existing devices
- Command history supports unlimited undo depth
- Composite operations fail gracefully: one offline device does not block the rest of the routine
- New automation rules are addable without modifying hub or device code
Design Decisions
Wouldn't an event bus be simpler?
An event bus is publish-subscribe with no central intelligence. Any subscriber can react to any event, and there's no coordination. A mediator is smarter. It knows the relationships between devices and can enforce rules: "don't turn on the AC if the windows are open." The hub can also prioritize, throttle, or sequence reactions. With a raw event bus, two listeners might both try to adjust the thermostat simultaneously. The mediator serializes these decisions.
Can't you just call device methods directly?
Direct calls (light.turnOff()) work fine for one-shot actions, but they leave no trace. You can't undo them, serialize them to a schedule, or replay them for testing. Commands give you all three. Each command is a first-class object that can be stored in a list, sent over a network, or persisted to a database. Scene macros become lists of command objects. Scheduling becomes "store the command list and execute it at 10 PM."
What if you just tagged devices with room names instead?
Tagging every device with a room name and a floor number works until someone asks "turn off the west wing" or "dim the guest suite." Tags don't nest naturally. Composite does. A floor group contains room groups, which contain device groups or individual devices. Any operation on a group propagates recursively. Adding a new level of hierarchy (building > wing > floor > room) is just another composite node.
How much should the Facade expose?
The facade should expose routines at the level users think in: "I'm going to bed," "I'm leaving the house." It should not expose individual device operations, because that defeats its purpose. If users need fine-grained control, they go through the hub or direct commands. The facade is the "easy button" that hides coordination complexity.
Interview Follow-ups
- Device discovery protocols: In real systems, devices announce themselves via mDNS/Bonjour, UPnP, or Zigbee/Z-Wave pairing. The hub maintains a device registry and handles capability negotiation. How would you model a discovery service that auto-registers devices without manual configuration?
- Rule engines for automation: Simple if-then rules become unmanageable at scale. Production systems use rule engines (e.g., Node-RED, Drools) or even ML models to infer user intent. How would you integrate a rule engine without coupling it to specific device types?
- Handling network partitions: A smart lock that loses connectivity to the hub must still function locally. How would you design offline-first devices that sync state when the network recovers? Consider eventual consistency and conflict resolution.
- Google Home / Alexa architecture: These platforms use a cloud-mediated model where voice commands hit a cloud service, which calls device-specific "skills" or "actions" via OAuth. How does this differ from a local mediator? What are the latency, privacy, and reliability trade-offs?
Code Implementation
from __future__ import annotations
from abc import ABC, abstractmethod
# ── SmartDevice interface and concrete devices ──────────────────────
class SmartDevice(ABC):
"""Interface for all smart home devices."""
@abstractmethod
def turn_on(self) -> None: ...
@abstractmethod
def turn_off(self) -> None: ...
@abstractmethod
def get_status(self) -> str: ...
@abstractmethod
def get_name(self) -> str: ...
class Light(SmartDevice):
def __init__(self, name: str):
self._name = name
self._is_on = False
self._brightness = 100
def turn_on(self) -> None:
self._is_on = True
print(f" {self._name}: ON (brightness {self._brightness}%)")
def turn_off(self) -> None:
self._is_on = False
print(f" {self._name}: OFF")
def set_brightness(self, level: int) -> None:
self._brightness = max(0, min(100, level))
print(f" {self._name}: brightness set to {self._brightness}%")
def get_status(self) -> str:
state = "ON" if self._is_on else "OFF"
return f"{self._name}: {state}, brightness={self._brightness}%"
def get_name(self) -> str:
return self._name
class Thermostat(SmartDevice):
def __init__(self, name: str, temperature: float = 72.0):
self._name = name
self._is_on = False
self._temperature = temperature
def turn_on(self) -> None:
self._is_on = True
print(f" {self._name}: ON at {self._temperature}F")
def turn_off(self) -> None:
self._is_on = False
print(f" {self._name}: OFF")
def set_temperature(self, temp: float) -> float:
prev = self._temperature
self._temperature = temp
print(f" {self._name}: temperature {prev}F -> {temp}F")
return prev
def get_temperature(self) -> float:
return self._temperature
def get_status(self) -> str:
state = "ON" if self._is_on else "OFF"
return f"{self._name}: {state}, temp={self._temperature}F"
def get_name(self) -> str:
return self._name
class DoorLock(SmartDevice):
def __init__(self, name: str):
self._name = name
self._is_locked = True
def turn_on(self) -> None:
self.unlock()
def turn_off(self) -> None:
self.lock()
def lock(self) -> None:
self._is_locked = True
print(f" {self._name}: LOCKED")
def unlock(self) -> None:
self._is_locked = False
print(f" {self._name}: UNLOCKED")
def get_status(self) -> str:
state = "LOCKED" if self._is_locked else "UNLOCKED"
return f"{self._name}: {state}"
def get_name(self) -> str:
return self._name
class SecurityCamera(SmartDevice):
def __init__(self, name: str):
self._name = name
self._is_recording = False
def turn_on(self) -> None:
self._is_recording = True
print(f" {self._name}: RECORDING")
def turn_off(self) -> None:
self._is_recording = False
print(f" {self._name}: STOPPED")
def get_status(self) -> str:
state = "RECORDING" if self._is_recording else "OFF"
return f"{self._name}: {state}"
def get_name(self) -> str:
return self._name
# ── Mediator ────────────────────────────────────────────────────────
class SensorListener(ABC):
"""Observer interface for sensor-driven automations."""
@abstractmethod
def on_event(self, event: str, source: str) -> None: ...
class SmartHomeHub:
"""Mediator: devices register here, hub orchestrates interactions."""
def __init__(self):
self._devices: dict[str, SmartDevice] = {}
self._listeners: list[SensorListener] = []
def register_device(self, device: SmartDevice) -> None:
self._devices[device.get_name()] = device
def get_device(self, name: str) -> SmartDevice | None:
return self._devices.get(name)
def add_listener(self, listener: SensorListener) -> None:
self._listeners.append(listener)
def notify(self, event: str, source: str) -> None:
"""Route a sensor event to all registered listeners."""
print(f" Hub: received '{event}' from {source}")
for listener in self._listeners:
listener.on_event(event, source)
def all_statuses(self) -> list[str]:
return [d.get_status() for d in self._devices.values()]
# ── Command pattern ─────────────────────────────────────────────────
class DeviceCommand(ABC):
@abstractmethod
def execute(self) -> None: ...
@abstractmethod
def undo(self) -> None: ...
class TurnOnCommand(DeviceCommand):
def __init__(self, device: SmartDevice):
self._device = device
def execute(self) -> None:
self._device.turn_on()
def undo(self) -> None:
self._device.turn_off()
class TurnOffCommand(DeviceCommand):
def __init__(self, device: SmartDevice):
self._device = device
def execute(self) -> None:
self._device.turn_off()
def undo(self) -> None:
self._device.turn_on()
class SetTemperatureCommand(DeviceCommand):
def __init__(self, thermostat: "Thermostat", new_temp: float):
self._thermostat = thermostat
self._new_temp = new_temp
self._prev_temp: float = thermostat.get_temperature()
def execute(self) -> None:
self._prev_temp = self._thermostat.set_temperature(self._new_temp)
def undo(self) -> None:
self._thermostat.set_temperature(self._prev_temp)
# ── Composite pattern ──────────────────────────────────────────────
class DeviceGroup:
"""Composite: rooms hold devices, floors hold rooms."""
def __init__(self, name: str):
self._name = name
self._children: list["DeviceGroup | SmartDevice"] = []
def add_child(self, child: "DeviceGroup | SmartDevice") -> None:
self._children.append(child)
def turn_off_all(self) -> None:
print(f" Turning off group '{self._name}':")
for child in self._children:
if isinstance(child, DeviceGroup):
child.turn_off_all()
else:
child.turn_off()
def turn_on_all(self) -> None:
print(f" Turning on group '{self._name}':")
for child in self._children:
if isinstance(child, DeviceGroup):
child.turn_on_all()
else:
child.turn_on()
def get_all_devices(self) -> list[SmartDevice]:
devices: list[SmartDevice] = []
for child in self._children:
if isinstance(child, DeviceGroup):
devices.extend(child.get_all_devices())
else:
devices.append(child)
return devices
# ── Observer: sensor-driven automation ──────────────────────────────
class MotionAutomation(SensorListener):
"""Turns on lights when motion is detected."""
def __init__(self, hub: SmartHomeHub, light_name: str):
self._hub = hub
self._light_name = light_name
def on_event(self, event: str, source: str) -> None:
if event == "motion_detected":
light = self._hub.get_device(self._light_name)
if light:
print(f" Automation: motion from {source} -> turning on {self._light_name}")
light.turn_on()
class TemperatureAutomation(SensorListener):
"""Adjusts thermostat when temperature is too high."""
def __init__(self, hub: SmartHomeHub, thermostat_name: str, threshold: float):
self._hub = hub
self._thermostat_name = thermostat_name
self._threshold = threshold
def on_event(self, event: str, source: str) -> None:
if event == "temp_high":
therm = self._hub.get_device(self._thermostat_name)
if therm and isinstance(therm, Thermostat):
print(f" Automation: high temp from {source} -> cooling to 68F")
therm.set_temperature(68.0)
# ── Facade ──────────────────────────────────────────────────────────
class SmartHomeFacade:
"""Simplified interface for common routines."""
def __init__(self, hub: SmartHomeHub):
self._hub = hub
self._history: list[DeviceCommand] = []
def _run(self, commands: list[DeviceCommand]) -> None:
for cmd in commands:
cmd.execute()
self._history.append(cmd)
def good_morning(self) -> None:
print("\n === Good Morning Routine ===")
commands: list[DeviceCommand] = []
for name in ["Living Room Light", "Kitchen Light"]:
dev = self._hub.get_device(name)
if dev:
commands.append(TurnOnCommand(dev))
therm = self._hub.get_device("Main Thermostat")
if therm and isinstance(therm, Thermostat):
commands.append(SetTemperatureCommand(therm, 72.0))
lock = self._hub.get_device("Front Door Lock")
if lock:
commands.append(TurnOnCommand(lock)) # unlock
self._run(commands)
def good_night(self) -> None:
print("\n === Good Night Routine ===")
commands: list[DeviceCommand] = []
for name in ["Living Room Light", "Kitchen Light", "Bedroom Light"]:
dev = self._hub.get_device(name)
if dev:
commands.append(TurnOffCommand(dev))
therm = self._hub.get_device("Main Thermostat")
if therm and isinstance(therm, Thermostat):
commands.append(SetTemperatureCommand(therm, 65.0))
lock = self._hub.get_device("Front Door Lock")
if lock:
commands.append(TurnOffCommand(lock)) # lock
cam = self._hub.get_device("Front Camera")
if cam:
commands.append(TurnOnCommand(cam))
self._run(commands)
def leave_home(self) -> None:
print("\n === Leave Home Routine ===")
commands: list[DeviceCommand] = []
for name in ["Living Room Light", "Kitchen Light", "Bedroom Light"]:
dev = self._hub.get_device(name)
if dev:
commands.append(TurnOffCommand(dev))
therm = self._hub.get_device("Main Thermostat")
if therm and isinstance(therm, Thermostat):
commands.append(SetTemperatureCommand(therm, 60.0))
lock = self._hub.get_device("Front Door Lock")
if lock:
commands.append(TurnOffCommand(lock)) # lock
cam = self._hub.get_device("Front Camera")
if cam:
commands.append(TurnOnCommand(cam))
self._run(commands)
def undo_last(self) -> None:
if self._history:
cmd = self._history.pop()
print(" Undo last command:")
cmd.undo()
else:
print(" Nothing to undo.")
# ── Demo ────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Smart Home Controller Demo ===\n")
# 1. Create devices
living_light = Light("Living Room Light")
kitchen_light = Light("Kitchen Light")
bedroom_light = Light("Bedroom Light")
thermostat = Thermostat("Main Thermostat", 72.0)
front_lock = DoorLock("Front Door Lock")
front_cam = SecurityCamera("Front Camera")
# 2. Register with hub (Mediator)
hub = SmartHomeHub()
for dev in [living_light, kitchen_light, bedroom_light,
thermostat, front_lock, front_cam]:
hub.register_device(dev)
# 3. Set up observer automations
motion_auto = MotionAutomation(hub, "Living Room Light")
temp_auto = TemperatureAutomation(hub, "Main Thermostat", 80.0)
hub.add_listener(motion_auto)
hub.add_listener(temp_auto)
# 4. Build composite hierarchy: Floor > Room > Device
living_room = DeviceGroup("Living Room")
living_room.add_child(living_light)
kitchen = DeviceGroup("Kitchen")
kitchen.add_child(kitchen_light)
bedroom = DeviceGroup("Bedroom")
bedroom.add_child(bedroom_light)
first_floor = DeviceGroup("1st Floor")
first_floor.add_child(living_room)
first_floor.add_child(kitchen)
second_floor = DeviceGroup("2nd Floor")
second_floor.add_child(bedroom)
house = DeviceGroup("Whole House")
house.add_child(first_floor)
house.add_child(second_floor)
# 5. Facade routines
facade = SmartHomeFacade(hub)
facade.good_morning()
print("\nDevice statuses:")
for s in hub.all_statuses():
print(f" {s}")
facade.good_night()
print("\nDevice statuses:")
for s in hub.all_statuses():
print(f" {s}")
# 6. Undo last command
print("\n--- Undo demo ---")
facade.undo_last()
# 7. Composite: turn off entire 1st floor
print("\n--- Composite: turn off 1st floor ---")
first_floor.turn_off_all()
# 8. Mediator: sensor event triggers automation
print("\n--- Mediator: sensor events ---")
hub.notify("motion_detected", "Hallway Sensor")
hub.notify("temp_high", "Living Room Sensor")
print("\nFinal statuses:")
for s in hub.all_statuses():
print(f" {s}")
print("\nDone.")Common Mistakes
- ✗Devices directly referencing each other: thermostat imports AC class, motion sensor imports light class
- ✗No command abstraction: can't undo, can't replay, can't schedule
- ✗Flat device list instead of hierarchy: "turn off living room" requires filtering by room tag instead of tree traversal
- ✗Missing error handling for offline devices: one failed device shouldn't block the entire routine
Key Points
- ✓Mediator eliminates device-to-device coupling: thermostat tells hub "temp is 80F", hub tells AC to activate
- ✓Command enables scene macros, scheduling, and undo: "good night" is a sequence of commands
- ✓Composite for physical hierarchy: "turn off 2nd floor" recursively turns off all devices in all rooms
- ✓Facade shields the user from complexity: one method triggers dozens of coordinated device actions