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.
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
Technology Stack
- Backend: Python 3.11 + FastAPI + Uvicorn (single worker for SSE compatibility)
- Frontend: Jinja2 Templates + ES6 Modules + jQuery
- Database: PostgreSQL 16 + PostgREST v12 (auto-generated REST API from schema)
- Webhook Fan-out: Flask-based dispatcher (shared with 0_MOBIUS.TILES, 0_MOBIUS.NVR)
- Scheduling: APScheduler with PostgreSQL persistence
- Proxy: Nginx 1.27 with self-signed TLS
- Containerization: Docker Compose (5 services)
- Secrets: AWS Secrets Manager (no .env files)
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
- Create: UI wizard collects label, device selections, settings.
InstanceManager.create_instance() writes to app_instances, registers device_subscriptions, instantiates runtime object, calls app.initialize()
- Update: Modify devices/settings via
PUT /api/instances/{id}. Manager updates DB, reloads subscriptions, re-initializes runtime object
- Pause: Cancels active timeouts, sets
is_paused=true. Optional duration auto-resumes via scheduler
- Resume: Sets
is_paused=false, re-evaluates current sensor state, calls master()
- Delete: Stops runtime, removes from memory, cascading delete on subscriptions
- Startup:
initialize_all_instances() loads all enabled instances from DB and instantiates runtime objects
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):
deviceId: Hubitat device ID (string)
name: Event type — "motion", "switch", "level", "illuminance", "pushed", "contact"
value: Event value — "active"/"inactive", "on"/"off", numeric values
displayName: Human-readable device name
descriptionText: Full event description
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
- Create
apps/{new_type}/app_logic.py — subclass BaseApp, implement abstract methods
- Set class attributes:
TYPE_NAME, DISPLAY_NAME, DESCRIPTION, VERSION
- Implement
get_settings_schema() (JSON Schema for UI form) and get_device_categories() (device picker definitions)
- Register in
apps/app_registry.py: from apps.new_type.app_logic import NewApp; register_app_type(NewApp)
- 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
- ES6 Modules:
import/export for code organization; controllers in static/js/controllers/
- jQuery DOM:
$().on() for events, $.ajax() for API calls
- Jinja2 Templates: Server-side rendering with
url_for(path=) (Starlette convention)
- Instance Wizard: Multi-step flow: Select App Type → Name Instance → Pick Devices → Configure Settings → Review & Create
- Device Picker: Dynamic device list from
/api/devices?capability=X, supports multiple selection per category
- Dashboard Polling: Periodic status refresh to show instance activity
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:
HUBITAT_*_4 (AWS) → HUBITAT_*_MAIN (app) — Primary hub: 192.168.10.72, app 268
HUBITAT_*_1 (AWS) → HUBITAT_*_OTHER_HUB_1 (app) — 192.168.10.69, app 1717
HUBITAT_*_2 (AWS) → HUBITAT_*_OTHER_HUB_2 (app) — 192.168.10.70, app 2151
HUBITAT_*_3 (AWS) → HUBITAT_*_OTHER_HUB_3 (app) — 192.168.10.71, app 1269
Level 12: Key Design Patterns
Patterns Used Throughout the System
- Singleton Services: Global factory functions (
get_instance_manager(), get_scheduler(), etc.) ensure single instances across the application
- Template Method (BaseApp): Abstract base class defines lifecycle skeleton; subclasses implement specific behavior
- Observer/Pub-Sub: Device subscriptions table acts as a subscription registry; webhook router dispatches events to observers
- PostgREST as Data Layer: All CRUD goes through REST API — uniform pattern, no raw SQL in Python code
- Memoization: Persistent state (saved to DB) tracks user manual overrides to avoid fighting user control
- TTL-Based Caching: Device cache expires after 5 minutes; webhook events reset TTL
- Timeout Rescheduling: On motion re-trigger, reschedule existing timeout instead of creating new one (prevents light flickering)
- Health Monitoring: Periodic sensor health checks with configurable fail-safe behavior
Architecture Summary
0_MOBIUS.SMART_HOME Engineering Overview
- Multi-Instance Core: Each automation is an independent instance with its own devices, settings, memoization, and runtime state
- Event-Driven: Hubitat webhooks flow through dispatcher → router → subscribed instances → automation logic
- PostgREST Data Layer: Automatic REST API from PostgreSQL schema — no ORM, no raw SQL in app code
- Extensible App System: New app types require only a BaseApp subclass and registry entry
- Persistent Scheduling: APScheduler with database backup for reliable timeout and health check jobs
- AWS-Only Config: All secrets in AWS Secrets Manager, no .env files, start.sh handles injection
- Single Worker SSE: Uvicorn with one worker for SSE compatibility; async event loop for concurrency
- Shared Webhook Dispatcher: One dispatcher serves multiple projects (0_MOBIUS.SMART_HOME, 0_MOBIUS.TILES) via fan-out
- Docker-First: 5 containers orchestrated by docker-compose.yml; full rebuild via deploy.sh
File Structure Reference
- Entry Point:
app.py — FastAPI application, routes, lifespan
- Services:
services/hubitat_client.py, device_cache.py, instance_manager.py, webhook_router.py, scheduler_service.py
- App Logic:
apps/base_app.py (abstract base), apps/app_registry.py (registration + DB sync)
- App Implementations:
apps/advanced_motion_lighting/app_logic.py
- Models:
models/event.py (DeviceEvent), models/instance.py (RuntimeInstanceState), models/device.py (HubitatDevice)
- Database Schema:
psql/init-db.sql
- Frontend:
static/js/controllers/, static/css/main.css
- Templates:
templates/base.html, dashboard.html, instance_wizard.html, instance_detail.html
- Infrastructure:
docker-compose.yml, Dockerfile, start.sh, deploy.sh, nginx/nginx.conf
- Webhook Dispatcher:
services/webhook_dispatcher.py, Dockerfile.dispatcher (hardlinks from ~/0_SCRIPTS/webhook_dispatcher/)
Access Points
- HTTPS (primary):
https://192.168.10.20:8444/
- Direct (no proxy):
http://192.168.10.20:5001/
- OpenAPI docs:
https://192.168.10.20:8444/docs
- Webhook endpoint:
http://192.168.10.20:5050/api/webhook/event
- PostgREST API:
http://localhost:3003/
- Database:
docker exec -it smarthome-postgres psql -U smarthome_api -d smarthome
Known Constraints
- Single Worker: SSE requires shared memory — cannot scale to multiple Uvicorn workers without switching to Redis pub/sub
- PostgREST Dependency: All CRUD goes through PostgREST; if it's down, the app cannot persist state
- Webhook Dispatcher Singleton: Only one instance runs system-wide (Docker container name collision); whichever project starts first wins
- Self-Signed Certs: Browsers show warnings; not suitable for public-facing deployment without proper CA certs
- Memoization Heuristic: User manual control detection uses timing (less than 3s = app echo); edge cases with very fast manual interactions