Architecture¶
suncast follows the Ports & Adapters (hexagonal) architecture pattern. Domain logic — solar position computation, shadow projection, and geometry handling — has zero I/O dependencies. The cosalette IoT framework handles MQTT connectivity, health reporting, error isolation, and graceful shutdown.
Overview¶
graph TD
subgraph External Inputs
YAML[geometry.yaml]
ASTRAL[astral library]
end
subgraph Domain
GEOM["GeometryConfig / fit_to_circle"]
SOLAR[compute_solar_position]
SHADOW[compute_building_shadows]
end
subgraph Rendering
RENDERER[ShadowRenderer]
RASTER["svg_to_png (optional)"]
end
subgraph Delivery
OUTPUT[OutputManager]
HTTP["HttpServer (optional)"]
end
subgraph cosalette
APP[App]
MQTT[MqttClient]
HR[HealthReporter]
end
YAML --> GEOM
ASTRAL --> SOLAR
GEOM --> SHADOW
SOLAR --> SHADOW
GEOM --> RENDERER
SHADOW --> RENDERER
RENDERER --> OUTPUT
RASTER --> OUTPUT
HTTP --> OUTPUT
OUTPUT --> APP
APP --> MQTT
APP --> HR
Layers¶
Domain (suncast.domain)¶
Pure computation — no I/O, no async. All modules are independently testable.
| Module | Purpose |
|---|---|
solar |
Solar position from lat/lon/time via astral (ADR-002) |
shadow |
Silhouette detection + parallel projection → shadow polygons |
geometry |
YAML/JSON loader, Pydantic validation, fit_to_circle auto-scaling |
geometry_svg |
SVG file importer with sidecar support (ADR-003) |
compute_solar_position returns a SunPosition dataclass with azimuth, elevation,
sunrise/sunset azimuths and times, and hourly azimuths for the sundial ring.
compute_building_shadows takes a GeometryConfig and SunPosition and returns a
list of ShadowResult (one per building with its shadow polygon and sun-facing edges).
Rendering¶
| Module | Purpose |
|---|---|
renderer |
SVG assembly: buildings, shadows, sundial ring, day/night arc, markers |
rasterize |
Optional PNG conversion via CairoSVG (ADR-004) |
ShadowRenderer.render() produces a complete SVG string. svg_to_png() converts that
string to PNG bytes when the png extra is installed.
Output¶
| Module | Purpose |
|---|---|
output |
Filesystem + MQTT delivery: writes files, publishes to svg/png channels |
http_server |
Optional aiohttp server for /shadow.svg and /shadow.png (ADR-004) |
OutputManager.deliver() orchestrates all three delivery channels (filesystem, MQTT,
HTTP cache) in a single call.
Data Flow¶
Each poll cycle follows this path through the pipeline:
sequenceDiagram
participant COS as cosalette scheduler
participant H as _shadow_handler
participant SOL as compute_solar_position
participant SH as compute_building_shadows
participant R as ShadowRenderer.render
participant O as OutputManager.deliver
COS->>H: trigger poll cycle
H->>SOL: latitude, longitude, timezone, now
SOL-->>H: SunPosition
H->>SH: geometry, sun
SH-->>H: list[ShadowResult]
H->>R: sun, shadows, geometry, settings
R-->>H: SVG string
H->>O: svg, output context
O->>O: write shadow.svg
O->>O: publish svg channel
opt png_enabled
O->>O: svg_to_png
O->>O: write shadow.png
O->>O: publish png channel
end
H-->>COS: return None
The handler returns None — suncast publishes visual output through dedicated MQTT
channels (svg, png) rather than the framework's automatic /state topic.
Pipeline Initialization¶
The init= callback (_build_pipeline) runs once at device startup and builds a
PipelineState dataclass containing:
- GeometryConfig — loaded from YAML/JSON/SVG, then auto-scaled via
fit_to_circle() - ShadowRenderer — stateless SVG assembler
- RenderSettings — colors, stroke width, marker style (from settings)
- OutputManager — configured delivery channels (from settings)
This state is injected into the handler via cosalette's type-based DI system.
cosalette Framework¶
suncast is built on cosalette, a lightweight framework for IoT-to-MQTT bridges. cosalette provides:
- App composition root — wires devices, adapters, settings, and lifecycle
- Device decorators —
@app.telemetry,@app.command,@app.device - MQTT management — auto-reconnect, LWT, topic conventions
- Health reporting — periodic heartbeats, per-device availability
- Error isolation — exceptions in one device don't crash the app
- Dependency injection — settings and services resolved by type annotation
- Graceful shutdown — SIGTERM/SIGINT → shutdown event → clean teardown
- Lifespan hooks — suncast uses this for the optional HTTP server
The create_app() factory in app.py is the composition root — it creates the App,
registers the telemetry device with a deferred interval, and wires the HTTP lifespan.
Further Reading¶
- cosalette documentation — the IoT framework
- ADR-001: Cosalette App Architecture — why suncast adopted cosalette
- ADR-002: Solar Position Computation — astral library selection
- ADR-003: House Geometry Configuration — YAML format and SVG import design
- ADR-004: Image Output and Delivery — filesystem, MQTT, HTTP, and PNG rasterization