Skip to content

Commit d30eee1

Browse files
committed
timeouts (in theory) added to Zeroconf listener and Dispatcher to suppress spurious "Removes". Virtual Household reverting to showing solar always
1 parent 46b3c82 commit d30eee1

File tree

3 files changed

+82
-34
lines changed

3 files changed

+82
-34
lines changed

custom_components/powersensor/PowersensorDiscoveryService.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
from typing import Optional
3+
import logging
34

45
from homeassistant.core import HomeAssistant
56
from zeroconf import ServiceBrowser, ServiceListener, Zeroconf
@@ -8,11 +9,15 @@
89

910
from .const import DOMAIN
1011

12+
_LOGGER = logging.getLogger(__name__)
13+
1114

1215
class PowersensorServiceListener(ServiceListener):
13-
def __init__(self, hass: HomeAssistant):
16+
def __init__(self, hass: HomeAssistant, debounce_timeout: float = 60):
1417
self._hass = hass
1518
self._plugs = {}
19+
self._pending_removals = {}
20+
self._debounce_seconds = debounce_timeout
1621

1722
def add_service(self, zc, type_, name):
1823
info = self.__add_plug(zc, type_, name)
@@ -25,17 +30,35 @@ def add_service(self, zc, type_, name):
2530
async def _async_service_add(self, *args):
2631
async_dispatcher_send(self._hass, f"{DOMAIN}_zeroconf_add_plug", *args)
2732

33+
async def _async_delayed_remove(self, name):
34+
"""Actually process the removal after delay."""
35+
try:
36+
await asyncio.sleep(self._debounce_seconds)
37+
_LOGGER.info(f"Request to remove service {name} still pending after timeout. Processing remove request...")
38+
if name in self._plugs:
39+
data = self._plugs[name].copy()
40+
del self._plugs[name]
41+
else:
42+
data = None
43+
asyncio.run_coroutine_threadsafe(
44+
self._async_service_remove(name, data),
45+
self._hass.loop
46+
)
47+
except asyncio.CancelledError:
48+
# Task was cancelled because service came back
49+
_LOGGER.info(f"Request to remove service {name} was canceled by request to update or add plug.")
50+
51+
# Either way were done with this task
52+
self._pending_removals.pop(name, None)
53+
2854
def remove_service(self, zc, type_, name):
29-
if name in self._plugs:
30-
data = self._plugs[name].copy()
31-
del self._plugs[name]
32-
else:
33-
data = None
55+
if name in self._pending_removals:
56+
# removal for this service is already pending
57+
return
58+
59+
_LOGGER.info(f"Scheduling removal for {name}")
60+
self._pending_removals[name] = asyncio.create_task(self._async_delayed_remove(name))
3461

35-
asyncio.run_coroutine_threadsafe(
36-
self._async_service_remove(name, data),
37-
self._hass.loop
38-
)
3962

4063
async def _async_service_remove(self, *args):
4164
async_dispatcher_send(self._hass, f"{DOMAIN}_zeroconf_remove_plug", *args)
@@ -52,8 +75,11 @@ async def _async_service_update(self, *args):
5275
async_dispatcher_send(self._hass, f"{DOMAIN}_zeroconf_update_plug", *args)
5376

