Skip to content

Feature/arnout/correct message count #250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 22 additions & 27 deletions plugwise_usb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -170,13 +172,8 @@
@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")

Check warning on line 176 in plugwise_usb/__init__.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/__init__.py#L176

Added line #L176 was not covered by tests

self._port = port

Expand Down Expand Up @@ -238,7 +235,9 @@
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(

Check warning on line 238 in plugwise_usb/__init__.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/__init__.py#L238

Added line #L238 was not covered by tests
"Unable to subscribe to node events without network connection initialized"
)
return self._network.subscribe_to_node_events(
node_event_callback,
events,
Expand All @@ -252,9 +251,7 @@
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()
Expand All @@ -271,17 +268,17 @@
"""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:
self._port = port

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(
Expand Down Expand Up @@ -319,9 +316,7 @@
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")

Check warning on line 319 in plugwise_usb/__init__.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/__init__.py#L319

Added line #L319 was not covered by tests
return await self._network.discover_nodes(load=True)

@raise_not_connected
Expand Down
1 change: 0 additions & 1 deletion plugwise_usb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,5 +704,4 @@ async def message_for_node(self, message: Any) -> None:

"""


# endregion
8 changes: 2 additions & 6 deletions plugwise_usb/connection/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,19 +209,15 @@ 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:
return (None, None)
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:
Expand Down
17 changes: 16 additions & 1 deletion plugwise_usb/connection/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,24 @@ def __init__(self) -> None:

@property
def queue_depth(self) -> int:
return self._sender.processed_messages - self._receiver.processed_messages
"""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
Expand Down
8 changes: 5 additions & 3 deletions plugwise_usb/connection/queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@

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:
Expand All @@ -103,7 +102,8 @@
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)
Expand Down Expand Up @@ -147,7 +147,9 @@
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(

Check warning on line 150 in plugwise_usb/connection/queue.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/connection/queue.py#L150

Added line #L150 was not covered by tests
"Awaiting plugwise responses %d", self._stick.queue_depth
)

await self._stick.write_to_stick(request)
self._submit_queue.task_done()
Expand Down
1 change: 1 addition & 0 deletions plugwise_usb/connection/receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 16 additions & 8 deletions plugwise_usb/connection/sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ 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."""
# Subscribe to ACCEPT stick responses, which contain the seq_id we need.
Expand Down Expand Up @@ -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:
Expand All @@ -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(
Expand All @@ -115,12 +121,15 @@ 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,
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",
Expand All @@ -141,7 +150,6 @@ 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."""
Expand Down
1 change: 1 addition & 0 deletions plugwise_usb/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Plugwise Stick constants."""

from __future__ import annotations

import datetime as dt
Expand Down
31 changes: 19 additions & 12 deletions plugwise_usb/helpers/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@
"""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()
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
Expand All @@ -72,13 +74,17 @@
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(

Check warning on line 77 in plugwise_usb/helpers/cache.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/helpers/cache.py#L77

Added line #L77 was not covered by tests
"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."""
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix docstring formatting issue

The static analysis tool correctly identifies a whitespace issue in the docstring.

Apply this diff to fix the docstring formatting:

-    """ "Save information to cache file."""
+    """Save information to cache file."""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
""" "Save information to cache file."""
"""Save information to cache file."""
🧰 Tools
🪛 Ruff (0.11.9)

83-83: No whitespaces allowed surrounding docstring text

(D210)

🤖 Prompt for AI Agents
In plugwise_usb/helpers/cache.py at line 83, the docstring has an extra leading
space before the opening quotes. Remove the extra space so the docstring starts
immediately with triple quotes followed by the text, ensuring proper formatting
and compliance with docstring standards.

if not self._initialized:
raise CacheError(f"Unable to save cache. Initialize cache file '{self._file_name}' first.")
raise CacheError(

Check warning on line 85 in plugwise_usb/helpers/cache.py

View check run for this annotation

Codecov / codecov/patch

plugwise_usb/helpers/cache.py#L85

Added line #L85 was not covered by tests
f"Unable to save cache. Initialize cache file '{self._file_name}' first."
)

current_data: dict[str, str] = {}
if not rewrite:
Expand Down Expand Up @@ -111,19 +117,20 @@
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:
Expand All @@ -146,10 +153,10 @@
_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:
Expand Down
3 changes: 2 additions & 1 deletion plugwise_usb/helpers/util.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Plugwise utility helpers."""

from __future__ import annotations

import re
Expand All @@ -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")
Expand Down
1 change: 1 addition & 0 deletions plugwise_usb/messages/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Priority(Enum):
MEDIUM = 2
LOW = 3


class PlugwiseMessage:
"""Plugwise message base class."""

Expand Down
Loading
Loading