SigenStor Battery Control & Monitoring Panel — System Design

Project Summary

A self-hosted web application for monitoring and controlling a Sigenergy SigenStor home battery system with solar PV. Single user (Mick), hosted on the Kubernetes cluster.


1. System Architecture

Overview

Four loosely-coupled services communicating over an internal API, deployed as pods in the hermes namespace:

┌──────────────┐     ┌─────────────────┐     ┌──────────────┐
│   Telemetry  │────▶│    Control      │────▶│  Optimiser   │
│   Poller     │◀────│    Service      │◀────│              │
└──────┬───────┘     └────────┬────────┘     └──────────────┘
       │                      │                       ▲
       ▼                      ▼                       │
┌──────────────┐     ┌─────────────────┐              │
│   Sigenergy  │     │    Web UI       │◀─────────────┤
│  (Modbus)    │     │   (React/Vite)  │     WebSocket
└──────────────┘     └─────────────────┘     / REST API

Services

ServiceLanguageRolePort
Telemetry PollerPython (FastAPI)Polls battery at configurable interval; writes to TSDB and Redis cache. Exposes /telemetry/live endpoint.8100
Control ServicePython (FastAPI)Receives charge/discharge commands, applies safety checks, translates to Modbus writes. Rate-limited. Single-writer pattern.8101
OptimiserPython (FastAPI + Celery)Runs on configurable schedule (default: every 15 min). Pulls price forecast + solar forecast + battery state → decides action. Calls Control Service API.8102
Web UITypeScript/React (Vite)Frontend SPA. Reads live telemetry, displays charts, allows manual schedules/overrides. WebSocket for real-time updates.3000 (dev), served at / in prod

Rationale: Python FastAPI everywhere

  • Consistent language → easier to maintain and share data models
  • Async-ready out of the box for I/O-heavy telemetry polling
  • Pydantic schemas for validation
  • Single container image strategy possible (slim base + shared dependencies)
  • Proven in our environment (ASX trading platform, smart-groceries, etc.)

Rationale: React + Vite frontend

  • Build tooling is simple and fast
  • WebSocket integration with useEffect hooks for live dashboard
  • Component libraries (shadcn/ui or Radix) for clean UI without bloat
  • No build step complexity like Next.js; this is a single-user app, not SEO-driven

2. SigenStor Integration: Local Modbus TCP

Why Modbus TCP over Cloud API?

CriterionModbus TCP (local)mySigen Cloud API
Latency~10ms (LAN)~500-2000ms (WAN + cloud)
ReliabilityDepends on battery LAN onlyDepends on internet + Sigenergy servers
Control latencyImmediate writeDelayed, rate-limited by cloud
Data privacyLocal onlyCloud stores all telemetry
CostFreePotentially API limits
Firmware dependencyLow (Modbus is standard)Medium (cloud protocol changes)

Decision: Modbus TCP as primary path. Fallback to cloud API if Modbus fails 3 consecutive times.

Modbus Integration Source

Based on the Home Assistant Sigenergy integration (custom_components/sigenergy), which is the community reference implementation for SigenStor Modbus access. The HA integration reveals that SigenStor devices expose a standard Modbus TCP server (port 502) with a custom register map.

Telemetry Registers (READ)

Register AddressData TypeDescriptionUnit
0x0000–0x00FFVariesSystem status & health
SOCU16Battery State of Charge (scaled ×10)%/10
VoltageU32Battery pack voltage (scaled ×10)V/10
TemperatureI16Internal temperature (scaled ×10)°C/10
CycleCountU32Total charge cycles completed
HealthPercentU16Battery health indicator%

Charge/Discharge Control Registers (WRITE)

Register AddressData TypeDescriptionValues
ModeControlU16Operating mode0=auto, 1=manual_charge, 2=manual_discharge, 3=force_hold
TargetPowerI32Target power setpoint (signed)Watts (- = discharge, + = charge)
ChargeWindowStartU16Scheduled charge start (HHMM 24h)HHMM
ChargeWindowEndU16Scheduled charge end (HHMM 24h)HHMM
DischargeEnableU16Enable/disable discharge during window0=off, 1=on

