Skip to content

Commit 35cf493

Browse files
authored
Merge pull request #24 from DiUS/chore/harmonisation-refactor
chore/harmonisation refactor
2 parents 38f2bb0 + 7a97982 commit 35cf493

25 files changed

+2213
-1234
lines changed

.github/workflows/tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ jobs:
2323
- name: "Run tests"
2424
run: |
2525
./scripts/run-tests.sh --cov-fail-under=100
26+
- name: "Run linting"
27+
run: |
28+
./scripts/lint.sh
Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,55 @@
1+
"""Thread-safe set implementation using asyncio for asynchronous operations."""
2+
13
import asyncio
4+
from collections.abc import Hashable
5+
26

37
class AsyncSet:
48
"""Thread/async-safe set."""
5-
6-
def __init__(self):
7-
self._items = set()
9+
10+
def __init__(self) -> None:
11+
"""Constructor for AsyncSet. Wraps set with an asyncio.Lock."""
12+
self._items: set[Hashable] = set()
813
self._lock = asyncio.Lock()
9-
14+
1015
async def add(self, item):
1116
"""Add item to set."""
1217
async with self._lock:
1318
self._items.add(item)
14-
19+
1520
async def discard(self, item):
1621
"""Remove item if present."""
1722
async with self._lock:
1823
self._items.discard(item)
19-
24+
2025
async def remove(self, item):
2126
"""Remove item, raise KeyError if not present."""
2227
async with self._lock:
2328
self._items.remove(item)
24-
29+
2530
async def pop(self):
2631
"""Remove and return arbitrary item."""
2732
async with self._lock:
2833
return self._items.pop()
29-
34+
3035
async def clear(self):
3136
"""Remove all items."""
3237
async with self._lock:
3338
self._items.clear()
34-
39+
3540
async def copy(self):
3641
"""Return a copy of the set."""
3742
async with self._lock:
3843
return self._items.copy()
39-
44+
4045
def __contains__(self, item):
4146
"""Check if item in set."""
4247
return item in self._items
43-
48+
4449
def __len__(self):
4550
"""Get size of set."""
4651
return len(self._items)
4752

4853
def __bool__(self):
49-
"""Check if set is empty in pythonic way"""
54+
"""Check if set is empty in pythonic way."""
5055
return bool(self._items)

custom_components/powersensor/PowersensorDiscoveryService.py

Lines changed: 74 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,166 @@
1+
"""Utilities to support zeroconf discovery of new plugs on the network."""
2+
13
import asyncio
2-
from typing import Optional
4+
from contextlib import suppress
5+
import logging
6+
7+
from zeroconf import BadTypeInNameException, ServiceBrowser, ServiceListener, Zeroconf
8+
from zeroconf.asyncio import AsyncServiceInfo
39

10+
import homeassistant.components.zeroconf
411
from homeassistant.core import HomeAssistant, callback
5-
from homeassistant.loader import bind_hass
6-
from zeroconf import ServiceBrowser, ServiceListener, Zeroconf
712
from homeassistant.helpers.dispatcher import async_dispatcher_send
8-
import homeassistant.components.zeroconf
13+
from homeassistant.loader import bind_hass
914

1015
from .const import (
1116
ZEROCONF_ADD_PLUG_SIGNAL,
1217
ZEROCONF_REMOVE_PLUG_SIGNAL,
1318
ZEROCONF_UPDATE_PLUG_SIGNAL,
1419
)
1520

16-
import logging
1721
_LOGGER = logging.getLogger(__name__)
1822

1923

2024
class PowersensorServiceListener(ServiceListener):
21-
def __init__(self, hass: HomeAssistant, debounce_timeout: float = 60):
25+
"""A zeroconf service listener that handles the discovery of plugs and signals the dispatcher."""
26+
27+
def __init__(self, hass: HomeAssistant, debounce_timeout: float = 60) -> None:
28+
"""Initialize the listener, set up various buffers to hold info."""
2229
self._hass = hass
23-
self._plugs = {}
24-
self._discoveries = {}
25-
self._pending_removals = {}
30+
self._plugs: dict[str, dict] = {}
31+
self._discoveries: dict[str, AsyncServiceInfo] = {}
32+
self._pending_removals: dict[str, asyncio.Task] = {}
2633
self._debounce_seconds = debounce_timeout
2734

