From 2c9b22c56593eb2e8c4d0fbb11f90e1551c3a86f Mon Sep 17 00:00:00 2001 From: ArnoutD Date: Mon, 2 Jun 2025 19:25:07 +0200 Subject: [PATCH 1/5] Only up the NodeResponse expected counter when the Stick sends an Accept --- plugwise_usb/connection/manager.py | 2 +- plugwise_usb/connection/sender.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/plugwise_usb/connection/manager.py b/plugwise_usb/connection/manager.py index 74f1203e3..31b073a2a 100644 --- a/plugwise_usb/connection/manager.py +++ b/plugwise_usb/connection/manager.py @@ -38,7 +38,7 @@ def __init__(self) -> None: @property def queue_depth(self) -> int: - return self._sender.processed_messages - self._receiver.processed_messages + return self._sender.expected_responses - self._receiver.processed_messages def correct_received_messages(self, correction: int) -> None: self._receiver.correct_processed_messages(correction) diff --git a/plugwise_usb/connection/sender.py b/plugwise_usb/connection/sender.py index a2dbe8f17..2cae4b947 100644 --- a/plugwise_usb/connection/sender.py +++ b/plugwise_usb/connection/sender.py @@ -38,16 +38,16 @@ def __init__(self, stick_receiver: StickReceiver, transport: Transport) -> None: self._loop = get_running_loop() self._receiver = stick_receiver self._transport = transport - self._processed_msgs = 0 + self._expected_responses = 0 self._stick_response: Future[StickResponse] | None = None self._stick_lock = Lock() self._current_request: None | PlugwiseRequest = None self._unsubscribe_stick_response: Callable[[], None] | None = None @property - def processed_messages(self) -> int: + def expected_responses(self) -> int: """Return the number of processed messages.""" - return self._processed_msgs + return self._expected_responses async def start(self) -> None: """Start the sender.""" @@ -115,6 +115,7 @@ async def write_request_to_port(self, request: PlugwiseRequest) -> None: ) ) else: + self._expected_responses += 1 request.seq_id = response.seq_id await request.subscribe_to_response( self._receiver.subscribe_to_stick_responses, @@ -141,7 +142,7 @@ async def write_request_to_port(self, request: PlugwiseRequest) -> None: finally: self._stick_response.cancel() self._stick_lock.release() - self._processed_msgs += 1 + async def _process_stick_response(self, response: StickResponse) -> None: """Process stick response.""" From 931f0b77606736987b79e58b2d92c7840b93fbc2 Mon Sep 17 00:00:00 2001 From: ArnoutD Date: Mon, 2 Jun 2025 19:26:06 +0200 Subject: [PATCH 2/5] Log type Node type in the representation --- plugwise_usb/messages/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 10104a986..275c68a3a 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -651,7 +651,7 @@ def frequency(self) -> int: def __repr__(self) -> str: """Convert request into writable str.""" - return f"{super().__repr__()[:-1]}, log_address_pointer={self._logaddress_pointer.value})" + return f"{super().__repr__()[:-1]}, hardware={self.hardware[4:10]}, log_address_pointer={self._logaddress_pointer.value})" class EnergyCalibrationResponse(PlugwiseResponse): From e9df7b9b934518b6374e6e949deb5c39dbf91293 Mon Sep 17 00:00:00 2001 From: ArnoutD Date: Mon, 2 Jun 2025 19:29:17 +0200 Subject: [PATCH 3/5] Only query logs for today on initialisation.. becomes a small set ... back to gather instead of loop --- plugwise_usb/nodes/circle.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 04c09fa4f..5d724e306 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -2,9 +2,10 @@ from __future__ import annotations -from asyncio import Task, create_task +from asyncio import Task, create_task, gather from collections.abc import Awaitable, Callable from dataclasses import replace +from math import floor from datetime import UTC, datetime from functools import wraps import logging @@ -443,7 +444,8 @@ async def get_missing_energy_logs(self) -> None: "Start with initial energy request for the last 10 log addresses for node %s.", self._mac_in_str, ) - total_addresses = 11 + + total_addresses = int(floor(datetime.now(tz=UTC).hour / 4) + 1) log_address = self._current_log_address while total_addresses > 0: await self.energy_log_update(log_address) @@ -470,6 +472,13 @@ async def get_missing_energy_logs(self) -> None: for task in tasks: await task + missing_addresses = sorted(missing_addresses, reverse=True) + tasks = [ + create_task(self.energy_log_update(address)) + for address in missing_addresses + ] + await gather(*tasks, return_exceptions=True) + if self._cache_enabled: await self._energy_log_records_save_to_cache() From 27e327331d72915e47be9abfc3d7afc433625600 Mon Sep 17 00:00:00 2001 From: ArnoutD Date: Mon, 2 Jun 2025 19:35:22 +0200 Subject: [PATCH 4/5] ruff check --fix --- plugwise_usb/connection/manager.py | 15 +++++++++++++++ plugwise_usb/connection/queue.py | 1 - plugwise_usb/connection/sender.py | 2 +- plugwise_usb/helpers/cache.py | 2 +- plugwise_usb/messages/properties.py | 2 +- plugwise_usb/messages/requests.py | 2 +- plugwise_usb/nodes/circle.py | 4 ++-- plugwise_usb/nodes/helpers/pulses.py | 2 +- plugwise_usb/nodes/helpers/subscription.py | 7 ++++++- plugwise_usb/nodes/scan.py | 2 +- plugwise_usb/nodes/sed.py | 2 +- pyproject.toml | 7 ++++--- tests/test_usb.py | 8 ++++---- 13 files changed, 38 insertions(+), 18 deletions(-) diff --git a/plugwise_usb/connection/manager.py b/plugwise_usb/connection/manager.py index 31b073a2a..1adef2248 100644 --- a/plugwise_usb/connection/manager.py +++ b/plugwise_usb/connection/manager.py @@ -38,9 +38,24 @@ def __init__(self) -> None: @property def queue_depth(self) -> int: + """Calculate and return the current depth of the message queue. + + Returns: + int: The number of expected responses that have not yet been processed. + + """ return self._sender.expected_responses - self._receiver.processed_messages def correct_received_messages(self, correction: int) -> None: + """Adjusts the count of received messages by applying a correction value. + + Args: + correction (int): The number to adjust the processed messages count by. Positive values increase the count, negative values decrease it. + + Returns: + None + + """ self._receiver.correct_processed_messages(correction) @property diff --git a/plugwise_usb/connection/queue.py b/plugwise_usb/connection/queue.py index d5d714fd2..ebf8f5abd 100644 --- a/plugwise_usb/connection/queue.py +++ b/plugwise_usb/connection/queue.py @@ -76,7 +76,6 @@ async def stop(self) -> None: async def submit(self, request: PlugwiseRequest) -> PlugwiseResponse | None: """Add request to queue and return the received node-response when applicable. - Raises an error when something fails. """ if request.waiting_for_response: diff --git a/plugwise_usb/connection/sender.py b/plugwise_usb/connection/sender.py index 2cae4b947..e845b23d8 100644 --- a/plugwise_usb/connection/sender.py +++ b/plugwise_usb/connection/sender.py @@ -48,7 +48,7 @@ def __init__(self, stick_receiver: StickReceiver, transport: Transport) -> None: def expected_responses(self) -> int: """Return the number of processed messages.""" return self._expected_responses - + async def start(self) -> None: """Start the sender.""" # Subscribe to ACCEPT stick responses, which contain the seq_id we need. diff --git a/plugwise_usb/helpers/cache.py b/plugwise_usb/helpers/cache.py index 256a59094..dc089b361 100644 --- a/plugwise_usb/helpers/cache.py +++ b/plugwise_usb/helpers/cache.py @@ -59,7 +59,7 @@ async def initialize_cache(self, create_root_folder: bool = False) -> None: cache_dir = self._get_writable_os_dir() await makedirs(cache_dir, exist_ok=True) self._cache_path = cache_dir - + self._cache_file = os_path_join(self._cache_path, self._file_name) self._cache_file_exists = await ospath.exists(self._cache_file) self._initialized = True diff --git a/plugwise_usb/messages/properties.py b/plugwise_usb/messages/properties.py index 9cc51e861..c98a7445e 100644 --- a/plugwise_usb/messages/properties.py +++ b/plugwise_usb/messages/properties.py @@ -391,7 +391,7 @@ def serialize(self) -> bytes: def deserialize(self, val: bytes) -> None: """Convert data into integer value based on log address formatted data.""" if val == b"00000000": - self._value = int(0) + self._value = 0 return Int.deserialize(self, val) self._value = (self.value - LOGADDR_OFFSET) // 32 diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index d01bd6b08..89672d57b 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -1263,7 +1263,7 @@ class CircleMeasureIntervalRequest(PlugwiseRequest): FIXME: Make sure production interval is a multiply of consumption !! - Response message: NodeResponse with ack-type POWER_LOG_INTERVAL_ACCEPTED + Response message: NodeResponse with ack-type POWER_LOG_INTERVAL_ACCEPTED """ _identifier = b"0057" diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 5d724e306..0fb705ba7 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -5,10 +5,10 @@ from asyncio import Task, create_task, gather from collections.abc import Awaitable, Callable from dataclasses import replace -from math import floor from datetime import UTC, datetime from functools import wraps import logging +from math import floor from typing import Any, TypeVar, cast from ..api import ( @@ -444,7 +444,7 @@ async def get_missing_energy_logs(self) -> None: "Start with initial energy request for the last 10 log addresses for node %s.", self._mac_in_str, ) - + total_addresses = int(floor(datetime.now(tz=UTC).hour / 4) + 1) log_address = self._current_log_address while total_addresses > 0: diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 1bd8e81dc..269ecc49e 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -7,7 +7,7 @@ import logging from typing import Final -from ...constants import LOGADDR_MAX, MINUTE_IN_SECONDS, DAY_IN_HOURS +from ...constants import DAY_IN_HOURS, LOGADDR_MAX, MINUTE_IN_SECONDS from ...exceptions import EnergyError _LOGGER = logging.getLogger(__name__) diff --git a/plugwise_usb/nodes/helpers/subscription.py b/plugwise_usb/nodes/helpers/subscription.py index a3b2c0554..94773373a 100644 --- a/plugwise_usb/nodes/helpers/subscription.py +++ b/plugwise_usb/nodes/helpers/subscription.py @@ -7,7 +7,6 @@ from dataclasses import dataclass from typing import Any - from ...api import NodeFeature @@ -21,7 +20,13 @@ class NodeFeatureSubscription: class FeaturePublisher: """Base Class to call awaitable of subscription when event happens.""" + def __init__(self) -> None: + """Initializes the instance with an empty dictionary to store feature update subscribers. + + The dictionary maps callback functions (Callable[[], None]) to their corresponding + NodeFeatureSubscription objects, allowing management of feature update subscriptions. + """ self._feature_update_subscribers: dict[ Callable[[], None], NodeFeatureSubscription, diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index ce90c69fb..36ad9c17b 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -49,7 +49,7 @@ # Sensitivity values for motion sensor configuration SENSITIVITY_HIGH_VALUE = 20 # 0x14 -SENSITIVITY_MEDIUM_VALUE = 30 # 0x1E +SENSITIVITY_MEDIUM_VALUE = 30 # 0x1E SENSITIVITY_OFF_VALUE = 255 # 0xFF # endregion diff --git a/plugwise_usb/nodes/sed.py b/plugwise_usb/nodes/sed.py index a583142bb..11cf5e4d7 100644 --- a/plugwise_usb/nodes/sed.py +++ b/plugwise_usb/nodes/sed.py @@ -239,7 +239,7 @@ async def set_awake_duration(self, seconds: int) -> bool: if self._battery_config.awake_duration == seconds: return False - + self._new_battery_config = replace( self._new_battery_config, awake_duration=seconds ) diff --git a/pyproject.toml b/pyproject.toml index bcb1c4b06..9fc592ae8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -258,8 +258,7 @@ lint.select = [ "S316", # suspicious-xml-expat-builder-usage "S317", # suspicious-xml-sax-usage "S318", # suspicious-xml-mini-dom-usage - "S319", # suspicious-xml-pull-dom-usage - "S320", # suspicious-xmle-tree-usage + "S319", # suspicious-xml-pull-dom-usag "S601", # paramiko-call "S602", # subprocess-popen-with-shell-equals-true "S604", # call-with-shell-equals-true @@ -278,7 +277,7 @@ lint.select = [ "TID251", # Banned imports "TRY004", # Prefer TypeError exception for invalid type # "TRY200", # TRY200 has been remapped to B904 - "TRY302", # Remove exception handler; error is immediately re-raised + "TRY203", # Remove exception handler; error is immediately re-raised "UP", # pyupgrade "W", # pycodestyle ] @@ -286,7 +285,9 @@ lint.select = [ lint.ignore = [ "D202", # No blank lines allowed after function docstring "D203", # 1 blank line required before class docstring + "D205", # 1 blank line required between summary line and description "D213", # Multi-line docstring summary should start at the second line + "D401", # First line of docstring should be in imperative mood "D406", # Section name should end with a newline "D407", # Section name underlining "E501", # line too long diff --git a/tests/test_usb.py b/tests/test_usb.py index 0631b7983..bc61fe1ff 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -585,7 +585,7 @@ async def test_stick_node_discovered_subscription( ) assert stick.nodes["5555555555555555"].node_info.version == "080007" assert stick.nodes["5555555555555555"].node_info.model == "Scan" - assert stick.nodes["5555555555555555"].node_info.model_type == None + assert stick.nodes["5555555555555555"].node_info.model_type is None assert stick.nodes["5555555555555555"].available assert stick.nodes["5555555555555555"].node_info.is_battery_powered assert sorted(stick.nodes["5555555555555555"].features) == sorted( @@ -1142,7 +1142,7 @@ def test_pulse_collection_consumption( # Collected pulses last day: assert tst_consumption.collected_pulses( test_timestamp - td(hours=24), is_consumption=True - ) == (45 + 22861, pulse_update_4) + ) == (45 + 22861, pulse_update_4) # pulse-count of 2500 is ignored, the code does not export this incorrect value tst_consumption.add_log(100, 2, (fixed_this_hour + td(hours=1)), 2222) @@ -2568,7 +2568,7 @@ async def test_node_discovery_and_load( ) assert stick.nodes["5555555555555555"].node_info.version == "080007" assert stick.nodes["5555555555555555"].node_info.model == "Scan" - assert stick.nodes["5555555555555555"].node_info.model_type == None + assert stick.nodes["5555555555555555"].node_info.model_type is None assert stick.nodes["5555555555555555"].available assert stick.nodes["5555555555555555"].node_info.is_battery_powered assert sorted(stick.nodes["5555555555555555"].features) == sorted( @@ -2633,7 +2633,7 @@ async def test_node_discovery_and_load( ) assert stick.nodes["8888888888888888"].node_info.version == "070051" assert stick.nodes["8888888888888888"].node_info.model == "Switch" - assert stick.nodes["8888888888888888"].node_info.model_type == None + assert stick.nodes["8888888888888888"].node_info.model_type is None assert stick.nodes["8888888888888888"].available assert stick.nodes["8888888888888888"].node_info.is_battery_powered assert sorted(stick.nodes["8888888888888888"].features) == sorted( From f521fa7a07ed75fc4e3a87ddce47d5c43e77692e Mon Sep 17 00:00:00 2001 From: ArnoutD Date: Mon, 2 Jun 2025 19:38:13 +0200 Subject: [PATCH 5/5] ruff format according to ruless --- plugwise_usb/__init__.py | 49 ++++++++++------------ plugwise_usb/api.py | 1 - plugwise_usb/connection/__init__.py | 8 +--- plugwise_usb/connection/queue.py | 7 +++- plugwise_usb/connection/receiver.py | 1 + plugwise_usb/connection/sender.py | 15 +++++-- plugwise_usb/constants.py | 1 + plugwise_usb/helpers/cache.py | 29 ++++++++----- plugwise_usb/helpers/util.py | 3 +- plugwise_usb/messages/__init__.py | 1 + plugwise_usb/messages/properties.py | 5 ++- plugwise_usb/messages/requests.py | 4 +- plugwise_usb/messages/responses.py | 11 ++--- plugwise_usb/network/__init__.py | 13 ++++-- plugwise_usb/network/cache.py | 4 +- plugwise_usb/nodes/celsius.py | 1 + plugwise_usb/nodes/circle.py | 5 +-- plugwise_usb/nodes/helpers/__init__.py | 2 + plugwise_usb/nodes/helpers/counter.py | 17 +++++--- plugwise_usb/nodes/helpers/pulses.py | 4 +- plugwise_usb/nodes/helpers/subscription.py | 5 ++- plugwise_usb/nodes/node.py | 10 ++--- plugwise_usb/nodes/scan.py | 16 ++++--- plugwise_usb/nodes/sed.py | 2 +- plugwise_usb/nodes/stealth.py | 1 + plugwise_usb/nodes/switch.py | 11 +++-- tests/stick_test_data.py | 4 +- tests/test_usb.py | 1 - 28 files changed, 137 insertions(+), 94 deletions(-) diff --git a/plugwise_usb/__init__.py b/plugwise_usb/__init__.py index 8870a24fd..da049f2e3 100644 --- a/plugwise_usb/__init__.py +++ b/plugwise_usb/__init__.py @@ -20,41 +20,43 @@ FuncT = TypeVar("FuncT", bound=Callable[..., Any]) -NOT_INITIALIZED_STICK_ERROR: Final[StickError] = StickError("Cannot load nodes when network is not initialized") +NOT_INITIALIZED_STICK_ERROR: Final[StickError] = StickError( + "Cannot load nodes when network is not initialized" +) _LOGGER = logging.getLogger(__name__) def raise_not_connected(func: FuncT) -> FuncT: """Validate existence of an active connection to Stick. Raise StickError when there is no active connection.""" + @wraps(func) def decorated(*args: Any, **kwargs: Any) -> Any: if not args[0].is_connected: - raise StickError( - "Not connected to USB-Stick, connect to USB-stick first." - ) + raise StickError("Not connected to USB-Stick, connect to USB-stick first.") return func(*args, **kwargs) + return cast(FuncT, decorated) def raise_not_initialized(func: FuncT) -> FuncT: """Validate if active connection is initialized. Raise StickError when not initialized.""" + @wraps(func) def decorated(*args: Any, **kwargs: Any) -> Any: if not args[0].is_initialized: raise StickError( - "Connection to USB-Stick is not initialized, " + - "initialize USB-stick first." + "Connection to USB-Stick is not initialized, " + + "initialize USB-stick first." ) return func(*args, **kwargs) + return cast(FuncT, decorated) class Stick: """Plugwise connection stick.""" - def __init__( - self, port: str | None = None, cache_enabled: bool = True - ) -> None: + def __init__(self, port: str | None = None, cache_enabled: bool = True) -> None: """Initialize Stick.""" self._loop = get_running_loop() self._loop.set_debug(True) @@ -170,13 +172,8 @@ def port(self) -> str | None: @port.setter def port(self, port: str) -> None: """Path to serial port of USB-Stick.""" - if ( - self._controller.is_connected - and port != self._port - ): - raise StickError( - "Unable to change port while connected. Disconnect first" - ) + if self._controller.is_connected and port != self._port: + raise StickError("Unable to change port while connected. Disconnect first") self._port = port @@ -238,7 +235,9 @@ def subscribe_to_node_events( Returns the function to be called to unsubscribe later. """ if self._network is None: - raise SubscriptionError("Unable to subscribe to node events without network connection initialized") + raise SubscriptionError( + "Unable to subscribe to node events without network connection initialized" + ) return self._network.subscribe_to_node_events( node_event_callback, events, @@ -252,9 +251,7 @@ def _validate_node_discovery(self) -> None: if self._network is None or not self._network.is_running: raise StickError("Plugwise network node discovery is not active.") - async def setup( - self, discover: bool = True, load: bool = True - ) -> None: + async def setup(self, discover: bool = True, load: bool = True) -> None: """Fully connect, initialize USB-Stick and discover all connected nodes.""" if not self.is_connected: await self.connect() @@ -271,8 +268,8 @@ async def connect(self, port: str | None = None) -> None: """Connect to USB-Stick. Raises StickError if connection fails.""" if self._controller.is_connected: raise StickError( - f"Already connected to {self._port}, " + - "Close existing connection before (re)connect." + f"Already connected to {self._port}, " + + "Close existing connection before (re)connect." ) if port is not None: @@ -280,8 +277,8 @@ async def connect(self, port: str | None = None) -> None: if self._port is None: raise StickError( - "Unable to connect. " + - "Path to USB-Stick is not defined, set port property first" + "Unable to connect. " + + "Path to USB-Stick is not defined, set port property first" ) await self._controller.connect_to_stick( @@ -319,9 +316,7 @@ async def load_nodes(self) -> bool: if self._network is None: raise NOT_INITIALIZED_STICK_ERROR if not self._network.is_running: - raise StickError( - "Cannot load nodes when network is not started" - ) + raise StickError("Cannot load nodes when network is not started") return await self._network.discover_nodes(load=True) @raise_not_connected diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index b1777fe56..c943515b9 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -704,5 +704,4 @@ async def message_for_node(self, message: Any) -> None: """ - # endregion diff --git a/plugwise_usb/connection/__init__.py b/plugwise_usb/connection/__init__.py index 06879100b..ff17f1400 100644 --- a/plugwise_usb/connection/__init__.py +++ b/plugwise_usb/connection/__init__.py @@ -209,9 +209,7 @@ async def get_node_details( ping_response: NodePingResponse | None = None if ping_first: # Define ping request with one retry - ping_request = NodePingRequest( - self.send, bytes(mac, UTF8), retries=1 - ) + ping_request = NodePingRequest(self.send, bytes(mac, UTF8), retries=1) try: ping_response = await ping_request.send() except StickError: @@ -219,9 +217,7 @@ async def get_node_details( if ping_response is None: return (None, None) - info_request = NodeInfoRequest( - self.send, bytes(mac, UTF8), retries=1 - ) + info_request = NodeInfoRequest(self.send, bytes(mac, UTF8), retries=1) try: info_response = await info_request.send() except StickError: diff --git a/plugwise_usb/connection/queue.py b/plugwise_usb/connection/queue.py index ebf8f5abd..cbdb50136 100644 --- a/plugwise_usb/connection/queue.py +++ b/plugwise_usb/connection/queue.py @@ -102,7 +102,8 @@ async def submit(self, request: PlugwiseRequest) -> PlugwiseResponse | None: if isinstance(request, NodePingRequest): # For ping requests it is expected to receive timeouts, so lower log level _LOGGER.debug( - "%s, cancel because timeout is expected for NodePingRequests", exc + "%s, cancel because timeout is expected for NodePingRequests", + exc, ) elif request.resend: _LOGGER.debug("%s, retrying", exc) @@ -146,7 +147,9 @@ async def _send_queue_worker(self) -> None: if self._stick.queue_depth > 3: await sleep(0.125) if self._stick.queue_depth > 3: - _LOGGER.warning("Awaiting plugwise responses %d", self._stick.queue_depth) + _LOGGER.warning( + "Awaiting plugwise responses %d", self._stick.queue_depth + ) await self._stick.write_to_stick(request) self._submit_queue.task_done() diff --git a/plugwise_usb/connection/receiver.py b/plugwise_usb/connection/receiver.py index d2c33d882..f79708391 100644 --- a/plugwise_usb/connection/receiver.py +++ b/plugwise_usb/connection/receiver.py @@ -513,4 +513,5 @@ async def _notify_node_response_subscribers( name=f"Postpone subscription task for {node_response.seq_id!r} retry {node_response.retries}", ) + # endregion diff --git a/plugwise_usb/connection/sender.py b/plugwise_usb/connection/sender.py index e845b23d8..1cb32de37 100644 --- a/plugwise_usb/connection/sender.py +++ b/plugwise_usb/connection/sender.py @@ -79,7 +79,9 @@ async def write_request_to_port(self, request: PlugwiseRequest) -> None: # Write message to serial port buffer serialized_data = request.serialize() - _LOGGER.debug("write_request_to_port | Write %s to port as %s", request, serialized_data) + _LOGGER.debug( + "write_request_to_port | Write %s to port as %s", request, serialized_data + ) self._transport.write(serialized_data) # Don't timeout when no response expected if not request.no_response: @@ -106,7 +108,11 @@ async def write_request_to_port(self, request: PlugwiseRequest) -> None: _LOGGER.warning("Exception for %s: %s", request, exc) request.assign_error(exc) else: - _LOGGER.debug("write_request_to_port | USB-Stick replied with %s to request %s", response, request) + _LOGGER.debug( + "write_request_to_port | USB-Stick replied with %s to request %s", + response, + request, + ) if response.response_type == StickResponseType.ACCEPT: if request.seq_id is not None: request.assign_error( @@ -121,7 +127,9 @@ async def write_request_to_port(self, request: PlugwiseRequest) -> None: self._receiver.subscribe_to_stick_responses, self._receiver.subscribe_to_node_responses, ) - _LOGGER.debug("write_request_to_port | request has subscribed : %s", request) + _LOGGER.debug( + "write_request_to_port | request has subscribed : %s", request + ) elif response.response_type == StickResponseType.TIMEOUT: _LOGGER.warning( "USB-Stick directly responded with communication timeout for %s", @@ -143,7 +151,6 @@ async def write_request_to_port(self, request: PlugwiseRequest) -> None: self._stick_response.cancel() self._stick_lock.release() - async def _process_stick_response(self, response: StickResponse) -> None: """Process stick response.""" if self._stick_response is None or self._stick_response.done(): diff --git a/plugwise_usb/constants.py b/plugwise_usb/constants.py index 42539e97f..7be02c34b 100644 --- a/plugwise_usb/constants.py +++ b/plugwise_usb/constants.py @@ -1,4 +1,5 @@ """Plugwise Stick constants.""" + from __future__ import annotations import datetime as dt diff --git a/plugwise_usb/helpers/cache.py b/plugwise_usb/helpers/cache.py index dc089b361..30e7a78ef 100644 --- a/plugwise_usb/helpers/cache.py +++ b/plugwise_usb/helpers/cache.py @@ -53,7 +53,9 @@ async def initialize_cache(self, create_root_folder: bool = False) -> None: """Set (and create) the plugwise cache directory to store cache file.""" if self._root_dir != "": if not create_root_folder and not await ospath.exists(self._root_dir): - raise CacheError(f"Unable to initialize caching. Cache folder '{self._root_dir}' does not exists.") + raise CacheError( + f"Unable to initialize caching. Cache folder '{self._root_dir}' does not exists." + ) cache_dir = self._root_dir else: cache_dir = self._get_writable_os_dir() @@ -72,13 +74,17 @@ def _get_writable_os_dir(self) -> str: if os_name == "nt": if (data_dir := os_getenv("APPDATA")) is not None: return os_path_join(data_dir, CACHE_DIR) - raise CacheError("Unable to detect writable cache folder based on 'APPDATA' environment variable.") + raise CacheError( + "Unable to detect writable cache folder based on 'APPDATA' environment variable." + ) return os_path_join(os_path_expand_user("~"), CACHE_DIR) async def write_cache(self, data: dict[str, str], rewrite: bool = False) -> None: - """"Save information to cache file.""" + """ "Save information to cache file.""" if not self._initialized: - raise CacheError(f"Unable to save cache. Initialize cache file '{self._file_name}' first.") + raise CacheError( + f"Unable to save cache. Initialize cache file '{self._file_name}' first." + ) current_data: dict[str, str] = {} if not rewrite: @@ -111,19 +117,20 @@ async def write_cache(self, data: dict[str, str], rewrite: bool = False) -> None if not self._cache_file_exists: self._cache_file_exists = True _LOGGER.debug( - "Saved %s lines to cache file %s", - str(len(data)), - self._cache_file + "Saved %s lines to cache file %s", str(len(data)), self._cache_file ) async def read_cache(self) -> dict[str, str]: """Return current data from cache file.""" if not self._initialized: - raise CacheError(f"Unable to save cache. Initialize cache file '{self._file_name}' first.") + raise CacheError( + f"Unable to save cache. Initialize cache file '{self._file_name}' first." + ) current_data: dict[str, str] = {} if not self._cache_file_exists: _LOGGER.debug( - "Cache file '%s' does not exists, return empty cache data", self._cache_file + "Cache file '%s' does not exists, return empty cache data", + self._cache_file, ) return current_data try: @@ -146,10 +153,10 @@ async def read_cache(self) -> dict[str, str]: _LOGGER.warning( "Skip invalid line '%s' in cache file %s", data, - str(self._cache_file) + str(self._cache_file), ) break - current_data[data[:index_separator]] = data[index_separator + 1:] + current_data[data[:index_separator]] = data[index_separator + 1 :] return current_data async def delete_cache(self) -> None: diff --git a/plugwise_usb/helpers/util.py b/plugwise_usb/helpers/util.py index 8e85878fc..19f9d1159 100644 --- a/plugwise_usb/helpers/util.py +++ b/plugwise_usb/helpers/util.py @@ -1,4 +1,5 @@ """Plugwise utility helpers.""" + from __future__ import annotations import re @@ -21,7 +22,7 @@ def validate_mac(mac: str) -> bool: return True -def version_to_model(version: str | None) -> tuple[str|None, str]: +def version_to_model(version: str | None) -> tuple[str | None, str]: """Translate hardware_version to device type.""" if version is None: return (None, "Unknown") diff --git a/plugwise_usb/messages/__init__.py b/plugwise_usb/messages/__init__.py index 9f2e7260f..e9c9306e6 100644 --- a/plugwise_usb/messages/__init__.py +++ b/plugwise_usb/messages/__init__.py @@ -19,6 +19,7 @@ class Priority(Enum): MEDIUM = 2 LOW = 3 + class PlugwiseMessage: """Plugwise message base class.""" diff --git a/plugwise_usb/messages/properties.py b/plugwise_usb/messages/properties.py index c98a7445e..566c47c06 100644 --- a/plugwise_usb/messages/properties.py +++ b/plugwise_usb/messages/properties.py @@ -9,7 +9,10 @@ from ..exceptions import MessageError from ..helpers.util import int_to_uint -DESERIALIZE_ERROR: Final[MessageError] = MessageError("Unable to return value. Deserialize data first") +DESERIALIZE_ERROR: Final[MessageError] = MessageError( + "Unable to return value. Deserialize data first" +) + class BaseType: """Generic single instance property.""" diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index 89672d57b..fc8343b81 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -311,7 +311,9 @@ async def _process_stick_response(self, stick_response: StickResponse) -> None: self, ) - async def _send_request(self, suppress_node_errors=False) -> PlugwiseResponse | None: + async def _send_request( + self, suppress_node_errors=False + ) -> PlugwiseResponse | None: """Send request.""" if self._send_fn is None: return None diff --git a/plugwise_usb/messages/responses.py b/plugwise_usb/messages/responses.py index 275c68a3a..cc0806a1b 100644 --- a/plugwise_usb/messages/responses.py +++ b/plugwise_usb/messages/responses.py @@ -778,19 +778,19 @@ def log_data(self) -> dict[int, tuple[datetime | None, int | None]]: if self.logdate1.value_set: log_data[1] = (self.logdate1.value, self.pulses1.value) else: - log_data[1] = (None, None) + log_data[1] = (None, None) if self.logdate2.value_set: log_data[2] = (self.logdate2.value, self.pulses2.value) else: - log_data[2] = (None, None) + log_data[2] = (None, None) if self.logdate3.value_set: log_data[3] = (self.logdate3.value, self.pulses3.value) else: - log_data[3] = (None, None) + log_data[3] = (None, None) if self.logdate4.value_set: log_data[4] = (self.logdate4.value, self.pulses4.value) else: - log_data[4] = (None, None) + log_data[4] = (None, None) return log_data def __repr__(self) -> str: @@ -856,12 +856,13 @@ def __init__(self) -> None: @property def switch_state(self) -> bool: """Return state of switch (True = On, False = Off).""" - return (self._power_state.value != 0) + return self._power_state.value != 0 def __repr__(self) -> str: """Convert request into writable str.""" return f"{super().__repr__()[:-1]}, power_state={self._power_state.value}, group={self.group.value})" + class NodeFeaturesResponse(PlugwiseResponse): """Returns supported features of node. diff --git a/plugwise_usb/network/__init__.py b/plugwise_usb/network/__init__.py index 877bbaf1d..cb53ec580 100644 --- a/plugwise_usb/network/__init__.py +++ b/plugwise_usb/network/__init__.py @@ -258,7 +258,9 @@ async def node_join_available_message(self, response: PlugwiseResponse) -> bool: ) mac = response.mac_decoded - _LOGGER.debug("node_join_available_message | sending NodeAddRequest for %s", mac) + _LOGGER.debug( + "node_join_available_message | sending NodeAddRequest for %s", mac + ) try: result = await self.register_node(mac) except NodeError as exc: @@ -556,9 +558,13 @@ async def set_energy_intervals( if production < 0: raise ValueError("Production interval must be non-negative") if production > 0 and production % consumption != 0: - raise ValueError("Production interval must be a multiple of consumption interval") + raise ValueError( + "Production interval must be a multiple of consumption interval" + ) - _LOGGER.debug("set_energy_intervals | cons=%s, prod=%s", consumption, production) + _LOGGER.debug( + "set_energy_intervals | cons=%s, prod=%s", consumption, production + ) request = CircleMeasureIntervalRequest( self._controller.send, bytes(mac, UTF8), consumption, production ) @@ -612,4 +618,3 @@ async def _notify_node_event_subscribers(self, event: NodeEvent, mac: str) -> No callback_list.append(callback(event, mac)) if len(callback_list) > 0: await gather(*callback_list) - diff --git a/plugwise_usb/network/cache.py b/plugwise_usb/network/cache.py index a9fb1eda2..f73ef3add 100644 --- a/plugwise_usb/network/cache.py +++ b/plugwise_usb/network/cache.py @@ -34,7 +34,9 @@ async def save_cache(self) -> None: node_value = "" else: node_value = str(node_type) - cache_data_to_save[str(address)] = f"{mac}{CACHE_DATA_SEPARATOR}{node_value}" + cache_data_to_save[str(address)] = ( + f"{mac}{CACHE_DATA_SEPARATOR}{node_value}" + ) await self.write_cache(cache_data_to_save) async def clear_cache(self) -> None: diff --git a/plugwise_usb/nodes/celsius.py b/plugwise_usb/nodes/celsius.py index c8245a747..6807c369d 100644 --- a/plugwise_usb/nodes/celsius.py +++ b/plugwise_usb/nodes/celsius.py @@ -2,6 +2,7 @@ TODO: Finish node """ + from __future__ import annotations import logging diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 0fb705ba7..7fd3d866d 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -510,10 +510,7 @@ async def energy_log_update(self, address: int | None) -> bool: for _slot in range(4, 0, -1): log_timestamp, log_pulses = response.log_data[_slot] _LOGGER.debug( - "In slot=%s: pulses=%s, timestamp=%s", - _slot, - log_pulses, - log_timestamp + "In slot=%s: pulses=%s, timestamp=%s", _slot, log_pulses, log_timestamp ) if log_timestamp is None or log_pulses is None: self._energy_counters.add_empty_log(response.log_address, _slot) diff --git a/plugwise_usb/nodes/helpers/__init__.py b/plugwise_usb/nodes/helpers/__init__.py index 1ef0b8a86..8b32732cb 100644 --- a/plugwise_usb/nodes/helpers/__init__.py +++ b/plugwise_usb/nodes/helpers/__init__.py @@ -25,9 +25,11 @@ class EnergyCalibration: def raise_not_loaded(func: FuncT) -> FuncT: """Raise NodeError when node is not loaded.""" + @wraps(func) def decorated(*args: Any, **kwargs: Any) -> Any: if not args[0].is_loaded: raise NodeError(f"Node {args[0].mac} is not loaded yet") return func(*args, **kwargs) + return cast(FuncT, decorated) diff --git a/plugwise_usb/nodes/helpers/counter.py b/plugwise_usb/nodes/helpers/counter.py index 8f64a3efb..c55aa46bd 100644 --- a/plugwise_usb/nodes/helpers/counter.py +++ b/plugwise_usb/nodes/helpers/counter.py @@ -80,9 +80,12 @@ def add_pulse_log( # pylint: disable=too-many-arguments import_only: bool = False, ) -> None: """Add pulse log.""" - if self._pulse_collection.add_log( - address, slot, timestamp, pulses, import_only - ) and not import_only: + if ( + self._pulse_collection.add_log( + address, slot, timestamp, pulses, import_only + ) + and not import_only + ): self.update() def get_pulse_logs(self) -> dict[int, dict[int, PulseLogRecord]]: @@ -160,7 +163,9 @@ def update(self) -> None: ( self._energy_statistics.hour_production, self._energy_statistics.hour_production_reset, - ) = self._counters[EnergyType.PRODUCTION_HOUR].update(self._pulse_collection) + ) = self._counters[EnergyType.PRODUCTION_HOUR].update( + self._pulse_collection + ) ( self._energy_statistics.day_production, self._energy_statistics.day_production_reset, @@ -295,7 +300,9 @@ def update( self._midnight_reset_passed = True if last_reset.hour == 1 and self._midnight_reset_passed: self._midnight_reset_passed = False - last_reset = last_reset.replace(hour=0, minute=0, second=0, microsecond=0) + last_reset = last_reset.replace( + hour=0, minute=0, second=0, microsecond=0 + ) pulses, last_update = pulse_collection.collected_pulses( last_reset, self._is_consumption diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index 269ecc49e..da757fe1c 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -173,7 +173,7 @@ def collected_pulses( self._mac, from_timestamp, is_consumption, - self._log_production + self._log_production, ) if not is_consumption: if self._log_production is None or not self._log_production: @@ -375,7 +375,7 @@ def _detect_rollover( _LOGGER.debug( "_update_rollover | %s | reset %s rollover", self._mac, - direction + direction, ) return False diff --git a/plugwise_usb/nodes/helpers/subscription.py b/plugwise_usb/nodes/helpers/subscription.py index 94773373a..09e742bcb 100644 --- a/plugwise_usb/nodes/helpers/subscription.py +++ b/plugwise_usb/nodes/helpers/subscription.py @@ -32,7 +32,6 @@ def __init__(self) -> None: NodeFeatureSubscription, ] = {} - def subscribe_to_feature_update( self, node_feature_callback: Callable[[NodeFeature, Any], Coroutine[Any, Any, None]], @@ -64,6 +63,8 @@ async def publish_feature_update_to_subscribers( self._feature_update_subscribers.values() ): if feature in node_feature_subscription.features: - callback_list.append(node_feature_subscription.callback_fn(feature, state)) + callback_list.append( + node_feature_subscription.callback_fn(feature, state) + ) if len(callback_list) > 0: await gather(*callback_list) diff --git a/plugwise_usb/nodes/node.py b/plugwise_usb/nodes/node.py index 13b6433ea..4bb47bbc5 100644 --- a/plugwise_usb/nodes/node.py +++ b/plugwise_usb/nodes/node.py @@ -51,6 +51,7 @@ CACHE_HARDWARE = "hardware" CACHE_NODE_INFO_TIMESTAMP = "node_info_timestamp" + class PlugwiseBaseNode(FeaturePublisher, ABC): """Abstract Base Class for a Plugwise node.""" @@ -344,9 +345,7 @@ def _setup_protocol( if ( required_version := FEATURE_SUPPORTED_AT_FIRMWARE.get(feature) ) is not None and ( - self._node_protocols.min - <= required_version - <= self._node_protocols.max + self._node_protocols.min <= required_version <= self._node_protocols.max and feature not in self._features ): self._features += (feature,) @@ -426,7 +425,6 @@ async def _available_update_state( self._last_seen is not None and timestamp is not None and int((timestamp - self._last_seen).total_seconds()) > 5 - ): self._last_seen = timestamp await self.publish_feature_update_to_subscribers( @@ -538,7 +536,9 @@ async def update_node_details( allowed_models = TYPE_MODEL.get(self._node_info.node_type.value) if allowed_models and model_info[0] not in allowed_models: # Replace model_info list - model_info = [allowed_models[0]] # Not correct for 1 but should not be a problem + model_info = [ + allowed_models[0] + ] # Not correct for 1 but should not be a problem self._node_info.model = model_info[0] # Handle + devices diff --git a/plugwise_usb/nodes/scan.py b/plugwise_usb/nodes/scan.py index 36ad9c17b..13fb4c6fb 100644 --- a/plugwise_usb/nodes/scan.py +++ b/plugwise_usb/nodes/scan.py @@ -173,8 +173,10 @@ def _motion_from_cache(self) -> bool: if (cached_motion_state := self._get_cache(CACHE_MOTION_STATE)) is not None: if ( cached_motion_state == "True" - and (motion_timestamp := self._motion_timestamp_from_cache()) is not None - and int((datetime.now(tz=UTC) - motion_timestamp).total_seconds()) < self._reset_timer_from_cache() * 60 + and (motion_timestamp := self._motion_timestamp_from_cache()) + is not None + and int((datetime.now(tz=UTC) - motion_timestamp).total_seconds()) + < self._reset_timer_from_cache() * 60 ): return True return False @@ -359,7 +361,7 @@ async def _switch_group(self, response: PlugwiseResponse) -> bool: _LOGGER.warning("%s received %s", self.name, response) await gather( self._available_update_state(True, response.timestamp), - self._motion_state_update(response.switch_state, response.timestamp) + self._motion_state_update(response.switch_state, response.timestamp), ) return True @@ -383,7 +385,9 @@ async def _motion_state_update( self._set_cache(CACHE_MOTION_STATE, "False") if self._motion_state.state is None or self._motion_state.state: if self._reset_timer_motion_on is not None: - reset_timer = int((timestamp - self._reset_timer_motion_on).total_seconds()) + reset_timer = int( + (timestamp - self._reset_timer_motion_on).total_seconds() + ) if self._motion_config.reset_timer is None: self._motion_config = replace( self._motion_config, @@ -475,7 +479,9 @@ async def scan_configure( MotionSensitivity.OFF: SENSITIVITY_OFF_VALUE, } # Default to medium - sensitivity_value = sensitivity_map.get(sensitivity_level, SENSITIVITY_MEDIUM_VALUE) + sensitivity_value = sensitivity_map.get( + sensitivity_level, SENSITIVITY_MEDIUM_VALUE + ) request = ScanConfigureRequest( self._send, self._mac_in_bytes, diff --git a/plugwise_usb/nodes/sed.py b/plugwise_usb/nodes/sed.py index 11cf5e4d7..a4d046320 100644 --- a/plugwise_usb/nodes/sed.py +++ b/plugwise_usb/nodes/sed.py @@ -238,7 +238,7 @@ async def set_awake_duration(self, seconds: int) -> bool: ) if self._battery_config.awake_duration == seconds: - return False + return False self._new_battery_config = replace( self._new_battery_config, awake_duration=seconds diff --git a/plugwise_usb/nodes/stealth.py b/plugwise_usb/nodes/stealth.py index 33be3907a..f33d18542 100644 --- a/plugwise_usb/nodes/stealth.py +++ b/plugwise_usb/nodes/stealth.py @@ -1,4 +1,5 @@ """Plugwise Stealth node object.""" + from ..nodes.circle import PlugwiseCircle diff --git a/plugwise_usb/nodes/switch.py b/plugwise_usb/nodes/switch.py index 62d82262e..b1c6d4f18 100644 --- a/plugwise_usb/nodes/switch.py +++ b/plugwise_usb/nodes/switch.py @@ -53,7 +53,12 @@ async def load(self) -> bool: self._loaded = True self._setup_protocol( SWITCH_FIRMWARE_SUPPORT, - (NodeFeature.BATTERY, NodeFeature.INFO, NodeFeature.PING, NodeFeature.SWITCH), + ( + NodeFeature.BATTERY, + NodeFeature.INFO, + NodeFeature.PING, + NodeFeature.SWITCH, + ), ) if await self.initialize(): await self._loaded_callback(NodeEvent.LOADED, self.mac) @@ -89,7 +94,7 @@ def switch(self) -> bool: """Current state of switch.""" return bool(self._switch_state) - #endregion + # endregion async def _switch_group(self, response: PlugwiseResponse) -> bool: """Switch group request from Switch.""" @@ -99,7 +104,7 @@ async def _switch_group(self, response: PlugwiseResponse) -> bool: ) await gather( self._available_update_state(True, response.timestamp), - self._switch_state_update(response.switch_state, response.timestamp) + self._switch_state_update(response.switch_state, response.timestamp), ) return True diff --git a/tests/stick_test_data.py b/tests/stick_test_data.py index 0f5cd92bc..4559bd3c7 100644 --- a/tests/stick_test_data.py +++ b/tests/stick_test_data.py @@ -60,11 +60,11 @@ + b"00044280" # log address 20 + b"01" # relay + b"01" # hz - + b"000000730007" # hw_ver + + b"000000730007" # hw_ver + b"4E0843A9" # fw_ver + b"01", # node_type (Circle+) ), - b"\x05\x05\x03\x030008014068\r\n":( + b"\x05\x05\x03\x030008014068\r\n": ( "Reply to CirclePlusAllowJoiningRequest", b"000000C1", # Success ack b"000000D9" # JOIN_ACCEPTED diff --git a/tests/test_usb.py b/tests/test_usb.py index bc61fe1ff..773a614b9 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -1118,7 +1118,6 @@ def test_pulse_collection_consumption( tst_consumption.add_log(94, 1, (fixed_this_hour - td(hours=24)), 1000) assert tst_consumption.collected_logs == 24 - # Test rollover by updating pulses before log record pulse_update_3 = fixed_this_hour + td(hours=1, minutes=0, seconds=30) test_timestamp = fixed_this_hour + td(hours=1)