Schedule Control Registers

Register AddressData TypeDescription
DailySchedule[8] × 2U16 × NUp to 8 daily time slots (start HHMM)
DailySchedule[8] × 2U16 × NMatching end times
ScheduleAction[N]U16 × NAction per slot: charge/discharge/hold

Safety Guardrails for Control Commands

Before any Modbus WRITE to the battery, the Control Service must validate:

  1. SoC bounds: Don’t discharge below 5% SoC (configurable), don’t overcharge above 98%.
  2. Temperature guard: No charge/discharge if battery temp > 45°C or < 0°C.
  3. Rate of change limit: Maximum power step is ±1kW per cycle to avoid thermal shock.
  4. Cooldown timer: Minimum 60s between successive write commands (prevent rapid toggling).
  5. Emergency stop: A persistent “emergency hold” flag that blocks all automatic writes and must be cleared by UI action.
  6. Acknowledge window: Write confirmation — read back the register after 2 seconds, verify the value changed as expected. If not, revert + alert.
  7. Audit log: Every write is logged with timestamp, previous value, new value, and reason (manual/automatic/override).

3. Amber Electric API Integration

Endpoint: https://api.amber.com.au

  • Authentication: Bearer token (Mick’s Amber account token, stored in Kubernetes Secret)
  • Rate limit: ~100 req/min (generous for our needs)
  • Data model: 30-minute intervals, both buy and sell prices

Key Endpoints

EndpointPurpose
GET /v4/market_pricesCurrent + forecasted buy prices (30-min intervals, ~5 days ahead)
GET /v4/market_feed_inFeed-in/sell prices for same periods
GET /v4/usageHistorical consumption data (for usage pattern learning)

Data Points Needed

  1. Current buy price (now and next 30-min window) — drives immediate decisions
  2. Forecasted buy prices (next 24 hours, 30-min resolution) — enables lookahead scheduling
  3. Feed-in rate — determines if exporting to grid is profitable
  4. Historical prices (past 7 days) — for pattern recognition in optimiser

4. Weather / Solar Forecast Source

Choice: Open-Meteo API

CriterionOpen-MeteoSolcastWhy Open-Meteo wins
CostFree, unlimitedFreemium, limitedNo cost, no rate limits
Forecast horizon16 days (hourly)7 days (30-min)More than enough
ResolutionHourly30-minSufficient for battery scheduling
SetupAPI key onlyAccount + panel configSimpler
Data qualityGood for cloud cover / irradianceExcellent for rooftop PVOverkill for our use case

Open-Meteo provides:

  • Cloud cover (0-100%) — main driver for solar generation estimate
  • Direct and diffuse radiation (W/m²) — more accurate than cloud cover alone
  • Temperature, wind speed — secondary factors

Solar Generation Estimation Model

Simple model based on:

estimated_generation = rated_panel_watts × irradiance_ratio × inverter_efficiency × degradation_factor

where:
  irradiance_ratio = (direct_radiation + diffuse_radiation) / standard_test_condition_irradiance
  standard_test_condition_irradiance = 1000 W/m²
  inverter_efficiency ≈ 0.96
  degradation_factor ≈ 0.98/year from install date

Configurable per-user:

  • Total panel wattage (kW)
  • Panel orientation (azimuth, tilt)
  • Installation year (for degradation curve)

5. Data Model & Time-Series Storage

PostgreSQL + TimescaleDB

WhyReasoning
FamiliarWe already use it for ASX trading platform, smart-groceries
TimescaleDBBuilt-in hypertables for time-series data, automatic compression/retention
Single storeTelemetry, schedules, audit logs all in one database — no coordination overhead
AnalyticsCan run direct SQL queries for usage patterns (e.g., average consumption by hour)

Tables

