0_MOBIUS.SMART_HOME - Engineering Architecture

System Overview: 0_MOBIUS.SMART_HOME is a multi-instance home automation platform built with Python/FastAPI, PostgreSQL, and a modular ES6+jQuery frontend. It migrates Hubitat Groovy apps to a modern web-based system where users create independent automation instances (e.g., "Office Lights", "Bedroom Lights") from app blueprints. Events from Hubitat hubs flow through a central webhook dispatcher, are routed to subscribed instances, and trigger automation logic with memoization, scheduling, and health monitoring. All configuration is managed via AWS Secrets Manager. Last updated: February 21, 2026.
5
Docker Services
4
Hubitat Hubs
1
App Type (Extensible)
8
Database Tables

Level 1: System Overview

High-Level Architecture: Hubitat hubs generate device events (motion, switch, illuminance, etc.) which POST to a central Webhook Dispatcher (Flask, port 5050). The dispatcher fans out events to all registered apps. The FastAPI core application receives events, routes them to subscribed app instances via PostgREST-backed subscriptions, and executes automation logic. The frontend provides a wizard-based UI for creating and managing instances. All secrets and configuration flow from AWS Secrets Manager through start.sh into Docker environment variables.
graph LR HUB[Hubitat Hubs
4 Hubs on LAN] -->|Maker API Events| WD[Webhook Dispatcher
Flask :5050] WD -->|Fan-out POST| APP[FastAPI App
:5001 → :5000] WD -->|Fan-out POST| OTHER[Other Apps
0_MOBIUS.TILES etc.] APP -->|REST API| UI[Web Browser
ES6 + jQuery] APP -->|PostgREST| PG[(PostgreSQL
:5435 → :5432)] APP -->|Maker API| HUB PR[PostgREST
:3003 → :3001] -->|Auto-REST| PG APP -->|CRUD via| PR NGX[Nginx
:8444 HTTPS] -->|Reverse Proxy| APP UI -->|HTTPS| NGX AWS[(AWS Secrets
Manager)] -.->|start.sh| APP AWS -.->|Credentials| WD style HUB fill:#d4edda style WD fill:#fd7e14,color:#fff style APP fill:#667eea,color:#fff style UI fill:#e1f5ff style PG fill:#17a2b8,color:#fff style PR fill:#6f42c1,color:#fff style NGX fill:#28a745,color:#fff style AWS fill:#fff3cd style OTHER fill:#ecf0f1
Hubitat Hubs (IoT)
Webhook Dispatcher
FastAPI Application
Frontend (Browser)
PostgreSQL Database
PostgREST API
Nginx Reverse Proxy
AWS Secrets Manager

Technology Stack

Level 2: Container Architecture

Docker Compose Services: Five containers orchestrated by docker-compose.yml. The webhook dispatcher is shared across projects (0_MOBIUS.SMART_HOME, 0_MOBIUS.TILES) via hardlinks from ~/0_SCRIPTS/webhook_dispatcher/. Only one dispatcher container runs system-wide (whichever project starts first wins). The FastAPI app runs with a single Uvicorn worker because SSE requires shared in-process memory.
graph TB subgraph DockerCompose["Docker Compose Network"] WD[webhook-dispatcher
Flask :5050
Python 3.11-slim] APP[smarthome-app
FastAPI :5001→:5000
Python 3.11 + Uvicorn] PG[smarthome-postgres
PostgreSQL :5435→:5432
postgres:16-alpine] PR[smarthome-postgrest
PostgREST :3003→:3001
postgrest:v12.0.2] NGX[smarthome-nginx
Nginx :8080/:8444→:80/:443
nginx:1.27-alpine] end subgraph External["External"] HUB[Hubitat Hubs
192.168.10.69-72] BROWSER[Web Browser] end HUB -->|POST /api/webhook/event| WD WD -->|POST to smarthome-app:5000| APP BROWSER -->|HTTPS :8444| NGX NGX -->|proxy_pass :5000| APP APP -->|REST :3001| PR PR -->|SQL :5432| PG APP -->|Maker API HTTP| HUB style WD fill:#fd7e14,color:#fff style APP fill:#667eea,color:#fff style PG fill:#17a2b8,color:#fff style PR fill:#6f42c1,color:#fff style NGX fill:#28a745,color:#fff style HUB fill:#d4edda style BROWSER fill:#e1f5ff
Service Image External Port Internal Port Purpose Notes
webhook-dispatcher Python 3.11-slim + Flask 5050 5050 Hubitat event fan-out Shared across projects via hardlink
smarthome-app Python 3.11 + FastAPI 5001 5000 Core automation engine Single worker (SSE requirement)
smarthome-postgres postgres:16-alpine 5435 5432 Database Schema from psql/init-db.sql
smarthome-postgrest postgrest/postgrest:v12.0.2 3003 3001 Auto-REST from schema All CRUD via REST, no raw SQL in app
smarthome-nginx nginx:1.27-alpine 8080 / 8444 80 / 443 HTTPS reverse proxy Self-signed certs generated by start.sh
Single Worker Requirement: FastAPI runs with --workers 1. Multiple workers have isolated memory, meaning SSE clients connected to one worker will never receive events from webhooks hitting another. The single async worker handles concurrency via Python's event loop.