2835
def add_service(self, zc, type_, name):
36+
"""Handle zeroconf messages for adding new devices."""
2937
self.cancel_any_pending_removal(name, "request to add")
3038
info = self.__add_plug(zc, type_, name)
3139
if info:
3240
asyncio.run_coroutine_threadsafe(
33-
self._async_service_add(self._plugs[name]),
34-
self._hass.loop
41+
self._async_service_add(self._plugs[name]), self._hass.loop
3542
)
3643

3744
async def _async_service_add(self, *args):
45+
"""Send add signal to dispatcher."""
3846
self.dispatch(ZEROCONF_ADD_PLUG_SIGNAL, *args)
3947

40-
4148
async def _async_delayed_remove(self, name):
4249
"""Actually process the removal after delay."""
4350
try:
4451
await asyncio.sleep(self._debounce_seconds)
45-
_LOGGER.info(f"Request to remove service {name} still pending after timeout. Processing remove request...")
52+
_LOGGER.info(
53+
"Request to remove service %s still pending after timeout. Processing remove request... ",
54+
name,
55+
)
4656
if name in self._plugs:
4757
data = self._plugs[name].copy()
4858
del self._plugs[name]
4959
else:
5060
data = None
5161
asyncio.run_coroutine_threadsafe(
52-
self._async_service_remove(name, data),
53-
self._hass.loop
62+
self._async_service_remove(name, data), self._hass.loop
5463
)
5564
except asyncio.CancelledError:
5665
# Task was cancelled because service came back
57-
_LOGGER.info(f"Request to remove service {name} was canceled by request to update or add plug.")
66+
_LOGGER.info(
67+
"Request to remove service %s was canceled by request to update or add plug. ",
68+
name,
69+
)
5870
raise
5971
finally:
6072
# Either way were done with this task
6173
self._pending_removals.pop(name, None)
6274

63-
6475
def remove_service(self, zc, type_, name):
76+
"""Handle zeroconf messages for removal of devices."""
6577
if name in self._pending_removals:
6678
# removal for this service is already pending
6779
return
6880

69-
_LOGGER.info(f"Scheduling removal for {name}")
81+
_LOGGER.info("Scheduling removal for %s", name)
7082
self._pending_removals[name] = asyncio.run_coroutine_threadsafe(
71-
self._async_delayed_remove(name),
72-
self._hass.loop
83+
self._async_delayed_remove(name), self._hass.loop
7384
)
7485

7586
async def _async_service_remove(self, *args):
76-
self.dispatch( ZEROCONF_REMOVE_PLUG_SIGNAL, *args)
87+
"""Send remove signal to dispatcher."""
88+
self.dispatch(ZEROCONF_REMOVE_PLUG_SIGNAL, *args)
7789

7890
def update_service(self, zc, type_, name):
91+
"""Handle zeroconf messages for updating device info."""
7992
self.cancel_any_pending_removal(name, "request to update")
8093
info = self.__add_plug(zc, type_, name)
8194
if info:
8295
asyncio.run_coroutine_threadsafe(
83-
self._async_service_update( self._plugs[name]),
84-
self._hass.loop
96+
self._async_service_update(self._plugs[name]), self._hass.loop
8597
)
8698

8799
async def _async_service_update(self, *args):
100+
"""Send update signal to dispatcher."""
88101
# remove from pending tasks if update received
89-
self.dispatch( ZEROCONF_UPDATE_PLUG_SIGNAL, *args)
102+
self.dispatch(ZEROCONF_UPDATE_PLUG_SIGNAL, *args)
90103

