PyTap is a Home Assistant custom component that passively monitors Tigo TAP solar energy systems via the RS-485 bus. It connects to a Tigo gateway (over TCP or serial), parses the proprietary protocol in real time, and exposes per-optimizer sensor entities — power, voltage, current, temperature, and more — directly in Home Assistant.
The integration embeds the pytap protocol parser library and bridges its event-driven output into Home Assistant's entity/device model using an async coordinator pattern.
┌──────────────────────────────────────────────────────────────────────┐
│ Home Assistant Instance │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ custom_components/pytap (this integration) │ │
│ │ │ │
│ │ Config Flow ─► Coordinator ─► Sensor Entities │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ pytap library │ │
│ │ (embedded protocol parser) │ │
│ └──────────────────────┬────────────────────────────────────┘ │
│ │ TCP / Serial │
└──────────────────────────┼──────────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ Tigo TAP Gateway │
│ (RS-485 bus master) │
└───────────┬───────────┘
│ RS-485
┌────────────┼────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│TS4 Node │ │TS4 Node │ │TS4 Node │ ... (PV optimizers)
└─────────┘ └─────────┘ └─────────┘
pytap/ # Repository root
├── docs/
│ └── architecture.md # This file
├── config/ # HA dev config directory
│ ├── configuration.yaml
│ └── custom_components → ../custom_components (symlink)
├── custom_components/
│ └── pytap/ # HA custom component
│ ├── __init__.py # Integration setup / teardown
│ ├── config_flow.py # UI-based configuration
│ ├── const.py # Domain, defaults, config keys
│ ├── coordinator.py # DataUpdateCoordinator (async bridge)
│ ├── diagnostics.py # HA diagnostics platform download
│ ├── energy.py # Pure energy accumulation helpers
│ ├── sensor.py # Sensor entities (power, voltage, etc.)
│ ├── manifest.json # HA integration metadata
│ ├── strings.json # UI strings (source)
│ ├── translations/
│ │ └── en.json # English translations
│ └── pytap/ # Embedded protocol parser library
│ ├── __init__.py # Library re-exports
│ ├── api.py # Public API (create_parser, parse_bytes, connect)
│ ├── core/
│ │ ├── parser.py # Protocol parser (bytes → events)
│ │ ├── types.py # Protocol types & constants
│ │ ├── events.py # Event dataclasses
│ │ ├── state.py # SlotClock, NodeTableBuilder, PersistentState
│ │ ├── source.py # TcpSource, SerialSource
│ │ ├── crc.py # CRC-16-CCITT
│ │ └── barcode.py # Tigo barcode encode/decode
├── tests/
│ ├── conftest.py # HA test fixtures
│ ├── test_config_flow.py # Config flow tests
│ ├── test_coordinator_persistence.py # Coordinator & persistence tests
│ ├── test_diagnostics.py # Diagnostics platform tests
│ ├── test_energy.py # Energy accumulation unit tests
│ ├── test_migration.py # Entity migration tests
│ └── test_sensor.py # Sensor platform tests
├── requirements.txt # Pinned HA + dev dependencies
└── pytest.ini # Test configuration
┌──────────────────────────────────────────────────────────────┐
│ Home Assistant Core │
│ (event loop, entity registry, device registry, frontend) │
└───────────┬───────────────────────────────┬──────────────────┘
│ async_setup_entry │ entity updates
▼ ▲
┌───────────────────────────────────────────────────────────────┐
│ Integration Layer (custom_components/pytap/) │
│ │
│ ┌──────────────┐ ┌───────────────────┐ ┌───────────────┐ │
│ │ Config Flow │ │ Coordinator │ │ Sensors │ │
│ │ │ │ │ │ │ │
│ │ • User form │ │ • Async bridge │ │ • Per-node │ │
│ │ • Validate │ │ • Thread mgmt │ │ entities │ │
│ │ connection│ │ • Event routing │ │ • Device │ │
│ │ • Store │ │ • State cache │ │ grouping │ │
│ │ config │ │ │ │ • Unit │ │
│ │ │ │ │ │ conversion │ │
│ └──────┬───────┘ └────────┬──────────┘ └───────┬───────┘ │
│ │ │ │ │
│ │ ConfigEntry │ pytap.Parser │ reads │
│ │ │ (in executor thread) │ coord │
│ └───────────────────┘ │ .data │
│ │ │ │
└─────────────────────────────┼─────────────────────┼──────────┘
│ │
▼ │
┌─────────────────────────────────────────────────────────────┐
│ Parser Library (pytap/pytap/) │
│ │
│ pytap.api.create_parser() → Parser │
│ parser.feed(bytes) → list[Event] │
│ pytap.api.connect(config) → Source │
│ │
│ No HA dependency — pure protocol logic, stdlib only │
└──────────────────────────────────────────────────────────────┘
Entry point for Home Assistant. Implements the two required lifecycle hooks:
| Function | Purpose |
|---|---|
async_setup_entry(hass, entry) |
Create coordinator, start data streaming, forward platform setup |
async_unload_entry(hass, entry) |
Stop coordinator, unload platforms, clean up |
Setup flow:
- Instantiate
PyTapDataUpdateCoordinatorwith host/port fromConfigEntry. - Call
coordinator.async_config_entry_first_refresh()to establish the initial connection and validate it works. - Store coordinator in
hass.data[DOMAIN][entry.entry_id]. - Forward setup to the
sensorplatform.
Teardown flow:
- Unload platforms.
- Coordinator cancels its background listener task.
- Remove coordinator from
hass.data.
Implements a multi-step ConfigFlow to collect connection parameters and module barcodes via the HA frontend:
| Step | Fields | Validation |
|---|---|---|
user |
host (required), port (default: 502) |
Attempt TCP connection to validate reachability |
modules_menu |
Menu (add_module / finish) |
Ensures explicit module-by-module setup |
add_module |
string (required), name (required), barcode (required), peak_power (optional, default: 455 Wp) |
Non-empty fields, barcode format, duplicate prevention, peak power range 1–1000 |
Step 1 — Connection: The user provides the gateway host and port. Validation opens a short-lived TCP connection using pytap.api.connect() (run in the executor). On success, proceeds to step 2. On failure, shows "cannot_connect".
Step 2 — Modules: The user adds modules one at a time from a menu-driven flow:
STRING— Required string/group name (e.g.,A,B,East,West).NAME— Required user-friendly name for the optimizer (e.g.,Panel_01,Roof_North_3).BARCODE— Required Tigo barcode from the module sticker (e.g.,S-1234567A).
Barcodes are validated against the X-NNNNNNNC format and duplicates are rejected. The module list is stored in ConfigEntry.data["modules"] as:
[
{"string": "A", "name": "Panel_01", "barcode": "A-1234567B", "peak_power": 455},
{"string": "A", "name": "Panel_02", "barcode": "C-2345678D", "peak_power": 455},
{"string": "B", "name": "Panel_03", "barcode": "E-3456789F", "peak_power": 400},
]Unique ID: Based on host:port to prevent duplicate entries for the same gateway.
Options Flow: The module list can be edited after setup via an Options Flow without removing the integration. This allows adding/removing optimizers as the installation evolves.
DOMAIN = "pytap"
DEFAULT_PORT = 502 # Tigo TAP default Modbus/TCP port
DEFAULT_SCAN_INTERVAL = 30 # Coordinator poll fallback (seconds)
CONF_MODULE_PEAK_POWER = "peak_power"
DEFAULT_PEAK_POWER = 455 # Wp (watts peak) — STC rating
CONF_LIVE_REPORTING = "live_reporting"
DEFAULT_LIVE_REPORTING = True # Push every reading as it arrives (default)
CONF_WRITE_INTERVAL = "write_interval"
DEFAULT_WRITE_INTERVAL = 30 # Seconds between averaged pushes (interval mode)The coordinator is the central bridge between the blocking pytap parser and Home Assistant's async event loop. It uses a push-based streaming model rather than the typical polling pattern.
┌─────────────────────────────────────────────────┐
│ PyTapDataUpdateCoordinator │
│ │
│ Main thread (HA event loop): │
│ • Exposes self.data to sensor entities │
│ • Calls async_set_updated_data() on events │
│ • Manages lifecycle (start/stop) │
│ │
│ Executor thread (blocking I/O): │
│ • pytap.api.connect() → Source │
│ • parser.feed(source.read()) → Events │
│ • Schedules callbacks back to event loop │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ self.data (dict[str, Any]) │ │
│ │ │ │
│ │ { │ │
│ │ "gateways": { │ │
│ │ 1: { "address": "...", "version": "1.2" }│
│ │ }, │ │
│ │ "nodes": { │ │
│ │ "S-1234567A": { │ │
│ │ "gateway_id": 1, │ │
│ │ "node_id": 42, │ │
│ │ "barcode": "S-1234567A", │ │
│ │ "name": "Panel_01", │ │
│ │ "string": "A", │ │
│ │ "peak_power": 455, │ │
│ │ "power": 343.0, │ │
│ │ "performance": 75.38, │ │
│ │ "voltage_in": 38.5, │ │
│ │ "voltage_out": 39.2, │ │
│ │ "current": 8.75, │ │
│ │ "temperature": 45.2, │ │
│ │ "dc_dc_duty_cycle": 0.78, │ │
│ │ "rssi": -65, │ │
│ │ "last_update": datetime(...) │ │
│ │ } │ │
│ │ }, │ │
│ │ "counters": { ... }, │ │
│ │ "discovered_barcodes": ["S-9999999Z"] │ │
│ │ } │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
The pytap library uses blocking I/O (socket.recv, serial.read). Since Home Assistant's core runs on asyncio, the coordinator must bridge the two worlds:
- Background listener task — An
asyncio.Taskcreated at setup that runs the blocking_listen()method in the executor viahass.async_add_executor_job(). - Event dispatch — When the executor thread receives parsed events, it schedules
coordinator.async_set_updated_data()back on the event loop viahass.loop.call_soon_threadsafe(). - Midnight reset timer — A
call_latertimer on the event loop fires at local midnight to proactively reset daily accumulators, ensuring daily sensors zero at exactly midnight even when no power reports arrive overnight. - Cancellation — On unload, the task is cancelled, the midnight timer is cancelled, and the source connection is closed, which unblocks the
read()call.
HA Event Loop (main thread) Executor Thread
───────────────────────── ────────────────
│ │
│ async_setup_entry() │
│ ─► create coordinator │
│ ─► start listener task ──────────►│
│ ─► schedule midnight reset │
│ │ source = connect(config)
│ │ parser = create_parser()
│ │
│ │ loop:
│ │ data = source.read(4096)
│ │ events = parser.feed(data)
│ ◄── call_soon_threadsafe ─────────│ for event in events:
│ async_set_updated_data(...) │ dispatch(event)
│ │
│ [midnight] _perform_midnight_ │
│ reset() → zero daily accum. │
│ → push update to sensors │
│ → reschedule for next midnight │
│ │
│ entities read coordinator.data │
│ and update their state │
│ │
│ async_unload_entry() │
│ ─► cancel midnight timer │
│ ─► cancel task ──────────────────►│ (source.close() → unblocks read)
│ │ exits
The coordinator handles connection failures and source timeouts with automatic reconnection:
- Initial connection failure — Raises
UpdateFailed, HA marks the integration as unavailable and retries using its standard backoff. - Mid-stream disconnection — The listener task catches the exception, logs a warning, waits
RECONNECT_DELAYseconds, and re-establishes the connection. - Silence timeout — If no data arrives for
RECONNECT_TIMEOUTseconds, the coordinator assumes the connection is stale and reconnects.
The coordinator delivers readings to Home Assistant in one of two user-selectable modes (set via the options flow's Change reporting settings step and stored as live_reporting / write_interval in ConfigEntry.data):
| Mode | live_reporting |
Behavior |
|---|---|---|
| Live (default) | True |
Every event that changes node data triggers an immediate async_set_updated_data push — sub-second latency. |
| Averaged interval | False |
Readings are buffered per barcode with monotonic timestamps. Once per write_interval seconds (5–300, default 30) the coordinator pushes a time-weighted average of the buffered readings via _build_averaged_snapshot(). |
Time-weighted averaging weights each buffered reading by the duration it remained the latest value (the time until the next reading, or until the flush for the final reading). This yields a physically representative average when readings arrive at uneven intervals, unlike a plain arithmetic mean.
Averaging applies only to the instantaneous fields pushed to entities (power, voltage in/out, current in/out, temperature, DC-DC duty cycle, RSSI); performance is recomputed from the averaged power. Energy accumulation and persisted node snapshots always use the raw readings, so daily/total energy stays accurate regardless of reporting mode.
The coordinator maintains a cumulative state dictionary. Incoming events are merged, not replaced:
PowerReportEvent— If the event'sbarcodematches a configured module, upserts intodata["nodes"][barcode]with all power fields +last_updatetimestamp. Events for unconfigured barcodes are discarded (but logged at DEBUG level for discovery — see below).InfrastructureEvent— Replacesdata["gateways"]and rebuilds the barcode→node_id mapping from scratch. The first infrastructure event in a session may arrive without barcodes (gateway identity only — node table not yet received); barcode resolution activates once the gateway sends the full node table. Subsequent events log the match count and any configured barcodes not found in the node table.TopologyEvent— Updates topology fields for matched nodes only.StringEvent— Logged for diagnostics; not stored in entity state.
All persistent state is consolidated into a single HA Store (<config>/.storage/pytap_<entry_id>_coordinator), written via homeassistant.helpers.storage.Store (version 2). The store contains:
barcode_to_node— Barcode↔node_id mappings learned from infrastructure events.discovered_barcodes— Set of unconfigured barcodes seen on the bus.parser_state— Serialised parser infrastructure state (gateway identities, versions, node tables) viaPersistentState.to_dict().energy_data— Per-barcode accumulator state (daily_energy_wh,daily_reset_date,total_energy_wh,readings_today,last_power_w,last_reading_ts).
Saves are debounced (10s delay) to avoid excessive writes. On shutdown, any pending unsaved changes are flushed immediately.
On startup, the coordinator loads all state from the HA Store. The parser receives a shared PersistentState object (deserialized from the store) which it mutates in memory as new infrastructure events arrive. The coordinator owns persistence — the parser never performs file I/O.
This single-store approach ensures:
- No raw file I/O on the event loop (HA Store is fully async).
- Automatic cleanup when a config entry is removed.
- Proper inclusion in HA backups.
- Version migration support via the Store's built-in versioning.
The coordinator builds a barcode allowlist from ConfigEntry.data["modules"] at startup. Only events whose barcode field matches the allowlist are forwarded to entity state.
For unconfigured barcodes, the coordinator logs a message at INFO level:
INFO: Discovered unconfigured Tigo optimizer barcode: S-9999999Z (gateway=1, node=55). Add it to your PyTap module list to start tracking.
This approach mirrors the taptap add-on pattern: users can monitor the HA log for unconfigured barcodes and add them via the Options Flow. Since barcode discovery messages (InfrastructureEvent) from the Tigo gateway can be infrequent (sometimes only during overnight enumeration cycles), users should allow up to 24 hours for full discovery.
The coordinator also maintains a data["discovered_barcodes"] set of all seen-but-unconfigured barcodes, persisted to the HA Store for survival across restarts. Discovered barcodes are persisted alongside barcode↔node_id mappings so that when a user later adds a previously-discovered barcode via the Options Flow, it can be resolved immediately from saved state without waiting for the next infrastructure event.
When the user adds or removes modules via the Options Flow, the integration reloads. On reload, the newly-created coordinator loads saved barcode↔node_id mappings from the HA Store. If a newly-added barcode already exists in the saved mappings, a placeholder entry is created in coordinator data so that sensor entities can bind immediately — no need to wait for the next power report or infrastructure event.
Creates sensor entities only for optimizer modules explicitly listed in the user's configuration. No auto-discovery — the user provides the list of barcodes they want to track.
Each configured Tigo TS4 optimizer module becomes a device in the HA device registry, with 12 sensor entities:
Device: "Tigo TS4 Panel_01" (user-defined name from config)
├── Sensor: Performance (%) — SensorStateClass.MEASUREMENT
├── Sensor: Power (W) — SensorDeviceClass.POWER
├── Sensor: Voltage In (V) — SensorDeviceClass.VOLTAGE
├── Sensor: Voltage Out (V) — SensorDeviceClass.VOLTAGE
├── Sensor: Current In (A) — SensorDeviceClass.CURRENT
├── Sensor: Current Out (A) — SensorDeviceClass.CURRENT
├── Sensor: Temperature (°C) — SensorDeviceClass.TEMPERATURE
├── Sensor: DC-DC Duty Cycle (%) — SensorStateClass.MEASUREMENT
├── Sensor: RSSI (dBm) — SensorDeviceClass.SIGNAL_STRENGTH
├── Sensor: Daily Energy (Wh) — SensorDeviceClass.ENERGY
├── Sensor: Total Energy (Wh) — SensorDeviceClass.ENERGY
└── Sensor: Readings Today (—) — EntityCategory.DIAGNOSTIC, SensorStateClass.TOTAL
Aggregate virtual devices are also created:
Tigo String <name>: 4 sensors (performance,power,daily_energy,total_energy)Tigo Installation: 4 sensors (performance,power,daily_energy,total_energy)
Devices are identified by barcode (stable across gateway restarts and node_id reassignments):
DeviceInfo(
identifiers={(DOMAIN, barcode)}, # e.g. ("pytap", "S-1234567A")
name=f"Tigo TS4 {module_config['name']}", # user-defined name
manufacturer="Tigo Energy",
model="TS4",
serial_number=barcode,
)Unlike auto-discovery integrations, entities are created deterministically from the configured module list:
- At
async_setup_entry, the sensor platform readsConfigEntry.data["modules"]. - For each configured module, it creates the full set of 12 per-optimizer sensor entities immediately.
- It also creates aggregate sensors per distinct string and for the whole installation.
- Entities start in an unavailable state until the first matching
PowerReportEventarrives from the bus. - When the coordinator receives a
PowerReportEventwith a barcode matching a configured module, the corresponding entities become available and display live data.
This approach is inspired by the taptap HA add-on which similarly requires users to define taptap_modules as STRING:NAME:SERIAL triplets.
Rationale for explicit configuration over auto-discovery:
- Predictable entity IDs — Users know exactly which entities will exist, enabling dashboards and automations to be set up before the first data arrives.
- No phantom entities — Auto-discovery can create entities for neighbor nodes on adjacent installations sharing the same RS-485 bus. Explicit barcodes prevent this.
- User-friendly names — Names are defined by the user (e.g., "Roof_East_Panel_03") rather than opaque node IDs.
- String grouping — The required
stringfield powers deterministic per-string aggregate entities.
The Tigo protocol identifies nodes by node_id (a transient 16-bit integer) and barcode (a stable hardware identifier). The mapping between them is learned from InfrastructureEvents (gateway enumeration).
Config: ["S-1234567A", "S-1234568B", ...] (user-provided barcodes)
│
▼
Coordinator: barcode → node_id mapping (learned from InfrastructureEvent)
│
▼
PowerReportEvent(node_id=42, barcode="S-1234567A")
│
▼
Coordinator: barcode in allowlist? → YES → update data["nodes"]["S-1234567A"]
NO → log discovery, discard
If the parser already resolves barcodes in PowerReportEvent.barcode (via its internal node table), the coordinator matches directly. For events where the barcode is None (node table not yet populated), the coordinator uses its own node_id → barcode mapping built from prior InfrastructureEvents.
Sensor entities inherit from CoordinatorEntity and implement _handle_coordinator_update():
@callback
def _handle_coordinator_update(self) -> None:
node_data = self.coordinator.data.get("nodes", {}).get(self._barcode)
if node_data:
self._attr_native_value = node_data.get(self._value_key)
self._attr_available = True
else:
self._attr_available = False
self.async_write_ha_state()Entities are marked unavailable only when:
- The node's barcode has never been seen on the bus (no
PowerReportEventreceived yet for this barcode).
Once a sensor receives its first value, it remains available and holds the last received value indefinitely. This is intentional: solar optimizers stop reporting overnight, and displaying the last known values (rather than marking entities unavailable) provides a better user experience for dashboards and history.
{
"domain": "pytap",
"name": "PyTap",
"codeowners": ["@azebro"],
"config_flow": true,
"documentation": "https://github.com/azebro/pytap",
"integration_type": "hub",
"iot_class": "local_push",
"requirements": [],
"version": "0.3.0"
}Key choices:
integration_type: "hub"— A single gateway entry manages multiple downstream devices (optimizer nodes).iot_class: "local_push"— Data is pushed from the device in real time (not polled). The coordinator streams events as they arrive from the bus.requirements: []— Thepytapparser library is embedded, not installed from PyPI. No external dependencies beyond stdlib.
Tigo Gateway (RS-485 bus master)
│
│ TCP stream (port 502)
▼
pytap TcpSource.read(4096)
│
│ raw bytes
▼
pytap Parser.feed(bytes)
│
│ Frame accumulator state machine
│ CRC validation
│ Frame dispatch by type
│ Transport correlation (request/response pairing)
│ PV packet extraction
│ Slot clock → timestamp mapping
│ Barcode resolution
│
│ list[Event]
▼
Coordinator._process_event(event)
│
│ Barcode in configured allowlist?
│ ├── YES → Merge into self.data["nodes"][barcode]
│ └── NO → Log discovery, add to discovered_barcodes set, discard
│
│ async_set_updated_data()
▼
Home Assistant Entity Registry
│
│ CoordinatorEntity._handle_coordinator_update()
│ → updates native_value, availability (keyed by barcode)
▼
HA Frontend / Automations / History
│
│ • Real-time dashboard cards (power, voltage, temp)
│ • History graphs
│ • Automations (e.g., alert on low power / high temp)
│ • Energy dashboard integration
The pytap parser library is bundled inside the custom component at custom_components/pytap/pytap/. This means:
- Zero external dependencies — Easier to install (just copy the folder into
custom_components/). - Version lock — The parser version always matches the integration version.
- No PyPI publishing required — Reduces release complexity for a niche integration.
The library maintains a clean boundary: it has no Home Assistant imports and can be used standalone (scripts, other platforms). Persistence is handled externally — the parser accepts a PersistentState object and mutates it in memory; the caller owns serialization.
Unlike most HA integrations that poll an API on an interval, PyTap uses a continuous streaming model:
| Aspect | Polling | Streaming (PyTap) |
|---|---|---|
| Latency | scan_interval seconds |
Sub-second |
| Bandwidth | Redundant requests | Only new data |
| HA CPU | Timer fires + HTTP call | Idle until event arrives |
| Complexity | Simple _async_update_data |
Background task + thread bridging |
Streaming is the correct choice because the Tigo bus produces a continuous flow of power reports (~1 per optimizer per 5-second slot cycle), and the bus protocol is already push-based.
The pytap library uses blocking socket.recv() / serial.read() calls. Rather than rewriting the library with asyncio, the coordinator runs the blocking listener in the HA executor thread pool:
self._listener_task = entry.async_create_background_task(
hass,
hass.async_add_executor_job(self._listen),
name="pytap_listener",
)This keeps the parser library simple and portable while integrating cleanly with HA's async architecture.
Rather than auto-discovering nodes from the bus, users explicitly configure which optimizer barcodes to track. This is modeled after the taptap HA add-on's taptap_modules pattern. Benefits:
- No phantom entities from neighbor installations sharing the RS-485 bus.
- Predictable entity IDs that can be referenced in automations before the first data arrives.
- User-defined names instead of opaque node IDs.
- Discovery logging surfaces unconfigured barcodes in the HA log so users can add them.
The HA device registry mirrors the physical topology:
Gateway 1 (via_device: None)
├── Node 42 (via_device: Gateway 1)
├── Node 43 (via_device: Gateway 1)
└── Node 44 (via_device: Gateway 1)
Gateway 2 (via_device: None)
├── Node 101 (via_device: Gateway 2)
└── Node 102 (via_device: Gateway 2)
This allows users to see which gateway each optimizer is connected through, matching the physical wiring.
The coordinator stores node data as a flat dictionary keyed by barcode (not node_id), since barcodes are stable identifiers while node IDs can change across gateway restarts. This makes entity lookups O(1) and avoids coupling entities to the pytap event type hierarchy.
| Parameter | Type | Default | Description |
|---|---|---|---|
host |
string | (required) | IP address or hostname of the Tigo gateway |
port |
int | 502 | TCP port for the gateway's RS-485 bridge |
modules |
list | (required) | List of optimizer modules as STRING:NAME:BARCODE:PEAK_POWER dicts |
live_reporting |
bool | True |
Push every reading immediately (default) vs. averaged interval mode |
write_interval |
int | 30 | Seconds between time-weighted averaged pushes when live_reporting is off (5–300) |
Module format: Each module is a dict with fields:
STRING— Required group name (e.g.,A,East).NAME— Required user-friendly label.BARCODE— Required Tigo barcode (e.g.,S-1234567A).PEAK_POWER— Optional peak panel power in Wp (default: 455).
Example: A:Panel_01:S-1234567A, A:Panel_02:S-1234568B, B:Panel_03:S-2345678C
| Constant | Value | Description |
|---|---|---|
RECONNECT_TIMEOUT |
60s | Seconds of silence before reconnecting |
RECONNECT_DELAY |
5s | Delay between reconnection attempts |
RECONNECT_RETRIES |
0 | Max retries (0 = infinite) |
Configurable at runtime without removing the integration:
- Module list — Add or remove optimizer barcodes as the installation evolves.
- Connection settings — Change the gateway host and port without reconfiguring.
- Reporting settings — Toggle live reporting, or set the averaged reporting interval (5–300 s) used when live reporting is off. The interval is collected on a follow-up step shown only when live reporting is turned off, so it stays hidden while live reporting is enabled.
Changes trigger a full integration reload. Previously-discovered barcode mappings are persisted and survive the reload.
| Scenario | Behavior |
|---|---|
| Gateway unreachable at setup | Config flow warns but proceeds (non-blocking) |
| Connection lost mid-stream | Coordinator reconnects automatically with backoff |
| CRC error in protocol data | Parser increments counters["crc_errors"], skips frame |
| Malformed frame (runt/giant) | Parser increments counter, resumes at next frame boundary |
| Node table page with trailing bytes | Parser tolerates trailing bytes on sentinel (count=0) and data pages; parses the declared entries and ignores extra bytes |
| Node address with bit-15 flag set | Parser masks node addresses to 15 bits (& 0x7FFF) when parsing node table entries; bit 15 is a protocol flag (router/repeater) not part of the node ID |
No data for RECONNECT_TIMEOUT |
Coordinator reconnects (stale connection detection) |
| Node stops reporting | Entity holds last received value and remains available |
| Barcode not yet identified | Entity stays unavailable until gateway enumeration resolves the barcode |
| Unconfigured barcode seen | Logged at INFO level for discovery; event data discarded |
| First infra event without node table | Logged at INFO; barcode resolution deferred until node table arrives |
| Test File | Scope |
|---|---|
test_config_flow.py |
Config flow form rendering, validation, error handling (16 tests) |
test_coordinator_persistence.py |
Event processing, persistence, barcode mapping lifecycle (32 tests) |
test_migration.py |
Config entry migration and legacy entity cleanup (11 tests) |
test_sensor.py |
Entity creation, state updates, availability (30 tests) |
| Test File | Scope |
|---|---|
test_parser.py |
End-to-end byte → event parsing with captured data |
test_types.py |
Protocol type construction and validation |
test_crc.py |
CRC calculation against known vectors |
test_barcode.py |
Barcode encode/decode round-trips |
test_api.py |
Public API function tests |
- Dev container — The repository includes a dev container with HA installed. Run
python3 -m homeassistant --config config/ --debugto test the full integration locally. - Mock source — For automated tests, the coordinator can be tested with a mock
Sourcethat replays captured byte sequences.