Skip to content

Sowel -- Data Model

Version: 2.0 -- 2026-05-10

This index covers the cross-cutting parts of Sowel's data model: the three-layer architecture, the entity relationship diagram, the plugin distribution layer, the reactive data flow, the event bus events, and the API surface.

Per-entity details are split across five focused pages aligned with the core concepts:

  • Devices -- physical layer, normalized data model
  • Equipments -- functional units, bindings, computed data
  • Zones -- spatial topology, aggregation rules, auto-orders
  • Modes -- orchestration, button bindings, calendar scheduling
  • Recipes -- automation templates, instances, runtime helpers

1. Three-Layer Architecture

Sowel separates concerns into three distinct layers, with orchestration entities (Modes, Recipes, Calendars) operating across them.

+-------------------------------------------------------------+
|  ORCHESTRATION                                              |
|  Modes  --  Recipes  --  Calendars  --  Buttons             |
|  Cross-cutting state machines that drive Equipments / Zones |
+-------------------------------------------------------------+
|  LAYER 1 -- TOPOLOGY (Zones)                                |
|  Spatial structure of the home                              |
|  Hierarchical: Home -> Floor -> Room                        |
|  Aggregates data automatically from child Equipments        |
+-------------------------------------------------------------+
|  LAYER 2 -- FUNCTIONAL (Equipments)                         |
|  What the user sees and controls                            |
|  Placed IN a Zone                                           |
|  Binds to one or more physical Devices                      |
+-------------------------------------------------------------+
|  LAYER 3 -- PHYSICAL (Devices)                              |
|  Hardware discovered from integration plugins               |
|  Auto-discovered, raw data and orders                       |
|  Never directly manipulated by the end user                 |
+-------------------------------------------------------------+

Guiding principle: A Device is what's on the network. An Equipment is what's in the room.



2. Entity Relationship Diagram

Plugin (PackageType: integration | recipe)
 +-- (integration) discovers --> Device
 +-- (recipe)      registers --> RecipeDefinition

Zone (hierarchy: parent -> children)
 |
 +-- Equipment (functional unit, user-facing)
      +-- DataBinding  --> DeviceData  (readable, alias-named)
      +-- OrderBinding --> DeviceOrder (writable, alias-named)
      +-- ComputedDataEntry (provider-supplied, no DB row)

Device (physical, auto-discovered, optional zoneId)
 +-- DeviceData  (readable properties)
 +-- DeviceOrder (writable commands)

Mode --< ZoneModeImpact >-- Zone
                            |
                            actions: order | recipe_toggle | recipe_params

RecipeInstance --(params)--> RecipeDefinition (from a recipe plugin)
 +-- RecipeState (key/value store, persisted)
 +-- RecipeLog  (event log)

CalendarProfile --< CalendarSlot (cron-scheduled mode changes)

ButtonActionBinding (Equipment.action -> mode/order/recipe/zone-order)

3. Plugin

Since spec 053, everything that ships out of band is a plugin -- both integrations and recipes. The PackageManager downloads them from GitHub and the appropriate loader (PluginLoader for integrations, RecipeLoader for recipes) instantiates them at runtime.

3.1 Interfaces

type PackageType = "integration" | "recipe";

interface PluginManifest {
  id: string; // unique plugin id, used in DB and registry
  name: string;
  version: string; // semver
  description: string;
  icon: string; // Lucide icon name
  repo: string; // GitHub owner/repo -- required for backup/restore reinstall
  type?: PackageType; // defaults to "integration"
  author?: string;
  sowelVersion?: string; // minimum compatible Sowel version
  settings?: IntegrationSettingDef[];
}

interface InstalledPackage {
  manifest: PluginManifest;
  enabled: boolean;
  installedAt: string;
  type: PackageType;
}

interface PluginInfo extends Omit<InstalledPackage, "type"> {
  status: IntegrationStatus; // connected | disconnected | error | not_configured
  deviceCount: number;
  offlineDeviceCount: number;
  latestVersion?: string; // populated when registry has a newer version
}

3.2 SQLite Schema

CREATE TABLE plugins (
  id TEXT PRIMARY KEY,                    -- manifest.id
  version TEXT NOT NULL,
  enabled INTEGER NOT NULL DEFAULT 1,
  installed_at TEXT NOT NULL DEFAULT (datetime('now')),
  manifest TEXT NOT NULL,                 -- JSON: full PluginManifest
  type TEXT NOT NULL DEFAULT 'integration'
);

Integration settings are stored in the generic settings table under keys like integration.<id>.<settingKey>.



4. Reactive Data Flow

The complete event-driven pipeline:

Integration message (MQTT, cloud API poll, etc.)
  |
  v
Integration Plugin (receives + parses)
  |
  v
Device Manager (updates DeviceData, persists, emits)
  |
  +--> Event: "device.data.updated"
  |
  v
Equipment Manager
  +-- Updates bound aliases via DataBindings
  +-- Pulls ComputedDataEntry from registered providers (energy, pool, ...)
  |
  +--> Event: "equipment.data.changed"
  |
  +--> Button Action Manager (if alias=="action")
  |     +-- Dispatches: mode activate/toggle, equipment order, recipe toggle, zone order
  |
  v
Zone Aggregator
  +-- Recomputes Zone aggregated data, propagates up the tree
  |
  +--> Event: "zone.data.changed"
  |
  v
Recipe Engine
  +-- Each running RecipeInstance reacts via its createInstance() handle
  +-- May emit equipment orders (back into the equipment manager)
  |
  +--> Events: "recipe.instance.state.changed" / ".error"
  |
  v