5477
def __add_plug(self, zc, type_, name):
55-
5678
info = zc.get_service_info(type_, name)
79+
task = self._pending_removals.pop(name, None)
80+
if task:
81+
task.cancel()
82+
_LOGGER.info(f"Cancelled pending removal for {name}")
5783
if info:
5884
self._plugs[name] = {'type': type_,
5985
'name': name,

custom_components/powersensor/PowersensorMessageDispatacher.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@
1111

1212
_LOGGER = logging.getLogger(__name__)
1313
class PowersensorMessageDispatcher:
14-
def __init__(self, hass: HomeAssistant, vhh: VirtualHousehold):
14+
def __init__(self, hass: HomeAssistant, vhh: VirtualHousehold,debounce_timeout: float = 60):
1515
self._hass = hass
1616
self._vhh = vhh
1717
self.plugs = dict()
1818
self._known_plugs = set()
1919
self._known_plug_names = dict()
2020
self.sensors = dict()
2121
self.on_start_sensor_queue = dict()
22+
self._pending_removals = {}
23+
self._debounce_seconds = debounce_timeout
2224
self._unsubscribe_from_signals = [
2325
async_dispatcher_connect(self._hass,
2426
f"{DOMAIN}_sensor_added_to_homeassistant",
@@ -31,7 +33,7 @@ def __init__(self, hass: HomeAssistant, vhh: VirtualHousehold):
3133
self._plug_updated),
3234
async_dispatcher_connect(self._hass,
3335
f"{DOMAIN}_zeroconf_remove_plug",
34-
self._plug_remove),
36+
self._schedule_plug_removal),
3537
async_dispatcher_connect(self._hass,
3638
f"{DOMAIN}_plug_added_to_homeassistant",
3739
self._acknowledge_plug_added_to_homeassistant),
@@ -127,6 +129,11 @@ def add_api(self, network_info):
127129

128130
async def handle_message(self, event: str, message: dict):
129131
mac = message['mac']
132+
task = self._pending_removals.pop(mac, None)
133+
if task:
134+
task.cancel()
135+
_LOGGER.info(f"Cancelled pending removal for {mac} by new message received from plug.")
136+
130137
if mac not in self.plugs.keys():
131138
if mac not in self.sensors:
132139
role = None
@@ -165,11 +172,17 @@ async def _acknowledge_plug_added_to_homeassistant(self, mac_address, host, port
165172
async def _plug_added(self, info):
166173
_LOGGER.debug(f" Request to add plug received: {info}")
167174
network_info = dict()
168-
network_info['mac'] = info['properties'][b'id'].decode('utf-8')
175+
mac = info['properties'][b'id'].decode('utf-8')
176+
network_info['mac'] = mac
169177
network_info['host'] = info['addresses'][0]
170178
network_info['port'] = info['port']
171179
network_info['name'] = info['name']
172180

181+
task = self._pending_removals.pop(mac, None)
182+
if task:
183+
task.cancel()
184+
_LOGGER.info(f"Cancelled pending removal for {mac} by request to add api.")
185+
173186
if self._safe_to_process_plug_queue:
174187
await self.enqueue_plug_for_adding(network_info)
175188
await self.process_plug_queue()
@@ -182,7 +195,10 @@ async def _plug_updated(self, info):
182195
host = info['addresses'][0]
183196
port = info['port']
184197
name = info['name']
185-
198+
task = self._pending_removals.pop(mac, None)
199+
if task:
200+
task.cancel()
201+
_LOGGER.info(f"Cancelled pending removal for {mac} by request to update api.")
186202
if mac in self.plugs:
187203
current_api: PlugApi = self.plugs[mac]
188204
if current_api._listener._ip == host and current_api._listener._port == port:
@@ -202,14 +218,32 @@ async def _plug_updated(self, info):
202218
await self.enqueue_plug_for_adding(network_info)
203219
await self.process_plug_queue()
204220

205-
async def _plug_remove(self,name, info):
221+
async def _schedule_plug_removal(self, name, info):
206222
_LOGGER.debug(f" Request to delete plug received: {info}")
207223
if name in self._known_plug_names:
208224
mac = self._known_plug_names[name]
209225
if mac in self.plugs:
210-
await self.plugs[mac].disconnect()
211-
del self.plugs[mac]
212-
del self._known_plug_names[name]
226+
if mac in self._pending_removals:
227+
# removal for this service is already pending
228+
return
229+
230+
_LOGGER.info(f"Scheduling removal for {name}")
231+
self._pending_removals[name] = asyncio.create_task(self._delayed_plug_remove(name,mac))
213232
else:
214233
_LOGGER.warning(f"Received request to delete api for gateway with name [{name}], but this name"
215234
f"is not associated with an existing PlugAPI. Ignoring...")
235+
236+
async def _delayed_plug_remove(self, name, mac):
237+
"""Actually process the removal after delay."""
238+
try:
239+
await asyncio.sleep(self._debounce_seconds)
240+
_LOGGER.info(f"Request to remove plug {mac} still pending after timeout. Processing remove request...")
241+
await self.plugs[mac].disconnect()
242+
del self.plugs[mac]
243+
del self._known_plug_names[name]
244+
except asyncio.CancelledError:
245+
# Task was cancelled because service came back
246+
_LOGGER.info(f"Request to remove plug {mac} was canceled by request to update, add plug or new message.")
247+
248+
# Either way were done with this task
249+
self._pending_removals.pop(name, None)

custom_components/powersensor/sensor.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,8 @@ async def handle_discovered_plug(plug_mac_address: str, host: str, port: int, na
5454

5555
async def handle_discovered_sensor(sensor_mac: str, sensor_role: str):
5656
if sensor_role == 'solar':
57-
add_solar_vhh_entities = False
58-
if "with_solar" not in my_data.keys() or not my_data["with_solar"] :
59-
add_solar_vhh_entities = True
60-
6157
my_data["with_solar"] = True # Remember for next time we start
6258

63-
if add_solar_vhh_entities:
64-
solar_household_entities = []
65-
for solar_measurement_type in ProductionMeasurements:
66-
household_entities.append(PowersensorHouseholdEntity(vhh, solar_measurement_type))
67-
68-
async_add_entities(solar_household_entities)
69-
7059
new_sensors = [
7160
PowersensorSensorEntity(hass, sensor_mac, SensorMeasurements.Battery),
7261
PowersensorSensorEntity(hass, sensor_mac, SensorMeasurements.WATTS),
@@ -92,10 +81,9 @@ async def handle_discovered_sensor(sensor_mac: str, sensor_role: str):
9281
for measurement_type in ConsumptionMeasurements:
9382
household_entities.append(PowersensorHouseholdEntity(vhh, measurement_type))
9483

95-
if "with_solar" in my_data.keys():
96-
if my_data["with_solar"]:
97-
for measurement_type in ProductionMeasurements:
98-
household_entities.append(PowersensorHouseholdEntity(vhh, measurement_type))
84+
85+
for measurement_type in ProductionMeasurements:
86+
household_entities.append(PowersensorHouseholdEntity(vhh, measurement_type))
9987

10088
async_add_entities(household_entities)
10189

0 commit comments

Comments
 (0)