Skip to content

Commit f6d6536

Browse files
committed
tries to fix issue where entities have hyphens. also use coordinator to keep track of device availability
1 parent b765ae0 commit f6d6536

File tree

4 files changed

+106
-13
lines changed

4 files changed

+106
-13
lines changed

custom_components/wattbox/__init__.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
CONF_USERNAME,
2525
)
2626
from homeassistant.core import HomeAssistant
27-
from homeassistant.exceptions import PlatformNotReady
27+
from homeassistant.exceptions import ConfigEntryNotReady, PlatformNotReady
2828
from homeassistant.helpers import discovery
2929
from homeassistant.helpers.dispatcher import async_dispatcher_send
3030
from homeassistant.helpers.event import async_track_time_interval
@@ -48,6 +48,7 @@
4848
STARTUP,
4949
TOPIC_UPDATE,
5050
)
51+
from .coordinator import WattBoxCoordinator
5152

5253
REQUIREMENTS: Final[list[str]] = ["pywattbox>=0.7.2"]
5354

@@ -203,18 +204,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
203204
wattbox = await _async_create_wattbox(hass, host, port, username, password)
204205
except Exception as error:
205206
_LOGGER.error("Error creating WattBox instance: %s", error)
206-
raise PlatformNotReady from error
207+
raise ConfigEntryNotReady from error
207208

208209
hass.data[DOMAIN_DATA][name] = wattbox
209210

211+
# Create coordinator for polling and availability tracking
212+
coordinator = WattBoxCoordinator(hass, wattbox, name, scan_interval)
213+
try:
214+
await coordinator.async_config_entry_first_refresh()
215+
except ConfigEntryNotReady:
216+
raise
217+
218+
hass.data.setdefault(DOMAIN, {})
219+
hass.data[DOMAIN][name] = coordinator
220+
210221
# Forward entry setup to platforms
211222
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
212223

213-
# Use the scan interval to trigger updates
214-
async_track_time_interval(
215-
hass, partial(update_data, hass=hass, name=name), scan_interval
216-
)
217-
218224
return True
219225

220226

@@ -236,5 +242,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
236242
# Remove the wattbox from data
237243
if name in hass.data[DOMAIN_DATA]:
238244
del hass.data[DOMAIN_DATA][name]
245+
# Remove the coordinator
246+
if DOMAIN in hass.data and name in hass.data[DOMAIN]:
247+
del hass.data[DOMAIN][name]
239248