-- Telemetry readings (hypertable, compressed after 7 days)
CREATE TABLE telemetry_readings (
    time TIMESTAMPTZ NOT NULL,
    soc_percent DECIMAL(5,2),
    voltage_v DECIMAL(6,2),
    temperature_c DECIMAL(4,1),
    charge_power_w INTEGER,      -- positive = charging, negative = discharging
    solar_generation_w INTEGER,
    house_consumption_w INTEGER,
    grid_import_w INTEGER,       -- positive = importing from grid
    grid_export_w INTEGER        -- positive = exporting to grid
);
 
SELECT create_hypertable('telemetry_readings', 'time');
 
-- Charge/discharge schedules
CREATE TABLE schedules (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    enabled BOOLEAN DEFAULT true,
    action TEXT CHECK (action IN ('charge', 'discharge', 'hold')),
    start_time TIME NOT NULL,     -- HH:MM:SS
    end_time TIME NOT NULL,
    days_of_week INTEGER[] DEFAULT ARRAY[1,2,3,4,5,6,7],  -- 1=Mon...
    priority INTEGER DEFAULT 0,  -- higher = overrides lower-priority overlapping schedules
    created TIMESTAMPTZ DEFAULT NOW()
);
 
-- Manual override history (audit)
CREATE TABLE control_audit (
    time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    action TEXT NOT NULL,          -- 'write_soc', 'set_mode', etc.
    reason TEXT NOT NULL,          -- 'manual', 'auto_price', 'auto_solar', 'override'
    previous_value JSONB,
    new_value JSONB,
    success BOOLEAN
);
 
-- Amber price data (cached)
CREATE TABLE amber_prices (
    time TIMESTAMPTZ NOT NULL,
    buy_price_cents DECIMAL(8,2), -- cents per kWh
    feedin_price_cents DECIMAL(8,2),
    forecast BOOLEAN DEFAULT false  -- true = forecast, false = actual
);
 
-- Solar forecast (cached from Open-Meteo)
CREATE TABLE solar_forecast (
    time TIMESTAMPTZ NOT NULL,
    irradiance_wm2 DECIMAL(6,1),   -- direct + diffuse radiation
    cloud_cover_pct INTEGER
);

Retention Policy

  • Raw telemetry: compressed after 7 days, dropped after 90 days (configurable)
  • Control audit log: kept indefinitely — important for debugging and safety
  • Amber prices: dropped after 14 days
  • Solar forecast: dropped after 24 hours

Redis Cache

Used for:

  • Latest telemetry reading (sub-second read from UI WebSocket)
  • Current Amber price (avoid hitting API on every page load)
  • Control cooldown timer (distributed lock — no concurrent writes)

6. The Optimiser: Decision Logic

Architecture

                    ┌──────────────────┐
                    │  Configuration   │
                    │ (user-set prefs) │
                    └────────┬─────────┘
                             │
             ┌───────────────┼───────────────┐
             ▼               ▼               ▼
     ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
     │ Price Engine │ │ Solar Engine │ │ Usage Engine │
     └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
            ▼                ▼                ▼
     ┌────────────────────────────────────────────┐
     │          Decision Aggregator               │
     │  (weighted scoring → pick best action)     │
     └──────────────────┬────────────────────────┘
                        ▼
              ┌─────────────────┐
              │ Control Service │
              │   (safe write)  │
              └─────────────────┘

Input Data for Each Decision Cycle

  1. Current battery state: SoC %, temperature, current power flow (charging/discharging/idle), health %
  2. Next 6 hours of Amber buy price forecast (30-min resolution)
  3. Next 6 hours of solar generation forecast
  4. Household consumption pattern for this time of day (learned from telemetry history, or default curve based on typical QLD household)
  5. Active schedules: Any manual charge/discharge windows currently active?

Price Engine Logic

Scoring function: price_score = normalized_cost × urgency

  • If buy price < $0.10/kWh (or negative): strong charge signal — battery should absorb cheap grid power
  • If buy price > $0.40/kWh and SoC > 50%: strong discharge/export signal — sell back at peak rate
  • If feed-in rate > buy price next hour: charge now, export later
  • Neutral zone (0.40): hold or maintain current state

