-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Developer reference for the integration's code structure, API details and data flow.
custom_components/sunsynk/
├── __init__.py — Entry setup/teardown, dashboard registration
├── manifest.json — Integration metadata (version, dependencies)
├── config_flow.py — Config Flow UI (initial setup + options)
├── coordinator.py — SunsynkCoordinator + SolarForecastCoordinator
├── const.py — Constants, sensor entity descriptions
├── sensor.py — Sensor platform (static + dynamic + forecast sensors)
├── number.py — Number platform (writable numeric settings)
├── switch.py — Switch platform (writable boolean settings)
├── text.py — Text platform (writable time slot times)
├── helpers.py — build_device_info()
├── dashboard.py — build_dashboard() — generates Lovelace config dict
├── strings.json — UI strings (used by HA at dev time)
├── translations/
│ └── en.json — UI strings (English, used at runtime)
├── api/
│ ├── __init__.py
│ ├── auth.py — RSA login + OAuth2 token management
│ └── client.py — All API endpoint calls
└── www/
└── sunsynk-power-flow-card.js — Bundled frontend card
HA startup
└── async_setup_entry()
├── Create SunsynkAuth (credentials only, no network call yet)
├── Create SunsynkCoordinator
├── coordinator.async_config_entry_first_refresh()
│ └── _async_update_data()
│ ├── auth.async_get_token() — RSA login → OAuth2 token
│ └── client.async_fetch_all() — 8 parallel GET requests
├── (optional) Create SolarForecastCoordinator
│ └── _async_update_data()
│ └── GET api.open-meteo.com/v1/forecast
├── async_forward_entry_setups() — sensor / number / switch / text
│ └── each platform's async_setup_entry()
│ └── async_add_entities()
└── async_create_task(_async_setup_dashboard())
└── build_dashboard() + Lovelace storage write
Every {refresh_interval} seconds:
└── SunsynkCoordinator._async_update_data()
├── auth.async_get_token() — cached; re-fetched only on expiry
└── client.async_fetch_all() — 8 parallel API calls
→ coordinator.data updated
→ all CoordinatorEntity sensors/numbers/switches notified
→ _add_dynamic_sensors() callback fires
→ registers any newly-discovered MPPT/phase/battery sensors
Every 30 minutes:
└── SolarForecastCoordinator._async_update_data()
└── GET api.open-meteo.com/v1/forecast
→ _parse() calculates kWh totals and current-hour values
→ SolarForecastSensor entities updated
The Sunsynk API uses a two-step authentication flow:
-
RSA login:
- The client fetches the server's RSA public key from
/api/v1/auth/authenticate(GET) - The password is encrypted with this public key using RSA-OAEP/SHA-256
- The encrypted payload is POSTed back to the same endpoint
- Response contains an OAuth2
access_tokenandexpires_in
- The client fetches the server's RSA public key from
-
Token caching:
- The token and expiry time are stored in
SunsynkAuth._tokenand_token_expiry - On each coordinator refresh,
async_get_token()checks if the token has expired (with a 60-second buffer) and re-fetches only when needed - A fresh
aiohttp.ClientSessionis created once per coordinator instance and reused
- The token and expiry time are stored in
SunsynkAuth is shared between the main coordinator and the write operations in coordinator.async_write_setting().
SunsynkClient wraps all API calls. async_fetch_all() fires all 8 endpoint calls concurrently using asyncio.gather():
| Method | Endpoint | Key in coordinator.data[serial]
|
|---|---|---|
async_get_inverter |
GET /api/v1/inverter/{sn} |
inverter |
async_get_pv |
GET /api/v1/inverter/{sn}/realtime/input |
pv |
async_get_grid |
GET /api/v1/inverter/grid/{sn}/realtime |
grid |
async_get_battery |
GET /api/v1/inverter/battery/{sn}/realtime |
battery |
async_get_load |
GET /api/v1/inverter/load/{sn}/realtime |
load |
async_get_output |
GET /api/v1/inverter/{sn}/realtime/output |
output |
async_get_temp |
GET /api/v1/inverter/{sn}/output/day |
temp |
async_get_settings |
GET /api/v1/common/setting/{sn}/read |
settings |
Writing a setting: POST /api/v1/common/setting/{sn}/set with the full settings payload (Sunsynk requires the entire setting group to be written together, not individual keys).
Extends DataUpdateCoordinator[dict[str, dict[str, Any]]]. The data dict is:
{
"SN123456": {
"inverter": { ... }, # from async_get_inverter
"pv": { ... }, # from async_get_pv
"grid": { ... }, # from async_get_grid
"battery": { ... }, # from async_get_battery
"load": { ... }, # from async_get_load
"output": { ... }, # from async_get_output
"temp": { ... }, # from async_get_temp
"settings": { ... }, # from async_get_settings
},
"SN789012": { ... },
}If a single serial's fetch fails, the coordinator keeps the previous data for that serial and logs an error — other serials continue updating normally.
async_write_setting(serial, key, value):
- Reads current settings from
coordinator.data(falls back to a fresh API fetch if unavailable) - Determines which setting group the key belongs to (
BATTERY_SETTING_KEYSorSYSTEM_MODE_SETTING_KEYS) - Writes the full group payload to the API (Sunsynk requires the whole group)
- Calls
async_request_refresh()to update state
Extends DataUpdateCoordinator[dict[str, Any]]. Fetches a single Open-Meteo URL and returns:
{
"today_kwh": 12.4, # estimated kWh for today
"tomorrow_kwh": 8.7, # estimated kWh for tomorrow
"cloud_cover": 35.0, # % for current hour
"precipitation": 0.0, # mm for current hour
"ghi": 520.0, # W/m² for current hour
"dni": 680.0, # W/m² for current hour
}The kWh calculation iterates over all hourly time slots, filters by date (today / tomorrow), and sums GHI_wh_m2 / 1000 * panel_kwp * performance_ratio. The current-hour values are found by matching the local wall-clock hour to the ISO timestamp list from the API.
ALL_STATIC_SENSORS is a tuple of SunsynkSensorEntityDescription instances defined in const.py. Each description has:
-
key— unique identifier, used as part ofunique_id -
endpoint— which key incoordinator.data[serial]to read from -
data_key— dot-notation path within that endpoint dict (e.g."pvIV.0.ppv")
SunsynkSensor.native_value resolves the dot-notation path via _resolve_value().
_build_dynamic_descriptions() is called after each coordinator update via a listener callback. It inspects the live data to determine how many MPPT strings, phases and battery slots exist, then creates and registers new SunsynkSensor entities for any that haven't been seen before.
SolarForecastSensor is a separate CoordinatorEntity backed by SolarForecastCoordinator. The forecast_key field in ForecastSensorDescription maps directly to a key in the coordinator's data dict.
build_dashboard(prefix, eid, forecast_eid) returns a pure Python dict representing a complete Lovelace dashboard configuration. No YAML parsing — it produces the dict directly so it can be serialised by HA's Lovelace storage system.
-
prefix— slugified device alias, used as fallback when entity registry lookup fails -
eid(key)— looks up the actualentity_idfrom HA's entity registry byunique_idsuffix; returnsNonefor optional entities that aren't registered -
forecast_eid(key)— same lookup for forecast entities;Noneif forecast is not configured
The has_forecast flag controls whether forecast cards are included. This prevents dead/unavailable entity references in the dashboard when forecast is not set up.
__init__.py's _async_setup_dashboard() is called as a background task after platform setup completes, ensuring all entities are already registered in the entity registry before the dashboard is generated.
hass.data[DOMAIN] = {
"{entry_id}": SunsynkCoordinator,
"{entry_id}_forecast": SolarForecastCoordinator, # only if configured
}- Add a
SunsynkSensorEntityDescriptionentry to the appropriate tuple inconst.py(e.g.BATTERY_SENSORS) - Set
endpointto the API response key anddata_keyto the dot-notation path within that response - If the value comes from a nested list, use dot-notation like
"pvIV.0.ppv"—_resolve_value()handles list indexing automatically
No other changes are needed. The sensor is automatically picked up by ALL_STATIC_SENSORS and registered on next startup.
- Add a
NumberEntityDescriptionorSwitchEntityDescriptiontonumber.pyorswitch.py - Set the
keyto match the exact Sunsynk API setting field name (e.g."chargeCurrent") - If the setting belongs to a named group (battery or system mode), add its key to
BATTERY_SETTING_KEYSorSYSTEM_MODE_SETTING_KEYSinconst.py— this ensures the entire group is written together