240249
return unload_ok
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""DataUpdateCoordinator for WattBox."""
2+
3+
import logging
4+
5+
from homeassistant.core import HomeAssistant
6+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
7+
from pywattbox.base import BaseWattBox
8+
9+
from datetime import timedelta
10+
11+
_LOGGER = logging.getLogger(__name__)
12+
13+
14+
class WattBoxCoordinator(DataUpdateCoordinator[BaseWattBox]):
15+
"""Coordinator to manage fetching WattBox data from the device.
16+
17+
When the device is unreachable, UpdateFailed is raised, which causes
18+
all entities using this coordinator to automatically become unavailable.
19+
When the device comes back, entities automatically become available again.
20+
"""
21+
22+
wattbox: BaseWattBox
23+
24+
def __init__(
25+
self,
26+
hass: HomeAssistant,
27+
wattbox: BaseWattBox,
28+
name: str,
29+
scan_interval: timedelta,
30+
) -> None:
31+
"""Initialize the coordinator."""
32+
super().__init__(
33+
hass,
34+
_LOGGER,
35+
name=f"WattBox {name}",
36+
update_interval=scan_interval,
37+
)
38+
self.wattbox = wattbox
39+
40+
async def _async_update_data(self) -> BaseWattBox:
41+
"""Fetch data from the WattBox device.
42+
43+
Raises UpdateFailed on any communication error, which signals
44+
to all listening entities that the device is unavailable.
45+
"""
46+
try:
47+
await self.wattbox.async_update()
48+
return self.wattbox
49+
except Exception as err:
50+
raise UpdateFailed(
51+
f"Error communicating with WattBox: {err}"
52+
) from err

custom_components/wattbox/entity.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from homeassistant.helpers import device_registry as dr
1111
from homeassistant.helpers.dispatcher import async_dispatcher_connect
1212
from homeassistant.helpers.entity import DeviceInfo, Entity
13+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
1314
from pywattbox.base import BaseWattBox
1415

1516
from .const import DOMAIN, DOMAIN_DATA, TOPIC_UPDATE
@@ -43,12 +44,18 @@ class WattBoxEntity(Entity):
4344
"""WattBox Entity class."""
4445

4546
_wattbox: BaseWattBox
47+
_coordinator: DataUpdateCoordinator | None
4648
_async_unsub_dispatcher_connect: Callable
4749
_attr_should_poll: Literal[False] = False
4850

4951
def __init__(self, hass: HomeAssistant, name: str, *_args: Any) -> None:
5052
self.hass = hass
5153
self._wattbox = self.hass.data[DOMAIN_DATA][name]
54+
55+
# Use coordinator if available (config entry path), otherwise
56+
# fall back to dispatcher pattern (legacy YAML path).
57+
self._coordinator = self.hass.data.get(DOMAIN, {}).get(name)
58+
5259
self.topic: str = TOPIC_UPDATE.format(DOMAIN, name)
5360
self._attr_extra_state_attributes: dict[str, Any] = {}
5461

@@ -75,6 +82,19 @@ def __init__(self, hass: HomeAssistant, name: str, *_args: Any) -> None:
7582

7683
self._attr_device_info = device_info
7784

85+
@property
86+
def available(self) -> bool:
87+
"""Return True if entity is available.
88+
89+
When using a coordinator (config entry path), availability is
90+
determined by whether the last data update succeeded. This causes
91+
all entities to become unavailable when the WattBox device is
92+
unreachable, and available again when it comes back.
93+
"""
94+
if self._coordinator is not None:
95+
return self._coordinator.last_update_success
96+
return True
97+
7898
async def async_added_to_hass(self) -> None:
7999
"""Register callbacks."""
80100

@@ -83,11 +103,22 @@ def update() -> None:
83103
"""Update the state."""
84104
self.async_schedule_update_ha_state(True)
85105

86-
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
87-
self.hass, self.topic, update
88-
)
106+
if self._coordinator is not None:
107+
# Config entry path: listen to coordinator updates
108+
self.async_on_remove(
109+
self._coordinator.async_add_listener(update)
110+
)
111+
else:
112+
# Legacy YAML path: listen to dispatcher updates
113+
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
114+
self.hass, self.topic, update
115+
)
89116

90117
async def async_will_remove_from_hass(self) -> None:
91118
"""Disconnect dispatcher listener when removed."""
92-
if hasattr(self, "_async_unsub_dispatcher_connect"):
119+
# Coordinator listeners are cleaned up via async_on_remove.
120+
# Only manually disconnect the dispatcher listener (YAML path).
121+
if self._coordinator is None and hasattr(
122+
self, "_async_unsub_dispatcher_connect"
123+
):
93124
self._async_unsub_dispatcher_connect()

custom_components/wattbox/sensor.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from homeassistant.exceptions import PlatformNotReady
1212
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1313
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
14+
from homeassistant.util import slugify
1415

1516
from .const import DOMAIN, DOMAIN_DATA, SENSOR_TYPES
1617
from .entity import WattBoxEntity
@@ -26,7 +27,7 @@ async def async_setup_entry(
2627
"""Set up WattBox sensors from a config entry."""
2728
try:
2829
conf_name: str = entry.data[CONF_NAME]
29-
clean_name = conf_name.replace(" ", "_").lower()
30+
clean_name = slugify(conf_name)
3031
entities: list[WattBoxSensor | WattBoxIntegrationSensor] = []
3132

3233
# Get available resources from entry data or use all sensor types
@@ -75,7 +76,7 @@ async def async_setup_platform(
7576
"""Setup sensor platform (legacy YAML support)."""
7677
try:
7778
conf_name: str = discovery_info[CONF_NAME]
78-
clean_name = conf_name.replace(" ", "_").lower()
79+
clean_name = slugify(conf_name)
7980
entities: list[WattBoxSensor | WattBoxIntegrationSensor] = []
8081

8182
resource: str

0 commit comments

Comments
 (0)