Level 3: Multi-Instance Architecture

Core Design Principle: Each user-created automation is a row in app_instances. App types (blueprints) define the automation logic, settings schema, and device categories. Users create instances from these blueprints, each with independent device selections, settings, memoization state, and runtime behavior. The InstanceManager handles lifecycle (CRUD, pause/resume) and maintains in-memory Python objects for each running instance.
graph TB subgraph Blueprints["App Types (Blueprints)"] AT1[advanced_motion_lighting
v2.0.0
Settings Schema + Device Categories] AT2[future_app_type
...
Extensible] end subgraph Instances["App Instances (User-Created)"] I1["Instance #1: Office Lights
motion_sensors: [123, 124]
switches: [456, 457]
noMotionTime: 5 min"] I2["Instance #2: Bedroom Lights
motion_sensors: [200]
switches: [300]
noMotionTime: 10 min"] I3["Instance #3: Garage
...
is_paused: true"] end subgraph Runtime["In-Memory Runtime Objects"] R1[AdvancedMotionLightingApp
Instance 1
_runtime + _memoization] R2[AdvancedMotionLightingApp
Instance 2
_runtime + _memoization] end subgraph Subscriptions["Device Subscriptions"] S1["device 123 → Instance 1 (motion)"] S2["device 124 → Instance 1 (motion)"] S3["device 456 → Instance 1 (switch)"] S4["device 200 → Instance 2 (motion)"] S5["device 300 → Instance 2 (switch)"] end AT1 --> I1 AT1 --> I2 AT1 --> I3 I1 --> R1 I2 --> R2 I1 --> S1 I1 --> S2 I1 --> S3 I2 --> S4 I2 --> S5 style AT1 fill:#667eea,color:#fff style AT2 fill:#ecf0f1 style I1 fill:#28a745,color:#fff style I2 fill:#28a745,color:#fff style I3 fill:#6c757d,color:#fff style R1 fill:#fd7e14,color:#fff style R2 fill:#fd7e14,color:#fff

Instance Lifecycle

Level 4: Webhook Event Flow

End-to-End Event Pipeline: A physical device event (e.g., motion sensor activates) travels from the Hubitat hub through the webhook dispatcher, into the FastAPI app, through the webhook router which queries subscriptions, and finally dispatches to the appropriate app instance's on_event() method. The event is logged, the device cache is updated, and the automation logic executes.
sequenceDiagram participant Hub as Hubitat Hub participant Disp as Webhook Dispatcher
:5050 participant API as FastAPI
/api/webhook/event participant Router as WebhookRouter participant Cache as DeviceCache participant Sub as device_subscriptions
(PostgREST) participant App as AppInstance
(in-memory) participant Sched as SchedulerService participant Log as event_log
(PostgREST) Hub->>Disp: POST {content: {deviceId, name, value, ...}} Note over Disp: Unwrap 'content' envelope Disp->>API: POST {deviceId, name, value, ...} API->>Router: route_event(payload) Router->>Router: Create DeviceEvent object par Update cache & query subscriptions Router->>Cache: update_device_attribute(id, name, value) Router->>Sub: GET where device_id=X AND event_type=Y end Sub-->>Router: [instance_id: 1, instance_id: 2, ...] loop For each subscribed instance Router->>App: app.on_event(event) App->>App: Check if paused App->>App: Route to handler (_handle_motion, etc.) App->>App: Execute master() logic App->>Hub: send_command(device_id, 'on') App->>Sched: schedule_timeout(300s) end Router->>Log: Write event_log entry Router-->>API: {routed_to: N instances}

