Releases: slippyex/sunsteer
v0.5.3
Changed
- The controller now acts on real PV headroom:
available = production − base_load
(base_load = 20th-percentile of recent consumption over a 60-min window) instead of a fixed
surplus + wp_nominalload-compensation. The heat pump is no longer held ON on grid power
when it modulates below its nominal draw. Falls back to the previous (sun-gated) logic when
no fresh inverter production is available; the fail-safe chain and the sun-gate are unchanged. - The exporter publishes
production_win/stateonly while the inverter reading is fresh
(dropped after ~90 s stale), so the controller never computes on a frozen value.
Added
- Gauges
surplus_control_base_load_wattsandsurplus_control_available_basis
(1 = production-based, 0 = nominal fallback).
v0.5.2
Changed
- The controller now acts on real PV headroom:
available = production − base_load
(base_load = 20th-percentile of recent consumption over a 60-min window) instead of a fixed
surplus + wp_nominalload-compensation. The heat pump is no longer held ON on grid power
when it modulates below its nominal draw. Falls back to the previous (sun-gated) logic when
no fresh inverter production is available; the fail-safe chain and the sun-gate are unchanged. - The exporter publishes
production_win/stateonly while the inverter reading is fresh
(dropped after ~90 s stale), so the controller never computes on a frozen value.
Added
- Gauges
surplus_control_base_load_wattsandsurplus_control_available_basis
(1 = production-based, 0 = nominal fallback).
v0.5.1
Changed
- The controller now acts on real PV headroom:
available = production − base_load
(base_load = 20th-percentile of recent consumption over a 60-min window) instead of a fixed
surplus + wp_nominalload-compensation. The heat pump is no longer held ON on grid power
when it modulates below its nominal draw. Falls back to the previous (sun-gated) logic when
no fresh inverter production is available; the fail-safe chain and the sun-gate are unchanged. - The exporter publishes
production_win/stateonly while the inverter reading is fresh
(dropped after ~90 s stale), so the controller never computes on a frozen value.
Added
- Gauges
surplus_control_base_load_wattsandsurplus_control_available_basis
(1 = production-based, 0 = nominal fallback).
v0.5.0
Changed
- The controller now acts on real PV headroom:
available = production − base_load
(base_load = 20th-percentile of recent consumption over a 60-min window) instead of a fixed
surplus + wp_nominalload-compensation. The heat pump is no longer held ON on grid power
when it modulates below its nominal draw. Falls back to the previous (sun-gated) logic when
no fresh inverter production is available; the fail-safe chain and the sun-gate are unchanged. - The exporter publishes
production_win/stateonly while the inverter reading is fresh
(dropped after ~90 s stale), so the controller never computes on a frozen value.
Added
- Gauges
surplus_control_base_load_wattsandsurplus_control_available_basis
(1 = production-based, 0 = nominal fallback).
v0.4.2
Added
- The web UI now shows the sun: the live solar elevation and today's PV window
(sunrise/sunset forPV_SUN_MIN_ELEVATION_DEG) in the status panel — cyan inside the
window, amber outside. Backed by two new gauges
surplus_control_sun_rise_timestamp_seconds/surplus_control_sun_set_timestamp_seconds
(NaN on a polar day/night).
[0.4.1] - 2026-06-16
A correctness fix to the surplus calculation, plus its observability. No breaking changes.
Fixed
- The load-compensated surplus is now sun-aware: below
PV_SUN_MIN_ELEVATION_DEG
(default 3°) the compensation is disabled, so the heat pump is released after dark instead
of being held ON on grid power. Previously the fixed+ wp_nominalcompensation could keep
the SG-Ready relay ON past sunset (observed: relay still on at ~00:00 with the WP idle).
Added
PV_SUN_MIN_ELEVATION_DEG(default3.0) to tune the elevation gate.- Gauge
surplus_control_sun_elevation_deg; the UI "why" card shows "Sun below the horizon"
when idle after dark.
[0.4.0] - 2026-06-16
Generic, vendor-neutral heat-pump telemetry. Breaking: the ViCare-specific service, DB table
and metric names are replaced by a generic contract behind a pluggable driver.
Changed
vicare-exporter→heatpump-exporterwith aHEATPUMP_DRIVER(vicare|mock) behind
aHeatPumpDriverprotocol — the heat-pump analogue ofMETER_DRIVER. ViCare is now one
driver; amockdriver renders the heat-pump card in the zero-config demo.- DB table
heatpump_vicare→heatpump_telemetry(migration003, data preserved). - Prometheus telemetry metrics
vicare_*→heatpump_*(vendor-API ops stayvicare_*). - control-ui is vendor-neutral; the heat-pump card name comes from
HEATPUMP_LABEL.
Added
docs/heatpump-interface.md— the generic telemetry contract + "bring your own driver".
Upgrade
- Apply migration
003(the composedb-migrateone-shot does this automatically). - Switch the image
vicare-exporter:0.3.x→heatpump-exporter:0.4.0withHEATPUMP_DRIVER=vicare
(keep your existingVICARE_*secrets). - Update any custom Grafana panels / external scrapers from
vicare_*/heatpump_vicareto
heatpump_*/heatpump_telemetry. SetHEATPUMP_LABEL(e.g.Vitocal 250 A06).
v0.4.1
A correctness fix to the surplus calculation, plus its observability. No breaking changes.
Fixed
- The load-compensated surplus is now sun-aware: below
PV_SUN_MIN_ELEVATION_DEG
(default 3°) the compensation is disabled, so the heat pump is released after dark instead
of being held ON on grid power. Previously the fixed+ wp_nominalcompensation could keep
the SG-Ready relay ON past sunset (observed: relay still on at ~00:00 with the WP idle).
Added
PV_SUN_MIN_ELEVATION_DEG(default3.0) to tune the elevation gate.- Gauge
surplus_control_sun_elevation_deg; the UI "why" card shows "Sun below the horizon"
when idle after dark.
[0.4.0] - 2026-06-16
Generic, vendor-neutral heat-pump telemetry. Breaking: the ViCare-specific service, DB table
and metric names are replaced by a generic contract behind a pluggable driver.
Changed
vicare-exporter→heatpump-exporterwith aHEATPUMP_DRIVER(vicare|mock) behind
aHeatPumpDriverprotocol — the heat-pump analogue ofMETER_DRIVER. ViCare is now one
driver; amockdriver renders the heat-pump card in the zero-config demo.- DB table
heatpump_vicare→heatpump_telemetry(migration003, data preserved). - Prometheus telemetry metrics
vicare_*→heatpump_*(vendor-API ops stayvicare_*). - control-ui is vendor-neutral; the heat-pump card name comes from
HEATPUMP_LABEL.
Added
docs/heatpump-interface.md— the generic telemetry contract + "bring your own driver".
Upgrade
- Apply migration
003(the composedb-migrateone-shot does this automatically). - Switch the image
vicare-exporter:0.3.x→heatpump-exporter:0.4.0withHEATPUMP_DRIVER=vicare
(keep your existingVICARE_*secrets). - Update any custom Grafana panels / external scrapers from
vicare_*/heatpump_vicareto
heatpump_*/heatpump_telemetry. SetHEATPUMP_LABEL(e.g.Vitocal 250 A06).
v0.4.0
Generic, vendor-neutral heat-pump telemetry. Breaking: the ViCare-specific service, DB table
and metric names are replaced by a generic contract behind a pluggable driver.
Changed
vicare-exporter→heatpump-exporterwith aHEATPUMP_DRIVER(vicare|mock) behind
aHeatPumpDriverprotocol — the heat-pump analogue ofMETER_DRIVER. ViCare is now one
driver; amockdriver renders the heat-pump card in the zero-config demo.- DB table
heatpump_vicare→heatpump_telemetry(migration003, data preserved). - Prometheus telemetry metrics
vicare_*→heatpump_*(vendor-API ops stayvicare_*). - control-ui is vendor-neutral; the heat-pump card name comes from
HEATPUMP_LABEL.
Added
docs/heatpump-interface.md— the generic telemetry contract + "bring your own driver".
Upgrade
- Apply migration
003(the composedb-migrateone-shot does this automatically). - Switch the image
vicare-exporter:0.3.x→heatpump-exporter:0.4.0withHEATPUMP_DRIVER=vicare
(keep your existingVICARE_*secrets). - Update any custom Grafana panels / external scrapers from
vicare_*/heatpump_vicareto
heatpump_*/heatpump_telemetry. SetHEATPUMP_LABEL(e.g.Vitocal 250 A06).
v0.3.2
[0.3.2] - 2026-06-15
Maintenance release: routine dependency and CI-action updates (via Dependabot). No code or
behaviour changes; the /state contract stays schema: 1. Upgrade by pulling the new images.
Changed
- Python dependencies bumped across the services:
fastapi0.137.0 → 0.137.1,starlette1.0.1 → 1.3.1,uvicorn[standard]0.30.6 → 0.49.0,
python-multipart0.0.27 → 0.0.32 (control-ui).prometheus_client0.21.0 → 0.25.0 (all exporters + controller).psycopg2-binary2.9.9 → 2.9.12 (all services).pymodbus3.6.9 → 3.13.1 (energy-exporter).tzdata2025.1 → 2026.2 (surplus-controller).
- CI GitHub Actions bumped (SHA-pinned):
actions/checkoutv4 → v6.0.3,
actions/setup-pythonv5 → v6.2.0,docker/build-push-actionv6 → v7.2.0,
docker/setup-qemu-actionv3 → v4.1.0,docker/login-actionv3 → v4.2.0,
docker/metadata-actionv5 → v6.1.0.
v0.3.1
[0.3.1] - 2026-06-15
A robustness, data-integrity and hardening pass. No breaking changes to the /state
contract (schema stays 1); existing deployments upgrade by pulling the new images.
Added
- The controller's
/statusendpoint is now a versioned contract (schema: 1),
documented in docs/status-interface.md; the UI warns and
degrades on a version mismatch instead of silently mis-rendering. SMA_IFACE_IPto pin the Speedwire multicast join to a specific network interface on
multi-homed /hostNetworkhosts.- Container-image CVE scanning (Trivy, HIGH/CRITICAL) in CI, alongside the existing
pip-auditdependency audit. - Dependabot for weekly updates of the SHA-pinned GitHub Actions and the pinned Python
dependencies. - A scoped mypy type-check gate in CI over the typed cores (decision/threshold/config,
decoder, extract, rate budget, and the DB/relay/driver boundaries). - Database query-path indexes (migration
002-query-indexes.sql) so the hot read paths
stay off full hypertable scans under the 365-day retention. - A documented "Network trust boundaries" section in SECURITY.md.
Changed
- Compose services now declare memory/CPU resource limits (mirroring the Kubernetes
manifests) so a leak or runaway can't OOM the whole host and take down the control loop. SHM_HOSTmay now be a hostname — it is resolved to an IP at startup. Previously a
hostname silently dropped every telegram (the source filter compares against an IP).- Hardened the Compose
timescaledbanddb-migrateservices (no-new-privileges;
db-migratealso drops all Linux capabilities); the Kubernetesvicare-exporterpod now
runs with a read-only root filesystem.
Fixed
- The SMA Speedwire meter now recovers from a silently-dead meter / dropped multicast
(socket read timeout → re-join) instead of blocking forever "alive but blind". - The PV forecast retries quickly after a transient failure instead of leaving the
adaptive threshold stuck on its base value for up to the full 3-hour refresh interval. - The SMA telegram decoder is resilient to unknown / truncated records (length-driven walk,
no desync that silently corrupts later values). - Daily production (controller PR calibration and the UI's today balance) now survives
an inverter/meter counter reset — summed from positive deltas instead ofmax − min,
which would otherwise report the whole lifetime span as one day. - A NaN inverter lifetime-yield register no longer writes a spurious
0(a phantom counter
reset) into the time series — it storesNULL. - The controller coerces a non-numeric
/statefield to "blind" (fail-safe OFF) instead
of crashing the control cycle. - Non-finite (
NaN/±Inf) Prometheus values no longer crash UI partials. - The ViCare exporter exits visibly (CrashLoopBackOff) after repeated invalid-credential
rejections instead of silently burning the API rate budget, and fails fast with a clear
message when a required environment variable is missing. - The control loop keeps telemetry (metrics/status) failures from being mis-counted as
control-cycle errors, and snapshots the forecast once per cycle so the decision log can't
disagree with the decision it records. - The UI savings/balance card degrades instead of returning HTTP 500 when a price or
nominal-power config column is NULL. - The adaptive threshold can no longer divide by zero on a degenerate
full_sun_ref_kwh. - A failed inverter Modbus read is now logged with its cause (so a code/register bug is
distinguishable from a genuinely unreachable inverter) instead of being swallowed silently. - All services now log the cause when a database connection drops and is re-established,
instead of two of the three reconnecting silently (the resilience primitives were
converged and are guarded against drift by a cross-service consistency test).
Security
- The web UI's HTTP Basic-auth check is constant-time with respect to the username (no
early-out that would leak username validity through response timing). - Configuration writes use
psycopg2.sql.Identifier— injection-proof by construction, not
only by the column whitelist. - The cached ViCare OAuth token is now written owner-only (0600) (restrictive umask plus
an explicitchmod), so anything sharing the PVC/UID can't read the long-lived refresh grant. - New
STATUS_BINDto restrict the controller's/status+/healthzserver to a single
interface (mirrorsSTATE_BIND). - An optional NetworkPolicy (
deploy/k8s/networkpolicy.yaml) restricts ingress to the
namespace, and the TimescaleDB pod gainedallowPrivilegeEscalation: false+
seccompProfile: RuntimeDefault(the subset safe for the database).
v0.3.0
Sunsteer v0.3.0
A hardening and portability release — no breaking changes (the /state contract stays schema: 1). Compose users upgrade by pulling the new images; Kubernetes users get a ready-to-adapt
manifest base.
✨ Highlights
- Pluggable relay drivers (RELAY_DRIVER) — the relay is now abstracted like the meter, with a documented relay interface.
- Kubernetes manifests (deploy/k8s/) — non-root kustomize base with an automatic db-migrate Job and pinned images.
- CSRF protection on the UI, loopback-by-default binding for the UI and Grafana, and non-root containers.
- Supply chain: digest-pinned base images, SHA-pinned Actions, and SBOM + SLSA provenance on every released image.
- CI now gates on ruff, pip-audit, and a real-TimescaleDB integration smoke.
🔒 Security
- Dependency bumps clearing known CVEs (starlette → 1.0.x, plus fastapi/jinja2/python-multipart).
- CHANGE_ME placeholders for credentials keep the UI fail-closed and make services fail fast.
🐛 Fixed
- Forecast honors PV_TZ; exporter threads no longer die silently; the UI degrades gracefully on DB outages; relay "success" requires a non-error RPC body; the hardware auto-off watchdog
is enforced.
Full detail in the CHANGELOG.