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
1 from __future__ import annotations
2 from abc import ABC, abstractmethod
3
4
5 # ── SmartDevice interface and concrete devices ──────────────────────
6
7 class SmartDevice(ABC):
8 """Interface for all smart home devices."""
9
10 @abstractmethod
11 def turn_on(self) -> None: ...
12
13 @abstractmethod
14 def turn_off(self) -> None: ...
15
16 @abstractmethod
17 def get_status(self) -> str: ...
18
19 @abstractmethod
20 def get_name(self) -> str: ...
21
22
23 class Light(SmartDevice):
24 def __init__(self, name: str):
25 self._name = name
26 self._is_on = False
27 self._brightness = 100
28
29 def turn_on(self) -> None:
30 self._is_on = True
31 print(f" {self._name}: ON (brightness {self._brightness}%)")
32
33 def turn_off(self) -> None:
34 self._is_on = False
35 print(f" {self._name}: OFF")
36
37 def set_brightness(self, level: int) -> None:
38 self._brightness = max(0, min(100, level))
39 print(f" {self._name}: brightness set to {self._brightness}%")
40
41 def get_status(self) -> str:
42 state = "ON" if self._is_on else "OFF"
43 return f"{self._name}: {state}, brightness={self._brightness}%"
44
45 def get_name(self) -> str:
46 return self._name
47
48
49 class Thermostat(SmartDevice):
50 def __init__(self, name: str, temperature: float = 72.0):
51 self._name = name
52 self._is_on = False
53 self._temperature = temperature
54
55 def turn_on(self) -> None:
56 self._is_on = True
57 print(f" {self._name}: ON at {self._temperature}F")
58
59 def turn_off(self) -> None:
60 self._is_on = False
61 print(f" {self._name}: OFF")
62
63 def set_temperature(self, temp: float) -> float:
64 prev = self._temperature
65 self._temperature = temp
66 print(f" {self._name}: temperature {prev}F -> {temp}F")
67 return prev
68
69 def get_temperature(self) -> float:
70 return self._temperature
71
72 def get_status(self) -> str:
73 state = "ON" if self._is_on else "OFF"
74 return f"{self._name}: {state}, temp={self._temperature}F"
75
76 def get_name(self) -> str:
77 return self._name
78
79
80 class DoorLock(SmartDevice):
81 def __init__(self, name: str):
82 self._name = name
83 self._is_locked = True
84
85 def turn_on(self) -> None:
86 self.unlock()
87
88 def turn_off(self) -> None:
89 self.lock()
90
91 def lock(self) -> None:
92 self._is_locked = True
93 print(f" {self._name}: LOCKED")
94
95 def unlock(self) -> None:
96 self._is_locked = False
97 print(f" {self._name}: UNLOCKED")
98
99 def get_status(self) -> str:
100 state = "LOCKED" if self._is_locked else "UNLOCKED"
101 return f"{self._name}: {state}"
102
103 def get_name(self) -> str:
104 return self._name
105
106
107 class SecurityCamera(SmartDevice):
108 def __init__(self, name: str):
109 self._name = name
110 self._is_recording = False
111
112 def turn_on(self) -> None:
113 self._is_recording = True
114 print(f" {self._name}: RECORDING")
115
116 def turn_off(self) -> None:
117 self._is_recording = False
118 print(f" {self._name}: STOPPED")
119
120 def get_status(self) -> str:
121 state = "RECORDING" if self._is_recording else "OFF"
122 return f"{self._name}: {state}"
123
124 def get_name(self) -> str:
125 return self._name
126
127
128 # ── Mediator ────────────────────────────────────────────────────────
129
130 class SensorListener(ABC):
131 """Observer interface for sensor-driven automations."""
132
133 @abstractmethod
134 def on_event(self, event: str, source: str) -> None: ...
135
136
137 class SmartHomeHub:
138 """Mediator: devices register here, hub orchestrates interactions."""
139
140 def __init__(self):
141 self._devices: dict[str, SmartDevice] = {}
142 self._listeners: list[SensorListener] = []
143
144 def register_device(self, device: SmartDevice) -> None:
145 self._devices[device.get_name()] = device
146
147 def get_device(self, name: str) -> SmartDevice | None:
148 return self._devices.get(name)
149
150 def add_listener(self, listener: SensorListener) -> None:
151 self._listeners.append(listener)
152
153 def notify(self, event: str, source: str) -> None:
154 """Route a sensor event to all registered listeners."""
155 print(f" Hub: received '{event}' from {source}")
156 for listener in self._listeners:
157 listener.on_event(event, source)
158
159 def all_statuses(self) -> list[str]:
160 return [d.get_status() for d in self._devices.values()]
161
162
163 # ── Command pattern ─────────────────────────────────────────────────
164
165 class DeviceCommand(ABC):
166 @abstractmethod
167 def execute(self) -> None: ...
168
169 @abstractmethod
170 def undo(self) -> None: ...
171
172
173 class TurnOnCommand(DeviceCommand):
174 def __init__(self, device: SmartDevice):
175 self._device = device
176
177 def execute(self) -> None:
178 self._device.turn_on()
179
180 def undo(self) -> None:
181 self._device.turn_off()
182
183
184 class TurnOffCommand(DeviceCommand):
185 def __init__(self, device: SmartDevice):
186 self._device = device
187
188 def execute(self) -> None:
189 self._device.turn_off()
190
191 def undo(self) -> None:
192 self._device.turn_on()
193
194
195 class SetTemperatureCommand(DeviceCommand):
196 def __init__(self, thermostat: "Thermostat", new_temp: float):
197 self._thermostat = thermostat
198 self._new_temp = new_temp
199 self._prev_temp: float = thermostat.get_temperature()
200
201 def execute(self) -> None:
202 self._prev_temp = self._thermostat.set_temperature(self._new_temp)
203
204 def undo(self) -> None:
205 self._thermostat.set_temperature(self._prev_temp)
206
207
208 # ── Composite pattern ──────────────────────────────────────────────
209
210 class DeviceGroup:
211 """Composite: rooms hold devices, floors hold rooms."""
212
213 def __init__(self, name: str):
214 self._name = name
215 self._children: list["DeviceGroup | SmartDevice"] = []
216
217 def add_child(self, child: "DeviceGroup | SmartDevice") -> None:
218 self._children.append(child)
219
220 def turn_off_all(self) -> None:
221 print(f" Turning off group '{self._name}':")
222 for child in self._children:
223 if isinstance(child, DeviceGroup):
224 child.turn_off_all()
225 else:
226 child.turn_off()
227
228 def turn_on_all(self) -> None:
229 print(f" Turning on group '{self._name}':")
230 for child in self._children:
231 if isinstance(child, DeviceGroup):
232 child.turn_on_all()
233 else:
234 child.turn_on()
235
236 def get_all_devices(self) -> list[SmartDevice]:
237 devices: list[SmartDevice] = []
238 for child in self._children:
239 if isinstance(child, DeviceGroup):
240 devices.extend(child.get_all_devices())
241 else:
242 devices.append(child)
243 return devices
244
245
246 # ── Observer: sensor-driven automation ──────────────────────────────
247
248 class MotionAutomation(SensorListener):
249 """Turns on lights when motion is detected."""
250
251 def __init__(self, hub: SmartHomeHub, light_name: str):
252 self._hub = hub
253 self._light_name = light_name
254
255 def on_event(self, event: str, source: str) -> None:
256 if event == "motion_detected":
257 light = self._hub.get_device(self._light_name)
258 if light:
259 print(f" Automation: motion from {source} -> turning on {self._light_name}")
260 light.turn_on()
261
262
263 class TemperatureAutomation(SensorListener):
264 """Adjusts thermostat when temperature is too high."""
265
266 def __init__(self, hub: SmartHomeHub, thermostat_name: str, threshold: float):
267 self._hub = hub
268 self._thermostat_name = thermostat_name
269 self._threshold = threshold
270
271 def on_event(self, event: str, source: str) -> None:
272 if event == "temp_high":
273 therm = self._hub.get_device(self._thermostat_name)
274 if therm and isinstance(therm, Thermostat):
275 print(f" Automation: high temp from {source} -> cooling to 68F")
276 therm.set_temperature(68.0)
277
278
279 # ── Facade ──────────────────────────────────────────────────────────
280
281 class SmartHomeFacade:
282 """Simplified interface for common routines."""
283
284 def __init__(self, hub: SmartHomeHub):
285 self._hub = hub
286 self._history: list[DeviceCommand] = []
287
288 def _run(self, commands: list[DeviceCommand]) -> None:
289 for cmd in commands:
290 cmd.execute()
291 self._history.append(cmd)
292
293 def good_morning(self) -> None:
294 print("\n === Good Morning Routine ===")
295 commands: list[DeviceCommand] = []
296 for name in ["Living Room Light", "Kitchen Light"]:
297 dev = self._hub.get_device(name)
298 if dev:
299 commands.append(TurnOnCommand(dev))
300 therm = self._hub.get_device("Main Thermostat")
301 if therm and isinstance(therm, Thermostat):
302 commands.append(SetTemperatureCommand(therm, 72.0))
303 lock = self._hub.get_device("Front Door Lock")
304 if lock:
305 commands.append(TurnOnCommand(lock)) # unlock
306 self._run(commands)
307
308 def good_night(self) -> None:
309 print("\n === Good Night Routine ===")
310 commands: list[DeviceCommand] = []
311 for name in ["Living Room Light", "Kitchen Light", "Bedroom Light"]:
312 dev = self._hub.get_device(name)
313 if dev:
314 commands.append(TurnOffCommand(dev))
315 therm = self._hub.get_device("Main Thermostat")
316 if therm and isinstance(therm, Thermostat):
317 commands.append(SetTemperatureCommand(therm, 65.0))
318 lock = self._hub.get_device("Front Door Lock")
319 if lock:
320 commands.append(TurnOffCommand(lock)) # lock
321 cam = self._hub.get_device("Front Camera")
322 if cam:
323 commands.append(TurnOnCommand(cam))
324 self._run(commands)
325
326 def leave_home(self) -> None:
327 print("\n === Leave Home Routine ===")
328 commands: list[DeviceCommand] = []
329 for name in ["Living Room Light", "Kitchen Light", "Bedroom Light"]:
330 dev = self._hub.get_device(name)
331 if dev:
332 commands.append(TurnOffCommand(dev))
333 therm = self._hub.get_device("Main Thermostat")
334 if therm and isinstance(therm, Thermostat):
335 commands.append(SetTemperatureCommand(therm, 60.0))
336 lock = self._hub.get_device("Front Door Lock")
337 if lock:
338 commands.append(TurnOffCommand(lock)) # lock
339 cam = self._hub.get_device("Front Camera")
340 if cam:
341 commands.append(TurnOnCommand(cam))
342 self._run(commands)
343
344 def undo_last(self) -> None:
345 if self._history:
346 cmd = self._history.pop()
347 print(" Undo last command:")
348 cmd.undo()
349 else:
350 print(" Nothing to undo.")
351
352
353 # ── Demo ────────────────────────────────────────────────────────────
354
355 if __name__ == "__main__":
356 print("=== Smart Home Controller Demo ===\n")
357
358 # 1. Create devices
359 living_light = Light("Living Room Light")
360 kitchen_light = Light("Kitchen Light")
361 bedroom_light = Light("Bedroom Light")
362 thermostat = Thermostat("Main Thermostat", 72.0)
363 front_lock = DoorLock("Front Door Lock")
364 front_cam = SecurityCamera("Front Camera")
365
366 # 2. Register with hub (Mediator)
367 hub = SmartHomeHub()
368 for dev in [living_light, kitchen_light, bedroom_light,
369 thermostat, front_lock, front_cam]:
370 hub.register_device(dev)
371
372 # 3. Set up observer automations
373 motion_auto = MotionAutomation(hub, "Living Room Light")
374 temp_auto = TemperatureAutomation(hub, "Main Thermostat", 80.0)
375 hub.add_listener(motion_auto)
376 hub.add_listener(temp_auto)
377
378 # 4. Build composite hierarchy: Floor > Room > Device
379 living_room = DeviceGroup("Living Room")
380 living_room.add_child(living_light)
381
382 kitchen = DeviceGroup("Kitchen")
383 kitchen.add_child(kitchen_light)
384
385 bedroom = DeviceGroup("Bedroom")
386 bedroom.add_child(bedroom_light)
387
388 first_floor = DeviceGroup("1st Floor")
389 first_floor.add_child(living_room)
390 first_floor.add_child(kitchen)
391
392 second_floor = DeviceGroup("2nd Floor")
393 second_floor.add_child(bedroom)
394
395 house = DeviceGroup("Whole House")
396 house.add_child(first_floor)
397 house.add_child(second_floor)
398
399 # 5. Facade routines
400 facade = SmartHomeFacade(hub)
401 facade.good_morning()
402
403 print("\nDevice statuses:")
404 for s in hub.all_statuses():
405 print(f" {s}")
406
407 facade.good_night()
408
409 print("\nDevice statuses:")
410 for s in hub.all_statuses():
411 print(f" {s}")
412
413 # 6. Undo last command
414 print("\n--- Undo demo ---")
415 facade.undo_last()
416
417 # 7. Composite: turn off entire 1st floor
418 print("\n--- Composite: turn off 1st floor ---")
419 first_floor.turn_off_all()
420
421 # 8. Mediator: sensor event triggers automation
422 print("\n--- Mediator: sensor events ---")
423 hub.notify("motion_detected", "Hallway Sensor")
424 hub.notify("temp_high", "Living Room Sensor")
425
426 print("\nFinal statuses:")
427 for s in hub.all_statuses():
428 print(f" {s}")
429 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