Webhook Payload Format

From Hubitat Maker API (wrapped in content object, unwrapped by dispatcher):

flowchart TB subgraph HubitatHubs["Hubitat Hubs (4 Hubs)"] H1["Hub Main
192.168.10.72
App 268"] H2["Hub 1
192.168.10.69
App 1717"] H3["Hub 2
192.168.10.70
App 2151"] H4["Hub 3
192.168.10.71
App 1269"] end subgraph Dispatcher["Webhook Dispatcher (:5050)"] RX[Receive POST] UW[Unwrap content] FAN[Fan-out to targets] end subgraph Targets["Webhook Targets"] SM["smarthome-app:5000
/api/webhook/event"] TILES["host.docker.internal:80
/api/webhook/event"] end H1 -->|POST| RX H2 -->|POST| RX H3 -->|POST| RX H4 -->|POST| RX RX --> UW UW --> FAN FAN --> SM FAN --> TILES style H1 fill:#28a745,color:#fff style H2 fill:#d4edda style H3 fill:#d4edda style H4 fill:#d4edda style RX fill:#fd7e14,color:#fff style UW fill:#fd7e14,color:#fff style FAN fill:#fd7e14,color:#fff style SM fill:#667eea,color:#fff style TILES fill:#ecf0f1

Level 5: Service Architecture

Internal Services: The application initializes singleton services at startup: HubitatClient for Maker API communication, DeviceCache for TTL-based device state caching, InstanceManager for instance CRUD and lifecycle, WebhookRouter for event routing, and SchedulerService for APScheduler-backed background jobs. All services use global factory functions (e.g., get_instance_manager()) to ensure single instances.
graph TB subgraph Startup["Application Startup (app.py lifespan)"] INIT[initialize_services] REG[initialize_registry] LOAD[initialize_all_instances] end subgraph Services["Singleton Services"] HC[HubitatClient
Maker API wrapper
Connection pooling + retries] DC[DeviceCache
TTL 300s
Memory + DB fallback] IM[InstanceManager
Instance CRUD
Runtime lifecycle] WR[WebhookRouter
Event routing
Subscription queries] SS[SchedulerService
APScheduler
DB persistence] end subgraph Registry["App Registry"] AR[AppRegistry
Type registration
DB sync] AML[AdvancedMotionLightingApp
registered class] end subgraph External["External Dependencies"] PREST[PostgREST :3001] HAPI[Hubitat Maker API] end INIT --> HC INIT --> DC INIT --> IM INIT --> WR INIT --> SS REG --> AR AR --> AML LOAD --> IM HC --> HAPI DC --> PREST IM --> PREST WR --> PREST SS --> PREST AR --> PREST style INIT fill:#667eea,color:#fff style REG fill:#667eea,color:#fff style LOAD fill:#667eea,color:#fff style HC fill:#28a745,color:#fff style DC fill:#17a2b8,color:#fff style IM fill:#fd7e14,color:#fff style WR fill:#dc3545,color:#fff style SS fill:#6f42c1,color:#fff style AR fill:#20c997,color:#fff
Service File Factory Function Key Methods
HubitatClient services/hubitat_client.py get_default_client() get_all_devices(), send_command(), get_modes(), is_connected()
DeviceCache services/device_cache.py get_default_cache() get_all(), get_devices_by_capability(), update_device_attribute()
InstanceManager services/instance_manager.py get_instance_manager() create_instance(), pause_instance(), resume_instance(), get_running_instance()
WebhookRouter services/webhook_router.py get_webhook_router() route_event(), route_mode_change()
SchedulerService services/scheduler_service.py get_scheduler() schedule_once(), reschedule(), cancel_for_instance()

Level 6: Database Schema

Database Design: PostgreSQL 16 with PostgREST providing automatic REST endpoints. All application writes go through PostgREST (no raw SQL in the app). The schema centers on three core tables: app_types (blueprints), app_instances (user automations), and device_subscriptions (event routing). Supporting tables handle caching, logging, scheduling, hub config, and location modes.
erDiagram app_types ||--o{ app_instances : "blueprint for" app_instances ||--o{ device_subscriptions : "subscribes to" app_instances ||--o{ scheduled_jobs : "schedules" app_instances ||--o{ event_log : "receives events" app_types { serial id PK varchar type_name UK varchar display_name text description jsonb settings_schema jsonb device_categories varchar version } app_instances { serial id PK int app_type_id FK varchar label jsonb settings jsonb device_selections boolean is_paused jsonb memoization_state timestamp last_activity_at } device_subscriptions { varchar hubitat_device_id int instance_id FK varchar event_type } device_cache { varchar hubitat_device_id PK varchar device_name jsonb capabilities jsonb attributes timestamp last_synced_at } event_log { serial id PK varchar hubitat_device_id varchar event_type varchar event_value jsonb routed_to_instances jsonb raw_payload timestamp received_at } scheduled_jobs { varchar job_id PK int instance_id FK varchar job_type timestamp execute_at jsonb payload varchar status } hub_config { serial id PK varchar hub_name UK varchar hub_ip int maker_api_app_number varchar maker_api_token_env } location_modes { int mode_id PK varchar mode_name boolean is_active }
Table Purpose Key Columns PostgREST Endpoint
app_types Available app blueprints type_name, settings_schema (JSONB), device_categories (JSONB) GET/POST /app_types
app_instances User-created automation instances label, settings (JSONB), device_selections (JSONB), memoization_state (JSONB) GET/POST/PATCH/DELETE /app_instances
device_subscriptions Maps devices to instances for event routing hubitat_device_id, instance_id, event_type (unique constraint) GET/POST/DELETE /device_subscriptions
device_cache Cached device state (reduces API polling) capabilities (JSONB), attributes (JSONB), last_synced_at GET/POST/PATCH /device_cache
event_log Audit trail of all webhook events routed_to_instances (JSONB), raw_payload (JSONB) GET/POST /event_log
scheduled_jobs Persistent background tasks job_type (timeout/pause_expire/health_check), execute_at, status GET/POST/PATCH /scheduled_jobs

Level 7: App Class Hierarchy

Extensibility Pattern: All app types inherit from BaseApp, which provides the common lifecycle (initialize, event handling, scheduling, memoization, pause/resume). Subclasses implement the abstract methods: initialize(), on_event(), master(), and class methods get_settings_schema() and get_device_categories(). Adding a new app type means creating a subclass and registering it in app_registry.py.
classDiagram class BaseApp { +str TYPE_NAME +str DISPLAY_NAME +str VERSION -dict _memoization -RuntimeInstanceState _runtime -bool _is_paused +initialize()* void +on_event(event)* void +master(**kwargs)* void +get_settings_schema()$ dict +get_device_categories()$ list +pause(duration_minutes) void +resume() void +on_mode_change(mode_name) void +schedule_timeout(delay) void +reschedule_timeout(delay) void +cancel_timeout() void +send_command(device_id, cmd, args) void +get_device_state(device_id) dict +get_setting(key, default) any +get_devices(category) list +get_memo(key) any +set_memo(key, value) void } class AdvancedMotionLightingApp { +str TYPE_NAME = "advanced_motion_lighting" +str VERSION = "2.0.0" -dict _functional_sensors +initialize() void +on_event(event) void +master(**kwargs) void -_handle_motion(event) void -_handle_switch(event) void -_handle_illuminance(event) void -_handle_button(event) void -_handle_contact(event) void -_control_lights(action) void -_check_illuminance() bool -_health_check() void -_get_timeout_seconds() int } class FutureAppType { +str TYPE_NAME = "thermostat_manager" +initialize() void +on_event(event) void +master(**kwargs) void } BaseApp <|-- AdvancedMotionLightingApp BaseApp <|-- FutureAppType class RuntimeInstanceState { +datetime last_motion_time +datetime last_switch_control +dict functional_sensors +str timeout_job_id +str health_check_job_id } class DeviceEvent { +str device_id +str event_type +str value +str device_name +bool is_motion_active +bool is_switch_on +float numeric_value } BaseApp --> RuntimeInstanceState : uses BaseApp --> DeviceEvent : receives

Adding a New App Type

  1. Create apps/{new_type}/app_logic.py — subclass BaseApp, implement abstract methods
  2. Set class attributes: TYPE_NAME, DISPLAY_NAME, DESCRIPTION, VERSION
  3. Implement get_settings_schema() (JSON Schema for UI form) and get_device_categories() (device picker definitions)
  4. Register in apps/app_registry.py: from apps.new_type.app_logic import NewApp; register_app_type(NewApp)
  5. On startup, registry syncs to app_types table; users can immediately create instances via wizard

Level 8: Application Startup Flow

flowchart TB START([start.sh executed]) --> AWS[Pull AWS Secrets
SMARTHOME + HUBITAT] AWS --> MAP[Map Hub Numbers
AWS _4 → _MAIN
AWS _1-3 → _OTHER_HUB_1-3] MAP --> CERT{SSL Certs
Exist?} CERT -->|No| GEN[Generate Self-Signed
nginx/certs/] CERT -->|Yes| SKIP[Skip Generation] GEN --> COMPOSE SKIP --> COMPOSE COMPOSE[docker compose up -d] --> PG_INIT[PostgreSQL Init
psql/init-db.sql] PG_INIT --> PR_READY[PostgREST Connects
to PostgreSQL] COMPOSE --> APP_START[FastAPI Lifespan Start] APP_START --> INIT_SVC[initialize_services
Create singletons] INIT_SVC --> INIT_REG[initialize_registry
Import + register app classes] INIT_REG --> SYNC_DB[Sync app_types to DB
POST via PostgREST] SYNC_DB --> LOAD_INST[initialize_all_instances
Load all enabled instances] LOAD_INST --> FOR_EACH[For each enabled instance] FOR_EACH --> INST_OBJ[Instantiate Python object
app_class - instance_data, manager] INST_OBJ --> INIT_APP[Call app.initialize
Schedule health checks etc.] INIT_APP --> READY([Application Ready
Accepting webhooks + UI requests]) style START fill:#28a745,color:#fff style AWS fill:#fff3cd style MAP fill:#fff3cd style COMPOSE fill:#667eea,color:#fff style READY fill:#28a745,color:#fff style INIT_SVC fill:#17a2b8,color:#fff style LOAD_INST fill:#fd7e14,color:#fff

Level 9: REST API Endpoints

API Design: FastAPI with automatic OpenAPI docs at /docs. The API covers health checks, app type discovery, instance CRUD, device queries, webhook reception, and mode management. The frontend consumes these endpoints via jQuery AJAX and ES6 fetch calls.
Method Endpoint Purpose Notes
GET /api/health Health check Returns {status: "ok"}
GET /api/status Detailed status Instance count, Hubitat connectivity
GET /api/app-types List app blueprints For wizard type selection
GET /api/app-types/{type}/schema Settings + device schema JSON Schema for dynamic form
GET /api/instances List all instances Dashboard data
POST /api/instances Create instance Body: app_type, label, devices, settings
GET /api/instances/{id} Get instance details Including current state
PUT /api/instances/{id} Update instance Reloads runtime object
DELETE /api/instances/{id} Delete instance Cascades subscriptions
POST /api/instances/{id}/pause Pause automation Optional: duration_minutes, reason
POST /api/instances/{id}/resume Resume automation Re-evaluates current state
GET /api/devices List devices Query: ?capability=motionSensor
POST /api/devices/{id}/command Send device command Body: command, args
POST /api/webhook/event Device event webhook From dispatcher
POST /api/webhook/mode Mode change webhook Notifies all instances
GET /api/modes Available location modes From Hubitat

Web UI Routes

Route Template Purpose
GET / dashboard.html Instance list with status cards
GET /instance/new instance_wizard.html Multi-step creation wizard
GET /instance/{id} instance_detail.html Edit instance devices/settings

Level 10: Frontend Architecture

flowchart TB subgraph Templates["Jinja2 Templates"] BASE[base.html
Layout + nav + CSS/JS includes] DASH[dashboard.html
Instance cards grid] WIZ[instance_wizard.html
Multi-step creation] DET[instance_detail.html
Edit instance] end subgraph JS["ES6 JavaScript Modules"] MAIN[main.js
Entry point + utilities] DC[dashboard-controller.js
Instance list, status polling] IC[instance-controller.js
Instance CRUD, device picker] end subgraph CSS["Stylesheets"] MCSS[main.css
Component-based styles] end subgraph API["FastAPI Backend"] REST[REST API Endpoints] end BASE --> DASH BASE --> WIZ BASE --> DET DASH --> DC WIZ --> IC DET --> IC DC -->|jQuery AJAX| REST IC -->|jQuery AJAX| REST BASE --> MAIN BASE --> MCSS style BASE fill:#667eea,color:#fff style DC fill:#28a745,color:#fff style IC fill:#28a745,color:#fff style REST fill:#fd7e14,color:#fff

Frontend Patterns

Level 11: Secrets & Configuration Flow

AWS-Only Policy: No .env files. All configuration lives in AWS Secrets Manager. start.sh pulls secrets, exports them as environment variables, and Docker Compose inherits them. The docker-compose.yml uses ${VAR:-default} syntax with no file-based env.
flowchart LR subgraph AWS["AWS Secrets Manager"] SM[SMARTHOME Secret
Ports, DB creds, API token] HB[HUBITAT Secret
Hub tokens, IPs, app numbers
Shared with 0_MOBIUS.TILES, 0_MOBIUS.NVR] end subgraph StartSH["start.sh"] PULL[pull_aws_secrets] MAP_HUB[Map Hub Numbers
_4 → _MAIN
_1 → _OTHER_HUB_1
_2 → _OTHER_HUB_2
_3 → _OTHER_HUB_3] EXPORT[export ENV VARS] end subgraph Docker["Docker Compose"] ENV["environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
HUBITAT_API_TOKEN_MAIN: ${...}
APP_API_TOKEN: ${...}"] end subgraph Containers["Running Containers"] APP_C[smarthome-app
os.environ access] PG_C[smarthome-postgres
POSTGRES_PASSWORD] PR_C[smarthome-postgrest
PGRST_DB_URI] end SM --> PULL HB --> PULL PULL --> MAP_HUB MAP_HUB --> EXPORT EXPORT --> ENV ENV --> APP_C ENV --> PG_C ENV --> PR_C style SM fill:#fff3cd style HB fill:#fff3cd style PULL fill:#667eea,color:#fff style MAP_HUB fill:#fd7e14,color:#fff
AWS Secret Key Fields Shared?
SMARTHOME APP_EXTERNAL_PORT, APP_INTERNAL_PORT, NGINX_HTTPS_PORT, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, POSTGREST_EXTERNAL_PORT, SERVER_IP, APP_API_TOKEN, WEBHOOK_PORT This project only
HUBITAT HUBITAT_API_TOKEN_1-4, HUBITAT_HUB_IP_1-4, HUBITAT_API_NUMBER_1-4 Shared with 0_MOBIUS.TILES, 0_MOBIUS.NVR

Hub Number Mapping

AWS stores numbered hubs (1-4). start.sh translates to descriptive names:

Level 12: Key Design Patterns

Patterns Used Throughout the System

Architecture Summary

0_MOBIUS.SMART_HOME Engineering Overview

File Structure Reference

Access Points

Known Constraints