Skip to content

Commit 4c2e0ee

Browse files
committed
Poll topology deviceStatus after AIO power change (0.1.6).
Restart AIO waits for topology EVDC offline via sigenergy-cloud 0.1.6. Aggressively poll topology deviceStatus when AIO power changes, and add sigenergy.refresh_device_status + button for manual/automation polls. Expose topology online binary sensor and raw device/communicate status.
1 parent a3f3ea3 commit 4c2e0ee

8 files changed

Lines changed: 397 additions & 34 deletions

File tree

custom_components/sigenergy/__init__.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848

4949
SERVICE_SET_INSTANT_MANUAL_CONTROL = "set_instant_manual_control"
5050
SERVICE_DISABLE_INSTANT_MANUAL_CONTROL = "disable_instant_manual_control"
51+
SERVICE_REFRESH_DEVICE_STATUS = "refresh_device_status"
5152

5253
_INSTANT_MANUAL_MODE_ALIASES = {
5354
"0": InstantManualMode.CHARGING,
@@ -85,6 +86,15 @@
8586
}
8687
)
8788

89+
_REFRESH_DEVICE_STATUS_SCHEMA = vol.Schema(
90+
{
91+
vol.Optional("duration_seconds", default=180): vol.All(
92+
vol.Coerce(int), vol.Range(min=0, max=600)
93+
),
94+
vol.Optional("entry_id"): cv.string,
95+
}
96+
)
97+
8898

8999
_RENAMED_UNIQUE_ID_SUFFIXES = {
90100
"dc_charger_latest_session": "dc_charger_last_session",
@@ -227,8 +237,6 @@ async def async_reload_entry(
227237

228238
def _async_register_services(hass: HomeAssistant) -> None:
229239
"""Register station-level Sigenergy services once."""
230-
if hass.services.has_service(DOMAIN, SERVICE_SET_INSTANT_MANUAL_CONTROL):
231-
return
232240

233241
async def async_set_instant_manual_control(call) -> None:
234242
entry = _service_config_entry(hass, call.data.get("entry_id"))
@@ -264,18 +272,37 @@ async def async_disable_instant_manual_control(call) -> None:
264272
}
265273
)
266274

267-
hass.services.async_register(
268-
DOMAIN,
269-
SERVICE_SET_INSTANT_MANUAL_CONTROL,
270-
async_set_instant_manual_control,
271-
schema=_SET_INSTANT_MANUAL_SCHEMA,
272-
)
273-
hass.services.async_register(
274-
DOMAIN,
275-
SERVICE_DISABLE_INSTANT_MANUAL_CONTROL,
276-
async_disable_instant_manual_control,
277-
schema=_DISABLE_INSTANT_MANUAL_SCHEMA,
278-
)
275+
async def async_refresh_device_status(call) -> None:
276+
entry = _service_config_entry(hass, call.data.get("entry_id"))
277+
data = entry.runtime_data
278+
duration = int(call.data.get("duration_seconds", 180))
279+
await data.status_coordinator.async_request_device_status_poll(
280+
source="service_refresh",
281+
duration_s=float(duration),
282+
enable_fast_poll=duration > 0,
283+
)
284+
285+
if not hass.services.has_service(DOMAIN, SERVICE_SET_INSTANT_MANUAL_CONTROL):
286+
hass.services.async_register(
287+
DOMAIN,
288+
SERVICE_SET_INSTANT_MANUAL_CONTROL,
289+
async_set_instant_manual_control,
290+
schema=_SET_INSTANT_MANUAL_SCHEMA,
291+
)
292+
if not hass.services.has_service(DOMAIN, SERVICE_DISABLE_INSTANT_MANUAL_CONTROL):
293+
hass.services.async_register(
294+
DOMAIN,
295+
SERVICE_DISABLE_INSTANT_MANUAL_CONTROL,
296+
async_disable_instant_manual_control,
297+
schema=_DISABLE_INSTANT_MANUAL_SCHEMA,
298+
)
299+
if not hass.services.has_service(DOMAIN, SERVICE_REFRESH_DEVICE_STATUS):
300+
hass.services.async_register(
301+
DOMAIN,
302+
SERVICE_REFRESH_DEVICE_STATUS,
303+
async_refresh_device_status,
304+
schema=_REFRESH_DEVICE_STATUS_SCHEMA,
305+
)
279306

280307

281308
def _service_config_entry(hass: HomeAssistant, entry_id: str | None) -> SigenConfigEntry:

custom_components/sigenergy/binary_sensor.py

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING
5+
from typing import TYPE_CHECKING, Any
66

