Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
55 changes: 24 additions & 31 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 @@ 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

Expand All @@ -189,10 +186,8 @@ async def energy_reset_request(self, mac: str) -> bool:
raise NodeError(f"{exc}") from exc

# Follow up by an energy-intervals (re)set
if (
result := await self.set_energy_intervals(
mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL
)
if result := await self.set_energy_intervals(
mac, DEFAULT_CONS_INTERVAL, NO_PRODUCTION_INTERVAL
):
return result

Expand Down Expand Up @@ -238,7 +233,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,
Expand All @@ -252,9 +249,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()
Expand All @@ -271,17 +266,17 @@ 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:
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 +314,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
Expand Down
3 changes: 2 additions & 1 deletion plugwise_usb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,15 @@ class EnergyStatistics:
day_production: float | None = None
day_production_reset: datetime | None = None


@dataclass
class SenseStatistics:
"""Sense statistics collection."""

temperature: float | None = None
humidity: float | None = None


class PlugwiseNode(Protocol):
"""Protocol definition of a Plugwise device node."""

Expand Down Expand Up @@ -704,5 +706,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
2 changes: 2 additions & 0 deletions plugwise_usb/connection/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ def __init__(self) -> None:

@property
def queue_depth(self) -> int:
"""Return estimated size of pending responses."""
return self._sender.processed_messages - self._receiver.processed_messages

def correct_received_messages(self, correction: int) -> None:
"""Correct received messages count."""
self._receiver.correct_processed_messages(correction)

@property
Expand Down
9 changes: 6 additions & 3 deletions plugwise_usb/connection/queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ 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:
Expand All @@ -103,7 +103,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)
Expand Down Expand Up @@ -147,7 +148,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()
Expand Down
3 changes: 2 additions & 1 deletion plugwise_usb/connection/receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def is_connected(self) -> bool:
return self._connection_state

def correct_processed_messages(self, correction: int) -> None:
"""Return the number of processed messages."""
"""Correct the number of processed messages."""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this read: Return the number of correct processed messages?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The reasons I believe Correct is the best choice of words:
The return value of this function is "None", so it cannot be return.
The input variable is "correction", and the correction value is added to self._processed_msgs
The (only?) calling function is in connection/manager.py which has a title "Correct received messages count."
The calling of said function in connection/manager.py is in connection/queue.py which makes a call when there is an error (StickTimeout). I have the feeling this sequence is called when some send-messages counter is already updated, but then there is a problem and the counter-counter needs to be updated to retain balance between the two. This is because the difference between send and processed message counters indicates the number of pending answers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agreed, I've come to the same conclusion after tracing the use of the function.

self._processed_msgs += correction

def connection_made(self, transport: SerialTransport) -> None:
Expand Down 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
16 changes: 12 additions & 4 deletions plugwise_usb/connection/sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def __init__(self, stick_receiver: StickReceiver, transport: Transport) -> None:
def processed_messages(self) -> int:
"""Return the number of processed messages."""
return self._processed_msgs

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 @@ -120,7 +126,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",
Expand Down
5 changes: 5 additions & 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 All @@ -15,6 +16,10 @@
LOCAL_TIMEZONE = dt.datetime.now(dt.UTC).astimezone().tzinfo
UTF8: Final = "utf-8"

# Value limits
MAX_UINT_2: Final = 255
MAX_UINT_4: Final = 65535

# Time
DAY_IN_HOURS: Final = 24
WEEK_IN_HOURS: Final = 168
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 @@ 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()
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 @@ 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."""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
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