Side effects (parallel, all subscribe to the bus)
  +-- WebSocket Server                 -> pushes events to UI clients
  +-- History Writer                   -> writes points to InfluxDB
  +-- MQTT Publishers                  -> emit on configured outbound topics
  +-- Notification Publishers          -> Telegram/webhook on threshold

Mode/Calendar/Button paths:
  CalendarManager (cron)  -> ModeManager.activate/deactivate
  ButtonActionBinding     -> ModeManager / EquipmentManager / RecipeManager / Zone order
  ModeManager.activate    -> equipment orders + recipe toggles + recipe param updates

5. Event Bus Events

All events are typed via EngineEvent, a TypeScript discriminated union. Handlers must never throw (wrap in try/catch + structured log).

System

Event Payload
system.started --
system.integration.connected integrationId
system.integration.disconnected integrationId
system.error error
system.alarm.raised alarmId, level, source, message
system.alarm.resolved alarmId, source, message
system.update.available current, latest, releaseUrl
system.update.progress step, message
system.update.error error
system.restart_required reason

Device

Event Payload
device.discovered device: Device
device.removed deviceId, deviceName
device.status_changed deviceId, deviceName, status
device.data.updated deviceId, deviceName, dataId, key, value, previous, timestamp, sourceTimestamp?
device.heartbeat deviceId, timestamp

Zone

Event Payload
zone.created zone: Zone
zone.updated zone: Zone
zone.removed zoneId, zoneName
zone.data.changed zoneId, aggregatedData: ZoneAggregatedData

Equipment

Event Payload
equipment.created equipment: Equipment
equipment.updated equipment: Equipment
equipment.removed equipmentId, equipmentName, zoneId
equipment.data.changed equipmentId, alias, value, previous, sourceTimestamp?
equipment.order.executed equipmentId, orderAlias, value
equipment.order.failed equipmentId, orderAlias, value, error

Recipe

Event Payload
recipe.instance.created instanceId, recipeId
recipe.instance.removed instanceId, recipeId
recipe.instance.started instanceId, recipeId
recipe.instance.stopped instanceId, recipeId
recipe.instance.state.changed instanceId, recipeId
recipe.instance.error instanceId, recipeId, error

Mode / Calendar / Sunlight / Settings

Event Payload
mode.created mode: Mode
mode.updated mode: Mode
mode.removed modeId, modeName
mode.activated modeId, modeName
mode.deactivated modeId, modeName
calendar.profile.changed profileId, profileName
sunlight.changed --
settings.changed keys: string[]

MQTT / Notification publishers

Event Payload
mqtt-broker.created / .updated broker: MqttBroker
mqtt-broker.removed brokerId
mqtt-publisher.created / .updated publisher: MqttPublisher
mqtt-publisher.removed publisherId, publisherName
mqtt-publisher.mapping.created publisherId, mapping: MqttPublisherMapping
mqtt-publisher.mapping.removed publisherId, mappingId
notification-publisher.created / .updated publisher: NotificationPublisher
notification-publisher.removed publisherId, publisherName
notification-publisher.mapping.created publisherId, mapping: NotificationPublisherMapping
notification-publisher.mapping.removed publisherId, mappingId

6. API Endpoints Summary

This is a non-exhaustive index. See API Reference for the full surface.

Zones

Method Route Description
GET /api/v1/zones List all zones (tree structure)
GET /api/v1/zones/:id Get zone with aggregated data
POST /api/v1/zones Create zone
PUT /api/v1/zones/:id Update zone
DELETE /api/v1/zones/:id Delete zone
POST /api/v1/zones/:id/orders/:orderKey Execute zone-level bulk order

Equipments

Method Route Description
GET /api/v1/equipments List all equipments
GET /api/v1/equipments/:id Get equipment with bindings + data
POST /api/v1/equipments Create equipment
PUT /api/v1/equipments/:id Update equipment
DELETE /api/v1/equipments/:id Delete equipment
POST /api/v1/equipments/:id/orders/:alias Execute an equipment order

Devices, Plugins, History

Method Route Description
GET /api/v1/devices List discovered devices
GET /api/v1/devices/:id Device with data + orders
GET /api/v1/integrations Runtime integration status
GET /api/v1/plugins Installed plugins (manifest + status)
POST /api/v1/plugins/install Install a plugin from registry / GitHub
DELETE /api/v1/plugins/:id Uninstall plugin
GET /api/v1/history Time-series history query

Modes / Recipes / Calendar / Buttons

Method Route Description
GET /api/v1/modes List modes
POST /api/v1/modes Create mode
POST /api/v1/modes/:id/activate Activate a mode (apply impacts)
POST /api/v1/modes/:id/deactivate Deactivate a mode
PUT /api/v1/modes/:id/zones/:zoneId/impact Set the zone-impact for a mode
GET /api/v1/recipes List available recipe definitions
POST /api/v1/recipes/:recipeId/instances Create a recipe instance
GET /api/v1/recipes/instances/:id Get instance + recent log
GET /api/v1/calendar/profiles List calendar profiles
PUT /api/v1/calendar/active Switch active profile
POST /api/v1/calendar/profiles/:id/slots Add a scheduling slot
GET /api/v1/button-actions List button bindings
POST /api/v1/button-actions Create a button binding

MQTT / Notifications / Dashboard / Charts

Method Route Description
* /api/v1/mqtt-brokers/... CRUD MQTT brokers
* /api/v1/mqtt-publishers/... CRUD MQTT publishers + mappings
* /api/v1/notification-publishers/... CRUD notification publishers
* /api/v1/dashboard/... Dashboard widgets
* /api/v1/charts/... Saved chart configs
* /api/v1/energy/... Energy dashboard, history, by-usage

For the complete and authoritative reference, see API Reference.