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
| Service | Language | Role | Port |
|---|---|---|---|
| Telemetry Poller | Python (FastAPI) | Polls battery at configurable interval; writes to TSDB and Redis cache. Exposes /telemetry/live endpoint. | 8100 |
| Control Service | Python (FastAPI) | Receives charge/discharge commands, applies safety checks, translates to Modbus writes. Rate-limited. Single-writer pattern. | 8101 |
| Optimiser | Python (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 UI | TypeScript/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
useEffecthooks 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?
| Criterion | Modbus TCP (local) | mySigen Cloud API |
|---|---|---|
| Latency | ~10ms (LAN) | ~500-2000ms (WAN + cloud) |
| Reliability | Depends on battery LAN only | Depends on internet + Sigenergy servers |
| Control latency | Immediate write | Delayed, rate-limited by cloud |
| Data privacy | Local only | Cloud stores all telemetry |
| Cost | Free | Potentially API limits |
| Firmware dependency | Low (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 Address | Data Type | Description | Unit |
|---|---|---|---|
| 0x0000–0x00FF | Varies | System status & health | — |
| SOC | U16 | Battery State of Charge (scaled ×10) | %/10 |
| Voltage | U32 | Battery pack voltage (scaled ×10) | V/10 |
| Temperature | I16 | Internal temperature (scaled ×10) | °C/10 |
| CycleCount | U32 | Total charge cycles completed | — |
| HealthPercent | U16 | Battery health indicator | % |
Charge/Discharge Control Registers (WRITE)
| Register Address | Data Type | Description | Values |
|---|---|---|---|
| ModeControl | U16 | Operating mode | 0=auto, 1=manual_charge, 2=manual_discharge, 3=force_hold |
| TargetPower | I32 | Target power setpoint (signed) | Watts (- = discharge, + = charge) |
| ChargeWindowStart | U16 | Scheduled charge start (HHMM 24h) | HHMM |
| ChargeWindowEnd | U16 | Scheduled charge end (HHMM 24h) | HHMM |
| DischargeEnable | U16 | Enable/disable discharge during window | 0=off, 1=on |
Schedule Control Registers
| Register Address | Data Type | Description |
|---|---|---|
| DailySchedule[8] × 2 | U16 × N | Up to 8 daily time slots (start HHMM) |
| DailySchedule[8] × 2 | U16 × N | Matching end times |
| ScheduleAction[N] | U16 × N | Action per slot: charge/discharge/hold |
Safety Guardrails for Control Commands
Before any Modbus WRITE to the battery, the Control Service must validate:
- SoC bounds: Don’t discharge below 5% SoC (configurable), don’t overcharge above 98%.
- Temperature guard: No charge/discharge if battery temp > 45°C or < 0°C.
- Rate of change limit: Maximum power step is ±1kW per cycle to avoid thermal shock.
- Cooldown timer: Minimum 60s between successive write commands (prevent rapid toggling).
- Emergency stop: A persistent “emergency hold” flag that blocks all automatic writes and must be cleared by UI action.
- Acknowledge window: Write confirmation — read back the register after 2 seconds, verify the value changed as expected. If not, revert + alert.
- 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
| Endpoint | Purpose |
|---|---|
GET /v4/market_prices | Current + forecasted buy prices (30-min intervals, ~5 days ahead) |
GET /v4/market_feed_in | Feed-in/sell prices for same periods |
GET /v4/usage | Historical consumption data (for usage pattern learning) |
Data Points Needed
- Current buy price (now and next 30-min window) — drives immediate decisions
- Forecasted buy prices (next 24 hours, 30-min resolution) — enables lookahead scheduling
- Feed-in rate — determines if exporting to grid is profitable
- Historical prices (past 7 days) — for pattern recognition in optimiser
4. Weather / Solar Forecast Source
Choice: Open-Meteo API
| Criterion | Open-Meteo | Solcast | Why Open-Meteo wins |
|---|---|---|---|
| Cost | Free, unlimited | Freemium, limited | No cost, no rate limits |
| Forecast horizon | 16 days (hourly) | 7 days (30-min) | More than enough |
| Resolution | Hourly | 30-min | Sufficient for battery scheduling |
| Setup | API key only | Account + panel config | Simpler |
| Data quality | Good for cloud cover / irradiance | Excellent for rooftop PV | Overkill 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
| Why | Reasoning |
|---|---|
| Familiar | We already use it for ASX trading platform, smart-groceries |
| TimescaleDB | Built-in hypertables for time-series data, automatic compression/retention |
| Single store | Telemetry, schedules, audit logs all in one database — no coordination overhead |
| Analytics | Can 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
- Current battery state: SoC %, temperature, current power flow (charging/discharging/idle), health %
- Next 6 hours of Amber buy price forecast (30-min resolution)
- Next 6 hours of solar generation forecast
- Household consumption pattern for this time of day (learned from telemetry history, or default curve based on typical QLD household)
- 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:
| Button | Action | Duration |
|---|---|---|
| Force Charge | Immediately charge at max rate (configurable, default 1kW) until SoC ≥ target | Until stopped manually or SoC reached |
| Force Hold | Block all automatic charging/discharging | Until stopped manually (persistent lock) |
| Force Export | Discharge 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
-
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
-
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
-
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
-
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/liveREST 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
| Component | Technology | Why |
|---|---|---|
| Backend services | Python 3.12 + FastAPI + Pydantic | Consistent, async-ready, well-typed |
| Time-series storage | PostgreSQL + TimescaleDB extension | Proven, analytics-friendly |
| Cache / pub-sub | Redis | Low-latency reads, distributed lock |
| Modbus TCP client | pymodbus Python library | Standard, maintained |
| HTTP client | httpx (async) | Consistent with FastAPI ecosystem |
| Frontend framework | React 18 + Vite | Simple build, great devX |
| WebSocket library | fastapi-websocket | Native FastAPI integration |
| Charts | Recharts or Chart.js | Mature, well-documented |
| UI components | shadcn/ui (React) | Clean, accessible, copy-paste components |
| Task scheduler | APScheduler (Python) | Lightweight, no separate worker needed for optimiser |
| Deployment | Kubernetes Deployments + Services + Ingress | Standard hermes cluster pattern |
| Configuration | Environment variables via Kubernetes Secrets/ConfigMaps | Standard, 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
| Secret | Contents |
|---|---|
sigenstor-modbus | Battery IP address, unit ID, Modbus register offsets |
sigenstor-amber | Amber Electric API token |
sigenstor-openmeteo | Open-Meteo API key |
sigenstor-db | Postgres connection string |
12. Risk Register
| Risk | Impact | Mitigation |
|---|---|---|
| Modbus register map changes with battery firmware update | Loss of telemetry/control | Monitor HA integration for updates; abstract register map behind config layer |
| Amber API rate limit exceeded during high-frequency polling | Price data stale | Cache aggressively, reduce poll frequency |
| Battery in unsafe state after network partition | Safety-critical | Emergency stop flag persists across restarts; always verify before write |
| Optimiser makes poor decision (e.g., charges during peak) | Extra cost | Manual override always available; optimiser has “learning” mode (read-only first month) |
| Modbus connection intermittent to battery | Lost telemetry/control data | Retry with exponential backoff; log failures; fallback to last-known-good state |