Skip to content

Releases: slippyex/sunsteer

v0.5.3

17 Jun 08:31

Choose a tag to compare

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_nominal load-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_w in /state only 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_watts and surplus_control_available_basis
    (1 = production-based, 0 = nominal fallback).

v0.5.2

17 Jun 06:09

Choose a tag to compare

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_nominal load-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_w in /state only 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_watts and surplus_control_available_basis
    (1 = production-based, 0 = nominal fallback).

v0.5.1

17 Jun 06:09

Choose a tag to compare

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_nominal load-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_w in /state only 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_watts and surplus_control_available_basis
    (1 = production-based, 0 = nominal fallback).

v0.5.0

16 Jun 13:34

Choose a tag to compare

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_nominal load-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_w in /state only 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_watts and surplus_control_available_basis
    (1 = production-based, 0 = nominal fallback).

v0.4.2

16 Jun 12:14

Choose a tag to compare

Added

  • The web UI now shows the sun: the live solar elevation and today's PV window
    (sunrise/sunset for PV_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_nominal compensation 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 (default 3.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-exporterheatpump-exporter with a HEATPUMP_DRIVER (vicare | mock) behind
    a HeatPumpDriver protocol — the heat-pump analogue of METER_DRIVER. ViCare is now one
    driver; a mock driver renders the heat-pump card in the zero-config demo.
  • DB table heatpump_vicareheatpump_telemetry (migration 003, data preserved).
  • Prometheus telemetry metrics vicare_*heatpump_* (vendor-API ops stay vicare_*).
  • 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 compose db-migrate one-shot does this automatically).
  • Switch the image vicare-exporter:0.3.xheatpump-exporter:0.4.0 with HEATPUMP_DRIVER=vicare
    (keep your existing VICARE_* secrets).
  • Update any custom Grafana panels / external scrapers from vicare_*/heatpump_vicare to
    heatpump_*/heatpump_telemetry. Set HEATPUMP_LABEL (e.g. Vitocal 250 A06).

v0.4.1

16 Jun 11:06

Choose a tag to compare

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_nominal compensation 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 (default 3.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-exporterheatpump-exporter with a HEATPUMP_DRIVER (vicare | mock) behind
    a HeatPumpDriver protocol — the heat-pump analogue of METER_DRIVER. ViCare is now one
    driver; a mock driver renders the heat-pump card in the zero-config demo.
  • DB table heatpump_vicareheatpump_telemetry (migration 003, data preserved).
  • Prometheus telemetry metrics vicare_*heatpump_* (vendor-API ops stay vicare_*).
  • 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 compose db-migrate one-shot does this automatically).
  • Switch the image vicare-exporter:0.3.xheatpump-exporter:0.4.0 with HEATPUMP_DRIVER=vicare
    (keep your existing VICARE_* secrets).
  • Update any custom Grafana panels / external scrapers from vicare_*/heatpump_vicare to
    heatpump_*/heatpump_telemetry. Set HEATPUMP_LABEL (e.g. Vitocal 250 A06).

v0.4.0

16 Jun 06:57

Choose a tag to compare

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-exporterheatpump-exporter with a HEATPUMP_DRIVER (vicare | mock) behind
    a HeatPumpDriver protocol — the heat-pump analogue of METER_DRIVER. ViCare is now one
    driver; a mock driver renders the heat-pump card in the zero-config demo.
  • DB table heatpump_vicareheatpump_telemetry (migration 003, data preserved).
  • Prometheus telemetry metrics vicare_*heatpump_* (vendor-API ops stay vicare_*).
  • 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 compose db-migrate one-shot does this automatically).
  • Switch the image vicare-exporter:0.3.xheatpump-exporter:0.4.0 with HEATPUMP_DRIVER=vicare
    (keep your existing VICARE_* secrets).
  • Update any custom Grafana panels / external scrapers from vicare_*/heatpump_vicare to
    heatpump_*/heatpump_telemetry. Set HEATPUMP_LABEL (e.g. Vitocal 250 A06).

v0.3.2

15 Jun 16:54

Choose a tag to compare

[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:
    • fastapi 0.137.0 → 0.137.1, starlette 1.0.1 → 1.3.1, uvicorn[standard] 0.30.6 → 0.49.0,
      python-multipart 0.0.27 → 0.0.32 (control-ui).
    • prometheus_client 0.21.0 → 0.25.0 (all exporters + controller).
    • psycopg2-binary 2.9.9 → 2.9.12 (all services).
    • pymodbus 3.6.9 → 3.13.1 (energy-exporter).
    • tzdata 2025.1 → 2026.2 (surplus-controller).
  • CI GitHub Actions bumped (SHA-pinned): actions/checkout v4 → v6.0.3,
    actions/setup-python v5 → v6.2.0, docker/build-push-action v6 → v7.2.0,
    docker/setup-qemu-action v3 → v4.1.0, docker/login-action v3 → v4.2.0,
    docker/metadata-action v5 → v6.1.0.

v0.3.1

15 Jun 16:14

Choose a tag to compare

[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 /status endpoint 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_IP to pin the Speedwire multicast join to a specific network interface on
    multi-homed / hostNetwork hosts.
  • Container-image CVE scanning (Trivy, HIGH/CRITICAL) in CI, alongside the existing
    pip-audit dependency 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_HOST may 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 timescaledb and db-migrate services (no-new-privileges;
    db-migrate also drops all Linux capabilities); the Kubernetes vicare-exporter pod 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 of max − 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 stores NULL.
  • The controller coerces a non-numeric /state field 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 explicit chmod), so anything sharing the PVC/UID can't read the long-lived refresh grant.
  • New STATUS_BIND to restrict the controller's /status + /healthz server to a single
    interface (mirrors STATE_BIND).
  • An optional NetworkPolicy (deploy/k8s/networkpolicy.yaml) restricts ingress to the
    namespace, and the TimescaleDB pod gained allowPrivilegeEscalation: false +
    seccompProfile: RuntimeDefault (the subset safe for the database).

v0.3.0

15 Jun 08:59

Choose a tag to compare

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.