Solar Engine Logic

  • If solar generation forecast > household consumption for the next N hours AND battery SoC < 90%: charge from excess solar (avoid grid export at low feed-in rates)
  • If clear sky → sudden cloud cover in forecast within 2 hours: discharge to cover the gap (use stored energy instead of expensive grid power)
  • Low solar day detected (high cloud cover all day): consider charging during cheapest price window

Usage Engine Logic

  • Learn typical consumption pattern by hour-of-day and day-of-week
  • If forecast shows a high-consumption peak ahead AND battery SoC > 30% AND grid price is rising: pre-discharge to ride the peak
  • If low-consumption period (overnight): hold or charge from cheap overnight rates

Decision Aggregation

Each engine produces a recommendation with a confidence score:

{
    action: "charge" | "discharge" | "hold",
    target_power_watt: int,     # suggested power level
    reason: string,
    confidence: float  # 0.0 to 1.0
}

Aggregator picks the highest-confidence action, unless a manual override or high-priority schedule conflicts. If conflict, user-defined priority rules resolve it (manual > auto, with explicit override capability).

Safety Integration

Optimiser never writes directly to the battery. It calls the Control Service API, which applies all safety checks before writing. The optimiser can be killed without leaving the battery in an unsafe state — the Control Service handles graceful degradation.


7. Manual Override / Boost System

Three quick-toggle buttons on the UI:

ButtonActionDuration
Force ChargeImmediately charge at max rate (configurable, default 1kW) until SoC ≥ targetUntil stopped manually or SoC reached
Force HoldBlock all automatic charging/dischargingUntil stopped manually (persistent lock)
Force ExportDischarge to grid at configured rate (e.g., during high feed-in period)Until stopped manually or SoC drops below floor

Override actions bypass the optimiser but still go through the Control Service safety checks. The UI shows “OVERRIDE ACTIVE” prominently, and any pending automatic schedules are paused.


8. Web UI Design (High-Level)

Pages / Sections

  1. Dashboard — real-time overview:

    • Battery SoC % with circular gauge + temperature
    • Current power flow arrows (solar → battery, grid ↔ house)
    • Today’s energy balance chart (charge vs discharge kWh)
    • Current Amber price + next 6 hours price trend
    • Quick-toggle buttons for overrides
  2. Schedules — manage time-based charge/discharge windows:

    • Weekly calendar view with colored blocks for each schedule
    • Edit/create form: action, start/end time, days of week, priority
    • Toggle enabled/disabled per schedule
  3. History — charts and data:

    • Daily/weekly/monthly energy consumption chart
    • Price history overlay on consumption
    • Solar generation vs forecast comparison (learn accuracy)
    • Export to CSV
  4. Settings — system configuration:

    • SigenStor Modbus connection details (IP, port, unit ID)
    • Amber API token
    • Open-Meteo API key + panel wattage config
    • Safety thresholds (min SoC, temp limits, etc.)
    • Optimiser settings (engine weights, decision interval)

Real-Time Updates

WebSocket connection to the Control Service:

  • Telemetry push every 10 seconds (configurable)
  • Immediate notification on price changes
  • Visual feedback when control commands execute
  • Override status broadcast to all open tabs

9. Phased Implementation Plan

Phase 1: Foundation & Telemetry (Weeks 1-2)

  • Create Kubernetes manifests for Postgres + TimescaleDB, Redis
  • Implement Telemetry Poller service:
    • Modbus TCP client to read battery registers
    • Write readings to TimescaleDB hypertable
    • Expose /telemetry/live REST endpoint
    • Basic health check (/healthz)
  • Deploy to cluster, verify data flow
  • Milestone: Live battery SoC visible in browser via curl

Phase 2: Control Service & Safety (Week 3)

  • Implement Control Service:
    • Modbus write capability with safety validation
    • Rate limiting + cooldown logic
    • Audit log to PostgreSQL
    • Emergency stop flag (persistent, cleared by UI action)
  • Integration tests: verify reads-after-write roundtrip
  • Deploy alongside Telemetry Poller
  • Milestone: Can send a charge/discharge command via API, see it execute

