|
| 1 | +# Architecture |
| 2 | + |
| 3 | +**Analysis Date:** 2026-03-27 |
| 4 | + |
| 5 | +## Pattern Overview |
| 6 | + |
| 7 | +**Overall:** Home Assistant Integration using Coordinator Pattern with Device Abstraction Layer |
| 8 | + |
| 9 | +**Key Characteristics:** |
| 10 | +- Home Assistant ConfigEntry-based integration with async coordinator |
| 11 | +- Device polymorphism (Legacy MQTT vs. ZenSDK BLE/Cloud protocols) |
| 12 | +- Entity-driven model where devices create platform-specific entities (sensors, numbers, switches, etc.) |
| 13 | +- Manager orchestration of power distribution and charging strategy across multiple devices |
| 14 | +- Multi-protocol connectivity: Cloud MQTT, Local MQTT (legacy), BLE (ZenSDK) |
| 15 | + |
| 16 | +## Layers |
| 17 | + |
| 18 | +**Coordinator Layer (ZendureManager):** |
| 19 | +- Purpose: Periodic update coordinator for Home Assistant integration, manages global power distribution logic |
| 20 | +- Location: `custom_components/zendure_ha/manager.py` |
| 21 | +- Contains: `ZendureManager` class extending `DataUpdateCoordinator[None]` |
| 22 | +- Depends on: Api, ZendureDevice, FuseGroup, EntityDevice |
| 23 | +- Used by: Home Assistant core, platform modules (sensor, number, select, switch, binary_sensor, button) |
| 24 | +- Responsibilities: Device lifecycle, power balancing, p1meter integration, operation mode management (OFF, MANUAL, MATCHING, etc.) |
| 25 | + |
| 26 | +**Device Layer (ZendureDevice):** |
| 27 | +- Purpose: Represents physical Zendure hardware devices with protocol-specific implementations |
| 28 | +- Location: `custom_components/zendure_ha/device.py` (base), `custom_components/zendure_ha/devices/*.py` (device-specific) |
| 29 | +- Contains: `ZendureDevice` (base), `ZendureLegacy` (MQTT protocol), `ZendureZenSdk` (BLE/Cloud protocol), `ZendureBattery` |
| 30 | +- Depends on: Entity layer, sensor/number/select/switch/binary_sensor modules |
| 31 | +- Used by: ZendureManager, Api |
| 32 | +- Responsibilities: MQTT/BLE publish-subscribe, power command execution (charge/discharge), property updates, battery management |
| 33 | + |
| 34 | +**Battery Abstraction (ZendureBattery):** |
| 35 | +- Purpose: Represents attachable battery modules within devices |
| 36 | +- Location: `custom_components/zendure_ha/device.py` |
| 37 | +- Contains: `ZendureBattery` subclass of `EntityDevice` |
| 38 | +- Depends on: EntityDevice |
| 39 | +- Used by: ZendureDevice |
| 40 | +- Responsibilities: Model inference from serial number, capacity tracking, integration into parent device |
| 41 | + |
| 42 | +**Entity Layer (EntityDevice, EntityZendure):** |
| 43 | +- Purpose: Base abstractions for Home Assistant entity creation and property tracking |
| 44 | +- Location: `custom_components/zendure_ha/entity.py` |
| 45 | +- Contains: `EntityDevice` (device container), `EntityZendure` (entity base mixin) |
| 46 | +- Depends on: Home Assistant helpers (device_registry, entity_registry, restore_state) |
| 47 | +- Used by: ZendureDevice, ZendureManager, all platform entities |
| 48 | +- Responsibilities: Device info creation, entity registry management, property-to-entity mapping, state persistence |
| 49 | + |
| 50 | +**Platform Layer:** |
| 51 | +- Purpose: Home Assistant platform implementations extending EntityZendure |
| 52 | +- Location: `custom_components/zendure_ha/{sensor,number,select,switch,binary_sensor,button}.py` |
| 53 | +- Contains: `ZendureSensor`, `ZendureNumber`, `ZendureSelect`, `ZendureSwitch`, `ZendureBinarySensor`, `ZendureButton` |
| 54 | +- Depends on: EntityZendure, Home Assistant platform classes |
| 55 | +- Used by: Home Assistant entity/component registry |
| 56 | +- Responsibilities: Platform-specific behavior, value rendering, state updates, callbacks |
| 57 | + |
| 58 | +**API & Transport Layer (Api):** |
| 59 | +- Purpose: MQTT client management, device factory, connection handling |
| 60 | +- Location: `custom_components/zendure_ha/api.py` |
| 61 | +- Contains: `Api` class with static MQTT clients, device creation registry |
| 62 | +- Depends on: paho-mqtt, bleak, device classes |
| 63 | +- Used by: ZendureManager, config flow |
| 64 | +- Responsibilities: Cloud/local MQTT connection setup, device instantiation by model name, authentication |
| 65 | + |
| 66 | +**Fuse Group Layer (FuseGroup):** |
| 67 | +- Purpose: Power distribution constraints across multiple devices |
| 68 | +- Location: `custom_components/zendure_ha/fusegroup.py` |
| 69 | +- Contains: `FuseGroup` class managing multiple `ZendureDevice` instances |
| 70 | +- Depends on: ZendureDevice |
| 71 | +- Used by: ZendureManager power balancing |
| 72 | +- Responsibilities: Device-level power limit calculation under fuse constraints, weighted distribution |
| 73 | + |
| 74 | +**Config Flow (Setup):** |
| 75 | +- Purpose: User configuration and authentication setup |
| 76 | +- Location: `custom_components/zendure_ha/config_flow.py` |
| 77 | +- Contains: `ZendureConfigFlow`, `ZendureOptionsFlowHandler` |
| 78 | +- Depends on: Api, const definitions |
| 79 | +- Used by: Home Assistant config entry flow |
| 80 | +- Responsibilities: Token validation, MQTT settings collection, P1 meter entity selection |
| 81 | + |
| 82 | +## Data Flow |
| 83 | + |
| 84 | +**Integration Setup:** |
| 85 | + |
| 86 | +1. User adds integration via config flow (CONF_APPTOKEN required) |
| 87 | +2. `async_setup_entry()` in `__init__.py` creates `ZendureManager` |
| 88 | +3. `ZendureManager.loadDevices()` calls `Api.Connect()` to authenticate and fetch MQTT credentials |
| 89 | +4. Devices are instantiated from factory in `Api.createdevice` based on product model |
| 90 | +5. Platforms forward-loaded (sensor, number, select, switch, binary_sensor, button) |
| 91 | +6. `ZendureManager.async_config_entry_first_refresh()` triggers first coordinator update |
| 92 | + |
| 93 | +**Device Message Flow:** |
| 94 | + |
| 95 | +1. MQTT message received on `iot/{prodkey}/{deviceId}/properties/read` topic |
| 96 | +2. `ZendureDevice.mqttProperties()` parses JSON payload |
| 97 | +3. Property key matched against entity names via `EntityDevice.createEntity` mapping |
| 98 | +4. `ZendureDevice.entityUpdate(key, value)` called |
| 99 | +5. Entity's `update_value(value)` updates state and triggers listeners |
| 100 | +6. Aggregate sensors (energy, switch counts) accumulate via `aggregate()` method |
| 101 | + |
| 102 | +**Power Distribution Flow (Manager Mode):** |
| 103 | + |
| 104 | +1. `ZendureManager.async_update_data()` runs at SCAN_INTERVAL (60s) |
| 105 | +2. `update_operation()` reads current operation mode (OFF, MANUAL, MATCHING, STORE_SOLAR) |
| 106 | +3. P1 meter power polled from Home Assistant state |
| 107 | +4. `calculate_power()` determines optimal charge/discharge per device |
| 108 | +5. `FuseGroup.charge_limit()` / `discharge_limit()` applies fuse constraints |
| 109 | +6. Device-specific `charge(power)` or `discharge(power)` methods invoked via MQTT |
| 110 | +7. Next update scheduled with SmartMode intervals (TIMEFAST=2.2s, TIMEZERO=4s) |
| 111 | + |
| 112 | +**State Persistence:** |
| 113 | + |
| 114 | +1. RestoreEntity subclasses (`ZendureRestoreSensor`, `ZendureRestoreNumber`, `ZendureRestoreSelect`) maintain state |
| 115 | +2. Home Assistant restore_state module persists data across restarts |
| 116 | +3. On startup, previous values loaded and set before first coordinator update |
| 117 | + |
| 118 | +## Key Abstractions |
| 119 | + |
| 120 | +**EntityDevice:** |
| 121 | +- Purpose: Container for device metadata, entities dictionary, platform-agnostic device representation |
| 122 | +- Examples: `ZendureDevice`, `ZendureManager`, `ZendureBattery` |
| 123 | +- Pattern: Base class providing device info (name, model, serial, manufacturer), entity tracking dict |
| 124 | + |
| 125 | +**EntityZendure:** |
| 126 | +- Purpose: Mixin providing common entity behavior (unique_id generation, device_info property, property name mapping) |
| 127 | +- Examples: All platform entities inherit from both `EntityZendure` and Home Assistant entity type |
| 128 | +- Pattern: snakecase property name → translation key conversion, entity registry integration |
| 129 | + |
| 130 | +**Device Polymorphism:** |
| 131 | +- Purpose: Support different communication protocols and feature sets per device type |
| 132 | +- Examples: `ZendureLegacy` (MQTT-only, older devices), `ZendureZenSdk` (cloud + BLE, newer ZenSDK protocol) |
| 133 | +- Pattern: Template method pattern where device subclasses override `charge()`, `discharge()`, `mqttProperties()` |
| 134 | + |
| 135 | +**CreateEntity Mapping:** |
| 136 | +- Purpose: Dynamic entity creation from property definitions |
| 137 | +- Examples: `createEntity` dict maps property names to (unit, device_class, state_class) tuples |
| 138 | +- Pattern: Configuration-driven entity instantiation, allows extensibility without code changes |
| 139 | + |
| 140 | +## Entry Points |
| 141 | + |
| 142 | +**Integration Setup:** |
| 143 | +- Location: `custom_components/zendure_ha/__init__.py::async_setup_entry()` |
| 144 | +- Triggers: User adds integration via config flow, Home Assistant service calls |
| 145 | +- Responsibilities: Create ZendureManager, load devices, setup platforms, register update listener |
| 146 | + |
| 147 | +**Config Flow:** |
| 148 | +- Location: `custom_components/zendure_ha/config_flow.py::ZendureConfigFlow.async_step_user()` |
| 149 | +- Triggers: User selects "Zendure" in add integration UI |
| 150 | +- Responsibilities: Collect token, validate authentication, persist config |
| 151 | + |
| 152 | +**Coordinator Update:** |
| 153 | +- Location: `custom_components/zendure_ha/manager.py::ZendureManager.async_update_data()` |
| 154 | +- Triggers: Scheduled at SCAN_INTERVAL (60s), can be forced via coordinator methods |
| 155 | +- Responsibilities: Update operation mode, check P1 meter, calculate power distribution, invoke device commands |
| 156 | + |
| 157 | +**Platform Setup:** |
| 158 | +- Location: `custom_components/zendure_ha/{platform}.py::async_setup_entry()` |
| 159 | +- Triggers: After platforms are forward-loaded by integration |
| 160 | +- Responsibilities: Register entity add callback for platform |
| 161 | + |
| 162 | +## Error Handling |
| 163 | + |
| 164 | +**Strategy:** Defensive async exception handling with logging |
| 165 | + |
| 166 | +**Patterns:** |
| 167 | +- Try-catch blocks in entity update handlers catch malformed values or type mismatches |
| 168 | +- `_LOGGER.error()` with traceback for unexpected exceptions |
| 169 | +- MQTT connection failures logged but don't prevent platform initialization |
| 170 | +- Device offline state set to 0 (DeviceState.OFFLINE) on connection loss |
| 171 | +- Invalid config entry returns False from `async_setup_entry()` to prevent loading |
| 172 | + |
| 173 | +## Cross-Cutting Concerns |
| 174 | + |
| 175 | +**Logging:** All modules use `logging.getLogger(__name__)` pattern, debug-level MQTT message logging controlled by CONF_MQTTLOG flag |
| 176 | + |
| 177 | +**Validation:** |
| 178 | +- Token validation in `Api.Connect()` returns None on auth failure |
| 179 | +- Entity update handlers validate value types before processing (e.g., int conversion) |
| 180 | +- MQTT payload parsed as JSON with error logging on decode failure |
| 181 | + |
| 182 | +**Authentication:** |
| 183 | +- App token passed in config entry data, used by Api to authenticate cloud MQTT connection |
| 184 | +- MQTT user/password from config_flow for local MQTT (legacy devices) |
| 185 | +- BLE connection uses bleak-retry-connector for reliability |
| 186 | + |
| 187 | +--- |
| 188 | + |
| 189 | +*Architecture analysis: 2026-03-27* |
0 commit comments