77
from homeassistant.components.binary_sensor import (
88
BinarySensorDeviceClass,
@@ -30,16 +30,23 @@ async def async_setup_entry(
3030
if not data.status_coordinator.module_enabled(MODULE_EVDC):
3131
return
3232

33-
async_add_entities(
34-
[
33+
entities: list[BinarySensorEntity] = []
34+
for dc_sn in data.status_coordinator.dc_sns():
35+
entities.append(
3536
SigenDCChargerPluggedInBinarySensor(
3637
data.status_coordinator,
3738
data.client.station_id,
3839
dc_sn,
3940
)
40-
for dc_sn in data.status_coordinator.dc_sns()
41-
]
42-
)
41+
)
42+
entities.append(
43+
SigenDCChargerTopologyOnlineBinarySensor(
44+
data.status_coordinator,
45+
data.client.station_id,
46+
dc_sn,
47+
)
48+
)
49+
async_add_entities(entities)
4350

4451

4552
class SigenDCChargerPluggedInBinarySensor(
@@ -67,3 +74,54 @@ def is_on(self) -> bool | None:
6774
dc_data = (self.coordinator.data.get("dc_chargers") or {}).get(self._dc_sn, {})
6875
value = dc_data.get("plugged_in")
6976
return bool(value) if value is not None else None
77+
78+
79+
class SigenDCChargerTopologyOnlineBinarySensor(
80+
SigenDCChargerStatusEntity,
81+
BinarySensorEntity,
82+
):
83+
"""EVDC online state from mySigen topology deviceStatus/communicateStatus.
84+
85+
This is the same view the app SigenStor detail panel uses. Prefer this over
86+
dcevse/status when deciding whether the EVDC has gone offline during an AIO
87+
power cycle.
88+
"""
89+
90+
_attr_translation_key = "dc_charger_topology_online"
91+
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
92+
_attr_icon = "mdi:lan-connect"
93+
_attr_entity_registry_enabled_default = True
94+
95+
def __init__(
96+
self,
97+
coordinator: SigenStatusCoordinator,
98+
station_id: str,
99+
dc_sn: str,
100+
) -> None:
101+
super().__init__(coordinator, station_id, dc_sn, "dc_charger_topology_online")
102+
103+
def _dc_data(self) -> dict[str, Any]:
104+
if self.coordinator.data is None:
105+
return {}
106+
return (self.coordinator.data.get("dc_chargers") or {}).get(self._dc_sn, {})
107+
108+
@property
109+
def is_on(self) -> bool | None:
110+
offline = self._dc_data().get("topology_offline")
111+
if offline is None:
112+
return None
113+
return not bool(offline)
114+
115+
@property
116+
def extra_state_attributes(self) -> dict[str, Any]:
117+
dc = self._dc_data()
118+
return {
119+
"topology_device_status": dc.get("topology_device_status"),
120+
"topology_communicate_status": dc.get("topology_communicate_status"),
121+
"topology_offline": dc.get("topology_offline"),
122+
"topology_node_found": dc.get("topology_node_found"),
123+
"description": (
124+
"Online when topology communicateStatus=2 and deviceStatus is "
125+
"not offline/powerOff. Source: GET device/devicetreepanel/topology."
126+
),
127+
}

custom_components/sigenergy/button.py

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ async def async_setup_entry(
4242
data.status_coordinator, station_id, data.client
4343
)
4444
)
45+
entities.append(
46+
SigenRefreshDeviceStatusButton(
47+
data.status_coordinator, station_id
48+
)
49+
)
4550

4651
if data.status_coordinator.module_enabled(MODULE_EVDC):
4752
dc_sns = data.status_coordinator.dc_sns()
@@ -124,6 +129,36 @@ async def async_press(self) -> None:
124129
await self.coordinator.async_request_refresh()
125130

126131

132+
class SigenRefreshDeviceStatusButton(SigenStatusEntity, ButtonEntity):
133+
"""Force an immediate topology + EVDC status poll (for automations)."""
134+
135+
_attr_translation_key = "refresh_device_status"
136+
_attr_icon = "mdi:refresh"
137+
_attr_entity_registry_enabled_default = True
138+
139+
def __init__(
140+
self,
141+
status_coordinator: SigenStatusCoordinator,
142+
station_id: str,
143+
) -> None:
144+
super().__init__(status_coordinator, station_id, "refresh_device_status")
145+
self._attr_extra_state_attributes = {
146+
"description": (
147+
"Forces topology deviceStatus/communicateStatus and EVDC "
148+
"status endpoints to refresh now, then keeps a short fast-poll "
149+
"window. Prefer service sigenergy.refresh_device_status for "
150+
"automations."
151+
),
152+
}
153+
154+
async def async_press(self) -> None:
155+
await self.coordinator.async_request_device_status_poll(
156+
source="manual_button",
157+
duration_s=60.0,
158+
enable_fast_poll=True,
159+
)
160+
161+
127162
class SigenRestartAioButton(SigenStatusEntity, ButtonEntity):
128163
"""Power-cycle the SigenStor AIO to clear latched EVSE/SECC estcom faults."""
129164

@@ -147,17 +182,27 @@ def __init__(
147182
self._attr_extra_state_attributes = {
148183
"description": (
149184
"Minimal AIO power-cycle via cloud POST /device/aio/on-off: off, "
150-
"poll until plant Power-off, wait for device/dcevse/status to change "
151-
"(EVDC reaction, ~10s observed), then powerOn=true and poll until "
152-
"running. Clears latched EVDC SECC state 9 / Charging fault. Drops "
153-
"site inverter/battery output briefly."
185+
"poll until plant Power-off, wait until topology reports EVDC "
186+
"offline (device/devicetreepanel/topology deviceStatus / "
187+
"communicateStatus), then powerOn=true and poll until running. "
188+
"Clears latched EVDC SECC state 9 / Charging fault. Drops site "
189+
"inverter/battery output briefly. Never leaves plant powered off."
154190
),
155191
}
156192

157193
async def async_press(self) -> None:
194+
# Aggressive deviceStatus polling while power is changing.
195+
self.coordinator.start_aio_power_fast_poll(
196+
source="aio_power_cycle", duration_s=self._POST_CYCLE_DURATION_S
197+
)
198+
await self.coordinator.async_request_refresh()
158199
try:
159-
# off_dwell_s=0: turn back on as soon as home reports Power-off.
160-
result = await self._client.restart_aio(off_dwell_s=0.0, poll_s=2.0)
200+
result = await self._client.restart_aio(
201+
off_dwell_s=0.0,
202+
poll_s=2.0,
203+
wait_evdc_offline=True,
204+
evdc_offline_wait_s=120.0,
205+
)
161206
except Exception as err: # noqa: BLE001 - surface cloud failures in UI
162207
raise HomeAssistantError(f"Sigen AIO restart failed: {err}") from err
163208
if not result.get("powered_on"):
@@ -168,7 +213,11 @@ async def async_press(self) -> None:
168213
**(self._attr_extra_state_attributes or {}),
169214
"last_restart_result": result,
170215
}
171-
await self.coordinator.async_request_refresh()
216+
await self.coordinator.async_request_device_status_poll(
217+
source="aio_power_cycle",
218+
duration_s=self._POST_CYCLE_DURATION_S,
219+
enable_fast_poll=True,
220+
)
172221
self._start_post_cycle_refresh_burst()
173222

174223
def _start_post_cycle_refresh_burst(self) -> None:
@@ -196,6 +245,8 @@ async def _tick(_now: Any = None) -> None:
196245
self._post_cycle_refresh_unsub()
197246
self._post_cycle_refresh_unsub = None
198247
return
248+
# Keep topology deviceStatus forced during the burst window.
249+
status.request_force_device_status_poll()
199250
await status.async_request_refresh()
200251
if settings is not None:
201252
await settings.async_request_refresh()
@@ -230,13 +281,21 @@ def __init__(
230281
self._attr_extra_state_attributes = {
231282
"description": (
232283
"Toggles Sigen AIO power once via POST /device/aio/on-off. "
233-
"Disabled by default — prefer Restart AIO for recovery."
284+
"Disabled by default — prefer Restart AIO for recovery. "
285+
"Triggers aggressive topology deviceStatus polling afterward."
234286
),
235287
}
236288

237289
async def async_press(self) -> None:
290+
self.coordinator.start_aio_power_fast_poll(
291+
source="aio_power_toggle", duration_s=180.0
292+
)
238293
try:
239294
await self._client.toggle_aio_power()
240295
except Exception as err: # noqa: BLE001
241296
raise HomeAssistantError(f"Sigen AIO power toggle failed: {err}") from err
242-
await self.coordinator.async_request_refresh()
297+
await self.coordinator.async_request_device_status_poll(
298+
source="aio_power_toggle",
299+
duration_s=180.0,
300+
enable_fast_poll=True,
301+
)

0 commit comments

Comments
 (0)