Skip to content

Commit b601bf4

Browse files
committed
Add heartbeat sensor that pings the wunda hub
Periodically ping the hub to check it's still responding. This should help keep the hub's network interface awake.
1 parent 83671ad commit b601bf4

File tree

6 files changed

+125
-5
lines changed

6 files changed

+125
-5
lines changed

custom_components/wundasmart/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
PLATFORMS: Final[list[Platform]] = [
2424
Platform.CLIMATE,
2525
Platform.WATER_HEATER,
26-
Platform.SENSOR
26+
Platform.SENSOR,
27+
Platform.BINARY_SENSOR
2728
]
2829

2930

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from homeassistant.core import HomeAssistant
2+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
3+
from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorDeviceClass
4+
from homeassistant.config_entries import ConfigEntry
5+
from homeassistant.helpers.event import async_track_time_interval
6+
from homeassistant.const import CONF_HOST
7+
from datetime import timedelta, datetime
8+
from . import WundasmartDataUpdateCoordinator
9+
from .const import DOMAIN, CONF_PING_INTERVAL, DEFAULT_PING_INTERVAL
10+
from icmplib import async_ping, NameLookupError
11+
import logging
12+
13+
_LOGGER = logging.getLogger(__name__)
14+
15+
ICMP_TIMEOUT = 1
16+
17+
18+
async def async_setup_entry(
19+
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
20+
) -> None:
21+
coordinator: WundasmartDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
22+
wunda_ip: str = entry.data[CONF_HOST]
23+
ping_interval = entry.options.get(CONF_PING_INTERVAL, DEFAULT_PING_INTERVAL)
24+
async_add_entities([WundaHeartbeatSensor(coordinator, wunda_ip, ping_interval)])
25+
26+
27+
class WundaHeartbeatSensor(BinarySensorEntity):
28+
"""Heartbeat sensor that pings the Wunda hub periodically."""
29+
30+
def __init__(self, coordinator, wunda_ip, poll_interval):
31+
super().__init__()
32+
self._attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
33+
self._attr_should_poll = False
34+
self._attr_device_info = coordinator.device_info
35+
self._attr_name = (coordinator.device_info or {}).get("name", "Smart HubSwitch") + " Heartbeat"
36+
if coordinator.device_sn:
37+
self._attr_unique_id = f"wunda.{coordinator.device_sn}.heartbeat"
38+
self._wunda_ip = wunda_ip
39+
self._poll_interval = timedelta(seconds=poll_interval)
40+
self._unsub_timer = None
41+
self._state = False
42+
self._attributes = {}
43+
44+
@property
45+
def is_on(self):
46+
return self._state
47+
48+
@property
49+
def extra_state_attributes(self):
50+
return self._attributes
51+
52+
async def async_added_to_hass(self):
53+
await super().async_added_to_hass()
54+
55+
# Schedule periodic updates
56+
self._unsub_timer = async_track_time_interval(
57+
self.hass, self._async_poll, self._poll_interval
58+
)
59+
60+
# Do first poll immediately
61+
await self._async_poll(None)
62+
63+
async def async_will_remove_from_hass(self):
64+
await super().async_will_remove_from_hass()
65+
if self._unsub_timer:
66+
self._unsub_timer()
67+
self._unsub_timer = None
68+
69+
async def _async_poll(self, now):
70+
await self.async_update_ha_state(force_refresh=True)
71+
72+
async def async_update(self):
73+
try:
74+
data = await async_ping(
75+
self._wunda_ip,
76+
count=1,
77+
timeout=ICMP_TIMEOUT,
78+
privileged=False
79+
)
80+
except NameLookupError:
81+
_LOGGER.debug("Error resolving host: %s", self._wunda_ip)
82+
self._state = False
83+
self._attributes = {}
84+
return
85+
except Exception as err:
86+
_LOGGER.debug("Ping failed for %s: %s", self._wunda_ip, err)
87+
self._state = False
88+
self._attributes = {}
89+
return
90+
91+
_LOGGER.debug(
92+
"async_ping %s: reachable=%s sent=%i received=%s",
93+
self._attr_name,
94+
data.is_alive,
95+
data.packets_sent,
96+
data.packets_received,
97+
)
98+
99+
self._state = data.is_alive
100+
if not self._state:
101+
self._attributes = {}
102+
return
103+
104+
self._attributes = {
105+
"lastseen": datetime.now().isoformat(),
106+
"min": data.min_rtt,
107+
"max": data.max_rtt,
108+
"avg": data.avg_rtt,
109+
"jitter": data.jitter,
110+
}

custom_components/wundasmart/config_flow.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from homeassistant import config_entries, core, exceptions
66
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL
7-
from homeassistant.data_entry_flow import FlowResult
7+
from homeassistant.config_entries import ConfigFlowResult
88
from homeassistant.core import callback
99

1010
from .const import *
@@ -137,7 +137,7 @@ class OptionsFlow(config_entries.OptionsFlow):
137137
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
138138
self.config_entry = config_entry
139139

140-
async def async_step_init(self, user_input: dict[str, Any] | None = None) -> FlowResult:
140+
async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
141141
if user_input is not None:
142142
return self.async_create_entry(title="", data=user_input)
143143

@@ -156,6 +156,11 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Flo
156156
CONF_READ_TIMEOUT,
157157
default=self.config_entry.options.get(
158158
CONF_READ_TIMEOUT, DEFAULT_READ_TIMEOUT
159+
)): int,
160+
vol.Optional(
161+
CONF_PING_INTERVAL,
162+
default=self.config_entry.options.get(
163+
CONF_PING_INTERVAL, DEFAULT_PING_INTERVAL
159164
)): int
160165
}
161166

custom_components/wundasmart/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99

1010
CONF_CONNECT_TIMEOUT = "connect_timeout"
1111
CONF_READ_TIMEOUT = "read_timeout"
12+
CONF_PING_INTERVAL = "ping_interval"
1213

1314
DEFAULT_SCAN_INTERVAL = 300
1415
DEFAULT_CONNECT_TIMEOUT = 5
1516
DEFAULT_READ_TIMEOUT = 5
17+
DEFAULT_PING_INTERVAL = 180
1618

1719
@dataclass
1820
class DeviceIdRanges:

custom_components/wundasmart/strings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"data": {
2727
"scan_interval": "[%key:common::config_flow::data::scan_interval%]",
2828
"connect_timeout": "Connect Timeout",
29-
"read_timeout": "Read Timeout"
29+
"read_timeout": "Read Timeout",
30+
"ping_interval": "Heartbeat Interval"
3031
},
3132
"title": "Wundasmart options"
3233
}

custom_components/wundasmart/translations/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"data": {
2727
"scan_interval": "Poll Interval",
2828
"connect_timeout": "Connect Timeout",
29-
"read_timeout": "Read Timeout"
29+
"read_timeout": "Read Timeout",
30+
"ping_interval": "Heartbeat Interval"
3031
},
3132
"title": "Wundasmart options"
3233
}

0 commit comments

Comments
 (0)