91104
async def _async_get_service_info(self, zc, type_, name):
92105
try:
93106
info = await zc.async_get_service_info(type_, name, timeout=3000)
94107
self._discoveries[name] = info
95-
except Exception as e:
96-
_LOGGER.error(f"Error retrieving info for {name}")
97-
108+
except (
109+
TimeoutError,
110+
OSError,
111+
BadTypeInNameException,
112+
NotImplementedError,
113+
) as err: # expected possible exceptions
114+
_LOGGER.error("Error retrieving info for %s: %s", name, err)
98115

99116
def __add_plug(self, zc, type_, name):
100117
info = zc.get_service_info(type_, name)
101118

102119
if info:
103-
self._plugs[name] = {'type': type_,
104-
'name': name,
105-
'addresses': ['.'.join(str(b) for b in addr) for addr in info.addresses],
106-
'port': info.port,
107-
'server': info.server,
108-
'properties': info.properties
109-
}
120+
self._plugs[name] = {
121+
"type": type_,
122+
"name": name,
123+
"addresses": [
124+
".".join(str(b) for b in addr) for addr in info.addresses
125+
],
126+
"port": info.port,
127+
"server": info.server,
128+
"properties": info.properties,
129+
}
110130
return info
111131

112132
def cancel_any_pending_removal(self, name, source):
133+
"""Cancel pending removal and don't send to dispatcher."""
113134
task = self._pending_removals.pop(name, None)
114135
if task:
115136
task.cancel()
116-
_LOGGER.info(f"Cancelled pending removal for {name} by {source}.")
137+
_LOGGER.info("Cancelled pending removal for %s by %s. ", name, source)
117138

118139
@callback
119140
@bind_hass
120-
def dispatch(self,signal_name, *args ):
141+
def dispatch(self, signal_name, *args):
142+
"""Send signal to dispatcher."""
121143
async_dispatcher_send(self._hass, signal_name, *args)
122144

145+
123146
class PowersensorDiscoveryService:
124-
def __init__(self, hass: HomeAssistant, service_type: str = "_powersensor._tcp.local."):
147+
"""A zeroconf service that handles the discovery of plugs."""
148+
149+
def __init__(
150+
self, hass: HomeAssistant, service_type: str = "_powersensor._tcp.local."
151+
) -> None:
152+
"""Constructor for zeroconf service that handles the discovery of plugs."""
125153
self._hass = hass
126154
self.service_type = service_type
127155

128-
self.zc: Optional[Zeroconf] = None
129-
self.listener: Optional[PowersensorServiceListener] = None
130-
self.browser: Optional[ServiceBrowser] = None
156+
self.zc: Zeroconf | None = None
157+
self.listener: PowersensorServiceListener | None = None
158+
self.browser: ServiceBrowser | None = None
131159
self.running = False
132-
self._task: Optional[asyncio.Task] = None
160+
self._task: asyncio.Task | None = None
133161

134162
async def start(self):
135-
"""Start the mDNS discovery service"""
163+
"""Start the mDNS discovery service."""
136164
if self.running:
137165
return
138166

@@ -147,23 +175,19 @@ async def start(self):
147175
self._task = asyncio.create_task(self._run())
148176

149177
async def _run(self):
150-
"""Background task that keeps the service alive"""
151-
try:
178+
"""Background task that keeps the service alive."""
179+
with suppress(asyncio.CancelledError):
152180
while self.running:
153181
await asyncio.sleep(1)
154-
except asyncio.CancelledError:
155-
pass
156182

157183
async def stop(self):
158-
"""Stop the mDNS discovery service"""
184+
"""Stop the mDNS discovery service."""
159185
self.running = False
160186

161187
if self._task:
162188
self._task.cancel()
163-
try:
189+
with suppress(asyncio.CancelledError):
164190
await self._task
165-
except asyncio.CancelledError:
166-
pass
167191

168192
if self.zc:
169193
# self.zc.close()

0 commit comments

Comments
 (0)