Phase 3: Web UI v1 (Weeks 4-5)

  • Set up React + Vite project with FastAPI backend serving static files
  • Build Dashboard page with live telemetry display
  • Implement WebSocket connection for real-time updates
  • Add manual override buttons (Force Charge/Hold/Export)
  • Basic styling (dark mode default — it’s an energy dashboard)
  • Milestone: Mick can see his battery status and manually control charging

Phase 4: Amber Integration & Optimiser v1 (Weeks 6-7)

  • Implement Amber price fetcher + caching layer
  • Add price display to UI (current + forecast)
  • Build Optimiser service:
    • Price Engine: score cheap vs expensive periods
    • Basic decision logic → charge on cheap, hold/expensive on dear
  • Manual time-of-day schedule system in UI
  • Milestone: Automatic charging during cheapest price window

Phase 5: Solar Forecast & Advanced Optimiser (Weeks 8-9)

  • Open-Meteo integration for solar generation forecast
  • Implement Solar Engine and Usage Engine logic
  • Decision Aggregator with weighted scoring
  • Add “optimiser mode” toggle: Auto vs Manual
  • Milestone: Optimiser uses price + weather to make intelligent decisions

Phase 6: Polish & Reliability (Week 10)

  • Error handling and retry logic for all integrations
  • Alerting: Slack notification on connection failures, safety violations
  • UI polish: charts with chart.js or recharts, responsive layout
  • Settings page for system configuration
  • History page with energy consumption analytics
  • Milestone: Production-ready system with monitoring

10. Tech Stack Summary

ComponentTechnologyWhy
Backend servicesPython 3.12 + FastAPI + PydanticConsistent, async-ready, well-typed
Time-series storagePostgreSQL + TimescaleDB extensionProven, analytics-friendly
Cache / pub-subRedisLow-latency reads, distributed lock
Modbus TCP clientpymodbus Python libraryStandard, maintained
HTTP clienthttpx (async)Consistent with FastAPI ecosystem
Frontend frameworkReact 18 + ViteSimple build, great devX
WebSocket libraryfastapi-websocketNative FastAPI integration
ChartsRecharts or Chart.jsMature, well-documented
UI componentsshadcn/ui (React)Clean, accessible, copy-paste components
Task schedulerAPScheduler (Python)Lightweight, no separate worker needed for optimiser
DeploymentKubernetes Deployments + Services + IngressStandard hermes cluster pattern
ConfigurationEnvironment variables via Kubernetes Secrets/ConfigMapsStandard, secure

11. Deployment Model

Kubernetes Resources

hermes namespace:
├── sigenstor-telemetry-poller    (Deployment)
├── sigenstor-control-service     (Deployment)
├── sigenstor-optimiser           (Deployment)
├── sigenstor-web                  (Deployment — serves frontend + API gateway)
├── postgres-sigenstor             (StatefulSet or managed external)
└── redis-sigenstor                (Deployment, small instance)

Ingress: sigenstor.paralla.org → routes to web service port 80

Secrets

SecretContents
sigenstor-modbusBattery IP address, unit ID, Modbus register offsets
sigenstor-amberAmber Electric API token
sigenstor-openmeteoOpen-Meteo API key
sigenstor-dbPostgres connection string

12. Risk Register

RiskImpactMitigation
Modbus register map changes with battery firmware updateLoss of telemetry/controlMonitor HA integration for updates; abstract register map behind config layer
Amber API rate limit exceeded during high-frequency pollingPrice data staleCache aggressively, reduce poll frequency
Battery in unsafe state after network partitionSafety-criticalEmergency stop flag persists across restarts; always verify before write
Optimiser makes poor decision (e.g., charges during peak)Extra costManual override always available; optimiser has “learning” mode (read-only first month)
Modbus connection intermittent to batteryLost telemetry/control dataRetry with exponential backoff; log failures; fallback to last-known-good state