From e240edc3f5e0d365a784a004c6c2caf325989f11 Mon Sep 17 00:00:00 2001 From: brefra Date: Mon, 3 Jan 2022 11:55:34 +0100 Subject: [PATCH 01/87] Rename NodeResponse into USBresponse --- plugwise/messages/responses.py | 48 +++++++++++++++++----------------- plugwise/nodes/__init__.py | 4 +-- plugwise/stick.py | 4 +-- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index 30c1e490c..5e04e5192 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -22,7 +22,7 @@ ) -class NodeResponse(PlugwiseMessage): +class USBresponse(PlugwiseMessage): """ Base class for response messages received by USB-Stick. """ @@ -92,7 +92,7 @@ def __len__(self): return 34 + arglen + self.len_correction -class NodeAckSmallResponse(NodeResponse): +class NodeAckSmallResponse(USBresponse): """ Acknowledge message without source MAC @@ -105,7 +105,7 @@ def __init__(self): super().__init__(MESSAGE_SMALL) -class NodeAckLargeResponse(NodeResponse): +class NodeAckLargeResponse(USBresponse): """ Acknowledge message with source MAC @@ -118,7 +118,7 @@ def __init__(self): super().__init__(MESSAGE_LARGE) -class CirclePlusQueryResponse(NodeResponse): +class CirclePlusQueryResponse(USBresponse): """ TODO: @@ -156,7 +156,7 @@ def deserialize(self, response): self.new_node_mac_id.value = b"00" + self.new_node_mac_id.value[2:] -class CirclePlusQueryEndResponse(NodeResponse): +class CirclePlusQueryEndResponse(USBresponse): """ TODO: PWAckReplyV1_0 @@ -177,7 +177,7 @@ def __len__(self): return 18 + arglen -class CirclePlusConnectResponse(NodeResponse): +class CirclePlusConnectResponse(USBresponse): """ CirclePlus connected to the network @@ -197,7 +197,7 @@ def __len__(self): return 18 + arglen -class NodeJoinAvailableResponse(NodeResponse): +class NodeJoinAvailableResponse(USBresponse): """ Message from an unjoined node to notify it is available to join a plugwise network @@ -207,7 +207,7 @@ class NodeJoinAvailableResponse(NodeResponse): ID = b"0006" -class StickInitResponse(NodeResponse): +class StickInitResponse(USBresponse): """ Returns the configuration and status of the USB-Stick @@ -240,7 +240,7 @@ def __init__(self): ] -class NodePingResponse(NodeResponse): +class NodePingResponse(USBresponse): """ Ping response from node @@ -265,7 +265,7 @@ def __init__(self): ] -class CirclePowerUsageResponse(NodeResponse): +class CirclePowerUsageResponse(USBresponse): """ Returns power usage as impulse counters for several different timeframes @@ -290,7 +290,7 @@ def __init__(self): ] -class CirclePlusScanResponse(NodeResponse): +class CirclePlusScanResponse(USBresponse): """ Returns the MAC of a registered node at the specified memory address @@ -306,7 +306,7 @@ def __init__(self): self.params += [self.node_mac, self.node_address] -class NodeRemoveResponse(NodeResponse): +class NodeRemoveResponse(USBresponse): """ Returns conformation (or not) if node is removed from the Plugwise network by having it removed from the memory of the Circle+ @@ -323,7 +323,7 @@ def __init__(self): self.params += [self.node_mac_id, self.status] -class NodeInfoResponse(NodeResponse): +class NodeInfoResponse(USBresponse): """ Returns the status information of Node @@ -352,7 +352,7 @@ def __init__(self): ] -class CircleCalibrationResponse(NodeResponse): +class CircleCalibrationResponse(USBresponse): """ returns the calibration settings of node @@ -370,7 +370,7 @@ def __init__(self): self.params += [self.gain_a, self.gain_b, self.off_tot, self.off_noise] -class CirclePlusRealTimeClockResponse(NodeResponse): +class CirclePlusRealTimeClockResponse(USBresponse): """ returns the real time clock of CirclePlus node @@ -388,7 +388,7 @@ def __init__(self): self.params += [self.time, self.day_of_week, self.date] -class CircleClockResponse(NodeResponse): +class CircleClockResponse(USBresponse): """ Returns the current internal clock of Node @@ -406,7 +406,7 @@ def __init__(self): self.params += [self.time, self.day_of_week, self.unknown, self.unknown2] -class CircleEnergyCountersResponse(NodeResponse): +class CircleEnergyCountersResponse(USBresponse): """ Returns historical energy usage of requested memory address Each response contains 4 energy counters at specified 1 hour timestamp @@ -440,7 +440,7 @@ def __init__(self): ] -class NodeAwakeResponse(NodeResponse): +class NodeAwakeResponse(USBresponse): """ A sleeping end device (SED: Scan, Sense, Switch) sends this message to announce that is awake. Awake types: @@ -462,7 +462,7 @@ def __init__(self): self.params += [self.awake_type] -class NodeSwitchGroupResponse(NodeResponse): +class NodeSwitchGroupResponse(USBresponse): """ A sleeping end device (SED: Scan, Sense, Switch) sends this message to switch groups on/off when the configured @@ -483,7 +483,7 @@ def __init__(self): ] -class NodeFeaturesResponse(NodeResponse): +class NodeFeaturesResponse(USBresponse): """ Returns supported features of node TODO: FeatureBitmask @@ -499,7 +499,7 @@ def __init__(self): self.params += [self.features] -class NodeJoinAckResponse(NodeResponse): +class NodeJoinAckResponse(USBresponse): """ Notification message when node (re)joined existing network again. Sent when a SED (re)joins the network e.g. when you reinsert the battery of a Scan @@ -514,7 +514,7 @@ def __init__(self): # sequence number is always FFFD -class NodeAckResponse(NodeResponse): +class NodeAckResponse(USBresponse): """ Acknowledge message in regular format Sent by nodes supporting plugwise 2.4 protocol version @@ -529,7 +529,7 @@ def __init__(self): self.ack_id = Int(0, 2, False) -class SenseReportResponse(NodeResponse): +class SenseReportResponse(USBresponse): """ Returns the current temperature and humidity of a Sense node. The interval this report is sent is configured by the 'SenseReportIntervalRequest' request @@ -546,7 +546,7 @@ def __init__(self): self.params += [self.humidity, self.temperature] -class CircleInitialRelaisStateResponse(NodeResponse): +class CircleInitialRelaisStateResponse(USBresponse): """ Returns the initial relais state. diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index dc4670937..82e0a6557 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -17,7 +17,7 @@ NodeInfoResponse, NodeJoinAckResponse, NodePingResponse, - NodeResponse, + USBresponse, ) from ..util import validate_mac, version_to_model @@ -180,7 +180,7 @@ def _request_ping(self, callback=None, ignore_sensor=True): def message_for_node(self, message): """Process received message.""" - assert isinstance(message, NodeResponse) + assert isinstance(message, USBresponse) if message.mac == self._mac: if message.timestamp is not None: _LOGGER.debug( diff --git a/plugwise/stick.py b/plugwise/stick.py index c14b416f1..7d6801f79 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -49,7 +49,7 @@ NodeInfoResponse, NodeJoinAvailableResponse, NodeRemoveResponse, - NodeResponse, + USBresponse, StickInitResponse, ) from .nodes.circle import PlugwiseCircle @@ -407,7 +407,7 @@ def _remove_node(self, mac): else: _LOGGER.warning("Node %s does not exists, unable to remove node.", mac) - def message_processor(self, message: NodeResponse): + def message_processor(self, message: USBresponse): """Received message from Plugwise network.""" mac = message.mac.decode(UTF8_DECODE) if isinstance(message, (NodeAckLargeResponse, NodeAckResponse)): From 2c7701d99af01dde3eb1e622239cf7c53f1984fc Mon Sep 17 00:00:00 2001 From: brefra Date: Mon, 3 Jan 2022 11:56:35 +0100 Subject: [PATCH 02/87] Rename NodeAckSmallResponse into StickResponse --- plugwise/constants.py | 4 ++-- plugwise/controller.py | 4 ++-- plugwise/messages/responses.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 76e22ca2f..9b6960e1b 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -44,7 +44,7 @@ # Acknowledge message types -# NodeAckSmallResponse +# StickResponse RESPONSE_TYPE_SUCCESS = b"00C1" RESPONSE_TYPE_ERROR = b"00C2" RESPONSE_TYPE_TIMEOUT = b"00E1" @@ -98,7 +98,7 @@ SLEEP_FAILED, ) STATUS_RESPONSES = { - # NodeAckSmallResponse + # StickResponse RESPONSE_TYPE_SUCCESS: "success", RESPONSE_TYPE_ERROR: "error", RESPONSE_TYPE_TIMEOUT: "timeout", diff --git a/plugwise/controller.py b/plugwise/controller.py index 4396b1a8f..d18e9da0a 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -33,7 +33,7 @@ from .messages.responses import ( NodeAckLargeResponse, NodeAckResponse, - NodeAckSmallResponse, + StickResponse, ) from .parser import PlugwiseParser from .util import inc_seq_id @@ -269,7 +269,7 @@ def message_handler(self, message): elif message.seq_id == b"0000" and self.last_seq_id == b"FFFB": self.last_seq_id = b"0000" - if isinstance(message, NodeAckSmallResponse): + if isinstance(message, StickResponse): self._log_status_message(message, message.ack_id) self._post_message_action( message.seq_id, message.ack_id, message.__class__.__name__ diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index 5e04e5192..25f852074 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -92,7 +92,7 @@ def __len__(self): return 34 + arglen + self.len_correction -class NodeAckSmallResponse(USBresponse): +class StickResponse(USBresponse): """ Acknowledge message without source MAC @@ -598,7 +598,7 @@ def get_message_response(message_id, length, seq_id): # No fixed sequence ID, continue at message ID if message_id == b"0000": if length == 20: - return NodeAckSmallResponse() + return StickResponse() if length == 36: return NodeAckLargeResponse() return None From 4e23bd2090f6831369d5ceb200d69f1fb2ce8934 Mon Sep 17 00:00:00 2001 From: brefra Date: Mon, 3 Jan 2022 11:58:55 +0100 Subject: [PATCH 03/87] Rename NodeAckLargeResponse into NodeResponse --- plugwise/constants.py | 4 ++-- plugwise/controller.py | 4 ++-- plugwise/messages/requests.py | 4 ++-- plugwise/messages/responses.py | 4 ++-- plugwise/nodes/circle.py | 4 ++-- plugwise/nodes/sed.py | 4 ++-- plugwise/stick.py | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 9b6960e1b..8563cc290 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -49,7 +49,7 @@ RESPONSE_TYPE_ERROR = b"00C2" RESPONSE_TYPE_TIMEOUT = b"00E1" -# NodeAckLargeResponse +# NodeResponse CLOCK_SET = b"00D7" JOIN_REQUEST_ACCEPTED = b"00D9" RELAY_SWITCHED_OFF = b"00DE" @@ -102,7 +102,7 @@ RESPONSE_TYPE_SUCCESS: "success", RESPONSE_TYPE_ERROR: "error", RESPONSE_TYPE_TIMEOUT: "timeout", - # NodeAckLargeResponse + # NodeResponse CLOCK_SET: "clock set", JOIN_REQUEST_ACCEPTED: "join accepted", REAL_TIME_CLOCK_ACCEPTED: "real time clock set", diff --git a/plugwise/controller.py b/plugwise/controller.py index d18e9da0a..8e37955a6 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -31,7 +31,7 @@ ) from .messages.requests import NodeInfoRequest, NodePingRequest, NodeRequest from .messages.responses import ( - NodeAckLargeResponse, + NodeResponse, NodeAckResponse, StickResponse, ) @@ -275,7 +275,7 @@ def message_handler(self, message): message.seq_id, message.ack_id, message.__class__.__name__ ) else: - if isinstance(message, (NodeAckResponse, NodeAckLargeResponse)): + if isinstance(message, (NodeAckResponse, NodeResponse)): self._log_status_message(message, message.ack_id) else: self._log_status_message(message) diff --git a/plugwise/messages/requests.py b/plugwise/messages/requests.py index a20ac0be1..ba33e94be 100644 --- a/plugwise/messages/requests.py +++ b/plugwise/messages/requests.py @@ -77,7 +77,7 @@ class NodeAllowJoiningRequest(NodeRequest): Enable or disable receiving joining request of unjoined nodes. Circle+ node will respond with an acknowledge message - Response message: NodeAckLargeResponse + Response message: NodeResponse """ ID = b"0008" @@ -176,7 +176,7 @@ class CircleSwitchRelayRequest(NodeRequest): """ switches relay on/off - Response message: NodeAckLargeResponse + Response message: NodeResponse """ ID = b"0017" diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index 25f852074..84cae90c1 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -105,7 +105,7 @@ def __init__(self): super().__init__(MESSAGE_SMALL) -class NodeAckLargeResponse(USBresponse): +class NodeResponse(USBresponse): """ Acknowledge message with source MAC @@ -600,6 +600,6 @@ def get_message_response(message_id, length, seq_id): if length == 20: return StickResponse() if length == 36: - return NodeAckLargeResponse() + return NodeResponse() return None return id_to_message.get(message_id, None) diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 59760eb63..11857dc86 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -36,7 +36,7 @@ CircleClockResponse, CircleEnergyCountersResponse, CirclePowerUsageResponse, - NodeAckLargeResponse, + NodeResponse, ) from ..nodes import PlugwiseNode @@ -247,7 +247,7 @@ def message_for_circle(self, message): self.mac, ) self._request_calibration(self.request_power_update) - elif isinstance(message, NodeAckLargeResponse): + elif isinstance(message, NodeResponse): self._node_ack_response(message) elif isinstance(message, CircleCalibrationResponse): self._response_calibration(message) diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index 128a572ea..23ad19a0f 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -24,7 +24,7 @@ SLEEP_SET, ) from ..messages.requests import NodeInfoRequest, NodePingRequest, NodeSleepConfigRequest -from ..messages.responses import NodeAckLargeResponse, NodeAwakeResponse +from ..messages.responses import NodeResponse, NodeAwakeResponse from ..nodes import PlugwiseNode _LOGGER = logging.getLogger(__name__) @@ -47,7 +47,7 @@ def message_for_sed(self, message): """ if isinstance(message, NodeAwakeResponse): self._process_awake_response(message) - elif isinstance(message, NodeAckLargeResponse): + elif isinstance(message, NodeResponse): if message.ack_id == SLEEP_SET: self.maintenance_interval = self._new_maintenance_interval else: diff --git a/plugwise/stick.py b/plugwise/stick.py index 7d6801f79..34625c380 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -44,7 +44,7 @@ StickInitRequest, ) from .messages.responses import ( - NodeAckLargeResponse, + NodeResponse, NodeAckResponse, NodeInfoResponse, NodeJoinAvailableResponse, @@ -410,7 +410,7 @@ def _remove_node(self, mac): def message_processor(self, message: USBresponse): """Received message from Plugwise network.""" mac = message.mac.decode(UTF8_DECODE) - if isinstance(message, (NodeAckLargeResponse, NodeAckResponse)): + if isinstance(message, (NodeResponse, NodeAckResponse)): if message.ack_id in STATE_ACTIONS: self._pass_message_to_node(message, mac) elif isinstance(message, NodeInfoResponse): From 91d27967461473772f6a2414233988a121194028 Mon Sep 17 00:00:00 2001 From: brefra Date: Mon, 3 Jan 2022 12:31:04 +0100 Subject: [PATCH 04/87] Make message priority an Enum --- plugwise/constants.py | 14 ++++++++++---- plugwise/controller.py | 4 ++-- plugwise/nodes/__init__.py | 4 ++-- plugwise/nodes/circle.py | 15 +++++++-------- plugwise/nodes/circle_plus.py | 4 ++-- plugwise/nodes/sed.py | 4 ++-- plugwise/stick.py | 6 +++--- 7 files changed, 28 insertions(+), 23 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 8563cc290..340b0d646 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -1,5 +1,7 @@ """Plugwise Stick and Smile constants.""" +from enum import Enum + # Copied homeassistant.consts ATTR_DEVICE_CLASS = "device_class" ATTR_NAME = "name" @@ -147,10 +149,14 @@ # Default sleep between sending messages SLEEP_TIME = 150 / 1000 -# Message priority levels -PRIORITY_HIGH = 1 -PRIORITY_LOW = 3 -PRIORITY_MEDIUM = 2 + +class Priority(int, Enum): + """Message priority levels for USB-stick.""" + + High = 1 + Medium = 2 + Low = 3 + # Max seconds the internal clock of plugwise nodes # are allowed to drift in seconds diff --git a/plugwise/controller.py b/plugwise/controller.py index 8e37955a6..d20dae9fa 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -22,12 +22,12 @@ from .constants import ( MESSAGE_RETRY, MESSAGE_TIME_OUT, - PRIORITY_MEDIUM, REQUEST_FAILED, REQUEST_SUCCESS, SLEEP_TIME, STATUS_RESPONSES, UTF8_DECODE, + Priority, ) from .messages.requests import NodeInfoRequest, NodePingRequest, NodeRequest from .messages.responses import ( @@ -125,7 +125,7 @@ def send( request: NodeRequest, callback=None, retry_counter=0, - priority=PRIORITY_MEDIUM, + priority: Priority = Priority.Medium, ): """Queue request message to be sent into Plugwise Zigbee network.""" _LOGGER.debug( diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 82e0a6557..674090d03 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -8,7 +8,7 @@ FEATURE_RELAY, FEATURE_RSSI_IN, FEATURE_RSSI_OUT, - PRIORITY_LOW, + Priority, UTF8_DECODE, ) from ..messages.requests import NodeFeaturesRequest, NodeInfoRequest, NodePingRequest @@ -160,7 +160,7 @@ def _request_info(self, callback=None): NodeInfoRequest(self._mac), callback, 0, - PRIORITY_LOW, + Priority.Low, ) def _request_features(self, callback=None): diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 11857dc86..1316ab89d 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -17,11 +17,10 @@ FEATURE_RSSI_OUT, MAX_TIME_DRIFT, MESSAGE_TIME_OUT, - PRIORITY_HIGH, - PRIORITY_LOW, PULSES_PER_KW_SECOND, RELAY_SWITCHED_OFF, RELAY_SWITCHED_ON, + Priority, ) from ..messages.requests import ( CircleCalibrationRequest, @@ -200,7 +199,7 @@ def _request_calibration(self, callback=None): CircleCalibrationRequest(self._mac), callback, 0, - PRIORITY_HIGH, + Priority.High, ) def _request_switch(self, state, callback=None): @@ -209,7 +208,7 @@ def _request_switch(self, state, callback=None): CircleSwitchRelayRequest(self._mac, state), callback, 0, - PRIORITY_HIGH, + Priority.High, ) def request_power_update(self, callback=None): @@ -628,7 +627,7 @@ def request_energy_counters(self, log_address=None, callback=None): CircleEnergyCountersRequest(self._mac, log_address), None, 0, - PRIORITY_LOW, + Priority.Low, ) else: # Collect energy counters of today and yesterday @@ -641,13 +640,13 @@ def request_energy_counters(self, log_address=None, callback=None): CircleEnergyCountersRequest(self._mac, req_log_address), None, 0, - PRIORITY_LOW, + Priority.Low, ) self.message_sender( CircleEnergyCountersRequest(self._mac, log_address), callback, 0, - PRIORITY_LOW, + Priority.Low, ) def _response_energy_counters(self, message: CircleEnergyCountersResponse): @@ -788,7 +787,7 @@ def get_clock(self, callback=None): CircleClockGetRequest(self._mac), callback, 0, - PRIORITY_LOW, + Priority.Low, ) def set_clock(self, callback=None): diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index e1306da6e..a4ec45653 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -2,7 +2,7 @@ from datetime import datetime import logging -from ..constants import MAX_TIME_DRIFT, PRIORITY_LOW, UTF8_DECODE +from ..constants import MAX_TIME_DRIFT, Priority, UTF8_DECODE from ..messages.requests import ( CirclePlusRealTimeClockGetRequest, CirclePlusRealTimeClockSetRequest, @@ -93,7 +93,7 @@ def get_real_time_clock(self, callback=None): CirclePlusRealTimeClockGetRequest(self._mac), callback, 0, - PRIORITY_LOW, + Priority.Low, ) def _response_realtime_clock(self, message): diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index 23ad19a0f..188b99d53 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -10,7 +10,7 @@ FEATURE_PING, FEATURE_RSSI_IN, FEATURE_RSSI_OUT, - PRIORITY_HIGH, + Priority, SED_AWAKE_BUTTON, SED_AWAKE_FIRST, SED_AWAKE_MAINTENANCE, @@ -88,7 +88,7 @@ def _process_awake_response(self, message): request_message.__class__.__name__, self.mac, ) - self.message_sender(request_message, callback, -1, PRIORITY_HIGH) + self.message_sender(request_message, callback, -1, Priority.High) self._sed_requests = {} else: if message.awake_type.value == SED_AWAKE_STATE: diff --git a/plugwise/stick.py b/plugwise/stick.py index 34625c380..01ef01286 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -22,7 +22,7 @@ NODE_TYPE_SENSE, NODE_TYPE_STEALTH, NODE_TYPE_SWITCH, - PRIORITY_LOW, + Priority, STATE_ACTIONS, UTF8_DECODE, WATCHDOG_DEAMON, @@ -634,7 +634,7 @@ def _update_loop(self): NodePingRequest(bytes(mac, UTF8_DECODE)), None, -1, - PRIORITY_LOW, + Priority.Low, ) _discover_counter = 0 else: @@ -747,7 +747,7 @@ def discover_node(self, mac: str, callback=None, force_discover=False): NodeInfoRequest(bytes(mac, UTF8_DECODE)), callback, 0, - PRIORITY_LOW, + Priority.Low, ) elif force_discover: self.msg_controller.send( From 837e725e3d254e477fe2c602ab2204be36f4fa5b Mon Sep 17 00:00:00 2001 From: brefra Date: Mon, 3 Jan 2022 12:31:30 +0100 Subject: [PATCH 05/87] Use UTC time in controller --- plugwise/controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index d20dae9fa..d4d179404 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -138,7 +138,7 @@ def send( ( priority, retry_counter, - datetime.now(), + datetime.utcnow(), [ request, callback, @@ -241,7 +241,7 @@ def _send_message_loop(self): str(seq_id), str(self.expected_responses[seq_id][2]), ) - self.expected_responses[seq_id][3] = datetime.now() + self.expected_responses[seq_id][3] = datetime.utcnow() # Send request self.connection.send(self.expected_responses[seq_id][0]) time.sleep(SLEEP_TIME) @@ -332,7 +332,7 @@ def _receive_timeout_loop(self): for seq_id in list(self.expected_responses.keys()): if self.expected_responses[seq_id][3] is not None: if self.expected_responses[seq_id][3] < ( - datetime.now() - timedelta(seconds=MESSAGE_TIME_OUT) + datetime.utcnow() - timedelta(seconds=MESSAGE_TIME_OUT) ): _mac = "" if self.expected_responses[seq_id][0].mac: From ed4034cdcc3e1d5074a37514bb22656ff9b98ca8 Mon Sep 17 00:00:00 2001 From: brefra Date: Mon, 3 Jan 2022 13:52:59 +0100 Subject: [PATCH 06/87] Rename NodeRequest into PlugwiseRequest --- plugwise/connections/__init__.py | 4 +- plugwise/controller.py | 4 +- plugwise/messages/requests.py | 72 ++++++++++++++++---------------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/plugwise/connections/__init__.py b/plugwise/connections/__init__.py index 0eb4e370a..7012ae80c 100644 --- a/plugwise/connections/__init__.py +++ b/plugwise/connections/__init__.py @@ -5,7 +5,7 @@ import time from ..constants import SLEEP_TIME -from ..messages.requests import NodeRequest +from ..messages.requests import PlugwiseRequest _LOGGER = logging.getLogger(__name__) @@ -96,7 +96,7 @@ def _writer_daemon(self): def _write_data(self, data): """Placeholder.""" - def send(self, message: NodeRequest, callback=None): + def send(self, message: PlugwiseRequest, callback=None): """Add message to write queue.""" self._write_queue.put_nowait((message, callback)) diff --git a/plugwise/controller.py b/plugwise/controller.py index d4d179404..20acaf026 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -29,7 +29,7 @@ UTF8_DECODE, Priority, ) -from .messages.requests import NodeInfoRequest, NodePingRequest, NodeRequest +from .messages.requests import NodeInfoRequest, NodePingRequest, PlugwiseRequest from .messages.responses import ( NodeResponse, NodeAckResponse, @@ -122,7 +122,7 @@ def connect_to_stick(self, callback=None) -> bool: def send( self, - request: NodeRequest, + request: PlugwiseRequest, callback=None, retry_counter=0, priority: Priority = Priority.Medium, diff --git a/plugwise/messages/requests.py b/plugwise/messages/requests.py index ba33e94be..5be58b2be 100644 --- a/plugwise/messages/requests.py +++ b/plugwise/messages/requests.py @@ -13,7 +13,7 @@ ) -class NodeRequest(PlugwiseMessage): +class PlugwiseRequest(PlugwiseMessage): """Base class for request messages to be send from by USB-Stick.""" def __init__(self, mac): @@ -22,7 +22,7 @@ def __init__(self, mac): self.mac = mac -class NodeNetworkInfoRequest(NodeRequest): +class NodeNetworkInfoRequest(PlugwiseRequest): """TODO: PublicNetworkInfoRequest No arguments @@ -31,7 +31,7 @@ class NodeNetworkInfoRequest(NodeRequest): ID = b"0001" -class CirclePlusConnectRequest(NodeRequest): +class CirclePlusConnectRequest(PlugwiseRequest): """ Request to connect a Circle+ to the Stick @@ -49,7 +49,7 @@ def serialize(self): return MESSAGE_HEADER + msg + checksum + MESSAGE_FOOTER -class NodeAddRequest(NodeRequest): +class NodeAddRequest(PlugwiseRequest): """ Inform node it is added to the Plugwise Network it to memory of Circle+ node @@ -72,12 +72,12 @@ def serialize(self): return MESSAGE_HEADER + msg + checksum + MESSAGE_FOOTER -class NodeAllowJoiningRequest(NodeRequest): +class NodeAllowJoiningRequest(PlugwiseRequest): """ Enable or disable receiving joining request of unjoined nodes. Circle+ node will respond with an acknowledge message - Response message: NodeResponse + Response message: NodeAckLargeResponse """ ID = b"0008" @@ -89,7 +89,7 @@ def __init__(self, accept: bool): self.args.append(Int(val, length=2)) -class NodeResetRequest(NodeRequest): +class NodeResetRequest(PlugwiseRequest): """ TODO: Some kind of reset request @@ -106,12 +106,12 @@ def __init__(self, mac, moduletype, timeout): ] -class StickInitRequest(NodeRequest): """ Initialize USB-Stick Response message: StickInitResponse """ +class StickInitRequest(PlugwiseRequest): ID = b"000A" @@ -121,7 +121,7 @@ def __init__(self): super().__init__("") -class NodeImagePrepareRequest(NodeRequest): +class NodeImagePrepareRequest(PlugwiseRequest): """ TODO: PWEswImagePrepareRequestV1_0 @@ -131,32 +131,32 @@ class NodeImagePrepareRequest(NodeRequest): ID = b"000B" -class NodePingRequest(NodeRequest): """ Ping node Response message: NodePingResponse """ +class NodePingRequest(PlugwiseRequest): ID = b"000D" -class CirclePowerUsageRequest(NodeRequest): """ Request current power usage Response message: CirclePowerUsageResponse """ +class CirclePowerUsageRequest(PlugwiseRequest): ID = b"0012" -class CircleClockSetRequest(NodeRequest): """ Set internal clock of node Response message: [Acknowledge message] """ +class CircleClockSetRequest(PlugwiseRequest): ID = b"0016" @@ -172,11 +172,11 @@ def __init__(self, mac, dt): self.args += [this_date, log_buf_addr, this_time, day_of_week] -class CircleSwitchRelayRequest(NodeRequest): +class CircleSwitchRelayRequest(PlugwiseRequest): """ switches relay on/off - Response message: NodeResponse + Response message: NodeAckLargeResponse """ ID = b"0017" @@ -187,7 +187,7 @@ def __init__(self, mac, on): self.args.append(Int(val, length=2)) -class CirclePlusScanRequest(NodeRequest): +class CirclePlusScanRequest(PlugwiseRequest): """ Get all linked Circle plugs from Circle+ a Plugwise network can have 64 devices the node ID value has a range from 0 to 63 @@ -203,7 +203,7 @@ def __init__(self, mac, node_address): self.node_address = node_address -class NodeRemoveRequest(NodeRequest): +class NodeRemoveRequest(PlugwiseRequest): """ Request node to be removed from Plugwise network by removing it from memory of Circle+ node. @@ -218,7 +218,7 @@ def __init__(self, mac_circle_plus, mac_to_unjoined): self.args.append(String(mac_to_unjoined, length=16)) -class NodeInfoRequest(NodeRequest): +class NodeInfoRequest(PlugwiseRequest): """ Request status info of node @@ -228,7 +228,7 @@ class NodeInfoRequest(NodeRequest): ID = b"0023" -class CircleCalibrationRequest(NodeRequest): +class CircleCalibrationRequest(PlugwiseRequest): """ Request power calibration settings of node @@ -238,7 +238,7 @@ class CircleCalibrationRequest(NodeRequest): ID = b"0026" -class CirclePlusRealTimeClockSetRequest(NodeRequest): +class CirclePlusRealTimeClockSetRequest(PlugwiseRequest): """ Set real time clock of CirclePlus @@ -255,7 +255,7 @@ def __init__(self, mac, dt): self.args += [this_time, day_of_week, this_date] -class CirclePlusRealTimeClockGetRequest(NodeRequest): +class CirclePlusRealTimeClockGetRequest(PlugwiseRequest): """ Request current real time clock of CirclePlus @@ -265,7 +265,7 @@ class CirclePlusRealTimeClockGetRequest(NodeRequest): ID = b"0029" -class CircleClockGetRequest(NodeRequest): +class CircleClockGetRequest(PlugwiseRequest): """ Request current internal clock of node @@ -275,7 +275,7 @@ class CircleClockGetRequest(NodeRequest): ID = b"003E" -class CircleEnableScheduleRequest(NodeRequest): +class CircleEnableScheduleRequest(PlugwiseRequest): """ Request to switch Schedule on or off @@ -292,7 +292,7 @@ def __init__(self, mac, on): self.args.append(Int(1, length=2)) -class NodeAddToGroupRequest(NodeRequest): +class NodeAddToGroupRequest(PlugwiseRequest): """ Add node to group @@ -309,7 +309,7 @@ def __init__(self, mac, group_mac, task_id, port_mask): self.args += [group_mac_val, task_id_val, port_mask_val] -class NodeRemoveFromGroupRequest(NodeRequest): +class NodeRemoveFromGroupRequest(PlugwiseRequest): """ Remove node from group @@ -324,7 +324,7 @@ def __init__(self, mac, group_mac): self.args += [group_mac_val] -class NodeBroadcastGroupSwitchRequest(NodeRequest): +class NodeBroadcastGroupSwitchRequest(PlugwiseRequest): """ Broadcast to group to switch @@ -339,7 +339,7 @@ def __init__(self, group_mac, switch_state: bool): self.args.append(Int(val, length=2)) -class CircleEnergyCountersRequest(NodeRequest): +class CircleEnergyCountersRequest(PlugwiseRequest): """ Request energy usage counters storaged a given memory address @@ -353,7 +353,7 @@ def __init__(self, mac, log_address): self.args.append(LogAddr(log_address, 8)) -class NodeSleepConfigRequest(NodeRequest): +class NodeSleepConfigRequest(PlugwiseRequest): """ Configure timers for SED nodes to minimize battery usage @@ -394,7 +394,7 @@ def __init__( ] -class NodeSelfRemoveRequest(NodeRequest): +class NodeSelfRemoveRequest(PlugwiseRequest): """ @@ -407,7 +407,7 @@ class NodeSelfRemoveRequest(NodeRequest): ID = b"0051" -class NodeMeasureIntervalRequest(NodeRequest): +class NodeMeasureIntervalRequest(PlugwiseRequest): """ Configure the logging interval of power measurement in minutes @@ -422,7 +422,7 @@ def __init__(self, mac, usage, production): self.args.append(Int(production, length=4)) -class NodeClearGroupMacRequest(NodeRequest): +class NodeClearGroupMacRequest(PlugwiseRequest): """ TODO: @@ -436,7 +436,7 @@ def __init__(self, mac, taskId): self.args.append(Int(taskId, length=2)) -class CircleSetScheduleValueRequest(NodeRequest): +class CircleSetScheduleValueRequest(PlugwiseRequest): """ Send chunk of On/Off/StandbyKiller Schedule to Circle(+) @@ -450,7 +450,7 @@ def __init__(self, mac, val): self.args.append(SInt(val, length=4)) -class NodeFeaturesRequest(NodeRequest): +class NodeFeaturesRequest(PlugwiseRequest): """ Request feature set node supports @@ -460,7 +460,7 @@ class NodeFeaturesRequest(NodeRequest): ID = b"005F" -class ScanConfigureRequest(NodeRequest): +class ScanConfigureRequest(PlugwiseRequest): """ Configure a Scan node @@ -488,7 +488,7 @@ def __init__(self, mac, reset_timer: int, sensitivity: int, light: bool): ] -class ScanLightCalibrateRequest(NodeRequest): +class ScanLightCalibrateRequest(PlugwiseRequest): """ Calibrate light sensitivity @@ -498,7 +498,7 @@ class ScanLightCalibrateRequest(NodeRequest): ID = b"0102" -class SenseReportIntervalRequest(NodeRequest): +class SenseReportIntervalRequest(PlugwiseRequest): """ Sets the Sense temperature and humidity measurement report interval in minutes. Based on this interval, periodically a 'SenseReportResponse' message is sent by the Sense node @@ -513,7 +513,7 @@ def __init__(self, mac, interval): self.args.append(Int(interval, length=2)) -class CircleInitialRelaisStateRequest(NodeRequest): +class CircleInitialRelaisStateRequest(PlugwiseRequest): """ Get or set initial Relais state From 0d4b05d18d98978c1c23526cf400c5784b56c216 Mon Sep 17 00:00:00 2001 From: brefra Date: Mon, 3 Jan 2022 13:56:59 +0100 Subject: [PATCH 07/87] Rename USBresponse into PlugwiseResponse --- plugwise/messages/responses.py | 48 +++++++++++++++++----------------- plugwise/nodes/__init__.py | 4 +-- plugwise/stick.py | 6 ++--- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index 84cae90c1..3ebd239d9 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -22,7 +22,7 @@ ) -class USBresponse(PlugwiseMessage): +class PlugwiseResponse(PlugwiseMessage): """ Base class for response messages received by USB-Stick. """ @@ -92,7 +92,7 @@ def __len__(self): return 34 + arglen + self.len_correction -class StickResponse(USBresponse): +class StickResponse(PlugwiseResponse): """ Acknowledge message without source MAC @@ -105,7 +105,7 @@ def __init__(self): super().__init__(MESSAGE_SMALL) -class NodeResponse(USBresponse): +class NodeResponse(PlugwiseResponse): """ Acknowledge message with source MAC @@ -118,7 +118,7 @@ def __init__(self): super().__init__(MESSAGE_LARGE) -class CirclePlusQueryResponse(USBresponse): +class CirclePlusQueryResponse(PlugwiseResponse): """ TODO: @@ -156,7 +156,7 @@ def deserialize(self, response): self.new_node_mac_id.value = b"00" + self.new_node_mac_id.value[2:] -class CirclePlusQueryEndResponse(USBresponse): +class CirclePlusQueryEndResponse(PlugwiseResponse): """ TODO: PWAckReplyV1_0 @@ -177,7 +177,7 @@ def __len__(self): return 18 + arglen -class CirclePlusConnectResponse(USBresponse): +class CirclePlusConnectResponse(PlugwiseResponse): """ CirclePlus connected to the network @@ -197,7 +197,7 @@ def __len__(self): return 18 + arglen -class NodeJoinAvailableResponse(USBresponse): +class NodeJoinAvailableResponse(PlugwiseResponse): """ Message from an unjoined node to notify it is available to join a plugwise network @@ -207,7 +207,7 @@ class NodeJoinAvailableResponse(USBresponse): ID = b"0006" -class StickInitResponse(USBresponse): +class StickInitResponse(PlugwiseResponse): """ Returns the configuration and status of the USB-Stick @@ -240,7 +240,7 @@ def __init__(self): ] -class NodePingResponse(USBresponse): +class NodePingResponse(PlugwiseResponse): """ Ping response from node @@ -265,7 +265,7 @@ def __init__(self): ] -class CirclePowerUsageResponse(USBresponse): +class CirclePowerUsageResponse(PlugwiseResponse): """ Returns power usage as impulse counters for several different timeframes @@ -290,7 +290,7 @@ def __init__(self): ] -class CirclePlusScanResponse(USBresponse): +class CirclePlusScanResponse(PlugwiseResponse): """ Returns the MAC of a registered node at the specified memory address @@ -306,7 +306,7 @@ def __init__(self): self.params += [self.node_mac, self.node_address] -class NodeRemoveResponse(USBresponse): +class NodeRemoveResponse(PlugwiseResponse): """ Returns conformation (or not) if node is removed from the Plugwise network by having it removed from the memory of the Circle+ @@ -323,7 +323,7 @@ def __init__(self): self.params += [self.node_mac_id, self.status] -class NodeInfoResponse(USBresponse): +class NodeInfoResponse(PlugwiseResponse): """ Returns the status information of Node @@ -352,7 +352,7 @@ def __init__(self): ] -class CircleCalibrationResponse(USBresponse): +class CircleCalibrationResponse(PlugwiseResponse): """ returns the calibration settings of node @@ -370,7 +370,7 @@ def __init__(self): self.params += [self.gain_a, self.gain_b, self.off_tot, self.off_noise] -class CirclePlusRealTimeClockResponse(USBresponse): +class CirclePlusRealTimeClockResponse(PlugwiseResponse): """ returns the real time clock of CirclePlus node @@ -388,7 +388,7 @@ def __init__(self): self.params += [self.time, self.day_of_week, self.date] -class CircleClockResponse(USBresponse): +class CircleClockResponse(PlugwiseResponse): """ Returns the current internal clock of Node @@ -406,7 +406,7 @@ def __init__(self): self.params += [self.time, self.day_of_week, self.unknown, self.unknown2] -class CircleEnergyCountersResponse(USBresponse): +class CircleEnergyCountersResponse(PlugwiseResponse): """ Returns historical energy usage of requested memory address Each response contains 4 energy counters at specified 1 hour timestamp @@ -440,7 +440,7 @@ def __init__(self): ] -class NodeAwakeResponse(USBresponse): +class NodeAwakeResponse(PlugwiseResponse): """ A sleeping end device (SED: Scan, Sense, Switch) sends this message to announce that is awake. Awake types: @@ -462,7 +462,7 @@ def __init__(self): self.params += [self.awake_type] -class NodeSwitchGroupResponse(USBresponse): +class NodeSwitchGroupResponse(PlugwiseResponse): """ A sleeping end device (SED: Scan, Sense, Switch) sends this message to switch groups on/off when the configured @@ -483,7 +483,7 @@ def __init__(self): ] -class NodeFeaturesResponse(USBresponse): +class NodeFeaturesResponse(PlugwiseResponse): """ Returns supported features of node TODO: FeatureBitmask @@ -499,7 +499,7 @@ def __init__(self): self.params += [self.features] -class NodeJoinAckResponse(USBresponse): +class NodeJoinAckResponse(PlugwiseResponse): """ Notification message when node (re)joined existing network again. Sent when a SED (re)joins the network e.g. when you reinsert the battery of a Scan @@ -514,7 +514,7 @@ def __init__(self): # sequence number is always FFFD -class NodeAckResponse(USBresponse): +class NodeAckResponse(PlugwiseResponse): """ Acknowledge message in regular format Sent by nodes supporting plugwise 2.4 protocol version @@ -529,7 +529,7 @@ def __init__(self): self.ack_id = Int(0, 2, False) -class SenseReportResponse(USBresponse): +class SenseReportResponse(PlugwiseResponse): """ Returns the current temperature and humidity of a Sense node. The interval this report is sent is configured by the 'SenseReportIntervalRequest' request @@ -546,7 +546,7 @@ def __init__(self): self.params += [self.humidity, self.temperature] -class CircleInitialRelaisStateResponse(USBresponse): +class CircleInitialRelaisStateResponse(PlugwiseResponse): """ Returns the initial relais state. diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 674090d03..242069d71 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -17,7 +17,7 @@ NodeInfoResponse, NodeJoinAckResponse, NodePingResponse, - USBresponse, + PlugwiseResponse, ) from ..util import validate_mac, version_to_model @@ -180,7 +180,7 @@ def _request_ping(self, callback=None, ignore_sensor=True): def message_for_node(self, message): """Process received message.""" - assert isinstance(message, USBresponse) + assert isinstance(message, PlugwiseResponse) if message.mac == self._mac: if message.timestamp is not None: _LOGGER.debug( diff --git a/plugwise/stick.py b/plugwise/stick.py index 01ef01286..c3e712801 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -44,13 +44,13 @@ StickInitRequest, ) from .messages.responses import ( - NodeResponse, NodeAckResponse, NodeInfoResponse, NodeJoinAvailableResponse, NodeRemoveResponse, - USBresponse, + NodeResponse, StickInitResponse, + PlugwiseResponse, ) from .nodes.circle import PlugwiseCircle from .nodes.circle_plus import PlugwiseCirclePlus @@ -407,7 +407,7 @@ def _remove_node(self, mac): else: _LOGGER.warning("Node %s does not exists, unable to remove node.", mac) - def message_processor(self, message: USBresponse): + def message_processor(self, message: PlugwiseResponse): """Received message from Plugwise network.""" mac = message.mac.decode(UTF8_DECODE) if isinstance(message, (NodeResponse, NodeAckResponse)): From dda6e4a2fb3394f87d22e874dc83ab1e539b303d Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 09:20:27 +0100 Subject: [PATCH 08/87] Rename NodeJoinAckResponse into NodeRejoinResponse --- plugwise/messages/responses.py | 4 ++-- plugwise/util.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index 3ebd239d9..0673fbe4d 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -499,7 +499,7 @@ def __init__(self): self.params += [self.features] -class NodeJoinAckResponse(PlugwiseResponse): +class NodeRejoinResponse(PlugwiseResponse): """ Notification message when node (re)joined existing network again. Sent when a SED (re)joins the network e.g. when you reinsert the battery of a Scan @@ -589,7 +589,7 @@ def get_message_response(message_id, length, seq_id): """ # First check for known sequence ID's if seq_id == b"FFFD": - return NodeJoinAckResponse() + return NodeRejoinResponse() if seq_id == b"FFFE": return NodeAwakeResponse() if seq_id == b"FFFF": diff --git a/plugwise/util.py b/plugwise/util.py index 0d5e496c2..ba7ca82b3 100644 --- a/plugwise/util.py +++ b/plugwise/util.py @@ -60,7 +60,7 @@ def inc_seq_id(seq_id, value=1) -> bytearray: temp_int = int(seq_id, 16) + value # Max seq_id = b'FFFB' # b'FFFC' reserved for message - # b'FFFD' reserved for 'NodeJoinAckResponse' message + # b'FFFD' reserved for 'NodeRejoinResponse' message # b'FFFE' reserved for 'NodeSwitchGroupResponse' message # b'FFFF' reserved for 'NodeAwakeResponse' message if temp_int >= 65532: From 16b37e75d3d95d95e650a8ae997861f226563da5 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 09:28:17 +0100 Subject: [PATCH 09/87] Make timestamp messages timezone aware --- plugwise/messages/responses.py | 4 ++-- plugwise/nodes/circle.py | 19 +++++++++++-------- plugwise/nodes/circle_plus.py | 10 +++++----- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index 0673fbe4d..b2e23f438 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -1,5 +1,5 @@ """All known response messages to be received from plugwise devices.""" -from datetime import datetime +from datetime import datetime, timezone from ..constants import MESSAGE_FOOTER, MESSAGE_HEADER, MESSAGE_LARGE, MESSAGE_SMALL from ..exceptions import ( @@ -43,7 +43,7 @@ def __init__(self, format_size=None): self.len_correction = 0 def deserialize(self, response): - self.timestamp = datetime.now() + self.timestamp = datetime.utcnow().replace(tzinfo=timezone.utc) if response[:4] != MESSAGE_HEADER: raise InvalidMessageHeader( f"Invalid message header {str(response[:4])} for {self.__class__.__name__}" diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 1316ab89d..8a69c556b 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -1,5 +1,5 @@ """Plugwise Circle node object.""" -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import logging from ..constants import ( @@ -83,7 +83,7 @@ def __init__(self, mac, address, message_sender): self._energy_pulses_today_now = None self._energy_pulses_yesterday = None self._new_relay_state = False - self._new_relay_stamp = datetime.now() - timedelta(seconds=MESSAGE_TIME_OUT) + self._new_relay_stamp = datetime.utcnow() - timedelta(seconds=MESSAGE_TIME_OUT) self._pulses_1s = None self._pulses_8s = None self._pulses_produced_1h = None @@ -180,7 +180,10 @@ def relay_state(self) -> bool: Return last known relay state or the new switch state by anticipating the acknowledge for new state is getting in before message timeout. """ - if self._new_relay_stamp + timedelta(seconds=MESSAGE_TIME_OUT) > datetime.now(): + if ( + self._new_relay_stamp + timedelta(seconds=MESSAGE_TIME_OUT) + > datetime.utcnow() + ): return self._new_relay_state return self._relay_state @@ -189,7 +192,7 @@ def relay_state(self, state): """Request the relay to switch state.""" self._request_switch(state) self._new_relay_state = state - self._new_relay_stamp = datetime.now() + self._new_relay_stamp = datetime.utcnow() if state != self._relay_state: self.do_callback(FEATURE_RELAY["id"]) @@ -761,13 +764,13 @@ def _response_energy_counters(self, message: CircleEnergyCountersResponse): def _response_clock(self, message: CircleClockResponse): log_date = datetime( - datetime.now().year, - datetime.now().month, - datetime.now().day, + datetime.utcnow().year, + datetime.utcnow().month, + datetime.utcnow().day, message.time.value.hour, message.time.value.minute, message.time.value.second, - ) + ).replace(tzinfo=timezone.utc) clock_offset = message.timestamp.replace(microsecond=0) - ( log_date + self.timezone_delta ) diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index a4ec45653..599ee04fc 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -1,5 +1,5 @@ """Plugwise Circle+ node object.""" -from datetime import datetime +from datetime import datetime, timezone import logging from ..constants import MAX_TIME_DRIFT, Priority, UTF8_DECODE @@ -98,13 +98,13 @@ def get_real_time_clock(self, callback=None): def _response_realtime_clock(self, message): realtime_clock_dt = datetime( - datetime.now().year, - datetime.now().month, - datetime.now().day, + datetime.utcnow().year, + datetime.utcnow().month, + datetime.utcnow().day, message.time.value.hour, message.time.value.minute, message.time.value.second, - ) + ).replace(tzinfo=timezone.utc) realtime_clock_offset = message.timestamp.replace(microsecond=0) - ( realtime_clock_dt + self.timezone_delta ) From 315c7e329ab747177b9ce87f6edfb536af725f3d Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 09:33:16 +0100 Subject: [PATCH 10/87] Add Enums of message response types --- plugwise/messages/responses.py | 51 ++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index b2e23f438..1903fcce1 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -1,5 +1,8 @@ """All known response messages to be received from plugwise devices.""" +from __future__ import annotations + from datetime import datetime, timezone +from enum import Enum from ..constants import MESSAGE_FOOTER, MESSAGE_HEADER, MESSAGE_LARGE, MESSAGE_SMALL from ..exceptions import ( @@ -22,6 +25,54 @@ ) +class StickResponseType(bytes, Enum): + """Response message types for stick.""" + + # Minimal value = b"00C0", maximum value = b"00F3" + # Below the currently known values: + + success = b"00C1" + failed = b"00C2" + timeout = b"00E1" + + +class NodeResponseType(bytes, Enum): + """Response types of a 'NodeResponse' reply message.""" + + ClockAccepted = b"00D7" + JoinAccepted = b"00D9" + RelaySwitchedOff = b"00DE" + RelaySwitchedOn = b"00D8" + RelaySwitchFailed = b"00E2" + SleepConfigAccepted = b"00F6" + SleepConfigFailed = b"00F7" # TODO: Validate + RealTimeClockAccepted = b"00DF" + RealTimeClockFailed = b"00E7" + + +class NodeAckResponseType(bytes, Enum): + """Response types of a 'NodeAckResponse' reply message.""" + + ScanConfigAccepted = b"00BE" + ScanConfigFailed = b"00BF" + ScanLightCalibrationAccepted = b"00BD" + SenseIntervalAccepted = b"00B3" + SenseIntervalFailed = b"00B4" + SenseBoundariesAccepted = b"00B5" + SenseBoundariesFailed = b"00B6" + + +class NodeAwakeResponseType(int, Enum): + """Response types of a 'NodeAwakeResponse' reply message.""" + + Maintenance = 0 # SED awake for maintenance + First = 1 # SED awake for the first time + Startup = 2 # SED awake after restart, e.g. after reinserting a battery + State = 3 # SED awake to report state (Motion / Temperature / Humidity + Unknown = 4 + Button = 5 # SED awake due to button press + + class PlugwiseResponse(PlugwiseMessage): """ Base class for response messages received by USB-Stick. From ee5093387b99a5e2a97cf50feca07eb336d61015 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 09:35:19 +0100 Subject: [PATCH 11/87] Add message priority enum --- plugwise/messages/requests.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugwise/messages/requests.py b/plugwise/messages/requests.py index 5be58b2be..d2620aff0 100644 --- a/plugwise/messages/requests.py +++ b/plugwise/messages/requests.py @@ -1,4 +1,6 @@ """All known request messages to be send to plugwise devices.""" +from enum import Enum + from ..constants import MESSAGE_FOOTER, MESSAGE_HEADER from ..messages import PlugwiseMessage from ..util import ( @@ -13,6 +15,14 @@ ) +class Priority(int, Enum): + """Message priority levels for USB-stick.""" + + High = 1 + Medium = 2 + Low = 3 + + class PlugwiseRequest(PlugwiseMessage): """Base class for request messages to be send from by USB-Stick.""" From 0823fb2f39987650001d7f488bd232a762b195a1 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 10:08:03 +0100 Subject: [PATCH 12/87] Apply consistency to message process methods --- plugwise/nodes/circle.py | 51 ++++++++++++++++++++--------------- plugwise/nodes/circle_plus.py | 13 +++++---- plugwise/nodes/scan.py | 13 ++++----- plugwise/nodes/sed.py | 6 ++--- plugwise/nodes/sense.py | 6 ++--- plugwise/nodes/switch.py | 8 +++--- 6 files changed, 56 insertions(+), 41 deletions(-) diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 8a69c556b..6210320be 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -237,7 +237,7 @@ def message_for_circle(self, message): """ if isinstance(message, CirclePowerUsageResponse): if self.calibration: - self._response_power_usage(message) + self._process_CirclePowerUsageResponse(message) _LOGGER.debug( "Power update for %s, last update %s", self.mac, @@ -250,9 +250,9 @@ def message_for_circle(self, message): ) self._request_calibration(self.request_power_update) elif isinstance(message, NodeResponse): - self._node_ack_response(message) + self._process_NodeResponse(message) elif isinstance(message, CircleCalibrationResponse): - self._response_calibration(message) + self._process_CircleCalibrationResponse(message) elif isinstance(message, CircleEnergyCountersResponse): if self.calibration: self._response_energy_counters(message) @@ -261,17 +261,17 @@ def message_for_circle(self, message): "Received power buffer log for %s before calibration information is known", self.mac, ) - self._request_calibration(self.request_energy_counters) + self._process_CircleEnergyCountersResponse(message) elif isinstance(message, CircleClockResponse): - self._response_clock(message) + self._process_CircleClockResponse(message) else: self.message_for_circle_plus(message) def message_for_circle_plus(self, message): """Pass messages to PlugwiseCirclePlus class""" - def _node_ack_response(self, message): - """Process switch response message""" + def _process_NodeResponse(self, message: NodeResponse) -> None: + """Process content of 'NodeResponse' message.""" if message.ack_id == RELAY_SWITCHED_ON: if not self._relay_state: _LOGGER.debug( @@ -294,8 +294,11 @@ def _node_ack_response(self, message): str(message.ack_id), self.mac, ) + def _process_CirclePowerUsageResponse( + self, message: CirclePowerUsageResponse + ) -> None: + """Process content of 'CirclePowerUsageResponse' message.""" - def _response_power_usage(self, message: CirclePowerUsageResponse): # Sometimes the circle returns -1 for some of the pulse counters # likely this means the circle measures very little power and is suffering from # rounding errors. Zero these out. However, negative pulse values are valid @@ -360,8 +363,10 @@ def _response_power_usage(self, message: CirclePowerUsageResponse): self._pulses_produced_1h = message.pulse_hour_produced.value self.do_callback(FEATURE_POWER_PRODUCTION_CURRENT_HOUR["id"]) - def _response_calibration(self, message: CircleCalibrationResponse): - """Store calibration properties""" + def _process_CircleCalibrationResponse( + self, message: CircleCalibrationResponse + ) -> None: + """Process content of 'CircleCalibrationResponse' message.""" for calibration in ("gain_a", "gain_b", "off_noise", "off_tot"): val = getattr(message, calibration).value setattr(self, "_" + calibration, val) @@ -652,12 +657,15 @@ def request_energy_counters(self, log_address=None, callback=None): Priority.Low, ) - def _response_energy_counters(self, message: CircleEnergyCountersResponse): - """ - Save historical energy information in local counters - Each response message contains 4 log counters (slots) - of the energy pulses collected during the previous hour of given timestamp - """ + def _process_CircleEnergyCountersResponse( + self, message: CircleEnergyCountersResponse + ) -> None: + """Process content of 'CircleEnergyCountersResponse' message.""" + + # Save historical energy information in local counters + # Each response message contains 4 log counters (slots) + # of the energy pulses collected during the previous hour of given timestamp + if message.logaddr.value == self._last_log_address: self._energy_last_populated_slot = 0 @@ -698,7 +706,7 @@ def _response_energy_counters(self, message: CircleEnergyCountersResponse): self._energy_last_rollover_timestamp = _utc_hour_timestamp _history_rollover = True _LOGGER.info( - "_response_energy_counters for %s | history rollover, reset date to %s", + "_process_CircleEnergyCountersResponse for %s | history rollover, reset date to %s", self.mac, str(_utc_hour_timestamp), ) @@ -709,7 +717,7 @@ def _response_energy_counters(self, message: CircleEnergyCountersResponse): and self._energy_consumption_today_reset < _local_midnight_timestamp ): _LOGGER.info( - "_response_energy_counters for %s | midnight rollover, reset date to %s", + "_process_CircleEnergyCountersResponse for %s | midnight rollover, reset date to %s", self.mac, str(_local_midnight_timestamp), ) @@ -728,7 +736,7 @@ def _response_energy_counters(self, message: CircleEnergyCountersResponse): _midnight_rollover = False else: _LOGGER.info( - "_response_energy_counters for %s | collection not running, len=%s, timestamp:%s=%s", + "_process_CircleEnergyCountersResponse for %s | collection not running, len=%s, timestamp:%s=%s", self.mac, str(len(self._energy_history)), str(self._energy_last_collected_timestamp), @@ -749,7 +757,7 @@ def _response_energy_counters(self, message: CircleEnergyCountersResponse): self._update_energy_today_now(False, _history_rollover, _midnight_rollover) else: _LOGGER.info( - "_response_energy_counters for %s | self._energy_history_collecting running", + "_process_CircleEnergyCountersResponse for %s | self._energy_history_collecting running", self.mac, str(_local_midnight_timestamp), ) @@ -762,7 +770,8 @@ def _response_energy_counters(self, message: CircleEnergyCountersResponse): if log_timestamp < _8_days_ago: del self._energy_history[log_timestamp] - def _response_clock(self, message: CircleClockResponse): + def _process_CircleClockResponse(self, message: CircleClockResponse) -> None: + """Process content of 'CircleClockResponse' message.""" log_date = datetime( datetime.utcnow().year, datetime.utcnow().month, diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index 599ee04fc..699032d64 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -30,9 +30,9 @@ def message_for_circle_plus(self, message): Process received message """ if isinstance(message, CirclePlusRealTimeClockResponse): - self._response_realtime_clock(message) + self._process_CirclePlusRealTimeClockResponse(message) elif isinstance(message, CirclePlusScanResponse): - self._process_scan_response(message) + self._process_CirclePlusScanResponse(message) else: _LOGGER.waning( "Unsupported message type '%s' received from circle with mac %s", @@ -47,8 +47,8 @@ def scan_for_nodes(self, callback=None): self.message_sender(CirclePlusScanRequest(self._mac, node_address)) self._scan_response[node_address] = False - def _process_scan_response(self, message): - """Process scan response message.""" + def _process_CirclePlusScanResponse(self, message: CirclePlusScanResponse) -> None: + """Process content of 'CirclePlusScanResponse' message.""" _LOGGER.debug( "Process scan response for address %s", message.node_address.value ) @@ -96,7 +96,10 @@ def get_real_time_clock(self, callback=None): Priority.Low, ) - def _response_realtime_clock(self, message): + def _process_CirclePlusRealTimeClockResponse( + self, message: CirclePlusRealTimeClockResponse + ) -> None: + """Process content of 'CirclePlusRealTimeClockResponse' message.""" realtime_clock_dt = datetime( datetime.utcnow().year, datetime.utcnow().month, diff --git a/plugwise/nodes/scan.py b/plugwise/nodes/scan.py index 0d63092ea..c44119001 100644 --- a/plugwise/nodes/scan.py +++ b/plugwise/nodes/scan.py @@ -55,9 +55,9 @@ def message_for_scan(self, message): str(message.power_state.value), self.mac, ) - self._process_switch_group(message) + self._process_NodeSwitchGroupResponse(message) elif isinstance(message, NodeAckResponse): - self._process_ack_message(message) + self._process_NodeAckResponse(message) else: _LOGGER.info( "Unsupported message %s received from %s", @@ -65,8 +65,8 @@ def message_for_scan(self, message): self.mac, ) - def _process_ack_message(self, message): - """Process acknowledge message""" + def _process_NodeAckResponse(self, message: NodeAckResponse): + """Process content of 'NodeAckResponse' message.""" if message.ack_id == SCAN_CONFIGURE_ACCEPTED: self._motion_reset_timer = self._new_motion_reset_timer self._daylight_mode = self._new_daylight_mode @@ -78,8 +78,9 @@ def _process_ack_message(self, message): self.mac, ) - def _process_switch_group(self, message): - """Switch group request from Scan""" + def _process_NodeSwitchGroupResponse( + self, message: NodeSwitchGroupResponse + ) -> None: if message.power_state.value == 0: # turn off => clear motion if self._motion_state: diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index 188b99d53..583f3cd40 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -46,7 +46,7 @@ def message_for_sed(self, message): Process received message """ if isinstance(message, NodeAwakeResponse): - self._process_awake_response(message) + self._process_NodeAwakeResponse(message) elif isinstance(message, NodeResponse): if message.ack_id == SLEEP_SET: self.maintenance_interval = self._new_maintenance_interval @@ -68,8 +68,8 @@ def message_for_switch(self, message): def message_for_sense(self, message): """Pass messages to PlugwiseSense class""" - def _process_awake_response(self, message): - """ "Process awake message""" + def _process_NodeAwakeResponse(self, message: NodeAwakeResponse) -> None: + """Process content of 'NodeAwakeResponse' message.""" _LOGGER.debug( "Awake message type '%s' received from %s", str(message.awake_type.value), diff --git a/plugwise/nodes/sense.py b/plugwise/nodes/sense.py index af6574f62..0346e6080 100644 --- a/plugwise/nodes/sense.py +++ b/plugwise/nodes/sense.py @@ -48,7 +48,7 @@ def message_for_sense(self, message): Process received message """ if isinstance(message, SenseReportResponse): - self._process_sense_report(message) + self._process_SenseReportResponse(message) else: _LOGGER.info( "Unsupported message %s received from %s", @@ -56,8 +56,8 @@ def message_for_sense(self, message): self.mac, ) - def _process_sense_report(self, message): - """process sense report message to extract current temperature and humidity values.""" + def _process_SenseReportResponse(self, message: SenseReportResponse) -> None: + """Process content of 'NodeAckResponse' message.""" if message.temperature.value != 65535: new_temperature = int( SENSE_TEMPERATURE_MULTIPLIER * (message.temperature.value / 65536) diff --git a/plugwise/nodes/switch.py b/plugwise/nodes/switch.py index f55f0c605..0d3ffdba7 100644 --- a/plugwise/nodes/switch.py +++ b/plugwise/nodes/switch.py @@ -37,10 +37,12 @@ def message_for_switch(self, message): self.mac, str(message.group), ) - self._process_switch_group(message) + self._process_NodeSwitchGroupResponse(message) - def _process_switch_group(self, message): - """Switch group request from Scan""" + def _process_NodeSwitchGroupResponse( + self, message: NodeSwitchGroupResponse + ) -> None: + """Process content of 'NodeSwitchGroupResponse' message.""" if message.power_state == 0: # turn off => clear motion if self._switch_state: From a7217cd5a7b8803a2f7a57ca28e9704430e4d9a1 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 10:46:35 +0100 Subject: [PATCH 13/87] Make use of super() for message handling at node classes --- plugwise/nodes/__init__.py | 25 ++++++++----------------- plugwise/nodes/circle.py | 13 +++++-------- plugwise/nodes/circle_plus.py | 14 +++++--------- plugwise/nodes/scan.py | 19 +++++++++---------- plugwise/nodes/sed.py | 30 +++++++++++++----------------- plugwise/nodes/sense.py | 15 +++++---------- plugwise/nodes/switch.py | 17 ++++++----------- 7 files changed, 51 insertions(+), 82 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 242069d71..2e9738751 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -178,9 +178,8 @@ def _request_ping(self, callback=None, ignore_sensor=True): callback, ) - def message_for_node(self, message): - """Process received message.""" - assert isinstance(message, PlugwiseResponse) + def message_for_node(self, message: PlugwiseResponse) -> None: + """Process received messages for base PlugwiseNode class.""" if message.mac == self._mac: if message.timestamp is not None: _LOGGER.debug( @@ -193,6 +192,7 @@ def message_for_node(self, message): if not self._available: self.available = True self._request_info() + self._last_update = message.timestamp if isinstance(message, NodePingResponse): self._process_ping_response(message) elif isinstance(message, NodeInfoResponse): @@ -202,20 +202,11 @@ def message_for_node(self, message): elif isinstance(message, NodeJoinAckResponse): self._process_join_ack_response(message) else: - self.message_for_circle(message) - self.message_for_sed(message) - else: - _LOGGER.debug( - "Skip message, mac of node (%s) != mac at message (%s)", - message.mac.decode(UTF8_DECODE), - self.mac, - ) - - def message_for_circle(self, message): - """Pass messages to PlugwiseCircle class""" - - def message_for_sed(self, message): - """Pass messages to NodeSED class""" + _LOGGER.warning( + "Unmanaged %s received for %s", + message.__class__.__name__, + self.mac, + ) def subscribe_callback(self, callback, sensor) -> bool: """Subscribe callback to execute when state change happens.""" diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 6210320be..399e36ee9 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -36,6 +36,7 @@ CircleEnergyCountersResponse, CirclePowerUsageResponse, NodeResponse, + PlugwiseResponse, ) from ..nodes import PlugwiseNode @@ -231,10 +232,9 @@ def request_power_update(self, callback=None): # No history collected yet, request energy history self.request_energy_counters() - def message_for_circle(self, message): - """ - Process received message - """ + def message_for_node(self, message: PlugwiseResponse) -> None: + """Process received messages for PlugwiseCircle class.""" + self._last_update = message.timestamp if isinstance(message, CirclePowerUsageResponse): if self.calibration: self._process_CirclePowerUsageResponse(message) @@ -265,10 +265,7 @@ def message_for_circle(self, message): elif isinstance(message, CircleClockResponse): self._process_CircleClockResponse(message) else: - self.message_for_circle_plus(message) - - def message_for_circle_plus(self, message): - """Pass messages to PlugwiseCirclePlus class""" + super().message_for_node(message) def _process_NodeResponse(self, message: NodeResponse) -> None: """Process content of 'NodeResponse' message.""" diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index 699032d64..f90f2afcb 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -7,6 +7,7 @@ CirclePlusRealTimeClockGetRequest, CirclePlusRealTimeClockSetRequest, CirclePlusScanRequest, +from ..messages.responses import ( ) from ..messages.responses import CirclePlusRealTimeClockResponse, CirclePlusScanResponse from ..nodes.circle import PlugwiseCircle @@ -25,20 +26,15 @@ def __init__(self, mac, address, message_sender): self._realtime_clock_offset = None self.get_real_time_clock(self.sync_realtime_clock) - def message_for_circle_plus(self, message): - """ - Process received message - """ + def message_for_node(self, message: PlugwiseResponse) -> None: + """Process received messages for PlugwiseCirclePlus class.""" + self._last_update = message.timestamp if isinstance(message, CirclePlusRealTimeClockResponse): self._process_CirclePlusRealTimeClockResponse(message) elif isinstance(message, CirclePlusScanResponse): self._process_CirclePlusScanResponse(message) else: - _LOGGER.waning( - "Unsupported message type '%s' received from circle with mac %s", - str(message.__class__.__name__), - self.mac, - ) + super().message_for_node(message) def scan_for_nodes(self, callback=None): """Scan for registered nodes.""" diff --git a/plugwise/nodes/scan.py b/plugwise/nodes/scan.py index c44119001..a0e5ff21e 100644 --- a/plugwise/nodes/scan.py +++ b/plugwise/nodes/scan.py @@ -14,7 +14,11 @@ SCAN_SENSITIVITY_OFF, ) from ..messages.requests import ScanConfigureRequest, ScanLightCalibrateRequest -from ..messages.responses import NodeAckResponse, NodeSwitchGroupResponse +from ..messages.responses import ( + NodeAckResponse, + NodeSwitchGroupResponse, + PlugwiseResponse, +) from ..nodes.sed import NodeSED _LOGGER = logging.getLogger(__name__) @@ -44,10 +48,9 @@ def motion(self) -> bool: """Return the last known motion state""" return self._motion_state - def message_for_scan(self, message): - """ - Process received message - """ + def message_for_node(self, message: PlugwiseResponse) -> None: + """Process received messages for PlugwiseScan class.""" + self._last_update = message.timestamp if isinstance(message, NodeSwitchGroupResponse): _LOGGER.debug( "Switch group %s to state %s received from %s", @@ -59,11 +62,7 @@ def message_for_scan(self, message): elif isinstance(message, NodeAckResponse): self._process_NodeAckResponse(message) else: - _LOGGER.info( - "Unsupported message %s received from %s", - message.__class__.__name__, - self.mac, - ) + super().message_for_node(message) def _process_NodeAckResponse(self, message: NodeAckResponse): """Process content of 'NodeAckResponse' message.""" diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index 583f3cd40..3b55ceb17 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -23,8 +23,15 @@ SED_STAY_ACTIVE, SLEEP_SET, ) -from ..messages.requests import NodeInfoRequest, NodePingRequest, NodeSleepConfigRequest -from ..messages.responses import NodeResponse, NodeAwakeResponse +from ..messages.requests import ( + NodeInfoRequest, + NodePingRequest, + NodeSleepConfigRequest, +from ..messages.responses import ( + NodeAwakeResponse, + NodeResponse, + PlugwiseResponse, +) from ..nodes import PlugwiseNode _LOGGER = logging.getLogger(__name__) @@ -41,10 +48,9 @@ def __init__(self, mac, address, message_sender): self._wake_up_interval = None self._battery_powered = True - def message_for_sed(self, message): - """ - Process received message - """ + def message_for_node(self, message: PlugwiseResponse) -> None: + """Process received messages for NodeSED class.""" + self._last_update = message.timestamp if isinstance(message, NodeAwakeResponse): self._process_NodeAwakeResponse(message) elif isinstance(message, NodeResponse): @@ -55,18 +61,8 @@ def message_for_sed(self, message): self.message_for_switch(message) self.message_for_sense(message) else: - self.message_for_scan(message) - self.message_for_switch(message) - self.message_for_sense(message) - - def message_for_scan(self, message): - """Pass messages to PlugwiseScan class""" - - def message_for_switch(self, message): - """Pass messages to PlugwiseSwitch class""" + super().message_for_node(message) - def message_for_sense(self, message): - """Pass messages to PlugwiseSense class""" def _process_NodeAwakeResponse(self, message: NodeAwakeResponse) -> None: """Process content of 'NodeAwakeResponse' message.""" diff --git a/plugwise/nodes/sense.py b/plugwise/nodes/sense.py index 0346e6080..bea2ba567 100644 --- a/plugwise/nodes/sense.py +++ b/plugwise/nodes/sense.py @@ -12,7 +12,7 @@ SENSE_TEMPERATURE_MULTIPLIER, SENSE_TEMPERATURE_OFFSET, ) -from ..messages.responses import SenseReportResponse +from ..messages.responses import PlugwiseResponse, SenseReportResponse from ..nodes.sed import NodeSED _LOGGER = logging.getLogger(__name__) @@ -43,18 +43,13 @@ def temperature(self) -> int: """Return the current temperature.""" return self._temperature - def message_for_sense(self, message): - """ - Process received message - """ + def message_for_node(self, message: PlugwiseResponse) -> None: + """Process received messages for PlugwiseSense class.""" + self._last_update = message.timestamp if isinstance(message, SenseReportResponse): self._process_SenseReportResponse(message) else: - _LOGGER.info( - "Unsupported message %s received from %s", - message.__class__.__name__, - self.mac, - ) + super().message_for_node(message) def _process_SenseReportResponse(self, message: SenseReportResponse) -> None: """Process content of 'NodeAckResponse' message.""" diff --git a/plugwise/nodes/switch.py b/plugwise/nodes/switch.py index 0d3ffdba7..d458af8bb 100644 --- a/plugwise/nodes/switch.py +++ b/plugwise/nodes/switch.py @@ -2,7 +2,7 @@ import logging from ..constants import FEATURE_PING, FEATURE_RSSI_IN, FEATURE_RSSI_OUT, FEATURE_SWITCH -from ..messages.responses import NodeSwitchGroupResponse +from ..messages.responses import NodeSwitchGroupResponse, PlugwiseResponse from ..nodes.sed import NodeSED _LOGGER = logging.getLogger(__name__) @@ -26,18 +26,13 @@ def switch(self) -> bool: """Return the last known switch state""" return self._switch_state - def message_for_switch(self, message): - """ - Process received message - """ + def message_for_node(self, message: PlugwiseResponse) -> None: + """Process received messages for PlugwiseSense class.""" + self._last_update = message.timestamp if isinstance(message, NodeSwitchGroupResponse): - _LOGGER.debug( - "Switch group request %s received from %s for group id %s", - str(message.power_state), - self.mac, - str(message.group), - ) self._process_NodeSwitchGroupResponse(message) + else: + super().message_for_node(message) def _process_NodeSwitchGroupResponse( self, message: NodeSwitchGroupResponse From 401138473989b06ace1d7b85168e223717caab64 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 10:51:55 +0100 Subject: [PATCH 14/87] Apply consistency to message process methods --- plugwise/nodes/__init__.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 2e9738751..7517961d8 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -194,9 +194,9 @@ def message_for_node(self, message: PlugwiseResponse) -> None: self._request_info() self._last_update = message.timestamp if isinstance(message, NodePingResponse): - self._process_ping_response(message) + self._process_NodePingResponse(message) elif isinstance(message, NodeInfoResponse): - self._process_info_response(message) + self._process_NodeInfoResponse(message) elif isinstance(message, NodeFeaturesResponse): self._process_features_response(message) elif isinstance(message, NodeJoinAckResponse): @@ -242,8 +242,8 @@ def _process_join_ack_response(self, message): self.mac, ) - def _process_ping_response(self, message): - """Process ping response message.""" + def _process_NodePingResponse(self, message: NodePingResponse) -> None: + """Process content of 'NodePingResponse' message.""" if self._rssi_in != message.rssi_in.value: self._rssi_in = message.rssi_in.value self.do_callback(FEATURE_RSSI_IN["id"]) @@ -254,13 +254,8 @@ def _process_ping_response(self, message): self._ping = message.ping_ms.value self.do_callback(FEATURE_PING["id"]) - def _process_info_response(self, message): - """Process info response message.""" - _LOGGER.debug( - "Response info message for node %s, last log address %s", - self.mac, - str(message.last_logaddr.value), - ) + def _process_NodeInfoResponse(self, message: NodeInfoResponse) -> None: + """Process content of 'NodeInfoResponse' message.""" if message.relay_state.serialize() == b"01": if not self._relay_state: self._relay_state = True From 10b2a606fa43950fc0481000aff5db3954513a2e Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 11:12:33 +0100 Subject: [PATCH 15/87] Make callbacks local to node classes based on NodeResponse or NodeAckResponse messages --- plugwise/nodes/__init__.py | 30 ++++++-- plugwise/nodes/circle.py | 91 ++++++++++++++++++------ plugwise/nodes/circle_plus.py | 62 ++++++++++++---- plugwise/nodes/scan.py | 57 ++++++++++++--- plugwise/nodes/sed.py | 130 +++++++++++++++++++--------------- 5 files changed, 261 insertions(+), 109 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 7517961d8..310cab451 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -17,6 +17,7 @@ NodeInfoResponse, NodeJoinAckResponse, NodePingResponse, + NodeResponse, PlugwiseResponse, ) from ..util import validate_mac, version_to_model @@ -53,6 +54,11 @@ def __init__(self, mac, address, message_sender): self._last_log_address = None self._device_features = None + # Local callback variables + self._callback_NodeInfo: callable | None = None + self._callback_NodePing: callable | None = None + self._callback_NodeFeature: callable | None = None + @property def available(self) -> bool: """Current network state of plugwise node.""" @@ -150,29 +156,31 @@ def rssi_out(self) -> int: return self._rssi_out return 0 - def do_ping(self, callback=None): + def do_ping(self, callback: callable | None = None) -> None: """Send network ping message to node.""" self._request_ping(callback, True) - def _request_info(self, callback=None): + def _request_info(self, callback: callable | None = None) -> None: """Request info from node.""" + self._callback_NodeInfo = callback self.message_sender( NodeInfoRequest(self._mac), - callback, - 0, Priority.Low, ) - def _request_features(self, callback=None): + def _request_features(self, callback: callable | None = None) -> None: """Request supported features for this node.""" + self._callback_NodeFeature = callback self.message_sender( NodeFeaturesRequest(self._mac), - callback, ) - def _request_ping(self, callback=None, ignore_sensor=True): + def _request_ping( + self, callback: callable | None = None, ignore_sensor=True + ) -> None: """Ping node.""" if ignore_sensor or FEATURE_PING["id"] in self._callbacks: + self._callback_NodePing = callback self.message_sender( NodePingRequest(self._mac), callback, @@ -254,6 +262,10 @@ def _process_NodePingResponse(self, message: NodePingResponse) -> None: self._ping = message.ping_ms.value self.do_callback(FEATURE_PING["id"]) + if self._callback_NodePing is not None: + self._callback_NodePing() + self._callback_NodePing = None + def _process_NodeInfoResponse(self, message: NodeInfoResponse) -> None: """Process content of 'NodeInfoResponse' message.""" if message.relay_state.serialize() == b"01": @@ -275,6 +287,10 @@ def _process_NodeInfoResponse(self, message: NodeInfoResponse) -> None: _LOGGER.debug("Hardware version = %s", str(self._hardware_version)) _LOGGER.debug("Firmware version = %s", str(self._firmware_version)) + if self._callback_NodeInfo is not None: + self._callback_NodeInfo() + self._callback_NodeInfo = None + def _process_features_response(self, message): """Process features message.""" _LOGGER.warning( diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 399e36ee9..d8208ab21 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -18,8 +18,6 @@ MAX_TIME_DRIFT, MESSAGE_TIME_OUT, PULSES_PER_KW_SECOND, - RELAY_SWITCHED_OFF, - RELAY_SWITCHED_ON, Priority, ) from ..messages.requests import ( @@ -29,6 +27,7 @@ CircleEnergyCountersRequest, CirclePowerUsageRequest, CircleSwitchRelayRequest, + Priority, ) from ..messages.responses import ( CircleCalibrationResponse, @@ -36,6 +35,7 @@ CircleEnergyCountersResponse, CirclePowerUsageResponse, NodeResponse, + NodeResponseType, PlugwiseResponse, ) from ..nodes import PlugwiseNode @@ -99,6 +99,16 @@ def __init__(self, mac, address, message_sender): minute=0, second=0, microsecond=0 ) - datetime.utcnow().replace(minute=0, second=0, microsecond=0) self._clock_offset = None + + # Local callback variables + self._callback_RelaySwitchedOn: callable | None = None + self._callback_RelaySwitchedOff: callable | None = None + self._callback_RelaySwitchFailed: callable | None = None + self._callback_CircleClockResponse: callable | None = None + self._callback_ClockAccepted: callable | None = None + self._callback_CircleCalibration: callable | None = None + self._callback_CirclePowerUsage: callable | None = None + self.get_clock(self.sync_clock) self._request_calibration() @@ -197,30 +207,37 @@ def relay_state(self, state): if state != self._relay_state: self.do_callback(FEATURE_RELAY["id"]) - def _request_calibration(self, callback=None): + def _request_calibration(self, callback: callable | None = None) -> None: """Request calibration info""" + self._callback_CircleCalibration = callback self.message_sender( CircleCalibrationRequest(self._mac), - callback, - 0, Priority.High, ) - def _request_switch(self, state, callback=None): + def _request_switch( + self, + state: bool, + success_callback: callable | None = None, + failed_callback: callable | None = None, + ) -> None: """Request to switch relay state and request state info""" + if state: + self._callback_RelaySwitchedOn = success_callback + else: + self._callback_RelaySwitchedOff = success_callback + self._callback_RelaySwitchFailed = failed_callback self.message_sender( CircleSwitchRelayRequest(self._mac, state), - callback, - 0, Priority.High, ) - def request_power_update(self, callback=None): + def request_power_update(self, callback: callable | None = None) -> None: """Request power usage and update energy counters""" if self._available: + self._callback_CirclePowerUsage = callback self.message_sender( CirclePowerUsageRequest(self._mac), - callback, ) if len(self._energy_history) > 0: # Request new energy counters if last one is more than one hour ago @@ -269,7 +286,12 @@ def message_for_node(self, message: PlugwiseResponse) -> None: def _process_NodeResponse(self, message: NodeResponse) -> None: """Process content of 'NodeResponse' message.""" - if message.ack_id == RELAY_SWITCHED_ON: + if message.ack_id == NodeResponseType.RelaySwitchedOn: + if self._callback_RelaySwitchedOn is not None: + self._callback_RelaySwitchedOn() + self._callback_RelaySwitchFailed = None + self._callback_RelaySwitchedOn = None + self._callback_RelaySwitchedOff = None if not self._relay_state: _LOGGER.debug( "Switch relay on for %s", @@ -277,7 +299,12 @@ def _process_NodeResponse(self, message: NodeResponse) -> None: ) self._relay_state = True self.do_callback(FEATURE_RELAY["id"]) - elif message.ack_id == RELAY_SWITCHED_OFF: + elif message.ack_id == NodeResponseType.RelaySwitchedOff: + if self._callback_RelaySwitchedOff is not None: + self._callback_RelaySwitchedOff() + self._callback_RelaySwitchFailed = None + self._callback_RelaySwitchedOn = None + self._callback_RelaySwitchedOff = None if self._relay_state: _LOGGER.debug( "Switch relay off for %s", @@ -285,12 +312,19 @@ def _process_NodeResponse(self, message: NodeResponse) -> None: ) self._relay_state = False self.do_callback(FEATURE_RELAY["id"]) + elif message.ack_id == NodeResponseType.RelaySwitchFailed: + if self._callback_RelaySwitchFailed is not None: + self._callback_RelaySwitchFailed() + self._callback_RelaySwitchFailed = None + self._callback_RelaySwitchedOn = None + self._callback_RelaySwitchedOff = None + elif message.ack_id == NodeResponseType.ClockAccepted: + if self._callback_ClockAccepted is not None: + self._callback_ClockAccepted() + self._callback_ClockAccepted = None else: - _LOGGER.debug( - "Unmanaged _node_ack_response %s received for %s", - str(message.ack_id), - self.mac, - ) + super()._process_NodeResponse(message) + def _process_CirclePowerUsageResponse( self, message: CirclePowerUsageResponse ) -> None: @@ -360,6 +394,10 @@ def _process_CirclePowerUsageResponse( self._pulses_produced_1h = message.pulse_hour_produced.value self.do_callback(FEATURE_POWER_PRODUCTION_CURRENT_HOUR["id"]) + if self._callback_CirclePowerUsage is not None: + self._callback_CirclePowerUsage() + self._callback_CirclePowerUsage = None + def _process_CircleCalibrationResponse( self, message: CircleCalibrationResponse ) -> None: @@ -369,6 +407,10 @@ def _process_CircleCalibrationResponse( setattr(self, "_" + calibration, val) self.calibration = True + if self._callback_CircleCalibration is not None: + self._callback_CircleCalibration() + self._callback_CircleCalibration = None + def pulses_to_kws(self, pulses, seconds=1): """ converts the amount of pulses to kWs using the calaboration offsets @@ -609,7 +651,9 @@ def _update_energy_today_hourly(self, start_today: datetime, end_today: datetime self._energy_pulses_today_hourly = _pulses_today_hourly self.do_callback(FEATURE_POWER_CONSUMPTION_TODAY["id"]) - def request_energy_counters(self, log_address=None, callback=None): + def request_energy_counters( + self, log_address=None, callback: callable | None = None + ): """Request power log of specified address""" _LOGGER.debug( "request_energy_counters for %s of address %s", self.mac, str(log_address) @@ -789,21 +833,24 @@ def _process_CircleClockResponse(self, message: CircleClockResponse) -> None: self.mac, str(self._clock_offset), ) + if self._callback_CircleClockResponse is not None: + self._callback_CircleClockResponse() + self._callback_CircleClockResponse = None - def get_clock(self, callback=None): + def get_clock(self, callback: callable | None = None) -> None: """get current datetime of internal clock of Circle.""" + self._callback_CircleClockResponse = callback self.message_sender( CircleClockGetRequest(self._mac), - callback, 0, Priority.Low, ) - def set_clock(self, callback=None): + def set_clock(self, callback: callable | None = None) -> None: """set internal clock of CirclePlus.""" + self._callback_ClockAccepted = callback self.message_sender( CircleClockSetRequest(self._mac, datetime.utcnow()), - callback, ) def sync_clock(self, max_drift=0): diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index f90f2afcb..45992f458 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -8,8 +8,12 @@ CirclePlusRealTimeClockSetRequest, CirclePlusScanRequest, from ..messages.responses import ( + CirclePlusRealTimeClockResponse, + CirclePlusScanResponse, + NodeResponse, + NodeResponseType, + PlugwiseResponse, ) -from ..messages.responses import CirclePlusRealTimeClockResponse, CirclePlusScanResponse from ..nodes.circle import PlugwiseCircle _LOGGER = logging.getLogger(__name__) @@ -22,23 +26,47 @@ def __init__(self, mac, address, message_sender): super().__init__(mac, address, message_sender) self._plugwise_nodes = {} self._scan_response = {} - self._scan_for_nodes_callback = None self._realtime_clock_offset = None self.get_real_time_clock(self.sync_realtime_clock) + # Local callback variables + self._callback_RealTimeClockAccepted: callable | None = None + self._callback_RealTimeClockFailed: callable | None = None + self._callback_CirclePlusRealTimeClockGet: callable | None = None + self._callback_CirclePlusRealTimeClockSet: callable | None = None + self._callback_CirclePlusScanResponse: callable | None = None + def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for PlugwiseCirclePlus class.""" self._last_update = message.timestamp if isinstance(message, CirclePlusRealTimeClockResponse): self._process_CirclePlusRealTimeClockResponse(message) + elif isinstance(message, NodeResponse): + self._process_NodeResponse(message) elif isinstance(message, CirclePlusScanResponse): self._process_CirclePlusScanResponse(message) else: super().message_for_node(message) - def scan_for_nodes(self, callback=None): + def _process_NodeResponse(self, message: NodeResponse) -> None: + """Process content of 'NodeResponse' message.""" + if message.ack_id == NodeResponseType.RealTimeClockAccepted: + if self._callback_RealTimeClockAccepted is not None: + self._callback_RealTimeClockAccepted() + self._callback_RealTimeClockAccepted = None + self._callback_RealTimeClockFailed = None + + elif message.ack_id == NodeResponseType.RealTimeClockFailed: + if self._callback_RealTimeClockFailed is not None: + self._callback_RealTimeClockFailed() + self._callback_RealTimeClockAccepted = None + self._callback_RealTimeClockFailed = None + else: + super()._process_NodeResponse(message) + + def scan_for_nodes(self, callback: callable | None = None) -> None: """Scan for registered nodes.""" - self._scan_for_nodes_callback = callback + self._callback_CirclePlusScanResponse = callback for node_address in range(0, 64): self.message_sender(CirclePlusScanRequest(self._mac, node_address)) self._scan_response[node_address] = False @@ -60,7 +88,7 @@ def _process_CirclePlusScanResponse(self, message: CirclePlusScanResponse) -> No self._plugwise_nodes[ message.node_mac.value.decode(UTF8_DECODE) ] = message.node_address.value - if self._scan_for_nodes_callback: + if self._callback_CirclePlusScanResponse: # Check if scan is complete before execute callback scan_complete = False self._scan_response[message.node_address.value] = True @@ -78,17 +106,17 @@ def _process_CirclePlusScanResponse(self, message: CirclePlusScanResponse) -> No break if node_address == 63: scan_complete = True - if scan_complete and self._scan_for_nodes_callback: - self._scan_for_nodes_callback(self._plugwise_nodes) - self._scan_for_nodes_callback = None + if scan_complete: + if self._callback_CirclePlusScanResponse: + self._callback_CirclePlusScanResponse(self._plugwise_nodes) + self._callback_CirclePlusScanResponse = None self._plugwise_nodes = {} - def get_real_time_clock(self, callback=None): + def get_real_time_clock(self, callback: callable | None = None) -> None: """get current datetime of internal clock of CirclePlus.""" + self._callback_CirclePlusRealTimeClockGet = callback self.message_sender( CirclePlusRealTimeClockGetRequest(self._mac), - callback, - 0, Priority.Low, ) @@ -116,12 +144,20 @@ def _process_CirclePlusRealTimeClockResponse( self.mac, str(self._clock_offset), ) + if self._callback_CirclePlusRealTimeClockGet is not None: + self._callback_CirclePlusRealTimeClockGet() + self._callback_CirclePlusRealTimeClockGet = None - def set_real_time_clock(self, callback=None): + def set_real_time_clock( + self, + success_callback: callable | None = None, + failed_callback: callable | None = None, + ) -> None: """set internal clock of CirclePlus.""" + self._callback_RealTimeClockAccepted = success_callback + self._callback_RealTimeClockFailed = failed_callback self.message_sender( CirclePlusRealTimeClockSetRequest(self._mac, datetime.utcnow()), - callback, ) def sync_realtime_clock(self, max_drift=0): diff --git a/plugwise/nodes/scan.py b/plugwise/nodes/scan.py index a0e5ff21e..56d598093 100644 --- a/plugwise/nodes/scan.py +++ b/plugwise/nodes/scan.py @@ -6,7 +6,6 @@ FEATURE_PING, FEATURE_RSSI_IN, FEATURE_RSSI_OUT, - SCAN_CONFIGURE_ACCEPTED, SCAN_DAYLIGHT_MODE, SCAN_MOTION_RESET_TIMER, SCAN_SENSITIVITY_HIGH, @@ -16,6 +15,7 @@ from ..messages.requests import ScanConfigureRequest, ScanLightCalibrateRequest from ..messages.responses import ( NodeAckResponse, + NodeAckResponseType, NodeSwitchGroupResponse, PlugwiseResponse, ) @@ -43,6 +43,11 @@ def __init__(self, mac, address, message_sender): self._new_daylight_mode = None self._new_sensitivity = None + # Local callback variables + self._callbackScanConfigAccepted: callable | None = None + self._callbackScanConfigFailed: callable | None = None + self._callbackScanLightCalibrateAccepted: callable | None = None + @property def motion(self) -> bool: """Return the last known motion state""" @@ -66,16 +71,42 @@ def message_for_node(self, message: PlugwiseResponse) -> None: def _process_NodeAckResponse(self, message: NodeAckResponse): """Process content of 'NodeAckResponse' message.""" - if message.ack_id == SCAN_CONFIGURE_ACCEPTED: + if message.ack_id == NodeAckResponseType.ScanConfigAccepted: self._motion_reset_timer = self._new_motion_reset_timer self._daylight_mode = self._new_daylight_mode self._sensitivity = self._new_sensitivity - else: + if self._callbackScanConfigAccepted is not None: + self._callbackScanConfigAccepted() + self._callbackScanConfigAccepted = None + self._callbackScanConfigFailed = None + if b"0050" in self._sed_requests: + del self._sed_requests[b"0050"] + _LOGGER.info( + "Scan configuration accepted by scan %s", + self.mac, + ) + elif message.ack_id == NodeAckResponseType.ScanConfigFailed: + self._new_motion_reset_timer = None + self._new_daylight_mode = None + self._new_sensitivity = None + if self._callbackScanConfigFailed is not None: + self._callbackScanConfigFailed() + self._callbackScanConfigAccepted = None + self._callbackScanConfigFailed = None + _LOGGER.warning( + "Scan configuration failed by scan %s", + self.mac, + ) + elif message.ack_id == NodeAckResponseType.ScanLightCalibrationAccepted: + if self._callbackScanLightCalibrateAccepted is not None: + self._callbackScanLightCalibrateAccepted() + self._callbackScanLightCalibrateAccepted = None _LOGGER.info( - "Unsupported ack message %s received for %s", - str(message.ack_id), + "Scan light calibration accepted by scan %s", self.mac, ) + else: + super()._process_NodeAckResponse(message) def _process_NodeSwitchGroupResponse( self, message: NodeSwitchGroupResponse @@ -97,16 +128,21 @@ def _process_NodeSwitchGroupResponse( self.mac, ) - def CalibrateLight(self, callback=None): + def CalibrateLight( + self, + callback: callable | None = None, + ) -> None: """Queue request to calibration light sensitivity""" - self._queue_request(ScanLightCalibrateRequest(self._mac), callback) + self._callbackScanLightCalibrateAccepted = callback + self._queue_request(ScanLightCalibrateRequest(self._mac)) def Configure_scan( self, motion_reset_timer=SCAN_MOTION_RESET_TIMER, sensitivity_level=SCAN_SENSITIVITY_MEDIUM, daylight_mode=SCAN_DAYLIGHT_MODE, - callback=None, + success_callback: callable | None = None, + failed_callback: callable | None = None, ): """Queue request to set motion reporting settings""" self._new_motion_reset_timer = motion_reset_timer @@ -119,11 +155,12 @@ def Configure_scan( # Default to medium: sensitivity_value = 30 # b'1E' self._new_sensitivity = sensitivity_level + self._callbackScanConfigAccepted = success_callback + self._callbackScanConfigFailed = failed_callback self._queue_request( ScanConfigureRequest( self._mac, motion_reset_timer, sensitivity_value, daylight_mode - ), - callback, + ) ) def SetMotionAction(self, callback=None): diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index 3b55ceb17..a78e97b5c 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -11,17 +11,11 @@ FEATURE_RSSI_IN, FEATURE_RSSI_OUT, Priority, - SED_AWAKE_BUTTON, - SED_AWAKE_FIRST, - SED_AWAKE_MAINTENANCE, - SED_AWAKE_STARTUP, - SED_AWAKE_STATE, SED_CLOCK_INTERVAL, SED_CLOCK_SYNC, SED_MAINTENANCE_INTERVAL, SED_SLEEP_FOR, SED_STAY_ACTIVE, - SLEEP_SET, ) from ..messages.requests import ( NodeInfoRequest, @@ -29,7 +23,9 @@ NodeSleepConfigRequest, from ..messages.responses import ( NodeAwakeResponse, + NodeAwakeResponseType, NodeResponse, + NodeResponseType, PlugwiseResponse, ) from ..nodes import PlugwiseNode @@ -48,21 +44,40 @@ def __init__(self, mac, address, message_sender): self._wake_up_interval = None self._battery_powered = True + # Local callback variables + self._callbackSleepConfigAccepted: callable | None = None + self._callbackSleepConfigFailed: callable | None = None + def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for NodeSED class.""" self._last_update = message.timestamp if isinstance(message, NodeAwakeResponse): self._process_NodeAwakeResponse(message) elif isinstance(message, NodeResponse): - if message.ack_id == SLEEP_SET: - self.maintenance_interval = self._new_maintenance_interval - else: - self.message_for_scan(message) - self.message_for_switch(message) - self.message_for_sense(message) + self._process_NodeResponse(message) else: super().message_for_node(message) + def _process_NodeResponse(self, message: NodeResponse) -> None: + """Process content of 'NodeResponse' message.""" + if message.ack_id == NodeResponseType.SleepConfigAccepted: + self._wake_up_interval = self._new_maintenance_interval + if self._callbackSleepConfigAccepted is not None: + self._callbackSleepConfigAccepted() + self._callbackSleepConfigAccepted = None + self._callbackSleepConfigFailed = None + if b"0050" in self._sed_requests: + del self._sed_requests[b"0050"] + elif message.ack_id == NodeResponseType.SleepConfigFailed: + self._new_maintenance_interval = None + if self._callbackSleepConfigFailed is not None: + self._callbackSleepConfigFailed() + self._callbackSleepConfigFailed = None + self._callbackSleepConfigAccepted = None + if b"0050" in self._sed_requests: + del self._sed_requests[b"0050"] + else: + super()._process_NodeResponse(message) def _process_NodeAwakeResponse(self, message: NodeAwakeResponse) -> None: """Process content of 'NodeAwakeResponse' message.""" @@ -72,55 +87,61 @@ def _process_NodeAwakeResponse(self, message: NodeAwakeResponse) -> None: self.mac, ) if ( - message.awake_type.value == SED_AWAKE_MAINTENANCE - or message.awake_type.value == SED_AWAKE_FIRST - or message.awake_type.value == SED_AWAKE_STARTUP - or message.awake_type.value == SED_AWAKE_BUTTON + message.awake_type.value == NodeAwakeResponseType.Maintenance + or message.awake_type.value == NodeAwakeResponseType.First + or message.awake_type.value == NodeAwakeResponseType.Startup + or message.awake_type.value == NodeAwakeResponseType.Button ): - for request in self._sed_requests: - (request_message, callback) = self._sed_requests[request] - _LOGGER.info( - "Send queued %s message to SED node %s", - request_message.__class__.__name__, - self.mac, - ) - self.message_sender(request_message, callback, -1, Priority.High) - self._sed_requests = {} + self._send_pending_requests() + elif message.awake_type.value == NodeAwakeResponseType.State: + _LOGGER.debug("Node %s awake for state change", self.mac) else: - if message.awake_type.value == SED_AWAKE_STATE: - _LOGGER.debug("Node %s awake for state change", self.mac) - else: - _LOGGER.info( - "Unknown awake message type (%s) received for node %s", - str(message.awake_type.value), - self.mac, - ) - - def _queue_request(self, request_message, callback=None): + _LOGGER.info( + "Unknown awake message type (%s) received for node %s", + str(message.awake_type.value), + self.mac, + ) + + def _send_pending_requests(self) -> None: + """Send pending requests to SED node.""" + for request in self._sed_requests: + request_message = self._sed_requests[request] + _LOGGER.info( + "Send queued %s message to SED node %s", + request_message.__class__.__name__, + self.mac, + ) + self.message_sender(request_message, Priority.High) + self._sed_requests = {} + + def _queue_request(self, message: PlugwiseRequest): """Queue request to be sent when SED is awake. Last message wins.""" - self._sed_requests[request_message.ID] = ( - request_message, - callback, + self._sed_requests[message.ID] = message + _LOGGER.info( + "Queue %s to be send at next awake of SED node %s", + message.__class__.__name__, + self.mac, ) + # Overrule method from PlugwiseNode class def _request_info(self, callback=None): """Request info from node""" - self._queue_request( - NodeInfoRequest(self._mac), - callback, - ) + self._callback_NodeInfo = callback + self._queue_request(NodeInfoRequest(self._mac)) - def _request_ping(self, callback=None, ignore_sensor=True): - """Ping node""" + # Overrule method from PlugwiseNode class + def _request_ping(self, callback=None, ignore_sensor=False): + """Ping node.""" if ( ignore_sensor or self._callbacks.get(FEATURE_PING["id"]) or self._callbacks.get(FEATURE_RSSI_IN["id"]) or self._callbacks.get(FEATURE_RSSI_OUT["id"]) + or callback is not None ): + self._callback_NodePing = callback self._queue_request( NodePingRequest(self._mac), - callback, ) else: _LOGGER.debug( @@ -128,10 +149,6 @@ def _request_ping(self, callback=None, ignore_sensor=True): self.mac, ) - def _wake_up_interval_accepted(self): - """Callback after wake up interval is received and accepted by SED.""" - self._wake_up_interval = self._new_maintenance_interval - def Configure_SED( self, stay_active=SED_STAY_ACTIVE, @@ -139,9 +156,11 @@ def Configure_SED( maintenance_interval=SED_MAINTENANCE_INTERVAL, clock_sync=SED_CLOCK_SYNC, clock_interval=SED_CLOCK_INTERVAL, - ): + success_callback: callable | None = None, + failed_callback: callable | None = None, + ) -> None: """Reconfigure the sleep/awake settings for a SED send at next awake of SED""" - message = NodeSleepConfigRequest( + _message = NodeSleepConfigRequest( self._mac, stay_active, maintenance_interval, @@ -149,10 +168,7 @@ def Configure_SED( clock_sync, clock_interval, ) - self._queue_request(message, self._wake_up_interval_accepted) + self._callbackSleepConfigAccepted = success_callback + self._callbackSleepConfigFailed = failed_callback + self._queue_request(_message) self._new_maintenance_interval = maintenance_interval - _LOGGER.info( - "Queue %s message to be send at next awake of SED node %s", - message.__class__.__name__, - self.mac, - ) From b48b41207a70aeb6b937d9fb1b4993555b698063 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 11:17:10 +0100 Subject: [PATCH 16/87] Cleanup and correction of imports --- plugwise/constants.py | 16 ---------------- plugwise/nodes/__init__.py | 8 ++++++-- plugwise/nodes/circle.py | 1 - plugwise/nodes/circle_plus.py | 4 +++- plugwise/nodes/sed.py | 4 +++- plugwise/stick.py | 2 +- 6 files changed, 13 insertions(+), 22 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 340b0d646..8b4839b69 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -129,13 +129,6 @@ ACK_CIRCLE_PLUS = b"00DD" ACK_POWER_LOG_INTERVAL_SET = b"00F8" -# SED Awake status ID -SED_AWAKE_MAINTENANCE = 0 # SED awake for maintenance -SED_AWAKE_FIRST = 1 # SED awake for the first time -SED_AWAKE_STARTUP = 2 # SED awake after restart, e.g. after reinserting a battery -SED_AWAKE_STATE = 3 # SED awake to report state (Motion / Temperature / Humidity -SED_AWAKE_UNKNOWN = 4 # TODO: Unknown -SED_AWAKE_BUTTON = 5 # SED awake due to button press # Max timeout in seconds MESSAGE_TIME_OUT = 15 # Stick responds with timeout messages after 10 sec. @@ -149,15 +142,6 @@ # Default sleep between sending messages SLEEP_TIME = 150 / 1000 - -class Priority(int, Enum): - """Message priority levels for USB-stick.""" - - High = 1 - Medium = 2 - Low = 3 - - # Max seconds the internal clock of plugwise nodes # are allowed to drift in seconds MAX_TIME_DRIFT = 5 diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 310cab451..a619c5f66 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -8,10 +8,14 @@ FEATURE_RELAY, FEATURE_RSSI_IN, FEATURE_RSSI_OUT, - Priority, UTF8_DECODE, ) -from ..messages.requests import NodeFeaturesRequest, NodeInfoRequest, NodePingRequest +from ..messages.requests import ( + NodeFeaturesRequest, + NodeInfoRequest, + NodePingRequest, + Priority, +) from ..messages.responses import ( NodeFeaturesResponse, NodeInfoResponse, diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index d8208ab21..cf5439949 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -18,7 +18,6 @@ MAX_TIME_DRIFT, MESSAGE_TIME_OUT, PULSES_PER_KW_SECOND, - Priority, ) from ..messages.requests import ( CircleCalibrationRequest, diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index 45992f458..5c642e3b7 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -2,11 +2,13 @@ from datetime import datetime, timezone import logging -from ..constants import MAX_TIME_DRIFT, Priority, UTF8_DECODE +from ..constants import MAX_TIME_DRIFT, UTF8_DECODE from ..messages.requests import ( CirclePlusRealTimeClockGetRequest, CirclePlusRealTimeClockSetRequest, CirclePlusScanRequest, + Priority, +) from ..messages.responses import ( CirclePlusRealTimeClockResponse, CirclePlusScanResponse, diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index a78e97b5c..2683b7fab 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -10,7 +10,6 @@ FEATURE_PING, FEATURE_RSSI_IN, FEATURE_RSSI_OUT, - Priority, SED_CLOCK_INTERVAL, SED_CLOCK_SYNC, SED_MAINTENANCE_INTERVAL, @@ -21,6 +20,9 @@ NodeInfoRequest, NodePingRequest, NodeSleepConfigRequest, + PlugwiseRequest, + Priority, +) from ..messages.responses import ( NodeAwakeResponse, NodeAwakeResponseType, diff --git a/plugwise/stick.py b/plugwise/stick.py index c3e712801..8b99ea92b 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -22,7 +22,6 @@ NODE_TYPE_SENSE, NODE_TYPE_STEALTH, NODE_TYPE_SWITCH, - Priority, STATE_ACTIONS, UTF8_DECODE, WATCHDOG_DEAMON, @@ -41,6 +40,7 @@ NodeInfoRequest, NodePingRequest, NodeRemoveRequest, + Priority, StickInitRequest, ) from .messages.responses import ( From 963a8388e462b8d40d9f91d268f7759aca715c1a Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 11:20:08 +0100 Subject: [PATCH 17/87] Add NodeRejoinResponse to sed --- plugwise/nodes/sed.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index 2683b7fab..6c19f653b 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -26,6 +26,7 @@ from ..messages.responses import ( NodeAwakeResponse, NodeAwakeResponseType, + NodeRejoinResponse, NodeResponse, NodeResponseType, PlugwiseResponse, @@ -57,6 +58,8 @@ def message_for_node(self, message: PlugwiseResponse) -> None: self._process_NodeAwakeResponse(message) elif isinstance(message, NodeResponse): self._process_NodeResponse(message) + elif isinstance(message, NodeRejoinResponse): + self._process_NodeRejoinResponse(message) else: super().message_for_node(message) @@ -81,6 +84,14 @@ def _process_NodeResponse(self, message: NodeResponse) -> None: else: super()._process_NodeResponse(message) + def _process_NodeRejoinResponse(self, message: NodeRejoinResponse) -> None: + """Process content of 'NodeAwakeResponse' message.""" + _LOGGER.info( + "Node %s has (re)joined plugwise network", + self.mac, + ) + self._send_pending_requests() + def _process_NodeAwakeResponse(self, message: NodeAwakeResponse) -> None: """Process content of 'NodeAwakeResponse' message.""" _LOGGER.debug( From 74a9aa0487c1f2f968eb63e614a8630f213d4713 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 11:21:31 +0100 Subject: [PATCH 18/87] Move all actions to _process methods --- plugwise/nodes/circle.py | 30 +++++++++--------------------- plugwise/nodes/scan.py | 13 +++++++------ 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index cf5439949..07e521209 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -252,32 +252,13 @@ def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for PlugwiseCircle class.""" self._last_update = message.timestamp if isinstance(message, CirclePowerUsageResponse): - if self.calibration: - self._process_CirclePowerUsageResponse(message) - _LOGGER.debug( - "Power update for %s, last update %s", - self.mac, - str(self._last_update), - ) - else: - _LOGGER.info( - "Received power update for %s before calibration information is known", - self.mac, - ) - self._request_calibration(self.request_power_update) + self._process_CirclePowerUsageResponse(message) elif isinstance(message, NodeResponse): self._process_NodeResponse(message) elif isinstance(message, CircleCalibrationResponse): self._process_CircleCalibrationResponse(message) elif isinstance(message, CircleEnergyCountersResponse): - if self.calibration: - self._response_energy_counters(message) - else: - _LOGGER.debug( - "Received power buffer log for %s before calibration information is known", - self.mac, - ) - self._process_CircleEnergyCountersResponse(message) + self._process_CircleEnergyCountersResponse(message) elif isinstance(message, CircleClockResponse): self._process_CircleClockResponse(message) else: @@ -334,6 +315,13 @@ def _process_CirclePowerUsageResponse( # rounding errors. Zero these out. However, negative pulse values are valid # for power producing appliances, like solar panels, so don't complain too loudly. + if not self.calibration: + _LOGGER.info( + "Received power update for %s before calibration information is known", + self.mac, + ) + self._request_calibration(self.request_power_update) + return # Power consumption last second if message.pulse_1s.value == -1: message.pulse_1s.value = 0 diff --git a/plugwise/nodes/scan.py b/plugwise/nodes/scan.py index 56d598093..76adb77ed 100644 --- a/plugwise/nodes/scan.py +++ b/plugwise/nodes/scan.py @@ -57,12 +57,6 @@ def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for PlugwiseScan class.""" self._last_update = message.timestamp if isinstance(message, NodeSwitchGroupResponse): - _LOGGER.debug( - "Switch group %s to state %s received from %s", - str(message.group.value), - str(message.power_state.value), - self.mac, - ) self._process_NodeSwitchGroupResponse(message) elif isinstance(message, NodeAckResponse): self._process_NodeAckResponse(message) @@ -111,6 +105,13 @@ def _process_NodeAckResponse(self, message: NodeAckResponse): def _process_NodeSwitchGroupResponse( self, message: NodeSwitchGroupResponse ) -> None: + """Process content of 'NodeSwitchGroupResponse' message.""" + _LOGGER.debug( + "Switch group %s to state %s received from %s", + str(message.group.value), + str(message.power_state.value), + self.mac, + ) if message.power_state.value == 0: # turn off => clear motion if self._motion_state: From cfc889c18aba7aa53f88122b3cbbc0088fc24505 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 11:38:04 +0100 Subject: [PATCH 19/87] Refactor message_processor method of Stick class --- plugwise/stick.py | 62 +++++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/plugwise/stick.py b/plugwise/stick.py index 8b99ea92b..f19325be2 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -44,13 +44,13 @@ StickInitRequest, ) from .messages.responses import ( - NodeAckResponse, NodeInfoResponse, NodeJoinAvailableResponse, NodeRemoveResponse, NodeResponse, - StickInitResponse, + NodeResponseType, PlugwiseResponse, + StickInitResponse, ) from .nodes.circle import PlugwiseCircle from .nodes.circle_plus import PlugwiseCirclePlus @@ -409,23 +409,33 @@ def _remove_node(self, mac): def message_processor(self, message: PlugwiseResponse): """Received message from Plugwise network.""" - mac = message.mac.decode(UTF8_DECODE) - if isinstance(message, (NodeResponse, NodeAckResponse)): - if message.ack_id in STATE_ACTIONS: - self._pass_message_to_node(message, mac) + if isinstance(message, NodeResponse): + self._process_NodeResponse(message) elif isinstance(message, NodeInfoResponse): - self._process_node_info_response(message, mac) + self._process_NodeInfoResponse(message) elif isinstance(message, StickInitResponse): - self._process_stick_init_response(message) + self._process_StickInitResponse(message) elif isinstance(message, NodeJoinAvailableResponse): - self._process_node_join_request(message, mac) + self._process_NodeJoinAvailableResponse(message) elif isinstance(message, NodeRemoveResponse): self._process_node_remove(message) else: - self._pass_message_to_node(message, mac) + self._pass_message_to_node(message) + + def _process_NodeResponse(self, message: NodeResponse) -> None: + """Process content of 'NodeResponse' message.""" + + if message.ack_id == NodeResponseType.JoinAccepted: + # Discovery newly accepted node + self.discover_node( + message.mac.decode.decode(UTF8_DECODE), self._discover_after_scan + ) + self._pass_message_to_node(message) - def _process_stick_init_response(self, stick_init_response: StickInitResponse): - """Process StickInitResponse message.""" + def _process_StickInitResponse( + self, stick_init_response: StickInitResponse + ) -> None: + """Process content of 'StickInitResponse' message.""" self._mac_stick = stick_init_response.mac if stick_init_response.network_is_online.value == 1: self._network_online = True @@ -445,17 +455,18 @@ def _process_stick_init_response(self, stick_init_response: StickInitResponse): self._watchdog_thread.daemon = True self._watchdog_thread.start() - def _process_node_info_response(self, node_info_response, mac): - """Process NodeInfoResponse message.""" - if not self._pass_message_to_node(node_info_response, mac, False): + def _process_NodeInfoResponse(self, message: NodeInfoResponse): + """Process content of 'NodeInfoResponse' message.""" + mac = message.mac.decode(UTF8_DECODE) + if not self._pass_message_to_node(message, False): _LOGGER.debug( "Received NodeInfoResponse from currently unknown node with mac %s with sequence id %s", mac, - str(node_info_response.seq_id), + str(message.seq_id), ) - if node_info_response.node_type.value == NODE_TYPE_CIRCLE_PLUS: + if message.node_type.value == NODE_TYPE_CIRCLE_PLUS: self._circle_plus_discovered = True - self._append_node(mac, 0, node_info_response.node_type.value) + self._append_node(mac, 0, message.node_type.value) if mac in self._nodes_not_discovered: del self._nodes_not_discovered[mac] else: @@ -467,15 +478,13 @@ def _process_node_info_response(self, node_info_response, mac): self._append_node( mac, self._nodes_to_discover[mac], - node_info_response.node_type.value, + message.node_type.value, ) - self._pass_message_to_node(node_info_response, mac) + self._pass_message_to_node(message) - def _process_node_join_request(self, node_join_request, mac): - """ - Process NodeJoinAvailableResponse message from a node that - is not part of a plugwise network yet and wants to join - """ + def _process_NodeJoinAvailableResponse(self, message: NodeJoinAvailableResponse): + """Process content of 'NodeJoinAvailableResponse' message.""" + mac = message.mac.decode(UTF8_DECODE) if self._device_nodes.get(mac): _LOGGER.debug( "Received node available message for node %s which is already joined.", @@ -521,12 +530,13 @@ def _process_node_remove(self, node_remove_response): unjoined_mac, ) - def _pass_message_to_node(self, message, mac, discover=True): + def _pass_message_to_node(self, message: PlugwiseResponse, discover=True): """ Pass message to node class to take action on message Returns True if message has passed onto existing known node """ + mac = message.mac.decode(UTF8_DECODE) if self._device_nodes.get(mac): self._device_nodes[mac].message_for_node(message) return True From 667f5021e4df8169aa90dabb557c3ce76bf0c32e Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 11:39:33 +0100 Subject: [PATCH 20/87] Add local callbacks to stick class --- plugwise/constants.py | 4 ---- plugwise/stick.py | 51 ++++++++++++++++++++++++++++++++----------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 8b4839b69..ae4cb7b9d 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -227,10 +227,6 @@ SENSE_TEMPERATURE_MULTIPLIER = 175.72 SENSE_TEMPERATURE_OFFSET = 46.85 -# Callback types -CB_NEW_NODE = "NEW_NODE" -CB_JOIN_REQUEST = "JOIN_REQUEST" - # Stick device features FEATURE_AVAILABLE = { "id": "available", diff --git a/plugwise/stick.py b/plugwise/stick.py index f19325be2..9887e719e 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -4,6 +4,7 @@ Main stick object to control associated plugwise plugs """ from datetime import datetime, timedelta +from enum import Enum import logging import sys import threading @@ -11,8 +12,6 @@ from .constants import ( ACCEPT_JOIN_REQUESTS, - CB_JOIN_REQUEST, - CB_NEW_NODE, MESSAGE_TIME_OUT, NODE_TYPE_CELSIUS_NR, NODE_TYPE_CELSIUS_SED, @@ -22,7 +21,6 @@ NODE_TYPE_SENSE, NODE_TYPE_STEALTH, NODE_TYPE_SWITCH, - STATE_ACTIONS, UTF8_DECODE, WATCHDOG_DEAMON, ) @@ -62,6 +60,13 @@ _LOGGER = logging.getLogger(__name__) +class StickCallback(str, Enum): + """Available callback types to be registered to the Stick class.""" + + NodeDiscovered = "NEW_NODE" + NodeRequestToJoin = "JOIN_REQUEST" + + class Stick: """Plugwise connection stick.""" @@ -94,6 +99,11 @@ def __init__(self, port, callback=None): self._update_thread = None self._watchdog_thread = None + # Local callback variables + self._callback_StickInit: callable | None = None + self._callback_NodeInfo: dict(str, callable) = {} + self._callback_NodeJoinAvailableResponse: dict(int, callable) = {} + if callback: self.auto_initialize(callback) @@ -183,7 +193,8 @@ def initialize_stick(self, callback=None, timeout=MESSAGE_TIME_OUT): if not self.msg_controller.connection.is_connected(): raise StickInitError _LOGGER.debug("Send init request to Plugwise Zigbee stick") - self.msg_controller.send(StickInitRequest(), callback) + self._callback_StickInit = callback + self.msg_controller.send(StickInitRequest()) time_counter = 0 while not self._stick_initialized and (time_counter < timeout): time_counter += 0.1 @@ -219,8 +230,13 @@ def disconnect(self): self.msg_controller.disconnect_from_stick() self.msg_controller = None - def subscribe_stick_callback(self, callback, callback_type): - """Subscribe callback to execute.""" + def subscribe_stick_callback(self, callback: callable, callback_type) -> int: + """Subscribe callback to execute. + Returns ID of callback registration + """ + + self._callback_NodeJoinAvailableResponse + if callback_type not in self._stick_callbacks: self._stick_callbacks[callback_type] = [] self._stick_callbacks[callback_type].append(callback) @@ -378,6 +394,7 @@ def node_state_updates(self, mac, state: bool): def node_join(self, mac: str, callback=None) -> bool: """Accept node to join Plugwise network by register mac in Circle+ memory""" + self._callback_NodeJoin if validate_mac(mac): self.msg_controller.send( NodeAddRequest(bytes(mac, UTF8_DECODE), True), callback @@ -455,6 +472,10 @@ def _process_StickInitResponse( self._watchdog_thread.daemon = True self._watchdog_thread.start() + if self._callback_StickInit is not None: + self._callback_StickInit() + self._callback_StickInit = None + def _process_NodeInfoResponse(self, message: NodeInfoResponse): """Process content of 'NodeInfoResponse' message.""" mac = message.mac.decode(UTF8_DECODE) @@ -482,6 +503,10 @@ def _process_NodeInfoResponse(self, message: NodeInfoResponse): ) self._pass_message_to_node(message) + if self._callback_NodeInfo.get(mac) is not None: + self._callback_NodeInfo() + self._callback_NodeInfo = None + def _process_NodeJoinAvailableResponse(self, message: NodeJoinAvailableResponse): """Process content of 'NodeJoinAvailableResponse' message.""" mac = message.mac.decode(UTF8_DECODE) @@ -492,19 +517,19 @@ def _process_NodeJoinAvailableResponse(self, message: NodeJoinAvailableResponse) ) else: if self._accept_join_requests: - # Send accept join request + # Auto accept active => accept join request _LOGGER.info( "Accepting network join request for node with mac %s", mac, ) - self.msg_controller.send(NodeAddRequest(node_join_request.mac, True)) + self.msg_controller.send(NodeAddRequest(message.mac, True)) self._nodes_not_discovered[mac] = (None, None) else: _LOGGER.debug( "New node with mac %s requesting to join Plugwise network, do callback", mac, ) - self.do_callback(CB_JOIN_REQUEST, mac) + self.do_callback(StickCallback.NodeRequestToJoin, mac) def _process_node_remove(self, node_remove_response): """ @@ -734,7 +759,7 @@ def _discover_after_scan(self): break if node_discovered: del self._nodes_not_discovered[node_discovered] - self.do_callback(CB_NEW_NODE, node_discovered) + self.do_callback(StickCallback.NodeDiscovered, node_discovered) self.auto_update() def discover_node(self, mac: str, callback=None, force_discover=False): @@ -748,19 +773,19 @@ def discover_node(self, mac: str, callback=None, force_discover=False): ) self.msg_controller.send( NodeInfoRequest(bytes(mac, UTF8_DECODE)), - callback, ) + self._callback_NodeInfo[mac] = callback else: (firstrequest, lastrequest) = self._nodes_not_discovered[mac] if not (firstrequest and lastrequest): self.msg_controller.send( NodeInfoRequest(bytes(mac, UTF8_DECODE)), - callback, 0, Priority.Low, ) + self._callback_NodeInfo[mac] = callback elif force_discover: self.msg_controller.send( NodeInfoRequest(bytes(mac, UTF8_DECODE)), - callback, ) + self._callback_NodeInfo[mac] = callback From e6f279fc198a280bb52696b15ba4fad78a854df3 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 11:41:27 +0100 Subject: [PATCH 21/87] Add logging for non processed message reponses --- plugwise/nodes/__init__.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index a619c5f66..69e74a7f2 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -17,9 +17,9 @@ Priority, ) from ..messages.responses import ( + NodeAckResponse, NodeFeaturesResponse, NodeInfoResponse, - NodeJoinAckResponse, NodePingResponse, NodeResponse, PlugwiseResponse, @@ -207,12 +207,14 @@ def message_for_node(self, message: PlugwiseResponse) -> None: self._last_update = message.timestamp if isinstance(message, NodePingResponse): self._process_NodePingResponse(message) + elif isinstance(message, NodeResponse): + self._process_NodeResponse(message) elif isinstance(message, NodeInfoResponse): self._process_NodeInfoResponse(message) elif isinstance(message, NodeFeaturesResponse): self._process_features_response(message) - elif isinstance(message, NodeJoinAckResponse): - self._process_join_ack_response(message) + elif isinstance(message, NodeAckResponse): + self._process_NodeAckResponse(message) else: _LOGGER.warning( "Unmanaged %s received for %s", @@ -247,13 +249,6 @@ def do_callback(self, sensor): err, ) - def _process_join_ack_response(self, message): - """Process join acknowledge response message""" - _LOGGER.info( - "Node %s has (re)joined plugwise network", - self.mac, - ) - def _process_NodePingResponse(self, message: NodePingResponse) -> None: """Process content of 'NodePingResponse' message.""" if self._rssi_in != message.rssi_in.value: @@ -301,3 +296,19 @@ def _process_features_response(self, message): "Node %s supports features %s", self.mac, str(message.features.value) ) self._device_features = message.features.value + + def _process_NodeAckResponse(self, message: NodeAckResponse) -> None: + """Process content of 'NodeAckResponse' message.""" + _LOGGER.warning( + "Unmanaged NodeAckResponse (%s) received for %s", + str(message.ack_id), + self.mac, + ) + + def _process_NodeResponse(self, message: NodeResponse) -> None: + """Process content of 'NodeResponse' message.""" + _LOGGER.warning( + "Unmanaged NodeResponse (%s) received for %s", + str(message.ack_id), + self.mac, + ) From c2360d7e8a4dc5d686879744812e4d23e69bbb51 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 11:51:15 +0100 Subject: [PATCH 22/87] Move non validated response types to response.py --- plugwise/constants.py | 6 ------ plugwise/messages/responses.py | 7 ++++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index ae4cb7b9d..3ad61ad65 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -124,12 +124,6 @@ SCAN_LIGHT_CALIBRATION_ACCEPTED: "Scan light calibration accepted", } -# TODO: responses -ACK_POWER_CALIBRATION = b"00DA" -ACK_CIRCLE_PLUS = b"00DD" -ACK_POWER_LOG_INTERVAL_SET = b"00F8" - - # Max timeout in seconds MESSAGE_TIME_OUT = 15 # Stick responds with timeout messages after 10 sec. MESSAGE_RETRY = 2 diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index 1903fcce1..bfa61e510 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -45,10 +45,15 @@ class NodeResponseType(bytes, Enum): RelaySwitchedOn = b"00D8" RelaySwitchFailed = b"00E2" SleepConfigAccepted = b"00F6" - SleepConfigFailed = b"00F7" # TODO: Validate RealTimeClockAccepted = b"00DF" RealTimeClockFailed = b"00E7" + # TODO: Validate these response types + SleepConfigFailed = b"00F7" + PowerLogIntervalAccepted = b"00F8" + PowerCalibrationAccepted = b"00DA" + CirclePlus = b"00DD" + class NodeAckResponseType(bytes, Enum): """Response types of a 'NodeAckResponse' reply message.""" From 33faa6df1ec97364b9da3cfef414bedde322c4a4 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 12:34:10 +0100 Subject: [PATCH 23/87] Add missing future imports --- plugwise/nodes/__init__.py | 4 +++- plugwise/nodes/circle.py | 3 ++- plugwise/nodes/circle_plus.py | 3 ++- plugwise/nodes/scan.py | 2 ++ plugwise/nodes/sed.py | 3 ++- plugwise/nodes/sense.py | 4 +++- plugwise/nodes/switch.py | 4 +++- plugwise/stick.py | 2 ++ 8 files changed, 19 insertions(+), 6 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 69e74a7f2..041216bf6 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -1,4 +1,6 @@ -"""Plugwise nodes.""" +"""Base class for Plugwise nodes.""" +from __future__ import annotations + from datetime import datetime import logging diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 07e521209..483e4cdb7 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -1,4 +1,5 @@ -"""Plugwise Circle node object.""" +"""Plugwise Circle node class.""" +from __future__ import annotations from datetime import datetime, timedelta, timezone import logging diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index 5c642e3b7..f01f7f1de 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -1,4 +1,5 @@ -"""Plugwise Circle+ node object.""" +"""Plugwise Circle+ node class.""" +from __future__ import annotations from datetime import datetime, timezone import logging diff --git a/plugwise/nodes/scan.py b/plugwise/nodes/scan.py index 76adb77ed..fbd8526e5 100644 --- a/plugwise/nodes/scan.py +++ b/plugwise/nodes/scan.py @@ -1,4 +1,6 @@ """Plugwise Scan node object.""" +from __future__ import annotations + import logging from ..constants import ( diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index 6c19f653b..0d3334ec5 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -1,8 +1,9 @@ -"""Plugwise SED (Sleeping Endpoint Device) base object.""" +"""Plugwise SED (Sleeping Endpoint Device) base class.""" # TODO: # - Expose awake state as sensor # - Set available state after 2 missed awake messages +from __future__ import annotations import logging diff --git a/plugwise/nodes/sense.py b/plugwise/nodes/sense.py index bea2ba567..760a9f587 100644 --- a/plugwise/nodes/sense.py +++ b/plugwise/nodes/sense.py @@ -1,4 +1,6 @@ -"""Plugwise Sense node object.""" +"""Plugwise Sense node class.""" +from __future__ import annotations + import logging from ..constants import ( diff --git a/plugwise/nodes/switch.py b/plugwise/nodes/switch.py index d458af8bb..fd2ccfa5b 100644 --- a/plugwise/nodes/switch.py +++ b/plugwise/nodes/switch.py @@ -1,4 +1,6 @@ -"""Plugwise switch node object.""" +"""Plugwise switch node class.""" +from __future__ import annotations + import logging from ..constants import FEATURE_PING, FEATURE_RSSI_IN, FEATURE_RSSI_OUT, FEATURE_SWITCH diff --git a/plugwise/stick.py b/plugwise/stick.py index 9887e719e..60873b608 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -3,6 +3,8 @@ Main stick object to control associated plugwise plugs """ +from __future__ import annotations + from datetime import datetime, timedelta from enum import Enum import logging From 459a9117a6b693d06733b614e4b2ad502a4ef824 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 15:22:36 +0100 Subject: [PATCH 24/87] Correct callback leftovers --- plugwise/nodes/__init__.py | 5 +---- plugwise/nodes/circle.py | 6 ------ plugwise/stick.py | 21 ++++++++------------- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 041216bf6..b73d18927 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -187,10 +187,7 @@ def _request_ping( """Ping node.""" if ignore_sensor or FEATURE_PING["id"] in self._callbacks: self._callback_NodePing = callback - self.message_sender( - NodePingRequest(self._mac), - callback, - ) + self.message_sender(NodePingRequest(self._mac)) def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for base PlugwiseNode class.""" diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 483e4cdb7..7236930fe 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -662,8 +662,6 @@ def request_energy_counters( # Request new energy counters self.message_sender( CircleEnergyCountersRequest(self._mac, log_address), - None, - 0, Priority.Low, ) else: @@ -675,14 +673,10 @@ def request_energy_counters( for req_log_address in range(log_address - 13, log_address): self.message_sender( CircleEnergyCountersRequest(self._mac, req_log_address), - None, - 0, Priority.Low, ) self.message_sender( CircleEnergyCountersRequest(self._mac, log_address), - callback, - 0, Priority.Low, ) diff --git a/plugwise/stick.py b/plugwise/stick.py index 60873b608..03eee99f1 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -398,9 +398,7 @@ def node_join(self, mac: str, callback=None) -> bool: """Accept node to join Plugwise network by register mac in Circle+ memory""" self._callback_NodeJoin if validate_mac(mac): - self.msg_controller.send( - NodeAddRequest(bytes(mac, UTF8_DECODE), True), callback - ) + self.msg_controller.send(NodeAddRequest(bytes(mac, UTF8_DECODE), True)) return True _LOGGER.warning("Invalid mac '%s' address, unable to join node manually.", mac) return False @@ -410,7 +408,6 @@ def node_unjoin(self, mac: str, callback=None) -> bool: if validate_mac(mac): self.msg_controller.send( NodeRemoveRequest(bytes(self.circle_plus_mac, UTF8_DECODE), mac), - callback, ) return True @@ -505,9 +502,10 @@ def _process_NodeInfoResponse(self, message: NodeInfoResponse): ) self._pass_message_to_node(message) - if self._callback_NodeInfo.get(mac) is not None: - self._callback_NodeInfo() - self._callback_NodeInfo = None + if self._callback_NodeInfo.get(mac): + if self._callback_NodeInfo[mac] is not None: + self._callback_NodeInfo[mac]() + self._callback_NodeInfo[mac] = None def _process_NodeJoinAvailableResponse(self, message: NodeJoinAvailableResponse): """Process content of 'NodeJoinAvailableResponse' message.""" @@ -669,8 +667,6 @@ def _update_loop(self): for mac in self._nodes_not_discovered: self.msg_controller.send( NodePingRequest(bytes(mac, UTF8_DECODE)), - None, - -1, Priority.Low, ) _discover_counter = 0 @@ -773,21 +769,20 @@ def discover_node(self, mac: str, callback=None, force_discover=False): None, None, ) + self._callback_NodeInfo[mac] = callback self.msg_controller.send( NodeInfoRequest(bytes(mac, UTF8_DECODE)), ) - self._callback_NodeInfo[mac] = callback else: (firstrequest, lastrequest) = self._nodes_not_discovered[mac] if not (firstrequest and lastrequest): + self._callback_NodeInfo[mac] = callback self.msg_controller.send( NodeInfoRequest(bytes(mac, UTF8_DECODE)), - 0, Priority.Low, ) - self._callback_NodeInfo[mac] = callback elif force_discover: + self._callback_NodeInfo[mac] = callback self.msg_controller.send( NodeInfoRequest(bytes(mac, UTF8_DECODE)), ) - self._callback_NodeInfo[mac] = callback From e5468622276c1a5e4c5138f56cbdb5eb5914f3c8 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 15:24:28 +0100 Subject: [PATCH 25/87] Make datetime timezone aware --- plugwise/stick.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugwise/stick.py b/plugwise/stick.py index 03eee99f1..2d7b77b2d 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -5,7 +5,7 @@ """ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from enum import Enum import logging import sys @@ -731,7 +731,7 @@ def _check_availability_of_seds(self, mac): """Helper to check if SED device is still sending its hartbeat.""" if self._device_nodes[mac].available: if self._device_nodes[mac].last_update < ( - datetime.now() + datetime.utcnow().replace(tzinfo=timezone.utc) - timedelta(minutes=(self._device_nodes[mac].maintenance_interval + 1)) ): _LOGGER.info( @@ -740,7 +740,7 @@ def _check_availability_of_seds(self, mac): mac, str(self._device_nodes[mac].last_update), str( - datetime.now() + datetime.utcnow().replace(tzinfo=timezone.utc) - timedelta( minutes=(self._device_nodes[mac].maintenance_interval + 1) ) From 62469dee8166f2e1c8e572595587412f10db7fe8 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 15:26:28 +0100 Subject: [PATCH 26/87] Remove duplicate decode --- plugwise/stick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/stick.py b/plugwise/stick.py index 2d7b77b2d..f1b53734e 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -444,7 +444,7 @@ def _process_NodeResponse(self, message: NodeResponse) -> None: if message.ack_id == NodeResponseType.JoinAccepted: # Discovery newly accepted node self.discover_node( - message.mac.decode.decode(UTF8_DECODE), self._discover_after_scan + message.mac.decode(UTF8_DECODE), self._discover_after_scan ) self._pass_message_to_node(message) From 8f5afcd8e043d95592d0a742413190ab89974c02 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 15:28:38 +0100 Subject: [PATCH 27/87] Apply some code formatting --- plugwise/nodes/__init__.py | 6 +----- plugwise/nodes/circle.py | 16 +--------------- plugwise/nodes/circle_plus.py | 2 +- plugwise/nodes/scan.py | 2 +- 4 files changed, 4 insertions(+), 22 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index b73d18927..85ae51c83 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -169,10 +169,7 @@ def do_ping(self, callback: callable | None = None) -> None: def _request_info(self, callback: callable | None = None) -> None: """Request info from node.""" self._callback_NodeInfo = callback - self.message_sender( - NodeInfoRequest(self._mac), - Priority.Low, - ) + self.message_sender(NodeInfoRequest(self._mac), Priority.Low) def _request_features(self, callback: callable | None = None) -> None: """Request supported features for this node.""" @@ -259,7 +256,6 @@ def _process_NodePingResponse(self, message: NodePingResponse) -> None: if self._ping != message.ping_ms.value: self._ping = message.ping_ms.value self.do_callback(FEATURE_PING["id"]) - if self._callback_NodePing is not None: self._callback_NodePing() self._callback_NodePing = None diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 7236930fe..f6d72516a 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -1,5 +1,6 @@ """Plugwise Circle node class.""" from __future__ import annotations + from datetime import datetime, timedelta, timezone import logging @@ -315,7 +316,6 @@ def _process_CirclePowerUsageResponse( # likely this means the circle measures very little power and is suffering from # rounding errors. Zero these out. However, negative pulse values are valid # for power producing appliances, like solar panels, so don't complain too loudly. - if not self.calibration: _LOGGER.info( "Received power update for %s before calibration information is known", @@ -370,7 +370,6 @@ def _process_CirclePowerUsageResponse( ) else: self._update_energy_current_hour(message.pulse_hour_consumed.value) - # Power produced current hour if message.pulse_hour_produced.value == -1: message.pulse_hour_produced.value = 0 @@ -381,7 +380,6 @@ def _process_CirclePowerUsageResponse( if self._pulses_produced_1h != message.pulse_hour_produced.value: self._pulses_produced_1h = message.pulse_hour_produced.value self.do_callback(FEATURE_POWER_PRODUCTION_CURRENT_HOUR["id"]) - if self._callback_CirclePowerUsage is not None: self._callback_CirclePowerUsage() self._callback_CirclePowerUsage = None @@ -454,7 +452,6 @@ def _collect_energy_pulses(self, start_utc: datetime, end_utc: datetime): ) self.request_energy_counters(_mem_address) _energy_history_failed = True - # Validate all history values where present if not _energy_history_failed: return _energy_pulses @@ -498,7 +495,6 @@ def _update_energy_today_now( if day_rollover and self._energy_rollover_day_finished: self._energy_rollover_day_started = True self._energy_rollover_day_finished = False - # Set counter if self._energy_rollover_hour_started: if self._energy_rollover_history_started: @@ -542,7 +538,6 @@ def _update_energy_today_now( self._energy_pulses_today_hourly + self._energy_pulses_current_hour ) - if _pulses_today_now is None: _LOGGER.info( "_update_energy_today_now for %s | skip update, hour: %s=%s=%s, history: %s=%s=%s, day: %s=%s=%s", @@ -691,7 +686,6 @@ def _process_CircleEnergyCountersResponse( if message.logaddr.value == self._last_log_address: self._energy_last_populated_slot = 0 - # Collect energy history pulses from received log address # Store pulse in self._energy_history using the timestamp in UTC as index _utc_hour_timestamp = datetime.utcnow().replace( @@ -716,11 +710,9 @@ def _process_CircleEnergyCountersResponse( # Store last populated _slot if message.logaddr.value == self._last_log_address: self._energy_last_populated_slot = _slot - # Store most recent timestamp of collected pulses if self._energy_last_collected_timestamp < _log_timestamp: self._energy_last_collected_timestamp = _log_timestamp - # Trigger history rollover if ( _log_timestamp == _utc_hour_timestamp @@ -733,7 +725,6 @@ def _process_CircleEnergyCountersResponse( self.mac, str(_utc_hour_timestamp), ) - # Trigger midnight rollover if ( _log_timestamp == _utc_midnight_timestamp @@ -746,7 +737,6 @@ def _process_CircleEnergyCountersResponse( ) self._energy_consumption_today_reset = _local_midnight_timestamp _midnight_rollover = True - # Reset energy collection progress if ( self._energy_history_collecting @@ -765,7 +755,6 @@ def _process_CircleEnergyCountersResponse( str(self._energy_last_collected_timestamp), str(_utc_hour_timestamp), ) - # Update energy counters if not self._energy_history_collecting: self._update_energy_previous_hour(_utc_hour_timestamp) @@ -784,7 +773,6 @@ def _process_CircleEnergyCountersResponse( self.mac, str(_local_midnight_timestamp), ) - # Cleanup energy history for more than 8 day's ago _8_days_ago = datetime.utcnow().replace( minute=0, second=0, microsecond=0 @@ -855,7 +843,6 @@ def _energy_timestamp_memory_address(self, utc_timestamp: datetime): ) if utc_timestamp > _utc_now_timestamp: return None - _seconds_offset = (_utc_now_timestamp - utc_timestamp).seconds _hours_offset = _seconds_offset / 3600 @@ -872,5 +859,4 @@ def _energy_timestamp_memory_address(self, utc_timestamp: datetime): _address -= 1 _slot = 4 _hours += 1 - return _address diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index f01f7f1de..ec22829bf 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -1,5 +1,6 @@ """Plugwise Circle+ node class.""" from __future__ import annotations + from datetime import datetime, timezone import logging @@ -58,7 +59,6 @@ def _process_NodeResponse(self, message: NodeResponse) -> None: self._callback_RealTimeClockAccepted() self._callback_RealTimeClockAccepted = None self._callback_RealTimeClockFailed = None - elif message.ack_id == NodeResponseType.RealTimeClockFailed: if self._callback_RealTimeClockFailed is not None: self._callback_RealTimeClockFailed() diff --git a/plugwise/nodes/scan.py b/plugwise/nodes/scan.py index fbd8526e5..b360e87a8 100644 --- a/plugwise/nodes/scan.py +++ b/plugwise/nodes/scan.py @@ -65,7 +65,7 @@ def message_for_node(self, message: PlugwiseResponse) -> None: else: super().message_for_node(message) - def _process_NodeAckResponse(self, message: NodeAckResponse): + def _process_NodeAckResponse(self, message: NodeAckResponse) -> None: """Process content of 'NodeAckResponse' message.""" if message.ack_id == NodeAckResponseType.ScanConfigAccepted: self._motion_reset_timer = self._new_motion_reset_timer From cca7a880e7a2fe4befed68cee5a487dc54e02789 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 15:40:21 +0100 Subject: [PATCH 28/87] Add addtional properties to PlugwiseRequest class To support restructure of StickMessageController class --- plugwise/messages/requests.py | 86 +++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 20 deletions(-) diff --git a/plugwise/messages/requests.py b/plugwise/messages/requests.py index d2620aff0..1ee336018 100644 --- a/plugwise/messages/requests.py +++ b/plugwise/messages/requests.py @@ -1,4 +1,7 @@ """All known request messages to be send to plugwise devices.""" +from __future__ import annotations + +from datetime import datetime from enum import Enum from ..constants import MESSAGE_FOOTER, MESSAGE_HEADER @@ -31,6 +34,41 @@ def __init__(self, mac): self.args = [] self.mac = mac + # Local property variables to support StickMessageController + self._send: datetime | None = None + self._stick_response: datetime | None = None + self._stick_state: bytes | None = None + + @property + def send(self) -> datetime | None: + """Timestamp message request is send to Stick.""" + return self._send + + @send.setter + def send(self, timestamp: datetime) -> None: + """Set timestamp message request is send to Stick.""" + self._send = timestamp + + @property + def stick_response(self) -> datetime | None: + """Timestamp Stick responded with.""" + return self._stick_response + + @stick_response.setter + def stick_response(self, timestamp: datetime) -> None: + """Set timestamp message request is send to Stick.""" + self._stick_response = timestamp + + @property + def stick_state(self) -> bytes | None: + """Stick 'StickResponse' acknowledge state.""" + return self._stick_state + + @stick_state.setter + def stick_state(self, state: bytes) -> None: + """Set 'StickResponse' acknowledge state.""" + self._stick_state = state + class NodeNetworkInfoRequest(PlugwiseRequest): """TODO: PublicNetworkInfoRequest @@ -116,14 +154,11 @@ def __init__(self, mac, moduletype, timeout): ] - """ - Initialize USB-Stick - - Response message: StickInitResponse - """ class StickInitRequest(PlugwiseRequest): + """Initialize USB-Stick.""" ID = b"000A" + Response = "StickInitResponse" def __init__(self): """message for that initializes the Stick""" @@ -141,34 +176,25 @@ class NodeImagePrepareRequest(PlugwiseRequest): ID = b"000B" - """ - Ping node - - Response message: NodePingResponse - """ class NodePingRequest(PlugwiseRequest): + """Ping node.""" ID = b"000D" + Response = "NodePingResponse" - """ - Request current power usage - - Response message: CirclePowerUsageResponse - """ class CirclePowerUsageRequest(PlugwiseRequest): + """Request current power usage.""" ID = b"0012" + Response = "CirclePowerUsageResponse" - """ - Set internal clock of node - - Response message: [Acknowledge message] - """ class CircleClockSetRequest(PlugwiseRequest): + """Set internal clock of node.""" ID = b"0016" + Response = "CirclePowerUsageResponse" def __init__(self, mac, dt): super().__init__(mac) @@ -363,6 +389,26 @@ def __init__(self, mac, log_address): self.args.append(LogAddr(log_address, 8)) +class CircleHandlesOffRequest(PlugwiseRequest): + """ + ?PWSetHandlesOffRequestV1_0 + + Response message: ? + """ + + ID = b"004D" + + +class CircleHandlesOnRequest(PlugwiseRequest): + """ + ?PWSetHandlesOnRequestV1_0 + + Response message: ? + """ + + ID = b"004E" + + class NodeSleepConfigRequest(PlugwiseRequest): """ Configure timers for SED nodes to minimize battery usage From 6322564d7fe785213dbcd6ae099696dc75a23c62 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 15:55:17 +0100 Subject: [PATCH 29/87] Rewrite StickMessageController Don't rely on predicted seq_id but on actual response of stick --- plugwise/constants.py | 84 +-------- plugwise/controller.py | 402 ++++++++++++++++------------------------- plugwise/util.py | 22 --- 3 files changed, 154 insertions(+), 354 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 3ad61ad65..883232233 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -44,89 +44,9 @@ MESSAGE_LARGE = "LARGE" MESSAGE_SMALL = "SMALL" -# Acknowledge message types - -# StickResponse -RESPONSE_TYPE_SUCCESS = b"00C1" -RESPONSE_TYPE_ERROR = b"00C2" -RESPONSE_TYPE_TIMEOUT = b"00E1" - -# NodeResponse -CLOCK_SET = b"00D7" -JOIN_REQUEST_ACCEPTED = b"00D9" -RELAY_SWITCHED_OFF = b"00DE" -RELAY_SWITCHED_ON = b"00D8" -RELAY_SWITCH_FAILED = b"00E2" -SLEEP_SET = b"00F6" -SLEEP_FAILED = b"00F7" # TODO: Validate -REAL_TIME_CLOCK_ACCEPTED = b"00DF" -REAL_TIME_CLOCK_FAILED = b"00E7" - -# NodeAckResponse -SCAN_CONFIGURE_ACCEPTED = b"00BE" -SCAN_CONFIGURE_FAILED = b"00BF" -SCAN_LIGHT_CALIBRATION_ACCEPTED = b"00BD" -SENSE_INTERVAL_ACCEPTED = b"00B3" -SENSE_INTERVAL_FAILED = b"00B4" -SENSE_BOUNDARIES_ACCEPTED = b"00B5" -SENSE_BOUNDARIES_FAILED = b"00B6" - -STATE_ACTIONS = ( - RELAY_SWITCHED_ON, - RELAY_SWITCHED_OFF, - SCAN_CONFIGURE_ACCEPTED, - SLEEP_SET, -) -REQUEST_SUCCESS = ( - CLOCK_SET, - JOIN_REQUEST_ACCEPTED, - REAL_TIME_CLOCK_ACCEPTED, - RELAY_SWITCHED_ON, - RELAY_SWITCHED_OFF, - SCAN_CONFIGURE_ACCEPTED, - SCAN_LIGHT_CALIBRATION_ACCEPTED, - SENSE_BOUNDARIES_ACCEPTED, - SENSE_INTERVAL_ACCEPTED, - SLEEP_SET, -) -REQUEST_FAILED = ( - REAL_TIME_CLOCK_FAILED, - RELAY_SWITCH_FAILED, - RESPONSE_TYPE_ERROR, - RESPONSE_TYPE_TIMEOUT, - SCAN_CONFIGURE_FAILED, - SENSE_BOUNDARIES_FAILED, - SENSE_INTERVAL_FAILED, - SLEEP_FAILED, -) -STATUS_RESPONSES = { - # StickResponse - RESPONSE_TYPE_SUCCESS: "success", - RESPONSE_TYPE_ERROR: "error", - RESPONSE_TYPE_TIMEOUT: "timeout", - # NodeResponse - CLOCK_SET: "clock set", - JOIN_REQUEST_ACCEPTED: "join accepted", - REAL_TIME_CLOCK_ACCEPTED: "real time clock set", - REAL_TIME_CLOCK_FAILED: "real time clock failed", - RELAY_SWITCHED_ON: "relay on", - RELAY_SWITCHED_OFF: "relay off", - RELAY_SWITCH_FAILED: "relay switching failed", - SLEEP_SET: "sleep settings accepted", - SLEEP_FAILED: "sleep settings failed", - # NodeAckResponse - SCAN_CONFIGURE_ACCEPTED: "Scan settings accepted", - SCAN_CONFIGURE_FAILED: "Scan settings failed", - SENSE_INTERVAL_ACCEPTED: "Sense report interval accepted", - SENSE_INTERVAL_FAILED: "Sense report interval failed", - SENSE_BOUNDARIES_ACCEPTED: "Sense boundaries accepted", - SENSE_BOUNDARIES_FAILED: "Sense boundaries failed", - SCAN_LIGHT_CALIBRATION_ACCEPTED: "Scan light calibration accepted", -} - # Max timeout in seconds MESSAGE_TIME_OUT = 15 # Stick responds with timeout messages after 10 sec. -MESSAGE_RETRY = 2 +MESSAGE_RETRY = 3 # plugwise year information is offset from y2k PLUGWISE_EPOCH = 2000 @@ -134,7 +54,7 @@ LOGADDR_OFFSET = 278528 # Default sleep between sending messages -SLEEP_TIME = 150 / 1000 +SLEEP_TIME = 0.1 # Max seconds the internal clock of plugwise nodes # are allowed to drift in seconds diff --git a/plugwise/controller.py b/plugwise/controller.py index 20acaf026..bc16b567a 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -10,37 +10,33 @@ - execution of callbacks after processing the response message """ +from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import logging from queue import Empty, PriorityQueue import threading import time +from typing import TypedDict from .connections.serial import PlugwiseUSBConnection from .connections.socket import SocketConnection -from .constants import ( - MESSAGE_RETRY, - MESSAGE_TIME_OUT, - REQUEST_FAILED, - REQUEST_SUCCESS, - SLEEP_TIME, - STATUS_RESPONSES, - UTF8_DECODE, - Priority, -) -from .messages.requests import NodeInfoRequest, NodePingRequest, PlugwiseRequest -from .messages.responses import ( - NodeResponse, - NodeAckResponse, - StickResponse, -) +from .constants import MESSAGE_RETRY, MESSAGE_TIME_OUT, SLEEP_TIME, UTF8_DECODE +from .messages.requests import PlugwiseRequest, Priority +from .messages.responses import PlugwiseResponse, StickResponse, StickResponseType from .parser import PlugwiseParser -from .util import inc_seq_id _LOGGER = logging.getLogger(__name__) +class MessageRequest(TypedDict): + """USB Request to send into Zigbee network.""" + + priority: Priority + timestamp: datetime + message: PlugwiseRequest + + class StickMessageController: """Handle connection and message sending and receiving""" @@ -48,9 +44,7 @@ def __init__(self, port: str, message_processor, node_state): """Initialize message controller""" self.connection = None self.discovery_finished = False - self.expected_responses = {} self.init_callback = None - self.last_seq_id = None self.message_processor = message_processor self.node_state = node_state self.parser = PlugwiseParser(self.message_handler) @@ -62,6 +56,12 @@ def __init__(self, port: str, message_processor, node_state): self._receive_timeout_thread_state = False self._send_message_thread_state = False + self._timeout_delta = timedelta(minutes=1) + self._open_requests: dict(bytes, PlugwiseRequest) = {} + self._stick_response: bool = False + self.last_seq_id: bytes | None = None + self.last_result: StickResponseType | None = None + @property def receive_timeout_thread_state(self) -> bool: """Required state of the receive timeout thread""" @@ -123,230 +123,170 @@ def connect_to_stick(self, callback=None) -> bool: def send( self, request: PlugwiseRequest, - callback=None, - retry_counter=0, priority: Priority = Priority.Medium, + retry: int = MESSAGE_RETRY, ): """Queue request message to be sent into Plugwise Zigbee network.""" - _LOGGER.debug( - "Queue %s to be send with retry counter %s and priority %s", + _LOGGER.info( + "Send queue = %s, Add %s, priority=%s, retry=%s", + str(self._send_message_queue.qsize()), request.__class__.__name__, - str(retry_counter), str(priority), + str(retry), ) - self._send_message_queue.put( - ( - priority, - retry_counter, - datetime.utcnow(), - [ - request, - callback, - retry_counter, - None, - ], - ) - ) - - def resend(self, seq_id): - """Resend message.""" - _mac = "" - if not self.expected_responses.get(seq_id): - _LOGGER.warning( - "Cannot resend unknown request %s", - str(seq_id), - ) - else: - if self.expected_responses[seq_id][0].mac: - _mac = self.expected_responses[seq_id][0].mac.decode(UTF8_DECODE) - _request = self.expected_responses[seq_id][0].__class__.__name__ - - if self.expected_responses[seq_id][2] == -1: - _LOGGER.debug("Drop single %s to %s ", _request, _mac) - elif self.expected_responses[seq_id][2] <= MESSAGE_RETRY: - if ( - isinstance(self.expected_responses[seq_id][0], NodeInfoRequest) - and not self.discovery_finished - ): - # Time out for node which is not discovered yet - # to speedup the initial discover phase skip retries and mark node as not discovered. - _LOGGER.debug( - "Skip retry %s to %s to speedup discover process", - _request, - _mac, - ) - if self.expected_responses[seq_id][1]: - self.expected_responses[seq_id][1]() - else: - _LOGGER.info( - "Resend %s for %s, retry %s of %s", - _request, - _mac, - str(self.expected_responses[seq_id][2] + 1), - str(MESSAGE_RETRY + 1), - ) - self.send( - self.expected_responses[seq_id][0], - self.expected_responses[seq_id][1], - self.expected_responses[seq_id][2] + 1, - ) - else: - _LOGGER.warning( - "Drop %s to %s because max retries %s reached", - _request, - _mac, - str(MESSAGE_RETRY + 1), - ) - # Report node as unavailable for missing NodePingRequest - if isinstance(self.expected_responses[seq_id][0], NodePingRequest): - self.node_state(_mac, False) - else: - _LOGGER.debug( - "Do a single ping request to %s to validate if node is reachable", - _mac, - ) - self.send( - NodePingRequest(self.expected_responses[seq_id][0].mac), - None, - MESSAGE_RETRY + 1, - ) - del self.expected_responses[seq_id] + _utc = datetime.utcnow().replace(tzinfo=timezone.utc) + self._send_message_queue.put((priority, _utc, retry, request)) def _send_message_loop(self): """Daemon to send messages waiting in queue.""" + _max_wait = SLEEP_TIME * 10 while self._send_message_thread_state: try: - _prio, _retry, _dt, request_set = self._send_message_queue.get( + _priority, _utc, _retry, _request = self._send_message_queue.get( block=True, timeout=1 ) except Empty: time.sleep(SLEEP_TIME) else: - # Calc next seq_id based last received ack message - # if previous seq_id is unknown use fake b"0000" - seq_id = inc_seq_id(self.last_seq_id) - self.expected_responses[seq_id] = request_set - if self.expected_responses[seq_id][2] == 0: - _LOGGER.info( - "Send %s to %s using seq_id %s", - self.expected_responses[seq_id][0].__class__.__name__, - self.expected_responses[seq_id][0].mac, - str(seq_id), - ) + _LOGGER.debug( + "Send %s to %s", + _request.__class__.__name__, + _request.mac, + ) + + timeout_counter = 0 + self._stick_response = False + self._stick_request = _request + + # Send request + self.connection.send(_request) + _request.send = datetime.utcnow().replace(tzinfo=timezone.utc) + + # Wait for response + while timeout_counter < _max_wait: + if self._stick_response: + break + time.sleep(SLEEP_TIME) + timeout_counter += 1 + + if timeout_counter > _max_wait: + _retry -= 1 + if _retry < 1: + _LOGGER.error( + "No response for %s after 3 retries. Drop request!", + _request.__class__.__name__, + ) + else: + _LOGGER.warning( + "No response for %s after %s retry. Retry request!", + _request.__class__.__name__, + str(MESSAGE_RETRY - _retry + 1), + ) + self.send(_request, _priority, _retry) else: _LOGGER.info( - "Resend %s to %s using seq_id %s, retry %s", - self.expected_responses[seq_id][0].__class__.__name__, - self.expected_responses[seq_id][0].mac, - str(seq_id), - str(self.expected_responses[seq_id][2]), + "Send queue = %s", + str(self._send_message_queue.qsize()), ) - self.expected_responses[seq_id][3] = datetime.utcnow() - # Send request - self.connection.send(self.expected_responses[seq_id][0]) - time.sleep(SLEEP_TIME) - timeout_counter = 0 - # Wait max 1 second for acknowledge response from USB-stick - while ( - self.last_seq_id != seq_id - and timeout_counter < 10 - and seq_id != b"0000" - and self.last_seq_id is not None - ): - time.sleep(0.1) - timeout_counter += 1 - if timeout_counter >= 10 and self._send_message_thread_state: - self.resend(seq_id) _LOGGER.debug("Send message loop stopped") - def message_handler(self, message): + def message_handler(self, message: PlugwiseResponse) -> None: """handle received message from Plugwise Zigbee network.""" - - # only save last seq_id and skip special ID's FFFD, FFFE, FFFF - if self.last_seq_id: - if int(self.last_seq_id, 16) < int(message.seq_id, 16) < 65533: - self.last_seq_id = message.seq_id - elif message.seq_id == b"0000" and self.last_seq_id == b"FFFB": - self.last_seq_id = b"0000" - if isinstance(message, StickResponse): - self._log_status_message(message, message.ack_id) - self._post_message_action( - message.seq_id, message.ack_id, message.__class__.__name__ - ) + if not self._stick_response: + if message.seq_id not in self._open_requests.keys(): + self._open_requests[message.seq_id] = self._stick_request + self._open_requests[message.seq_id].stick_response = message.timestamp + self._open_requests[message.seq_id].stick_state = message.ack_id + self._stick_response = True + self._log_status_of_request(message.seq_id) + else: - if isinstance(message, (NodeAckResponse, NodeResponse)): - self._log_status_message(message, message.ack_id) + # Forward message to Stick + if message.seq_id in self._open_requests: + if isinstance(self._open_requests[message.seq_id].mac, bytes): + _target = " to " + self._open_requests[message.seq_id].mac.decode( + UTF8_DECODE + ) + else: + _target = "" + _LOGGER.info( + "forward %s after %s%s with seq_id=%s", + message.__class__.__name__, + self._open_requests[message.seq_id].__class__.__name__, + _target, + str(message.seq_id), + ) else: - self._log_status_message(message) - self.message_processor(message) - if ( - message.seq_id != b"FFFF" - and message.seq_id != b"FFFE" - and message.seq_id != b"FFFD" - ): - self._post_message_action( - message.seq_id, None, message.__class__.__name__ + _LOGGER.warning( + "Forward %s with seq_id=%s", + message.__class__.__name__, + str(message.seq_id), ) + self.message_processor(message) + if message.seq_id in self._open_requests.keys(): + del self._open_requests[message.seq_id] - def _post_message_action(self, seq_id, ack_response=None, request="unknown"): - """Execute action if request has been successful..""" - if seq_id in self.expected_responses: - if ack_response in (*REQUEST_SUCCESS, None): - if self.expected_responses[seq_id][1]: - _LOGGER.debug( - "Execute action %s of request with seq_id %s", - self.expected_responses[seq_id][1].__name__, - str(seq_id), - ) - try: - self.expected_responses[seq_id][1]() - # TODO: narrow exception - except Exception as err: # pylint: disable=broad-except - _LOGGER.error( - "Execution of %s for request with seq_id %s failed: %s", - self.expected_responses[seq_id][1].__name__, - str(seq_id), - err, - ) - del self.expected_responses[seq_id] - elif ack_response in REQUEST_FAILED: - self.resend(seq_id) + def _log_status_of_request(self, seq_id: bytes) -> None: + """.""" + if isinstance(self._open_requests[seq_id].mac, bytes): + _target = " to " + self._open_requests[seq_id].mac.decode(UTF8_DECODE) else: - if not self.last_seq_id: - if b"0000" in self.expected_responses: - self.expected_responses[seq_id] = self.expected_responses[b"0000"] - del self.expected_responses[b"0000"] - self.last_seq_id = seq_id - else: - _LOGGER.info( - "Drop unexpected %s%s using seq_id %s", - STATUS_RESPONSES.get(ack_response, "") + " ", - request, - str(seq_id), - ) + _target = "" + if self._open_requests[seq_id].stick_state == StickResponseType.success: + _LOGGER.debug( + "Stick accepted %s%s with seq_id=%s", + self._open_requests[seq_id].__class__.__name__, + _target, + str(seq_id), + ) + elif self._open_requests[seq_id].stick_state == StickResponseType.timeout: + _LOGGER.warning( + "Stick 'time out' received for %s%s with seq_id=%s", + self._open_requests[seq_id].__class__.__name__, + _target, + str(seq_id), + ) + elif self._open_requests[seq_id].stick_state == StickResponseType.failed: + _LOGGER.error( + "Stick failed received for %s%s with seq_id=%s", + self._open_requests[seq_id].__class__.__name__, + _target, + str(seq_id), + ) + else: + _LOGGER.warning( + "Unknown StickResponseType %s received for %s%s with seq_id=%s", + str(self._open_requests[seq_id].stick_state), + self._open_requests[seq_id].__class__.__name__, + _target, + str(seq_id), + ) def _receive_timeout_loop(self): - """Daemon to time out open requests without any (n)ack response message.""" + """Daemon to time out open requests without any response message.""" while self._receive_timeout_thread_state: - for seq_id in list(self.expected_responses.keys()): - if self.expected_responses[seq_id][3] is not None: - if self.expected_responses[seq_id][3] < ( - datetime.utcnow() - timedelta(seconds=MESSAGE_TIME_OUT) - ): - _mac = "" - if self.expected_responses[seq_id][0].mac: - _mac = self.expected_responses[seq_id][0].mac.decode( - UTF8_DECODE - ) - _LOGGER.info( - "No response within %s seconds timeout for %s to %s with sequence ID %s", - str(MESSAGE_TIME_OUT), - self.expected_responses[seq_id][0].__class__.__name__, - _mac, - str(seq_id), + _utcnow = datetime.utcnow().replace(tzinfo=timezone.utc) + for seq_id in list(self._open_requests.keys()): + if ( + self._open_requests[seq_id].stick_response + self._timeout_delta + > _utcnow + ): + if isinstance(self._open_requests[seq_id].mac, bytes): + _target = " to " + self._open_requests[seq_id].mac.decode( + UTF8_DECODE ) - self.resend(seq_id) + else: + _target = "" + _LOGGER.warning( + "_receive_timeout_loop found old %s%s with seq_id=%s, send=%s, stick_response=%s", + self._open_requests[seq_id].__class__.__name__, + _target, + str(seq_id), + str(self._open_requests[seq_id].send), + str(self._open_requests[seq_id].stick_response), + ) + del self._open_requests[seq_id] receive_timeout_checker = 0 while ( receive_timeout_checker < MESSAGE_TIME_OUT @@ -356,44 +296,6 @@ def _receive_timeout_loop(self): receive_timeout_checker += 1 _LOGGER.debug("Receive timeout loop stopped") - def _log_status_message(self, message, status=None): - """Log status messages..""" - if status: - if status in STATUS_RESPONSES: - _LOGGER.debug( - "Received %s %s for request with seq_id %s", - STATUS_RESPONSES[status], - message.__class__.__name__, - str(message.seq_id), - ) - else: - if self.expected_responses.get(message.seq_id): - _LOGGER.warning( - "Received unmanaged (%s) %s in response to %s with seq_id %s", - str(status), - message.__class__.__name__, - str( - self.expected_responses[message.seq_id][ - 1 - ].__class__.__name__ - ), - str(message.seq_id), - ) - else: - _LOGGER.warning( - "Received unmanaged (%s) %s for unknown request with seq_id %s", - str(status), - message.__class__.__name__, - str(message.seq_id), - ) - else: - _LOGGER.info( - "Received %s from %s with sequence id %s", - message.__class__.__name__, - message.mac.decode(UTF8_DECODE), - str(message.seq_id), - ) - def disconnect_from_stick(self): """Disconnect from stick and raise error if it fails""" self._send_message_thread_state = False diff --git a/plugwise/util.py b/plugwise/util.py index ba7ca82b3..95aad0c8c 100644 --- a/plugwise/util.py +++ b/plugwise/util.py @@ -49,28 +49,6 @@ def version_to_model(version): return model if model is not None else "Unknown" -def inc_seq_id(seq_id, value=1) -> bytearray: - """ - Increment sequence id by value - - :return: 4 bytes - """ - if seq_id is None: - return b"0000" - temp_int = int(seq_id, 16) + value - # Max seq_id = b'FFFB' - # b'FFFC' reserved for message - # b'FFFD' reserved for 'NodeRejoinResponse' message - # b'FFFE' reserved for 'NodeSwitchGroupResponse' message - # b'FFFF' reserved for 'NodeAwakeResponse' message - if temp_int >= 65532: - temp_int = 0 - temp_str = str(hex(temp_int)).lstrip("0x").upper() - while len(temp_str) < 4: - temp_str = "0" + temp_str - return temp_str.encode() - - def uint_to_int(val, octals): """compute the 2's compliment of int value val for negative values""" bits = octals << 2 From 539c95e72039d93882843540b6f6bde8ee462628 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 17:41:55 +0100 Subject: [PATCH 30/87] Move retry & priority to PlugwiseRequest class --- plugwise/controller.py | 56 ++++++++++++++++++++++------------- plugwise/messages/requests.py | 22 ++++++++++++++ plugwise/nodes/__init__.py | 4 ++- plugwise/nodes/circle.py | 50 ++++++++++++------------------- plugwise/nodes/circle_plus.py | 13 ++++---- plugwise/nodes/sed.py | 3 +- plugwise/stick.py | 14 ++++----- 7 files changed, 94 insertions(+), 68 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index bc16b567a..7d6c36bc0 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -123,26 +123,25 @@ def connect_to_stick(self, callback=None) -> bool: def send( self, request: PlugwiseRequest, - priority: Priority = Priority.Medium, - retry: int = MESSAGE_RETRY, ): """Queue request message to be sent into Plugwise Zigbee network.""" _LOGGER.info( "Send queue = %s, Add %s, priority=%s, retry=%s", str(self._send_message_queue.qsize()), request.__class__.__name__, - str(priority), - str(retry), + str(request.priority), + str(request.retry_counter), ) _utc = datetime.utcnow().replace(tzinfo=timezone.utc) - self._send_message_queue.put((priority, _utc, retry, request)) + _retry = MESSAGE_RETRY - request.retry_counter + 1 + self._send_message_queue.put((request.priority, _retry, _utc, request)) def _send_message_loop(self): """Daemon to send messages waiting in queue.""" - _max_wait = SLEEP_TIME * 10 + _max_wait = SLEEP_TIME * 15 while self._send_message_thread_state: try: - _priority, _utc, _retry, _request = self._send_message_queue.get( + _priority, _retry, _utc, _request = self._send_message_queue.get( block=True, timeout=1 ) except Empty: @@ -161,6 +160,7 @@ def _send_message_loop(self): # Send request self.connection.send(_request) _request.send = datetime.utcnow().replace(tzinfo=timezone.utc) + self._stick_request.retry_counter += 1 # Wait for response while timeout_counter < _max_wait: @@ -173,16 +173,17 @@ def _send_message_loop(self): _retry -= 1 if _retry < 1: _LOGGER.error( - "No response for %s after 3 retries. Drop request!", + "Stick does not respond to %s after %s retries. Drop request", _request.__class__.__name__, + str(MESSAGE_RETRY - _retry + 1), ) else: _LOGGER.warning( - "No response for %s after %s retry. Retry request!", + "Stick does not respond to %s after %s retries. Retry request", _request.__class__.__name__, str(MESSAGE_RETRY - _retry + 1), ) - self.send(_request, _priority, _retry) + self.send(_request) else: _LOGGER.info( "Send queue = %s", @@ -202,7 +203,7 @@ def message_handler(self, message: PlugwiseResponse) -> None: self._log_status_of_request(message.seq_id) else: - # Forward message to Stick + # Forward message to Stick class if message.seq_id in self._open_requests: if isinstance(self._open_requests[message.seq_id].mac, bytes): _target = " to " + self._open_requests[message.seq_id].mac.decode( @@ -242,11 +243,13 @@ def _log_status_of_request(self, seq_id: bytes) -> None: ) elif self._open_requests[seq_id].stick_state == StickResponseType.timeout: _LOGGER.warning( - "Stick 'time out' received for %s%s with seq_id=%s", + "Stick 'time out' received for %s%s with seq_id=%s, retry request", self._open_requests[seq_id].__class__.__name__, _target, str(seq_id), ) + self._open_requests[seq_id].stick_state = None + self.send(self._open_requests[seq_id]) elif self._open_requests[seq_id].stick_state == StickResponseType.failed: _LOGGER.error( "Stick failed received for %s%s with seq_id=%s", @@ -278,14 +281,27 @@ def _receive_timeout_loop(self): ) else: _target = "" - _LOGGER.warning( - "_receive_timeout_loop found old %s%s with seq_id=%s, send=%s, stick_response=%s", - self._open_requests[seq_id].__class__.__name__, - _target, - str(seq_id), - str(self._open_requests[seq_id].send), - str(self._open_requests[seq_id].stick_response), - ) + if self._open_requests[seq_id].retry_counter >= MESSAGE_RETRY: + _LOGGER.warning( + "No response for %s%s => drop request (seq_id=%s, retry=%s, last try=%s, last stick_response=%s)", + self._open_requests[seq_id].__class__.__name__, + _target, + str(seq_id), + str(self._open_requests[seq_id].retry_counter), + str(self._open_requests[seq_id].send), + str(self._open_requests[seq_id].stick_response), + ) + else: + _LOGGER.warning( + "No response for %s%s => retry request (seq_id=%s, retry=%s, last try=%s, last stick_response=%s)", + self._open_requests[seq_id].__class__.__name__, + _target, + str(seq_id), + str(self._open_requests[seq_id].retry_counter), + str(self._open_requests[seq_id].send), + str(self._open_requests[seq_id].stick_response), + ) + self.send(self._open_requests[seq_id]) del self._open_requests[seq_id] receive_timeout_checker = 0 while ( diff --git a/plugwise/messages/requests.py b/plugwise/messages/requests.py index 1ee336018..798aec92b 100644 --- a/plugwise/messages/requests.py +++ b/plugwise/messages/requests.py @@ -38,6 +38,28 @@ def __init__(self, mac): self._send: datetime | None = None self._stick_response: datetime | None = None self._stick_state: bytes | None = None + self._retry_counter: int = 0 + self._priority: Priority = Priority.Medium + + @property + def priority(self) -> Priority: + """Priority level.""" + return self._priority + + @priority.setter + def priority(self, priority: Priority) -> None: + """Set priority level.""" + self._priority = priority + + @property + def retry_counter(self) -> int: + """Total number of retries.""" + return self._retry_counter + + @retry_counter.setter + def retry_counter(self, retry: int) -> None: + """Set new retry counter""" + self._retry_counter = retry @property def send(self) -> datetime | None: diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 85ae51c83..cddcd54f8 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -169,7 +169,9 @@ def do_ping(self, callback: callable | None = None) -> None: def _request_info(self, callback: callable | None = None) -> None: """Request info from node.""" self._callback_NodeInfo = callback - self.message_sender(NodeInfoRequest(self._mac), Priority.Low) + _node_request = NodeInfoRequest(self._mac) + _node_request.priority = Priority.Low + self.message_sender(_node_request) def _request_features(self, callback: callable | None = None) -> None: """Request supported features for this node.""" diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index f6d72516a..e0c299970 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -211,10 +211,7 @@ def relay_state(self, state): def _request_calibration(self, callback: callable | None = None) -> None: """Request calibration info""" self._callback_CircleCalibration = callback - self.message_sender( - CircleCalibrationRequest(self._mac), - Priority.High, - ) + self.message_sender(CircleCalibrationRequest(self._mac)) def _request_switch( self, @@ -228,18 +225,15 @@ def _request_switch( else: self._callback_RelaySwitchedOff = success_callback self._callback_RelaySwitchFailed = failed_callback - self.message_sender( - CircleSwitchRelayRequest(self._mac, state), - Priority.High, - ) + _relay_request = CircleSwitchRelayRequest(self._mac, state) + _relay_request.priority = Priority.High + self.message_sender(_relay_request) def request_power_update(self, callback: callable | None = None) -> None: """Request power usage and update energy counters""" if self._available: self._callback_CirclePowerUsage = callback - self.message_sender( - CirclePowerUsageRequest(self._mac), - ) + self.message_sender(CirclePowerUsageRequest(self._mac)) if len(self._energy_history) > 0: # Request new energy counters if last one is more than one hour ago if self._energy_last_collected_timestamp < datetime.utcnow().replace( @@ -655,25 +649,21 @@ def request_energy_counters( self._request_info(self.request_energy_counters) else: # Request new energy counters - self.message_sender( - CircleEnergyCountersRequest(self._mac, log_address), - Priority.Low, - ) + _log_request = CircleEnergyCountersRequest(self._mac, log_address) + _log_request.priority = Priority.Low + self.message_sender(_log_request) else: # Collect energy counters of today and yesterday # Each request contains will return 4 hours, except last request # TODO: validate range of log_addresses self._energy_history_collecting = True - for req_log_address in range(log_address - 13, log_address): - self.message_sender( - CircleEnergyCountersRequest(self._mac, req_log_address), - Priority.Low, + for req_log_address in range(log_address - 13, log_address + 1): + _log_request = CircleEnergyCountersRequest( + self._mac, req_log_address ) - self.message_sender( - CircleEnergyCountersRequest(self._mac, log_address), - Priority.Low, - ) + _log_request.priority = Priority.Low + self.message_sender(_log_request) def _process_CircleEnergyCountersResponse( self, message: CircleEnergyCountersResponse @@ -810,18 +800,16 @@ def _process_CircleClockResponse(self, message: CircleClockResponse) -> None: def get_clock(self, callback: callable | None = None) -> None: """get current datetime of internal clock of Circle.""" self._callback_CircleClockResponse = callback - self.message_sender( - CircleClockGetRequest(self._mac), - 0, - Priority.Low, - ) + _clock_request = CircleClockGetRequest(self._mac) + _clock_request.priority = Priority.Low + self.message_sender(_clock_request) def set_clock(self, callback: callable | None = None) -> None: """set internal clock of CirclePlus.""" self._callback_ClockAccepted = callback - self.message_sender( - CircleClockSetRequest(self._mac, datetime.utcnow()), - ) + _clock_request = CircleClockSetRequest(self._mac, datetime.utcnow()) + _clock_request.priority = Priority.High + self.message_sender(_clock_request) def sync_clock(self, max_drift=0): """Resync clock of node if time has drifted more than MAX_TIME_DRIFT""" diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index ec22829bf..b3fb05040 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -118,10 +118,9 @@ def _process_CirclePlusScanResponse(self, message: CirclePlusScanResponse) -> No def get_real_time_clock(self, callback: callable | None = None) -> None: """get current datetime of internal clock of CirclePlus.""" self._callback_CirclePlusRealTimeClockGet = callback - self.message_sender( - CirclePlusRealTimeClockGetRequest(self._mac), - Priority.Low, - ) + _clock_request = CirclePlusRealTimeClockGetRequest(self._mac) + _clock_request.priority = Priority.Low + self.message_sender(_clock_request) def _process_CirclePlusRealTimeClockResponse( self, message: CirclePlusRealTimeClockResponse @@ -159,9 +158,9 @@ def set_real_time_clock( """set internal clock of CirclePlus.""" self._callback_RealTimeClockAccepted = success_callback self._callback_RealTimeClockFailed = failed_callback - self.message_sender( - CirclePlusRealTimeClockSetRequest(self._mac, datetime.utcnow()), - ) + _clock_request = CirclePlusRealTimeClockSetRequest(self._mac, datetime.utcnow()) + _clock_request.priority = Priority.High + self.message_sender(_clock_request) def sync_realtime_clock(self, max_drift=0): """Sync real time clock of node if time has drifted more than max drifted.""" diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index 0d3334ec5..1809878e1 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -125,12 +125,13 @@ def _send_pending_requests(self) -> None: request_message.__class__.__name__, self.mac, ) - self.message_sender(request_message, Priority.High) + self.message_sender(request_message) self._sed_requests = {} def _queue_request(self, message: PlugwiseRequest): """Queue request to be sent when SED is awake. Last message wins.""" self._sed_requests[message.ID] = message + self._sed_requests[message.ID].priority = Priority.High _LOGGER.info( "Queue %s to be send at next awake of SED node %s", message.__class__.__name__, diff --git a/plugwise/stick.py b/plugwise/stick.py index f1b53734e..d7ce0e3c1 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -665,10 +665,9 @@ def _update_loop(self): # Do a single ping for undiscovered nodes once per 10 update cycles if _discover_counter == 10: for mac in self._nodes_not_discovered: - self.msg_controller.send( - NodePingRequest(bytes(mac, UTF8_DECODE)), - Priority.Low, - ) + _ping_request = NodePingRequest(bytes(mac, UTF8_DECODE)) + _ping_request.priority = Priority.Low + self.msg_controller.send(_ping_request) _discover_counter = 0 else: _discover_counter += 1 @@ -777,10 +776,9 @@ def discover_node(self, mac: str, callback=None, force_discover=False): (firstrequest, lastrequest) = self._nodes_not_discovered[mac] if not (firstrequest and lastrequest): self._callback_NodeInfo[mac] = callback - self.msg_controller.send( - NodeInfoRequest(bytes(mac, UTF8_DECODE)), - Priority.Low, - ) + _node_request = NodeInfoRequest(bytes(mac, UTF8_DECODE)) + _node_request.priority = Priority.Low + self.msg_controller.send(_node_request) elif force_discover: self._callback_NodeInfo[mac] = callback self.msg_controller.send( From 6e3fec4f41941b23413ca7045f7c7290402507a0 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 17:56:23 +0100 Subject: [PATCH 31/87] Increase power update frequency Only if controller is not busy --- plugwise/controller.py | 7 +++++++ plugwise/stick.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/plugwise/controller.py b/plugwise/controller.py index 7d6c36bc0..bd80236b8 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -62,6 +62,13 @@ def __init__(self, port: str, message_processor, node_state): self.last_seq_id: bytes | None = None self.last_result: StickResponseType | None = None + @property + def busy(self) -> bool: + """Indicator if controller is busy with sending messages.""" + if self._send_message_queue.qsize() < 2: + return False + return True + @property def receive_timeout_thread_state(self) -> bool: """Required state of the receive timeout thread""" diff --git a/plugwise/stick.py b/plugwise/stick.py index d7ce0e3c1..a53e4cb88 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -680,6 +680,10 @@ def _update_loop(self): ): time.sleep(1) update_loop_checker += 1 + if not self.msg_controller.busy: + # wait 2 seconds + time.sleep(2) + break # TODO: narrow exception except Exception as err: # pylint: disable=broad-except From 6b377dc0110655976e51fc3e621a0d8f14d8f092 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 17:57:08 +0100 Subject: [PATCH 32/87] Cleanup controller --- plugwise/controller.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index bd80236b8..d9c31214a 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -59,8 +59,6 @@ def __init__(self, port: str, message_processor, node_state): self._timeout_delta = timedelta(minutes=1) self._open_requests: dict(bytes, PlugwiseRequest) = {} self._stick_response: bool = False - self.last_seq_id: bytes | None = None - self.last_result: StickResponseType | None = None @property def busy(self) -> bool: From 071c0ea1bc7491c0c11ed48e4945e01a71b6c7a1 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 17:59:07 +0100 Subject: [PATCH 33/87] Correct description of controller --- plugwise/controller.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index d9c31214a..dbd5233cb 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -3,11 +3,9 @@ The controller will: - handle the connection (connect/disconnect) to the USB-Stick -- take care for message acknowledgements based on sequence id's -- resend message requests when timeouts occurs +- resend message requests when stick responds with timeouts - holds a sending queue and submit messages based on the message priority (high, medium, low) - passes received messages back to message processor (stick.py) -- execution of callbacks after processing the response message """ from __future__ import annotations From 2c5868e90865d01e784270fb806292aadb21bb2e Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 20:27:52 +0100 Subject: [PATCH 34/87] Drop duplicate requests at controller --- plugwise/controller.py | 83 +++++++++++++++++++++++++---------- plugwise/messages/requests.py | 12 ++++- 2 files changed, 71 insertions(+), 24 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index dbd5233cb..e80e8f838 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -128,20 +128,27 @@ def send( request: PlugwiseRequest, ): """Queue request message to be sent into Plugwise Zigbee network.""" - _LOGGER.info( - "Send queue = %s, Add %s, priority=%s, retry=%s", - str(self._send_message_queue.qsize()), - request.__class__.__name__, - str(request.priority), - str(request.retry_counter), - ) - _utc = datetime.utcnow().replace(tzinfo=timezone.utc) - _retry = MESSAGE_RETRY - request.retry_counter + 1 - self._send_message_queue.put((request.priority, _retry, _utc, request)) + + if self._duplicate_request(request): + _LOGGER.warning( + "Drop duplicate %s for %s", + request.__class__.__name__, + request.target_mac, + ) + else: + _LOGGER.info( + "Send queue = %s, Add %s, priority=%s, retry=%s", + str(self._send_message_queue.qsize()), + request.__class__.__name__, + str(request.priority), + str(request.retry_counter), + ) + _utc = datetime.utcnow().replace(tzinfo=timezone.utc) + _retry = MESSAGE_RETRY - request.retry_counter + 1 + self._send_message_queue.put((request.priority, _retry, _utc, request)) def _send_message_loop(self): """Daemon to send messages waiting in queue.""" - _max_wait = SLEEP_TIME * 15 while self._send_message_thread_state: try: _priority, _retry, _utc, _request = self._send_message_queue.get( @@ -166,18 +173,19 @@ def _send_message_loop(self): self._stick_request.retry_counter += 1 # Wait for response - while timeout_counter < _max_wait: + while timeout_counter < MESSAGE_TIME_OUT: if self._stick_response: break time.sleep(SLEEP_TIME) - timeout_counter += 1 + timeout_counter += SLEEP_TIME - if timeout_counter > _max_wait: + if timeout_counter > MESSAGE_TIME_OUT: _retry -= 1 if _retry < 1: _LOGGER.error( - "Stick does not respond to %s after %s retries. Drop request", + "Stick does not respond to %s for %s after %s retries. Drop request", _request.__class__.__name__, + _request.target_mac, str(MESSAGE_RETRY - _retry + 1), ) else: @@ -208,17 +216,11 @@ def message_handler(self, message: PlugwiseResponse) -> None: else: # Forward message to Stick class if message.seq_id in self._open_requests: - if isinstance(self._open_requests[message.seq_id].mac, bytes): - _target = " to " + self._open_requests[message.seq_id].mac.decode( - UTF8_DECODE - ) - else: - _target = "" _LOGGER.info( "forward %s after %s%s with seq_id=%s", message.__class__.__name__, self._open_requests[message.seq_id].__class__.__name__, - _target, + self._open_requests[message.seq_id].target_mac, str(message.seq_id), ) else: @@ -276,7 +278,7 @@ def _receive_timeout_loop(self): for seq_id in list(self._open_requests.keys()): if ( self._open_requests[seq_id].stick_response + self._timeout_delta - > _utcnow + < _utcnow ): if isinstance(self._open_requests[seq_id].mac, bytes): _target = " to " + self._open_requests[seq_id].mac.decode( @@ -315,6 +317,41 @@ def _receive_timeout_loop(self): receive_timeout_checker += 1 _LOGGER.debug("Receive timeout loop stopped") + def _duplicate_request(self, request: PlugwiseRequest) -> bool: + """Check if request target towards same node already exists in queue.""" + if request.target_mac == "": + return False + if request.__class__.__name__ in ( + "CirclePlusScanRequest", + "CircleClockSetRequest", + "CirclePlusRealTimeClockSetRequest", + "CircleEnergyCountersRequest", + ): + return False + # Check queue + for ( + _priority, + _retry, + _utc, + _queued_request, + ) in self._send_message_queue.queue: + if _queued_request.target_mac: + if ( + _queued_request.mac == request.mac + and _queued_request.__class__.__name__ == request.__class__.__name__ + ): + return True + # Check for open requests + for _seq_id in self._open_requests.keys(): + if self._open_requests[_seq_id].target_mac: + if ( + self._open_requests[_seq_id].mac == request.mac + and self._open_requests[_seq_id].__class__.__name__ + == request.__class__.__name__ + ): + return True + return False + def disconnect_from_stick(self): """Disconnect from stick and raise error if it fails""" self._send_message_thread_state = False diff --git a/plugwise/messages/requests.py b/plugwise/messages/requests.py index 798aec92b..6950708b5 100644 --- a/plugwise/messages/requests.py +++ b/plugwise/messages/requests.py @@ -4,7 +4,7 @@ from datetime import datetime from enum import Enum -from ..constants import MESSAGE_FOOTER, MESSAGE_HEADER +from ..constants import MESSAGE_FOOTER, MESSAGE_HEADER, UTF8_DECODE from ..messages import PlugwiseMessage from ..util import ( DateTime, @@ -41,6 +41,16 @@ def __init__(self, mac): self._retry_counter: int = 0 self._priority: Priority = Priority.Medium + @property + def target_mac(self) -> str: + """ + MAC address in readable form. + Returns empty string if no MAC exists + """ + if isinstance(self.mac, bytes): + return self.mac.decode(UTF8_DECODE) + return "" + @property def priority(self) -> Priority: """Priority level.""" From ece93bc0f033fe6039bb8fc8133cec7981c16203 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 20:30:28 +0100 Subject: [PATCH 35/87] Use pending requests in busy state of controller --- plugwise/controller.py | 85 +++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index e80e8f838..c9bdca87c 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -55,14 +55,15 @@ def __init__(self, port: str, message_processor, node_state): self._send_message_thread_state = False self._timeout_delta = timedelta(minutes=1) - self._open_requests: dict(bytes, PlugwiseRequest) = {} + self._pending_request: dict(bytes, PlugwiseRequest) = {} self._stick_response: bool = False @property def busy(self) -> bool: """Indicator if controller is busy with sending messages.""" if self._send_message_queue.qsize() < 2: - return False + if len(self._pending_request) < 2: + return False return True @property @@ -206,21 +207,21 @@ def message_handler(self, message: PlugwiseResponse) -> None: """handle received message from Plugwise Zigbee network.""" if isinstance(message, StickResponse): if not self._stick_response: - if message.seq_id not in self._open_requests.keys(): - self._open_requests[message.seq_id] = self._stick_request - self._open_requests[message.seq_id].stick_response = message.timestamp - self._open_requests[message.seq_id].stick_state = message.ack_id + if message.seq_id not in self._pending_request.keys(): + self._pending_request[message.seq_id] = self._stick_request + self._pending_request[message.seq_id].stick_response = message.timestamp + self._pending_request[message.seq_id].stick_state = message.ack_id self._stick_response = True self._log_status_of_request(message.seq_id) else: # Forward message to Stick class - if message.seq_id in self._open_requests: + if message.seq_id in self._pending_request: _LOGGER.info( "forward %s after %s%s with seq_id=%s", message.__class__.__name__, - self._open_requests[message.seq_id].__class__.__name__, - self._open_requests[message.seq_id].target_mac, + self._pending_request[message.seq_id].__class__.__name__, + self._pending_request[message.seq_id].target_mac, str(message.seq_id), ) else: @@ -230,43 +231,43 @@ def message_handler(self, message: PlugwiseResponse) -> None: str(message.seq_id), ) self.message_processor(message) - if message.seq_id in self._open_requests.keys(): - del self._open_requests[message.seq_id] + if message.seq_id in self._pending_request.keys(): + del self._pending_request[message.seq_id] def _log_status_of_request(self, seq_id: bytes) -> None: """.""" - if isinstance(self._open_requests[seq_id].mac, bytes): - _target = " to " + self._open_requests[seq_id].mac.decode(UTF8_DECODE) + if isinstance(self._pending_request[seq_id].mac, bytes): + _target = " to " + self._pending_request[seq_id].mac.decode(UTF8_DECODE) else: _target = "" - if self._open_requests[seq_id].stick_state == StickResponseType.success: + if self._pending_request[seq_id].stick_state == StickResponseType.success: _LOGGER.debug( "Stick accepted %s%s with seq_id=%s", - self._open_requests[seq_id].__class__.__name__, + self._pending_request[seq_id].__class__.__name__, _target, str(seq_id), ) - elif self._open_requests[seq_id].stick_state == StickResponseType.timeout: + elif self._pending_request[seq_id].stick_state == StickResponseType.timeout: _LOGGER.warning( "Stick 'time out' received for %s%s with seq_id=%s, retry request", - self._open_requests[seq_id].__class__.__name__, + self._pending_request[seq_id].__class__.__name__, _target, str(seq_id), ) - self._open_requests[seq_id].stick_state = None - self.send(self._open_requests[seq_id]) - elif self._open_requests[seq_id].stick_state == StickResponseType.failed: + self._pending_request[seq_id].stick_state = None + self.send(self._pending_request[seq_id]) + elif self._pending_request[seq_id].stick_state == StickResponseType.failed: _LOGGER.error( "Stick failed received for %s%s with seq_id=%s", - self._open_requests[seq_id].__class__.__name__, + self._pending_request[seq_id].__class__.__name__, _target, str(seq_id), ) else: _LOGGER.warning( "Unknown StickResponseType %s received for %s%s with seq_id=%s", - str(self._open_requests[seq_id].stick_state), - self._open_requests[seq_id].__class__.__name__, + str(self._pending_request[seq_id].stick_state), + self._pending_request[seq_id].__class__.__name__, _target, str(seq_id), ) @@ -275,39 +276,39 @@ def _receive_timeout_loop(self): """Daemon to time out open requests without any response message.""" while self._receive_timeout_thread_state: _utcnow = datetime.utcnow().replace(tzinfo=timezone.utc) - for seq_id in list(self._open_requests.keys()): + for seq_id in list(self._pending_request.keys()): if ( - self._open_requests[seq_id].stick_response + self._timeout_delta + self._pending_request[seq_id].stick_response + self._timeout_delta < _utcnow ): - if isinstance(self._open_requests[seq_id].mac, bytes): - _target = " to " + self._open_requests[seq_id].mac.decode( + if isinstance(self._pending_request[seq_id].mac, bytes): + _target = " to " + self._pending_request[seq_id].mac.decode( UTF8_DECODE ) else: _target = "" - if self._open_requests[seq_id].retry_counter >= MESSAGE_RETRY: + if self._pending_request[seq_id].retry_counter >= MESSAGE_RETRY: _LOGGER.warning( "No response for %s%s => drop request (seq_id=%s, retry=%s, last try=%s, last stick_response=%s)", - self._open_requests[seq_id].__class__.__name__, + self._pending_request[seq_id].__class__.__name__, _target, str(seq_id), - str(self._open_requests[seq_id].retry_counter), - str(self._open_requests[seq_id].send), - str(self._open_requests[seq_id].stick_response), + str(self._pending_request[seq_id].retry_counter), + str(self._pending_request[seq_id].send), + str(self._pending_request[seq_id].stick_response), ) else: _LOGGER.warning( "No response for %s%s => retry request (seq_id=%s, retry=%s, last try=%s, last stick_response=%s)", - self._open_requests[seq_id].__class__.__name__, + self._pending_request[seq_id].__class__.__name__, _target, str(seq_id), - str(self._open_requests[seq_id].retry_counter), - str(self._open_requests[seq_id].send), - str(self._open_requests[seq_id].stick_response), + str(self._pending_request[seq_id].retry_counter), + str(self._pending_request[seq_id].send), + str(self._pending_request[seq_id].stick_response), ) - self.send(self._open_requests[seq_id]) - del self._open_requests[seq_id] + self.send(self._pending_request[seq_id]) + del self._pending_request[seq_id] receive_timeout_checker = 0 while ( receive_timeout_checker < MESSAGE_TIME_OUT @@ -342,11 +343,11 @@ def _duplicate_request(self, request: PlugwiseRequest) -> bool: ): return True # Check for open requests - for _seq_id in self._open_requests.keys(): - if self._open_requests[_seq_id].target_mac: + for _seq_id in self._pending_request.keys(): + if self._pending_request[_seq_id].target_mac: if ( - self._open_requests[_seq_id].mac == request.mac - and self._open_requests[_seq_id].__class__.__name__ + self._pending_request[_seq_id].mac == request.mac + and self._pending_request[_seq_id].__class__.__name__ == request.__class__.__name__ ): return True From 9fa4c16fa26b62b4ccb14dd954de29b461657dae Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 20:35:00 +0100 Subject: [PATCH 36/87] Limit message retries at discovery phase --- plugwise/stick.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugwise/stick.py b/plugwise/stick.py index a53e4cb88..1033f1476 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -14,6 +14,7 @@ from .constants import ( ACCEPT_JOIN_REQUESTS, + MESSAGE_RETRY, MESSAGE_TIME_OUT, NODE_TYPE_CELSIUS_NR, NODE_TYPE_CELSIUS_SED, @@ -667,6 +668,7 @@ def _update_loop(self): for mac in self._nodes_not_discovered: _ping_request = NodePingRequest(bytes(mac, UTF8_DECODE)) _ping_request.priority = Priority.Low + _ping_request.retry_counter = MESSAGE_RETRY - 1 self.msg_controller.send(_ping_request) _discover_counter = 0 else: @@ -680,7 +682,7 @@ def _update_loop(self): ): time.sleep(1) update_loop_checker += 1 - if not self.msg_controller.busy: + if not self.msg_controller.busy and self._run_update_thread: # wait 2 seconds time.sleep(2) break @@ -773,15 +775,16 @@ def discover_node(self, mac: str, callback=None, force_discover=False): None, ) self._callback_NodeInfo[mac] = callback - self.msg_controller.send( - NodeInfoRequest(bytes(mac, UTF8_DECODE)), - ) + _node_request = NodeInfoRequest(bytes(mac, UTF8_DECODE)) + _node_request.retry_counter = MESSAGE_RETRY - 1 + self.msg_controller.send(_node_request) else: (firstrequest, lastrequest) = self._nodes_not_discovered[mac] if not (firstrequest and lastrequest): self._callback_NodeInfo[mac] = callback _node_request = NodeInfoRequest(bytes(mac, UTF8_DECODE)) _node_request.priority = Priority.Low + _node_request.retry_counter = MESSAGE_RETRY - 1 self.msg_controller.send(_node_request) elif force_discover: self._callback_NodeInfo[mac] = callback From 1b1c67baf0b5c7006823a9b3413f7dbfb98dec26 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 20:40:12 +0100 Subject: [PATCH 37/87] Do not forward JoinAccepted to nodes --- plugwise/stick.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugwise/stick.py b/plugwise/stick.py index 1033f1476..bad0a4015 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -447,7 +447,8 @@ def _process_NodeResponse(self, message: NodeResponse) -> None: self.discover_node( message.mac.decode(UTF8_DECODE), self._discover_after_scan ) - self._pass_message_to_node(message) + else: + self._pass_message_to_node(message) def _process_StickInitResponse( self, stick_init_response: StickInitResponse From a3fd8fa3f7529746fb9a7cfaccf2ddd40b7be5ad Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 21:00:31 +0100 Subject: [PATCH 38/87] Lower logger level for expected messages Use constants for messages with hardcode sequence ID's --- plugwise/controller.py | 30 ++++++++++++++++++++++++------ plugwise/messages/responses.py | 11 ++++++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index c9bdca87c..24dfeb8e1 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -21,7 +21,14 @@ from .connections.socket import SocketConnection from .constants import MESSAGE_RETRY, MESSAGE_TIME_OUT, SLEEP_TIME, UTF8_DECODE from .messages.requests import PlugwiseRequest, Priority -from .messages.responses import PlugwiseResponse, StickResponse, StickResponseType +from .messages.responses import ( + AWAKE_RESPONSE, + REJOIN_RESPONSE_ID, + SWITCH_GROUP_RESPONSE, + PlugwiseResponse, + StickResponse, + StickResponseType, +) from .parser import PlugwiseParser _LOGGER = logging.getLogger(__name__) @@ -225,11 +232,22 @@ def message_handler(self, message: PlugwiseResponse) -> None: str(message.seq_id), ) else: - _LOGGER.warning( - "Forward %s with seq_id=%s", - message.__class__.__name__, - str(message.seq_id), - ) + if message.seq_id in ( + REJOIN_RESPONSE_ID, + AWAKE_RESPONSE, + SWITCH_GROUP_RESPONSE, + ): + _LOGGER.info( + "Forward %s with seq_id=%s", + message.__class__.__name__, + str(message.seq_id), + ) + else: + _LOGGER.warning( + "Forward unexpected %s with seq_id=%s", + message.__class__.__name__, + str(message.seq_id), + ) self.message_processor(message) if message.seq_id in self._pending_request.keys(): del self._pending_request[message.seq_id] diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index bfa61e510..98f1cb858 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -24,6 +24,10 @@ UnixTimestamp, ) +REJOIN_RESPONSE_ID = b"FFFD" +AWAKE_RESPONSE = b"FFFE" +SWITCH_GROUP_RESPONSE = b"FFFF" + class StickResponseType(bytes, Enum): """Response message types for stick.""" @@ -643,12 +647,13 @@ def get_message_response(message_id, length, seq_id): """ Return message class based on sequence ID, Length of message or message ID. """ + # First check for known sequence ID's - if seq_id == b"FFFD": + if seq_id == REJOIN_RESPONSE_ID: return NodeRejoinResponse() - if seq_id == b"FFFE": + if seq_id == AWAKE_RESPONSE: return NodeAwakeResponse() - if seq_id == b"FFFF": + if seq_id == SWITCH_GROUP_RESPONSE: return NodeSwitchGroupResponse() # No fixed sequence ID, continue at message ID From 2a38c984b2f2981bca3b9ae92236294f86cea5ca Mon Sep 17 00:00:00 2001 From: brefra Date: Wed, 5 Jan 2022 09:35:15 +0100 Subject: [PATCH 39/87] Delete finished request from timeout thread to prevent race condition in mutating self._pending_request --- plugwise/controller.py | 6 ++++-- plugwise/messages/requests.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index 24dfeb8e1..86976bcb1 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -250,7 +250,7 @@ def message_handler(self, message: PlugwiseResponse) -> None: ) self.message_processor(message) if message.seq_id in self._pending_request.keys(): - del self._pending_request[message.seq_id] + self._pending_request[message.seq_id].finished = True def _log_status_of_request(self, seq_id: bytes) -> None: """.""" @@ -295,7 +295,9 @@ def _receive_timeout_loop(self): while self._receive_timeout_thread_state: _utcnow = datetime.utcnow().replace(tzinfo=timezone.utc) for seq_id in list(self._pending_request.keys()): - if ( + if self._pending_request[seq_id].finished: + del self._pending_request[seq_id] + elif ( self._pending_request[seq_id].stick_response + self._timeout_delta < _utcnow ): diff --git a/plugwise/messages/requests.py b/plugwise/messages/requests.py index 6950708b5..9b9761a45 100644 --- a/plugwise/messages/requests.py +++ b/plugwise/messages/requests.py @@ -40,6 +40,17 @@ def __init__(self, mac): self._stick_state: bytes | None = None self._retry_counter: int = 0 self._priority: Priority = Priority.Medium + self._finished: bool = False + + @property + def finished(self) -> bool: + """Indicate if response to request has been received.""" + return self._finished + + @finished.setter + def finished(self, state: bool) -> None: + """Set finish state to indicate if response has been received.""" + self._finished = state @property def target_mac(self) -> str: From 4be2f3e2432a442f3f3eb1f1242f1603af58ac39 Mon Sep 17 00:00:00 2001 From: brefra Date: Wed, 5 Jan 2022 09:35:52 +0100 Subject: [PATCH 40/87] Introduce SPECIAL_SEQ_IDS --- plugwise/controller.py | 10 ++-------- plugwise/messages/responses.py | 2 ++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index 86976bcb1..d30861d7c 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -22,9 +22,7 @@ from .constants import MESSAGE_RETRY, MESSAGE_TIME_OUT, SLEEP_TIME, UTF8_DECODE from .messages.requests import PlugwiseRequest, Priority from .messages.responses import ( - AWAKE_RESPONSE, - REJOIN_RESPONSE_ID, - SWITCH_GROUP_RESPONSE, + SPECIAL_SEQ_IDS, PlugwiseResponse, StickResponse, StickResponseType, @@ -232,11 +230,7 @@ def message_handler(self, message: PlugwiseResponse) -> None: str(message.seq_id), ) else: - if message.seq_id in ( - REJOIN_RESPONSE_ID, - AWAKE_RESPONSE, - SWITCH_GROUP_RESPONSE, - ): + if message.seq_id in SPECIAL_SEQ_IDS: _LOGGER.info( "Forward %s with seq_id=%s", message.__class__.__name__, diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index 98f1cb858..d9772132a 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -28,6 +28,8 @@ AWAKE_RESPONSE = b"FFFE" SWITCH_GROUP_RESPONSE = b"FFFF" +SPECIAL_SEQ_IDS = (REJOIN_RESPONSE_ID, AWAKE_RESPONSE, SWITCH_GROUP_RESPONSE) + class StickResponseType(bytes, Enum): """Response message types for stick.""" From 1ac22ccde6738c96e9610ab22bde20df248ad896 Mon Sep 17 00:00:00 2001 From: brefra Date: Wed, 5 Jan 2022 10:59:31 +0100 Subject: [PATCH 41/87] Do not mark request duplicate if previous finished --- plugwise/controller.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index d30861d7c..d4f40bf25 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -266,8 +266,10 @@ def _log_status_of_request(self, seq_id: bytes) -> None: _target, str(seq_id), ) - self._pending_request[seq_id].stick_state = None - self.send(self._pending_request[seq_id]) + _request = self._pending_request[seq_id] + _request.stick_state = None + self._pending_request[seq_id].finished = True + self.send(_request) elif self._pending_request[seq_id].stick_state == StickResponseType.failed: _LOGGER.error( "Stick failed received for %s%s with seq_id=%s", @@ -363,6 +365,7 @@ def _duplicate_request(self, request: PlugwiseRequest) -> bool: self._pending_request[_seq_id].mac == request.mac and self._pending_request[_seq_id].__class__.__name__ == request.__class__.__name__ + and not self._pending_request[_seq_id].finished ): return True return False From 15e698f84554f506aa1617de1e7d62872b419ea7 Mon Sep 17 00:00:00 2001 From: brefra Date: Fri, 7 Jan 2022 23:54:26 +0100 Subject: [PATCH 42/87] Add 'drop_at_timeout' property to request messages --- plugwise/controller.py | 61 ++++++++++++++++++++--------------- plugwise/messages/requests.py | 11 +++++++ plugwise/stick.py | 13 ++++---- 3 files changed, 53 insertions(+), 32 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index d4f40bf25..1339d04be 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -185,27 +185,35 @@ def _send_message_loop(self): time.sleep(SLEEP_TIME) timeout_counter += SLEEP_TIME - if timeout_counter > MESSAGE_TIME_OUT: - _retry -= 1 - if _retry < 1: - _LOGGER.error( - "Stick does not respond to %s for %s after %s retries. Drop request", - _request.__class__.__name__, - _request.target_mac, - str(MESSAGE_RETRY - _retry + 1), - ) + if _request.drop_at_timeout: + _LOGGER.error( + "Stick does not respond to %s for %s, drop request as request is set to be dropped at timeout", + _request.__class__.__name__, + _request.target_mac, + ) + else: + if timeout_counter > MESSAGE_TIME_OUT: + _retry -= 1 + if _retry < 1: + _LOGGER.error( + "Stick does not respond to %s for %s after %s retries. Drop request", + _request.__class__.__name__, + _request.target_mac, + str(MESSAGE_RETRY - _retry + 1), + ) + else: + _LOGGER.warning( + "Stick does not respond to %s after %s retries. Retry request", + _request.__class__.__name__, + str(MESSAGE_RETRY - _retry + 1), + ) + self.send(_request) else: - _LOGGER.warning( - "Stick does not respond to %s after %s retries. Retry request", - _request.__class__.__name__, - str(MESSAGE_RETRY - _retry + 1), + _LOGGER.info( + "Send queue = %s", + str(self._send_message_queue.qsize()), ) - self.send(_request) - else: - _LOGGER.info( - "Send queue = %s", - str(self._send_message_queue.qsize()), - ) + _LOGGER.debug("Send message loop stopped") def message_handler(self, message: PlugwiseResponse) -> None: @@ -260,16 +268,17 @@ def _log_status_of_request(self, seq_id: bytes) -> None: str(seq_id), ) elif self._pending_request[seq_id].stick_state == StickResponseType.timeout: - _LOGGER.warning( - "Stick 'time out' received for %s%s with seq_id=%s, retry request", - self._pending_request[seq_id].__class__.__name__, - _target, - str(seq_id), - ) _request = self._pending_request[seq_id] _request.stick_state = None self._pending_request[seq_id].finished = True - self.send(_request) + if not _request.drop_at_timeout: + _LOGGER.warning( + "Stick 'time out' received for %s%s with seq_id=%s, retry request", + self._pending_request[seq_id].__class__.__name__, + _target, + str(seq_id), + ) + self.send(_request) elif self._pending_request[seq_id].stick_state == StickResponseType.failed: _LOGGER.error( "Stick failed received for %s%s with seq_id=%s", diff --git a/plugwise/messages/requests.py b/plugwise/messages/requests.py index 9b9761a45..47adcf3e4 100644 --- a/plugwise/messages/requests.py +++ b/plugwise/messages/requests.py @@ -35,6 +35,7 @@ def __init__(self, mac): self.mac = mac # Local property variables to support StickMessageController + self._drop_at_timeout: bool = False self._send: datetime | None = None self._stick_response: datetime | None = None self._stick_state: bytes | None = None @@ -42,6 +43,16 @@ def __init__(self, mac): self._priority: Priority = Priority.Medium self._finished: bool = False + @property + def drop_at_timeout(self) -> bool: + """Indicates if message should be dropped at first timeout.""" + self._drop_at_timeout + + @drop_at_timeout.setter + def drop_at_timeout(self, state: bool) -> None: + """Configure the action the controller should takes on message timeouts.""" + self._drop_at_timeout = state + @property def finished(self) -> bool: """Indicate if response to request has been received.""" diff --git a/plugwise/stick.py b/plugwise/stick.py index bad0a4015..2d91442ef 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -669,6 +669,7 @@ def _update_loop(self): for mac in self._nodes_not_discovered: _ping_request = NodePingRequest(bytes(mac, UTF8_DECODE)) _ping_request.priority = Priority.Low + _ping_request.drop_at_timeout = True _ping_request.retry_counter = MESSAGE_RETRY - 1 self.msg_controller.send(_ping_request) _discover_counter = 0 @@ -771,12 +772,12 @@ def discover_node(self, mac: str, callback=None, force_discover=False): if not validate_mac(mac) or self._device_nodes.get(mac): return if mac not in self._nodes_not_discovered: - self._nodes_not_discovered[mac] = ( - None, - None, - ) - self._callback_NodeInfo[mac] = callback - _node_request = NodeInfoRequest(bytes(mac, UTF8_DECODE)) + self._nodes_not_discovered.append(mac) + + _node_request = NodeInfoRequest(bytes(mac, UTF8_DECODE)) + if not force_discover: + _node_request.priority = Priority.Low + _node_request.drop_at_timeout = True _node_request.retry_counter = MESSAGE_RETRY - 1 self.msg_controller.send(_node_request) else: From f04249495074261b28187fcb2a20005e939ac399 Mon Sep 17 00:00:00 2001 From: brefra Date: Sat, 8 Jan 2022 00:03:42 +0100 Subject: [PATCH 43/87] Make IGNORE_DUPLICATES const --- plugwise/controller.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index 1339d04be..0b997c682 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -30,6 +30,12 @@ from .parser import PlugwiseParser _LOGGER = logging.getLogger(__name__) +IGNORE_DUPLICATES = ( + "CircleClockSetRequest", + "CircleEnergyLogsRequest", + "CirclePlusScanRequest", + "CirclePlusRealTimeClockSetRequest", +) class MessageRequest(TypedDict): @@ -347,12 +353,7 @@ def _duplicate_request(self, request: PlugwiseRequest) -> bool: """Check if request target towards same node already exists in queue.""" if request.target_mac == "": return False - if request.__class__.__name__ in ( - "CirclePlusScanRequest", - "CircleClockSetRequest", - "CirclePlusRealTimeClockSetRequest", - "CircleEnergyCountersRequest", - ): + if request.__class__.__name__ in IGNORE_DUPLICATES: return False # Check queue for ( From 5b8b8bc221ca834c986b68ce5419cbbcc12c10a6 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 22:01:42 +0100 Subject: [PATCH 44/87] Add typing to _init_ of node classes --- plugwise/nodes/__init__.py | 2 +- plugwise/nodes/circle.py | 2 +- plugwise/nodes/circle_plus.py | 2 +- plugwise/nodes/scan.py | 2 +- plugwise/nodes/sed.py | 2 +- plugwise/nodes/sense.py | 2 +- plugwise/nodes/switch.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index cddcd54f8..1bae6b81a 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -34,7 +34,7 @@ class PlugwiseNode: """Base class for a Plugwise node.""" - def __init__(self, mac, address, message_sender): + def __init__(self, mac: str, address: int, message_sender: callable): mac = mac.upper() if not validate_mac(mac): _LOGGER.warning( diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index e0c299970..551726772 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -47,7 +47,7 @@ class PlugwiseCircle(PlugwiseNode): """provides interface to the Plugwise Circle nodes and base class for Circle+ nodes""" - def __init__(self, mac, address, message_sender): + def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) self._features = ( FEATURE_ENERGY_CONSUMPTION_TODAY["id"], diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index b3fb05040..3de4b3782 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -26,7 +26,7 @@ class PlugwiseCirclePlus(PlugwiseCircle): """provides interface to the Plugwise Circle+ nodes""" - def __init__(self, mac, address, message_sender): + def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) self._plugwise_nodes = {} self._scan_response = {} diff --git a/plugwise/nodes/scan.py b/plugwise/nodes/scan.py index b360e87a8..e6b5f90a4 100644 --- a/plugwise/nodes/scan.py +++ b/plugwise/nodes/scan.py @@ -29,7 +29,7 @@ class PlugwiseScan(NodeSED): """provides interface to the Plugwise Scan nodes""" - def __init__(self, mac, address, message_sender): + def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) self._features = ( FEATURE_MOTION["id"], diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index 1809878e1..30b2961b1 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -40,7 +40,7 @@ class NodeSED(PlugwiseNode): """provides base class for SED based nodes like Scan, Sense & Switch""" - def __init__(self, mac, address, message_sender): + def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) self._sed_requests = {} self.maintenance_interval = SED_MAINTENANCE_INTERVAL diff --git a/plugwise/nodes/sense.py b/plugwise/nodes/sense.py index 760a9f587..e2eef089c 100644 --- a/plugwise/nodes/sense.py +++ b/plugwise/nodes/sense.py @@ -23,7 +23,7 @@ class PlugwiseSense(NodeSED): """provides interface to the Plugwise Sense nodes""" - def __init__(self, mac, address, message_sender): + def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) self._features = ( FEATURE_HUMIDITY["id"], diff --git a/plugwise/nodes/switch.py b/plugwise/nodes/switch.py index fd2ccfa5b..3a2ce9588 100644 --- a/plugwise/nodes/switch.py +++ b/plugwise/nodes/switch.py @@ -13,7 +13,7 @@ class PlugwiseSwitch(NodeSED): """provides interface to the Plugwise Switch nodes""" - def __init__(self, mac, address, message_sender): + def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) self._features = ( FEATURE_PING["id"], From b40b29e5d3976a262cf5ee3a1900f63baf175e54 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 22:06:02 +0100 Subject: [PATCH 45/87] Rename const MESSAGE_SMALL & MESSAGE_LARGE MESSAGE_SMALL => STICK_MESSAGE_SIZE MESSAGE_LARGE => NODE_MESSAGE_SIZE --- plugwise/constants.py | 4 ++-- plugwise/messages/responses.py | 22 +++++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 883232233..04e8b718e 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -41,8 +41,8 @@ # Plugwise message identifiers MESSAGE_FOOTER = b"\x0d\x0a" MESSAGE_HEADER = b"\x05\x05\x03\x03" -MESSAGE_LARGE = "LARGE" -MESSAGE_SMALL = "SMALL" +NODE_MESSAGE_SIZE = "LARGE" +STICK_MESSAGE_SIZE = "SMALL" # Max timeout in seconds MESSAGE_TIME_OUT = 15 # Stick responds with timeout messages after 10 sec. diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index d9772132a..5007277f4 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -4,7 +4,12 @@ from datetime import datetime, timezone from enum import Enum -from ..constants import MESSAGE_FOOTER, MESSAGE_HEADER, MESSAGE_LARGE, MESSAGE_SMALL +from ..constants import ( + MESSAGE_FOOTER, + MESSAGE_HEADER, + NODE_MESSAGE_SIZE, + STICK_MESSAGE_SIZE, +) from ..exceptions import ( InvalidMessageChecksum, InvalidMessageFooter, @@ -97,9 +102,9 @@ def __init__(self, format_size=None): self.seq_id = None self.msg_id = None self.ack_id = None - if self.format_size == MESSAGE_SMALL: + if self.format_size == STICK_MESSAGE_SIZE: self.len_correction = -12 - elif self.format_size == MESSAGE_LARGE: + elif self.format_size == NODE_MESSAGE_SIZE: self.len_correction = 4 else: self.len_correction = 0 @@ -128,10 +133,13 @@ def deserialize(self, response): self.msg_id = response[4:8] self.seq_id = response[8:12] response = response[12:] - if self.format_size == MESSAGE_SMALL or self.format_size == MESSAGE_LARGE: + if ( + self.format_size == STICK_MESSAGE_SIZE + or self.format_size == NODE_MESSAGE_SIZE + ): self.ack_id = response[:4] response = response[4:] - if self.format_size != MESSAGE_SMALL: + if self.format_size != STICK_MESSAGE_SIZE: self.mac = response[:16] response = response[16:] response = self._parse_params(response) @@ -164,7 +172,7 @@ class StickResponse(PlugwiseResponse): ID = b"0000" def __init__(self): - super().__init__(MESSAGE_SMALL) + super().__init__(STICK_MESSAGE_SIZE) class NodeResponse(PlugwiseResponse): @@ -177,7 +185,7 @@ class NodeResponse(PlugwiseResponse): ID = b"0000" def __init__(self): - super().__init__(MESSAGE_LARGE) + super().__init__(NODE_MESSAGE_SIZE) class CirclePlusQueryResponse(PlugwiseResponse): From 8746d2905f7d254362b5b5bbc06310b67d992852 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 22:08:57 +0100 Subject: [PATCH 46/87] Change NODE_TYPE_* into a NodeType Enum --- plugwise/constants.py | 25 +++++++++++++++---------- plugwise/stick.py | 27 +++++++++------------------ 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 04e8b718e..88e696667 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -66,16 +66,21 @@ # Automatically accept new join requests ACCEPT_JOIN_REQUESTS = False -# Node types -NODE_TYPE_STICK = 0 -NODE_TYPE_CIRCLE_PLUS = 1 # AME_NC -NODE_TYPE_CIRCLE = 2 # AME_NR -NODE_TYPE_SWITCH = 3 # AME_SEDSwitch -NODE_TYPE_SENSE = 5 # AME_SEDSense -NODE_TYPE_SCAN = 6 # AME_SEDScan -NODE_TYPE_CELSIUS_SED = 7 # AME_CelsiusSED -NODE_TYPE_CELSIUS_NR = 8 # AME_CelsiusNR -NODE_TYPE_STEALTH = 9 # AME_STEALTH_ZE + +class NodeType(int, Enum): + """USB Node types""" + + Stick = 0 + CirclePlus = 1 # AME_NC + Circle = 2 # AME_NR + Switch = 3 # AME_SEDSwitch + Sense = 5 # AME_SEDSense + Scan = 6 # AME_SEDScan + CelsiusSED = 7 # AME_CelsiusSED + CelsiusNR = 8 # AME_CelsiusNR + Stealth = 9 # AME_STEALTH_ZE + + # 10 AME_MSPBOOTLOAD # 11 AME_STAR diff --git a/plugwise/stick.py b/plugwise/stick.py index 2d91442ef..1d92cb3cb 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -16,16 +16,9 @@ ACCEPT_JOIN_REQUESTS, MESSAGE_RETRY, MESSAGE_TIME_OUT, - NODE_TYPE_CELSIUS_NR, - NODE_TYPE_CELSIUS_SED, - NODE_TYPE_CIRCLE, - NODE_TYPE_CIRCLE_PLUS, - NODE_TYPE_SCAN, - NODE_TYPE_SENSE, - NODE_TYPE_STEALTH, - NODE_TYPE_SWITCH, UTF8_DECODE, WATCHDOG_DEAMON, + NodeType, ) from .controller import StickMessageController from .exceptions import ( @@ -353,29 +346,27 @@ def _append_node(self, mac, address, node_type): str(node_type), mac, ) - if node_type == NODE_TYPE_CIRCLE_PLUS: + if node_type == NodeType.CirclePlus: self._device_nodes[mac] = PlugwiseCirclePlus( mac, address, self.msg_controller.send ) - elif node_type == NODE_TYPE_CIRCLE: + elif node_type == NodeType.Circle: self._device_nodes[mac] = PlugwiseCircle( mac, address, self.msg_controller.send ) - elif node_type == NODE_TYPE_SWITCH: + elif node_type == NodeType.Switch: self._device_nodes[mac] = None - elif node_type == NODE_TYPE_SENSE: + elif node_type == NodeType.Sense: self._device_nodes[mac] = PlugwiseSense( mac, address, self.msg_controller.send ) - elif node_type == NODE_TYPE_SCAN: + elif node_type == NodeType.Scan: self._device_nodes[mac] = PlugwiseScan( mac, address, self.msg_controller.send ) - elif node_type == NODE_TYPE_CELSIUS_SED: + elif node_type == NodeType.CelsiusSED or NodeType.CelsiusNR: self._device_nodes[mac] = None - elif node_type == NODE_TYPE_CELSIUS_NR: - self._device_nodes[mac] = None - elif node_type == NODE_TYPE_STEALTH: + elif node_type == NodeType.Stealth: self._device_nodes[mac] = PlugwiseStealth( mac, address, self.msg_controller.send ) @@ -486,7 +477,7 @@ def _process_NodeInfoResponse(self, message: NodeInfoResponse): mac, str(message.seq_id), ) - if message.node_type.value == NODE_TYPE_CIRCLE_PLUS: + if message.node_type.value == NodeType.CirclePlus: self._circle_plus_discovered = True self._append_node(mac, 0, message.node_type.value) if mac in self._nodes_not_discovered: From 4b1971f4bcf079ea6da07a3fc7d8d08412410d1b Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 22:33:18 +0100 Subject: [PATCH 47/87] Define USB device property features --- plugwise/constants.py | 17 ++++++++++++++ plugwise/nodes/__init__.py | 45 +++++++++++++------------------------- plugwise/nodes/circle.py | 15 ++++++++----- plugwise/nodes/scan.py | 22 +++++++++---------- plugwise/nodes/sed.py | 10 ++++----- plugwise/nodes/sense.py | 25 ++++++++++----------- plugwise/nodes/switch.py | 14 ++++++------ 7 files changed, 73 insertions(+), 75 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 88e696667..0b0868720 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -122,6 +122,23 @@ class NodeType(int, Enum): "143.1": "Anna", } +# USB Stick device features + + +class USB(str, Enum): + """USB property ID's.""" + + available = "available" + humidity = "humidity" + motion = "motion" + ping = "ping" + relay = "relay" + switch = "switch" + temperature = "temperature" + rssi_in = "RSSI_in" + rssi_out = "RSSI_out" + + # Defaults for SED's (Sleeping End Devices) SED_STAY_ACTIVE = 10 # Time in seconds the SED keep itself awake to receive and respond to other messages SED_SLEEP_FOR = 60 # Time in minutes the SED will sleep diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 1bae6b81a..191c8951e 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -4,14 +4,7 @@ from datetime import datetime import logging -from ..constants import ( - FEATURE_AVAILABLE, - FEATURE_PING, - FEATURE_RELAY, - FEATURE_RSSI_IN, - FEATURE_RSSI_OUT, - UTF8_DECODE, -) +from ..constants import USB, UTF8_DECODE from ..messages.requests import ( NodeFeaturesRequest, NodeInfoRequest, @@ -73,22 +66,14 @@ def available(self) -> bool: @available.setter def available(self, state: bool): """Set current network availability state of plugwise node.""" - if state: - if not self._available: - self._available = True - _LOGGER.debug( - "Mark node %s available", - self.mac, - ) - self.do_callback(FEATURE_AVAILABLE["id"]) - else: - if self._available: - self._available = False - _LOGGER.debug( - "Mark node %s unavailable", - self.mac, - ) - self.do_callback(FEATURE_AVAILABLE["id"]) + if state and not self._available: + self._available = True + _LOGGER.debug("Mark node %s available", self.mac) + self.do_callback(USB.available) + elif not state and self._available: + self._available = False + _LOGGER.debug("Mark node %s unavailable", self.mac) + self.do_callback(USB.available) @property def battery_powered(self) -> bool: @@ -184,7 +169,7 @@ def _request_ping( self, callback: callable | None = None, ignore_sensor=True ) -> None: """Ping node.""" - if ignore_sensor or FEATURE_PING["id"] in self._callbacks: + if ignore_sensor or USB.ping in self._callbacks: self._callback_NodePing = callback self.message_sender(NodePingRequest(self._mac)) @@ -251,13 +236,13 @@ def _process_NodePingResponse(self, message: NodePingResponse) -> None: """Process content of 'NodePingResponse' message.""" if self._rssi_in != message.rssi_in.value: self._rssi_in = message.rssi_in.value - self.do_callback(FEATURE_RSSI_IN["id"]) + self.do_callback(USB.rssi_in) if self._rssi_out != message.rssi_out.value: self._rssi_out = message.rssi_out.value - self.do_callback(FEATURE_RSSI_OUT["id"]) + self.do_callback(USB.rssi_out) if self._ping != message.ping_ms.value: self._ping = message.ping_ms.value - self.do_callback(FEATURE_PING["id"]) + self.do_callback(USB.ping) if self._callback_NodePing is not None: self._callback_NodePing() self._callback_NodePing = None @@ -267,11 +252,11 @@ def _process_NodeInfoResponse(self, message: NodeInfoResponse) -> None: if message.relay_state.serialize() == b"01": if not self._relay_state: self._relay_state = True - self.do_callback(FEATURE_RELAY["id"]) + self.do_callback(USB.relay) else: if self._relay_state: self._relay_state = False - self.do_callback(FEATURE_RELAY["id"]) + self.do_callback(USB.relay) self._hardware_version = message.hw_ver.value.decode(UTF8_DECODE) self._firmware_version = message.fw_ver.value self._node_type = message.node_type.value diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 551726772..da7dcfa71 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -6,7 +6,6 @@ from ..constants import ( FEATURE_ENERGY_CONSUMPTION_TODAY, - FEATURE_PING, FEATURE_POWER_CONSUMPTION_CURRENT_HOUR, FEATURE_POWER_CONSUMPTION_PREVIOUS_HOUR, FEATURE_POWER_CONSUMPTION_TODAY, @@ -14,12 +13,10 @@ FEATURE_POWER_PRODUCTION_CURRENT_HOUR, FEATURE_POWER_USE, FEATURE_POWER_USE_LAST_8_SEC, - FEATURE_RELAY, - FEATURE_RSSI_IN, - FEATURE_RSSI_OUT, MAX_TIME_DRIFT, MESSAGE_TIME_OUT, PULSES_PER_KW_SECOND, + USB, ) from ..messages.requests import ( CircleCalibrationRequest, @@ -41,6 +38,12 @@ ) from ..nodes import PlugwiseNode +_FEATURES = ( + USB.available, + USB.ping, + USB.rssi_in, + USB.rssi_out, +) _LOGGER = logging.getLogger(__name__) @@ -274,7 +277,7 @@ def _process_NodeResponse(self, message: NodeResponse) -> None: self.mac, ) self._relay_state = True - self.do_callback(FEATURE_RELAY["id"]) + self.do_callback(USB.relay) elif message.ack_id == NodeResponseType.RelaySwitchedOff: if self._callback_RelaySwitchedOff is not None: self._callback_RelaySwitchedOff() @@ -287,7 +290,7 @@ def _process_NodeResponse(self, message: NodeResponse) -> None: self.mac, ) self._relay_state = False - self.do_callback(FEATURE_RELAY["id"]) + self.do_callback(USB.relay) elif message.ack_id == NodeResponseType.RelaySwitchFailed: if self._callback_RelaySwitchFailed is not None: self._callback_RelaySwitchFailed() diff --git a/plugwise/nodes/scan.py b/plugwise/nodes/scan.py index e6b5f90a4..964d97517 100644 --- a/plugwise/nodes/scan.py +++ b/plugwise/nodes/scan.py @@ -4,15 +4,12 @@ import logging from ..constants import ( - FEATURE_MOTION, - FEATURE_PING, - FEATURE_RSSI_IN, - FEATURE_RSSI_OUT, SCAN_DAYLIGHT_MODE, SCAN_MOTION_RESET_TIMER, SCAN_SENSITIVITY_HIGH, SCAN_SENSITIVITY_MEDIUM, SCAN_SENSITIVITY_OFF, + USB, ) from ..messages.requests import ScanConfigureRequest, ScanLightCalibrateRequest from ..messages.responses import ( @@ -23,6 +20,12 @@ ) from ..nodes.sed import NodeSED +_FEATURES = ( + USB.motion, + USB.ping, + USB.rssi_in, + USB.rssi_out, +) _LOGGER = logging.getLogger(__name__) @@ -31,12 +34,7 @@ class PlugwiseScan(NodeSED): def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) - self._features = ( - FEATURE_MOTION["id"], - FEATURE_PING["id"], - FEATURE_RSSI_IN["id"], - FEATURE_RSSI_OUT["id"], - ) + self._features = _FEATURES self._motion_state = False self._motion_reset_timer = None self._daylight_mode = None @@ -118,12 +116,12 @@ def _process_NodeSwitchGroupResponse( # turn off => clear motion if self._motion_state: self._motion_state = False - self.do_callback(FEATURE_MOTION["id"]) + self.do_callback(USB.motion) elif message.power_state.value == 1: # turn on => motion if not self._motion_state: self._motion_state = True - self.do_callback(FEATURE_MOTION["id"]) + self.do_callback(USB.motion) else: _LOGGER.warning( "Unknown power_state (%s) received from %s", diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index 30b2961b1..550b4b3fe 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -8,14 +8,12 @@ import logging from ..constants import ( - FEATURE_PING, - FEATURE_RSSI_IN, - FEATURE_RSSI_OUT, SED_CLOCK_INTERVAL, SED_CLOCK_SYNC, SED_MAINTENANCE_INTERVAL, SED_SLEEP_FOR, SED_STAY_ACTIVE, + USB, ) from ..messages.requests import ( NodeInfoRequest, @@ -149,9 +147,9 @@ def _request_ping(self, callback=None, ignore_sensor=False): """Ping node.""" if ( ignore_sensor - or self._callbacks.get(FEATURE_PING["id"]) - or self._callbacks.get(FEATURE_RSSI_IN["id"]) - or self._callbacks.get(FEATURE_RSSI_OUT["id"]) + or self._callbacks.get(USB.ping) + or self._callbacks.get(USB.rssi_in) + or self._callbacks.get(USB.rssi_out) or callback is not None ): self._callback_NodePing = callback diff --git a/plugwise/nodes/sense.py b/plugwise/nodes/sense.py index e2eef089c..931332478 100644 --- a/plugwise/nodes/sense.py +++ b/plugwise/nodes/sense.py @@ -4,20 +4,23 @@ import logging from ..constants import ( - FEATURE_HUMIDITY, - FEATURE_PING, - FEATURE_RSSI_IN, - FEATURE_RSSI_OUT, - FEATURE_TEMPERATURE, SENSE_HUMIDITY_MULTIPLIER, SENSE_HUMIDITY_OFFSET, SENSE_TEMPERATURE_MULTIPLIER, SENSE_TEMPERATURE_OFFSET, + USB, ) from ..messages.responses import PlugwiseResponse, SenseReportResponse from ..nodes.sed import NodeSED _LOGGER = logging.getLogger(__name__) +_FEATURES = ( + USB.humidity, + USB.ping, + USB.rssi_in, + USB.rssi_out, + USB.temperature, +) class PlugwiseSense(NodeSED): @@ -25,13 +28,7 @@ class PlugwiseSense(NodeSED): def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) - self._features = ( - FEATURE_HUMIDITY["id"], - FEATURE_PING["id"], - FEATURE_RSSI_IN["id"], - FEATURE_RSSI_OUT["id"], - FEATURE_TEMPERATURE["id"], - ) + self._features = _FEATURES self._temperature = None self._humidity = None @@ -67,7 +64,7 @@ def _process_SenseReportResponse(self, message: SenseReportResponse) -> None: self.mac, str(self._temperature), ) - self.do_callback(FEATURE_TEMPERATURE["id"]) + self.do_callback(USB.temperature) if message.humidity.value != 65535: new_humidity = int( SENSE_HUMIDITY_MULTIPLIER * (message.humidity.value / 65536) @@ -80,4 +77,4 @@ def _process_SenseReportResponse(self, message: SenseReportResponse) -> None: self.mac, str(self._humidity), ) - self.do_callback(FEATURE_HUMIDITY["id"]) + self.do_callback(USB.humidity) diff --git a/plugwise/nodes/switch.py b/plugwise/nodes/switch.py index 3a2ce9588..c390b472b 100644 --- a/plugwise/nodes/switch.py +++ b/plugwise/nodes/switch.py @@ -3,7 +3,7 @@ import logging -from ..constants import FEATURE_PING, FEATURE_RSSI_IN, FEATURE_RSSI_OUT, FEATURE_SWITCH +from ..constants import USB from ..messages.responses import NodeSwitchGroupResponse, PlugwiseResponse from ..nodes.sed import NodeSED @@ -16,10 +16,10 @@ class PlugwiseSwitch(NodeSED): def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) self._features = ( - FEATURE_PING["id"], - FEATURE_RSSI_IN["id"], - FEATURE_RSSI_OUT["id"], - FEATURE_SWITCH["id"], + USB.ping, + USB.rssi_in, + USB.rssi_out, + USB.switch, ) self._switch_state = False @@ -44,12 +44,12 @@ def _process_NodeSwitchGroupResponse( # turn off => clear motion if self._switch_state: self._switch_state = False - self.do_callback(FEATURE_SWITCH["id"]) + self.do_callback(USB.switch) elif message.power_state == 1: # turn on => motion if not self._switch_state: self._switch_state = True - self.do_callback(FEATURE_SWITCH["id"]) + self.do_callback(USB.switch) else: _LOGGER.debug( "Unknown power_state (%s) received from %s", From ada27157ee74ebe486240db5f1db5259298a555f Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 22:39:25 +0100 Subject: [PATCH 48/87] Add DAY_IN_SECONDS constant --- plugwise/constants.py | 1 + plugwise/nodes/circle_plus.py | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 0b0868720..5377ed52c 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -30,6 +30,7 @@ ### Stick constants ### +DAY_IN_SECONDS = 86400 UTF8_DECODE = "utf-8" # Serial connection settings for plugwise USB stick diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index 3de4b3782..1277f7173 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone import logging -from ..constants import MAX_TIME_DRIFT, UTF8_DECODE +from ..constants import DAY_IN_SECONDS, MAX_TIME_DRIFT, UTF8_DECODE from ..messages.requests import ( CirclePlusRealTimeClockGetRequest, CirclePlusRealTimeClockSetRequest, @@ -126,19 +126,18 @@ def _process_CirclePlusRealTimeClockResponse( self, message: CirclePlusRealTimeClockResponse ) -> None: """Process content of 'CirclePlusRealTimeClockResponse' message.""" - realtime_clock_dt = datetime( - datetime.utcnow().year, - datetime.utcnow().month, - datetime.utcnow().day, - message.time.value.hour, - message.time.value.minute, - message.time.value.second, - ).replace(tzinfo=timezone.utc) - realtime_clock_offset = message.timestamp.replace(microsecond=0) - ( - realtime_clock_dt + self.timezone_delta + _dt_of_circle_plus = datetime.utcnow().replace( + hour=message.time.value.hour, + minute=message.time.value.minute, + second=message.time.value.second, + microsecond=0, + tzinfo=timezone.utc, + ) + realtime_clock_offset = ( + message.timestamp.replace(microsecond=0) - _dt_of_circle_plus ) if realtime_clock_offset.days == -1: - self._realtime_clock_offset = realtime_clock_offset.seconds - 86400 + self._realtime_clock_offset = realtime_clock_offset.seconds - DAY_IN_SECONDS else: self._realtime_clock_offset = realtime_clock_offset.seconds _LOGGER.debug( From e5661f0e8f410a7b4176bb95c2b4fae331593757 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 22:44:35 +0100 Subject: [PATCH 49/87] Reorder NodePintResponse --- plugwise/messages/responses.py | 50 +++++++++++++++++----------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index 5007277f4..d7048a3a0 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -277,6 +277,31 @@ class NodeJoinAvailableResponse(PlugwiseResponse): ID = b"0006" +class NodePingResponse(PlugwiseResponse): + """ + Ping response from node + + - incomingLastHopRssiTarget (received signal strength indicator) + - lastHopRssiSource + - timediffInMs + + Response to : NodePingRequest + """ + + ID = b"000E" + + def __init__(self): + super().__init__() + self.rssi_in = Int(0, length=2) + self.rssi_out = Int(0, length=2) + self.ping_ms = Int(0, 4, False) + self.params += [ + self.rssi_in, + self.rssi_out, + self.ping_ms, + ] + + class StickInitResponse(PlugwiseResponse): """ Returns the configuration and status of the USB-Stick @@ -310,31 +335,6 @@ def __init__(self): ] -class NodePingResponse(PlugwiseResponse): - """ - Ping response from node - - - incomingLastHopRssiTarget (received signal strength indicator) - - lastHopRssiSource - - timediffInMs - - Response to : NodePingRequest - """ - - ID = b"000E" - - def __init__(self): - super().__init__() - self.rssi_in = Int(0, length=2) - self.rssi_out = Int(0, length=2) - self.ping_ms = Int(0, 4, False) - self.params += [ - self.rssi_in, - self.rssi_out, - self.ping_ms, - ] - - class CirclePowerUsageResponse(PlugwiseResponse): """ Returns power usage as impulse counters for several different timeframes From b9010257774e1eb44047339dce7505cdb442fe27 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 22:49:14 +0100 Subject: [PATCH 50/87] Use DAY_IN_MINUTES & HOUR_IN_MINUTES constants --- plugwise/constants.py | 3 +++ plugwise/messages/requests.py | 12 ++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 5377ed52c..4dd431d0b 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -30,6 +30,9 @@ ### Stick constants ### +DAY_IN_MINUTES = 1440 +HOUR_IN_MINUTES = 60 + DAY_IN_SECONDS = 86400 UTF8_DECODE = "utf-8" diff --git a/plugwise/messages/requests.py b/plugwise/messages/requests.py index 47adcf3e4..78722ea77 100644 --- a/plugwise/messages/requests.py +++ b/plugwise/messages/requests.py @@ -4,7 +4,13 @@ from datetime import datetime from enum import Enum -from ..constants import MESSAGE_FOOTER, MESSAGE_HEADER, UTF8_DECODE +from ..constants import ( + DAY_IN_MINUTES, + HOUR_IN_MINUTES, + MESSAGE_FOOTER, + MESSAGE_HEADER, + UTF8_DECODE, +) from ..messages import PlugwiseMessage from ..util import ( DateTime, @@ -253,7 +259,9 @@ class CircleClockSetRequest(PlugwiseRequest): def __init__(self, mac, dt): super().__init__(mac) passed_days = dt.day - 1 - month_minutes = (passed_days * 24 * 60) + (dt.hour * 60) + dt.minute + month_minutes = ( + (passed_days * DAY_IN_MINUTES) + (dt.hour * HOUR_IN_MINUTES) + dt.minute + ) this_date = DateTime(dt.year, dt.month, month_minutes) this_time = Time(dt.hour, dt.minute, dt.second) day_of_week = Int(dt.weekday(), 2) From 8d11924054df30abc0562fa03e6cc0131f2acfb6 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 22:50:57 +0100 Subject: [PATCH 51/87] Reorder properties PlugwiseNode class --- plugwise/nodes/__init__.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 191c8951e..55f0a5ba4 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -80,6 +80,18 @@ def battery_powered(self) -> bool: """Return True if node is a SED (battery powered) device.""" return self._battery_powered + @property + def features(self) -> tuple: + """Return the abstracted features supported by this plugwise device.""" + return self._features + + @property + def firmware_version(self) -> str: + """Return firmware version.""" + if self._firmware_version is not None: + return str(self._firmware_version) + return "Unknown" + @property def hardware_model(self) -> str: """Return hardware model.""" @@ -94,18 +106,6 @@ def hardware_version(self) -> str: return self._hardware_version return "Unknown" - @property - def features(self) -> tuple: - """Return the abstracted features supported by this plugwise device.""" - return self._features - - @property - def firmware_version(self) -> str: - """Return firmware version.""" - if self._firmware_version is not None: - return str(self._firmware_version) - return "Unknown" - @property def last_update(self) -> datetime: """Return datetime of last received update.""" From 6fb35498dc8e648c7ac06beb0d5fea9c00fc97c6 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 22:54:16 +0100 Subject: [PATCH 52/87] Add more typing to PlugwiseNode class --- plugwise/nodes/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 55f0a5ba4..731fa5150 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -39,10 +39,10 @@ def __init__(self, mac: str, address: int, message_sender: callable): self._features = () self._address = address self._callbacks = {} - self._last_update = None - self._available = False - self._battery_powered = False - self._measures_power = False + self._last_update: datetime | None = None + self._available: bool = False + self._battery_powered: bool = False + self._measures_power: bool = False self._rssi_in = None self._rssi_out = None self._ping = None @@ -195,7 +195,7 @@ def message_for_node(self, message: PlugwiseResponse) -> None: elif isinstance(message, NodeInfoResponse): self._process_NodeInfoResponse(message) elif isinstance(message, NodeFeaturesResponse): - self._process_features_response(message) + self._process_NodeFeaturesResponse(message) elif isinstance(message, NodeAckResponse): self._process_NodeAckResponse(message) else: @@ -205,7 +205,7 @@ def message_for_node(self, message: PlugwiseResponse) -> None: self.mac, ) - def subscribe_callback(self, callback, sensor) -> bool: + def subscribe_callback(self, callback: callable, sensor: str) -> bool: """Subscribe callback to execute when state change happens.""" if sensor in self._features: if sensor not in self._callbacks: @@ -214,7 +214,7 @@ def subscribe_callback(self, callback, sensor) -> bool: return True return False - def unsubscribe_callback(self, callback, sensor): + def unsubscribe_callback(self, callback: callable, sensor: str): """Unsubscribe callback to execute when state change happens.""" if sensor in self._callbacks: self._callbacks[sensor].remove(callback) @@ -272,7 +272,7 @@ def _process_NodeInfoResponse(self, message: NodeInfoResponse) -> None: self._callback_NodeInfo() self._callback_NodeInfo = None - def _process_features_response(self, message): + def _process_NodeFeaturesResponse(self, message: NodeFeaturesResponse): """Process features message.""" _LOGGER.warning( "Node %s supports features %s", self.mac, str(message.features.value) From ee2217a3b02dfba1e930d792f3fd43f4adb802bc Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 23:01:14 +0100 Subject: [PATCH 53/87] Add typing to Stick class --- plugwise/stick.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugwise/stick.py b/plugwise/stick.py index 1d92cb3cb..ced9a9dfc 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -46,6 +46,7 @@ PlugwiseResponse, StickInitResponse, ) +from .nodes import PlugwiseNode from .nodes.circle import PlugwiseCircle from .nodes.circle_plus import PlugwiseCirclePlus from .nodes.scan import PlugwiseScan @@ -77,7 +78,7 @@ def __init__(self, port, callback=None): self._auto_update_timer = 0 self._circle_plus_discovered = False self._circle_plus_retries = 0 - self._device_nodes = {} + self._device_nodes: dict[str, PlugwiseNode] = {} self._joined_nodes = 0 self._mac_stick = None self._messages_for_undiscovered_nodes = [] @@ -339,7 +340,7 @@ def scan_timeout_expired(self): if self.scan_callback: self.scan_callback() - def _append_node(self, mac, address, node_type): + def _append_node(self, mac: str, address: int, node_type): """Add node to list of controllable nodes""" _LOGGER.debug( "Add new node type (%s) with mac %s", From 35efa774c30871a6a74da822f6c31317e63d850b Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 4 Jan 2022 23:04:22 +0100 Subject: [PATCH 54/87] Make Plugwise DateTime class timezone aware --- plugwise/util.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/plugwise/util.py b/plugwise/util.py index 95aad0c8c..35e546fe9 100644 --- a/plugwise/util.py +++ b/plugwise/util.py @@ -4,7 +4,7 @@ Plugwise protocol helpers """ import binascii -import datetime +from datetime import date, datetime, time, timedelta, timezone import re import struct @@ -195,7 +195,7 @@ def __init__(self, value, length=8): def deserialize(self, val): Int.deserialize(self, val) - self.value = datetime.datetime.fromtimestamp(self.value) + self.value = datetime.fromtimestamp(self.value) class Year2k(Int): @@ -207,8 +207,7 @@ def deserialize(self, val): class DateTime(CompositeType): - """datetime value as used in the general info response - format is: YYMMmmmm + """Plugwise datetime value in the general info response format of: YYMMmmmm where year is offset value from the epoch which is Y2K and last four bytes are offset from the beginning of the month in minutes """ @@ -224,10 +223,12 @@ def deserialize(self, val): CompositeType.deserialize(self, val) if self.minutes.value == 65535: self.value = None + elif self.month.value == 0: + self.value = None else: - self.value = datetime.datetime( + self.value = datetime( year=self.year.value, month=self.month.value, day=1 - ) + datetime.timedelta(minutes=self.minutes.value) + ).replace(tzinfo=timezone.utc) + timedelta(minutes=self.minutes.value) class Time(CompositeType): @@ -242,8 +243,8 @@ def __init__(self, hour=0, minute=0, second=0): def deserialize(self, val): CompositeType.deserialize(self, val) - self.value = datetime.time( - self.hour.value, self.minute.value, self.second.value + self.value = time( + hour=self.hour.value, minute=self.minute.value, second=self.second.value ) @@ -271,10 +272,10 @@ def __init__(self, hour=0, minute=0, second=0): def deserialize(self, val): CompositeType.deserialize(self, val) - self.value = datetime.time( - int(self.hour.value), - int(self.minute.value), - int(self.second.value), + self.value = time( + hour=int(self.hour.value), + minute=int(self.minute.value), + second=int(self.second.value), ) @@ -290,10 +291,10 @@ def __init__(self, day=0, month=0, year=0): def deserialize(self, val): CompositeType.deserialize(self, val) - self.value = datetime.date( - int(self.year.value) + PLUGWISE_EPOCH, - int(self.month.value), - int(self.day.value), + self.value = date( + year=int(self.year.value) + PLUGWISE_EPOCH, + month=int(self.month.value), + day=int(self.day.value), ) From ab00cba28c57e54f9c8ffcec966a24e05cc06110 Mon Sep 17 00:00:00 2001 From: brefra Date: Wed, 5 Jan 2022 16:01:41 +0100 Subject: [PATCH 55/87] Standardize callback scan_finished --- plugwise/stick.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/plugwise/stick.py b/plugwise/stick.py index ced9a9dfc..f3e418d8c 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -71,7 +71,6 @@ def __init__(self, port, callback=None): self.circle_plus_mac = None self.init_callback = None self.msg_controller = None - self.scan_callback = None self._accept_join_requests = ACCEPT_JOIN_REQUESTS self._auto_update_manually = False @@ -97,6 +96,7 @@ def __init__(self, port, callback=None): self._watchdog_thread = None # Local callback variables + self._callback_scan_finished: callable | None = None self._callback_StickInit: callable | None = None self._callback_NodeInfo: dict(str, callable) = {} self._callback_NodeJoinAvailableResponse: dict(int, callable) = {} @@ -254,9 +254,9 @@ def allow_join_requests(self, enable: bool, accept: bool): else: self._accept_join_requests = False - def scan(self, callback=None): + def scan(self, callback: callable | None = None) -> None: """Scan and try to detect all registered nodes.""" - self.scan_callback = callback + self._callback_scan_finished = callback self.scan_for_registered_nodes() def scan_circle_plus(self): @@ -322,8 +322,9 @@ def node_discovered_by_scan(self, nodes_off_line=False): if mac in self._nodes_not_discovered: del self._nodes_not_discovered[mac] self.msg_controller.discovery_finished = True - if self.scan_callback: - self.scan_callback() + if self._callback_scan_finished: + self._callback_scan_finished() + self._callback_scan_finished = None def scan_timeout_expired(self): """Timeout for initial scan.""" @@ -337,8 +338,9 @@ def scan_timeout_expired(self): else: if mac in self._nodes_not_discovered: del self._nodes_not_discovered[mac] - if self.scan_callback: - self.scan_callback() + if self._callback_scan_finished: + self._callback_scan_finished() + self._callback_scan_finished = None def _append_node(self, mac: str, address: int, node_type): """Add node to list of controllable nodes""" From 745e32635400e0451f44f6dc90a590b81b9011f0 Mon Sep 17 00:00:00 2001 From: brefra Date: Wed, 5 Jan 2022 16:10:38 +0100 Subject: [PATCH 56/87] Make node available when message is received --- plugwise/nodes/__init__.py | 47 ++++++++++++++--------------------- plugwise/nodes/circle.py | 3 +++ plugwise/nodes/circle_plus.py | 1 + plugwise/nodes/scan.py | 1 + plugwise/nodes/sed.py | 1 + plugwise/nodes/sense.py | 1 + plugwise/nodes/switch.py | 1 + 7 files changed, 26 insertions(+), 29 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 731fa5150..7e6ec9001 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -175,35 +175,24 @@ def _request_ping( def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for base PlugwiseNode class.""" - if message.mac == self._mac: - if message.timestamp is not None: - _LOGGER.debug( - "Previous update %s of node %s, last message %s", - str(self._last_update), - self.mac, - str(message.timestamp), - ) - self._last_update = message.timestamp - if not self._available: - self.available = True - self._request_info() - self._last_update = message.timestamp - if isinstance(message, NodePingResponse): - self._process_NodePingResponse(message) - elif isinstance(message, NodeResponse): - self._process_NodeResponse(message) - elif isinstance(message, NodeInfoResponse): - self._process_NodeInfoResponse(message) - elif isinstance(message, NodeFeaturesResponse): - self._process_NodeFeaturesResponse(message) - elif isinstance(message, NodeAckResponse): - self._process_NodeAckResponse(message) - else: - _LOGGER.warning( - "Unmanaged %s received for %s", - message.__class__.__name__, - self.mac, - ) + self._last_update = message.timestamp + self.available = True + if isinstance(message, NodePingResponse): + self._process_NodePingResponse(message) + elif isinstance(message, NodeResponse): + self._process_NodeResponse(message) + elif isinstance(message, NodeInfoResponse): + self._process_NodeInfoResponse(message) + elif isinstance(message, NodeFeaturesResponse): + self._process_NodeFeaturesResponse(message) + elif isinstance(message, NodeAckResponse): + self._process_NodeAckResponse(message) + else: + _LOGGER.warning( + "Unmanaged %s received for %s", + message.__class__.__name__, + self.mac, + ) def subscribe_callback(self, callback: callable, sensor: str) -> bool: """Subscribe callback to execute when state change happens.""" diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index da7dcfa71..71cb355be 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -249,6 +249,9 @@ def request_power_update(self, callback: callable | None = None) -> None: def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for PlugwiseCircle class.""" + if not self.available: + self.available = True + self._request_info() self._last_update = message.timestamp if isinstance(message, CirclePowerUsageResponse): self._process_CirclePowerUsageResponse(message) diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index 1277f7173..89ca74b31 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -42,6 +42,7 @@ def __init__(self, mac: str, address: int, message_sender: callable): def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for PlugwiseCirclePlus class.""" + self.available = True self._last_update = message.timestamp if isinstance(message, CirclePlusRealTimeClockResponse): self._process_CirclePlusRealTimeClockResponse(message) diff --git a/plugwise/nodes/scan.py b/plugwise/nodes/scan.py index 964d97517..27072d5d0 100644 --- a/plugwise/nodes/scan.py +++ b/plugwise/nodes/scan.py @@ -55,6 +55,7 @@ def motion(self) -> bool: def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for PlugwiseScan class.""" + self.available = True self._last_update = message.timestamp if isinstance(message, NodeSwitchGroupResponse): self._process_NodeSwitchGroupResponse(message) diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index 550b4b3fe..552013cdb 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -52,6 +52,7 @@ def __init__(self, mac: str, address: int, message_sender: callable): def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for NodeSED class.""" + self.available = True self._last_update = message.timestamp if isinstance(message, NodeAwakeResponse): self._process_NodeAwakeResponse(message) diff --git a/plugwise/nodes/sense.py b/plugwise/nodes/sense.py index 931332478..cb653c188 100644 --- a/plugwise/nodes/sense.py +++ b/plugwise/nodes/sense.py @@ -44,6 +44,7 @@ def temperature(self) -> int: def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for PlugwiseSense class.""" + self.available = True self._last_update = message.timestamp if isinstance(message, SenseReportResponse): self._process_SenseReportResponse(message) diff --git a/plugwise/nodes/switch.py b/plugwise/nodes/switch.py index c390b472b..d1c785ebd 100644 --- a/plugwise/nodes/switch.py +++ b/plugwise/nodes/switch.py @@ -30,6 +30,7 @@ def switch(self) -> bool: def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for PlugwiseSense class.""" + self.available = True self._last_update = message.timestamp if isinstance(message, NodeSwitchGroupResponse): self._process_NodeSwitchGroupResponse(message) From 60925b981575acb0e6cc4343a4526fde4a705cc4 Mon Sep 17 00:00:00 2001 From: brefra Date: Wed, 5 Jan 2022 16:15:23 +0100 Subject: [PATCH 57/87] Don't log warning for timeout to ping requests --- plugwise/controller.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index 0b997c682..78f6c7865 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -20,7 +20,7 @@ from .connections.serial import PlugwiseUSBConnection from .connections.socket import SocketConnection from .constants import MESSAGE_RETRY, MESSAGE_TIME_OUT, SLEEP_TIME, UTF8_DECODE -from .messages.requests import PlugwiseRequest, Priority +from .messages.requests import NodePingRequest, PlugwiseRequest, Priority from .messages.responses import ( SPECIAL_SEQ_IDS, PlugwiseResponse, @@ -274,6 +274,13 @@ def _log_status_of_request(self, seq_id: bytes) -> None: str(seq_id), ) elif self._pending_request[seq_id].stick_state == StickResponseType.timeout: + if not isinstance(self._pending_request, NodePingRequest): + _LOGGER.warning( + "Stick 'time out' received for %s%s with seq_id=%s, retry request", + self._pending_request[seq_id].__class__.__name__, + _target, + str(seq_id), + ) _request = self._pending_request[seq_id] _request.stick_state = None self._pending_request[seq_id].finished = True From ecfad4e2d51d763ecd05d000b9fb92a070dcfc32 Mon Sep 17 00:00:00 2001 From: brefra Date: Wed, 5 Jan 2022 16:22:30 +0100 Subject: [PATCH 58/87] Simplify logic for _nodes_not_discovered --- plugwise/stick.py | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/plugwise/stick.py b/plugwise/stick.py index f3e418d8c..264682424 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -84,7 +84,7 @@ def __init__(self, port, callback=None): self._network_id = None self._network_online = False self._nodes_discovered = None - self._nodes_not_discovered = {} + self._nodes_not_discovered: list(str) = [] self._nodes_off_line = 0 self._nodes_to_discover = {} self._port = port @@ -310,7 +310,7 @@ def node_discovered_by_scan(self, nodes_off_line=False): ): if self._nodes_off_line == 0: self._nodes_to_discover = {} - self._nodes_not_discovered = {} + self._nodes_not_discovered = [] else: for mac in self._nodes_to_discover: if not self._device_nodes.get(mac): @@ -320,7 +320,7 @@ def node_discovered_by_scan(self, nodes_off_line=False): ) else: if mac in self._nodes_not_discovered: - del self._nodes_not_discovered[mac] + self._nodes_not_discovered.remove(mac) self.msg_controller.discovery_finished = True if self._callback_scan_finished: self._callback_scan_finished() @@ -330,14 +330,16 @@ def scan_timeout_expired(self): """Timeout for initial scan.""" if not self.msg_controller.discovery_finished: for mac in self._nodes_to_discover: - if mac not in self._device_nodes.keys(): + if ( + mac in self._device_nodes.keys() + and mac in self._nodes_not_discovered + ): + self._nodes_not_discovered.remove(mac) + else: _LOGGER.info( "Failed to discover node type for registered MAC '%s'. This is expected for battery powered nodes, they will be discovered at their first awake", str(mac), ) - else: - if mac in self._nodes_not_discovered: - del self._nodes_not_discovered[mac] if self._callback_scan_finished: self._callback_scan_finished() self._callback_scan_finished = None @@ -484,7 +486,7 @@ def _process_NodeInfoResponse(self, message: NodeInfoResponse): self._circle_plus_discovered = True self._append_node(mac, 0, message.node_type.value) if mac in self._nodes_not_discovered: - del self._nodes_not_discovered[mac] + self._nodes_not_discovered.remove(mac) else: if mac in self._nodes_to_discover: _LOGGER.info( @@ -519,7 +521,7 @@ def _process_NodeJoinAvailableResponse(self, message: NodeJoinAvailableResponse) mac, ) self.msg_controller.send(NodeAddRequest(message.mac, True)) - self._nodes_not_discovered[mac] = (None, None) + self._nodes_not_discovered.append(mac) else: _LOGGER.debug( "New node with mac %s requesting to join Plugwise network, do callback", @@ -757,7 +759,7 @@ def _discover_after_scan(self): node_discovered = mac break if node_discovered: - del self._nodes_not_discovered[node_discovered] + self._nodes_not_discovered.remove(node_discovered) self.do_callback(StickCallback.NodeDiscovered, node_discovered) self.auto_update() @@ -773,17 +775,4 @@ def discover_node(self, mac: str, callback=None, force_discover=False): _node_request.priority = Priority.Low _node_request.drop_at_timeout = True _node_request.retry_counter = MESSAGE_RETRY - 1 - self.msg_controller.send(_node_request) - else: - (firstrequest, lastrequest) = self._nodes_not_discovered[mac] - if not (firstrequest and lastrequest): - self._callback_NodeInfo[mac] = callback - _node_request = NodeInfoRequest(bytes(mac, UTF8_DECODE)) - _node_request.priority = Priority.Low - _node_request.retry_counter = MESSAGE_RETRY - 1 - self.msg_controller.send(_node_request) - elif force_discover: - self._callback_NodeInfo[mac] = callback - self.msg_controller.send( - NodeInfoRequest(bytes(mac, UTF8_DECODE)), - ) + self.msg_controller.send(_node_request) From c9d8e0cbcb5d565f04ad0f599be35d9d5653ba29 Mon Sep 17 00:00:00 2001 From: brefra Date: Wed, 5 Jan 2022 16:24:00 +0100 Subject: [PATCH 59/87] Correct _callback_NodeInfo --- plugwise/stick.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugwise/stick.py b/plugwise/stick.py index 264682424..bc07b53b0 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -500,10 +500,10 @@ def _process_NodeInfoResponse(self, message: NodeInfoResponse): ) self._pass_message_to_node(message) - if self._callback_NodeInfo.get(mac): - if self._callback_NodeInfo[mac] is not None: + if mac in self._callback_NodeInfo.keys(): + if self._callback_NodeInfo[mac]: self._callback_NodeInfo[mac]() - self._callback_NodeInfo[mac] = None + del self._callback_NodeInfo[mac] def _process_NodeJoinAvailableResponse(self, message: NodeJoinAvailableResponse): """Process content of 'NodeJoinAvailableResponse' message.""" @@ -767,6 +767,7 @@ def discover_node(self, mac: str, callback=None, force_discover=False): """Helper to try to discovery the node (type) based on mac.""" if not validate_mac(mac) or self._device_nodes.get(mac): return + self._callback_NodeInfo[mac] = callback if mac not in self._nodes_not_discovered: self._nodes_not_discovered.append(mac) From bff7e32f5ea6e8389ee101525da67275dd672ff9 Mon Sep 17 00:00:00 2001 From: brefra Date: Wed, 5 Jan 2022 16:26:44 +0100 Subject: [PATCH 60/87] Only discover unknown nodes after JoinAccept --- plugwise/stick.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/plugwise/stick.py b/plugwise/stick.py index bc07b53b0..0b2d661bb 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -440,9 +440,10 @@ def _process_NodeResponse(self, message: NodeResponse) -> None: if message.ack_id == NodeResponseType.JoinAccepted: # Discovery newly accepted node - self.discover_node( - message.mac.decode(UTF8_DECODE), self._discover_after_scan - ) + if not self._device_nodes.get(message.mac.decode(UTF8_DECODE)): + self.discover_node( + message.mac.decode(UTF8_DECODE), self._discover_after_scan + ) else: self._pass_message_to_node(message) @@ -488,17 +489,16 @@ def _process_NodeInfoResponse(self, message: NodeInfoResponse): if mac in self._nodes_not_discovered: self._nodes_not_discovered.remove(mac) else: - if mac in self._nodes_to_discover: - _LOGGER.info( - "Node with mac %s discovered", - mac, - ) - self._append_node( - mac, - self._nodes_to_discover[mac], - message.node_type.value, - ) - self._pass_message_to_node(message) + _LOGGER.info( + "Node with mac %s discovered", + mac, + ) + self._append_node( + mac, + self._nodes_to_discover[mac], + message.node_type.value, + ) + self._pass_message_to_node(message) if mac in self._callback_NodeInfo.keys(): if self._callback_NodeInfo[mac]: From 12fa81fe9185fc8ddb6eee0e282b8bd24d4b8648 Mon Sep 17 00:00:00 2001 From: brefra Date: Wed, 5 Jan 2022 16:35:14 +0100 Subject: [PATCH 61/87] Mark power nodes unavailable after 5 minutes and do a regular Ping to detect when node is back on-line --- plugwise/nodes/__init__.py | 18 ++++++++++-------- plugwise/stick.py | 27 +++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 7e6ec9001..e3b5a2c5c 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -147,9 +147,10 @@ def rssi_out(self) -> int: return self._rssi_out return 0 - def do_ping(self, callback: callable | None = None) -> None: + def do_ping(self, forced=False, callback: callable | None = None) -> None: """Send network ping message to node.""" - self._request_ping(callback, True) + if forced or USB.ping in self._callbacks: + self._request_ping(callback) def _request_info(self, callback: callable | None = None) -> None: """Request info from node.""" @@ -165,13 +166,14 @@ def _request_features(self, callback: callable | None = None) -> None: NodeFeaturesRequest(self._mac), ) - def _request_ping( - self, callback: callable | None = None, ignore_sensor=True - ) -> None: + def _request_ping(self, callback: callable | None = None) -> None: """Ping node.""" - if ignore_sensor or USB.ping in self._callbacks: - self._callback_NodePing = callback - self.message_sender(NodePingRequest(self._mac)) + self._callback_NodePing = callback + _request = NodePingRequest(self._mac) + if self.available: + _request.priority = Priority.Low + _request.retry_counter = MESSAGE_RETRY - 1 + self.message_sender(_request) def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for base PlugwiseNode class.""" diff --git a/plugwise/stick.py b/plugwise/stick.py index 0b2d661bb..484071828 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -640,6 +640,9 @@ def _update_loop(self): day_of_month = datetime.now().day try: while self._run_update_thread: + _available_ts = datetime.utcnow().replace( + tzinfo=timezone.utc + ) - timedelta(minutes=5) if datetime.now().day != day_of_month: day_of_month = datetime.now().day _sync_clock = True @@ -649,10 +652,26 @@ def _update_loop(self): # Check availability state of SED's self._check_availability_of_seds(mac) else: - # Do ping request for all non SED's - self._device_nodes[mac].do_ping() - - if self._device_nodes[mac].measures_power: + # Mark devices unavailable + if ( + self._device_nodes[mac].available + and self._device_nodes[mac].last_update < _available_ts + ): + _LOGGER.warning( + "Set %s to unavailable, last update: %s", + mac, + str(self._device_nodes[mac].last_update), + ) + self._device_nodes[mac].available = False + self._device_nodes[mac].do_ping(True) + else: + # Do ping request for all non SED's + self._device_nodes[mac].do_ping() + + if ( + self._device_nodes[mac].available + and self._device_nodes[mac].measures_power + ): # Request current power usage self._device_nodes[mac].request_power_update() # Sync internal clock of power measure nodes once a day From 4a38cfdf93dde96efb0845dd771f5558c13e8504 Mon Sep 17 00:00:00 2001 From: brefra Date: Thu, 6 Jan 2022 21:41:10 +0100 Subject: [PATCH 62/87] Rename _request_info into _request_NodeInfo --- plugwise/nodes/__init__.py | 14 +++++++------- plugwise/nodes/circle.py | 3 ++- plugwise/nodes/sed.py | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index e3b5a2c5c..ea76af927 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -152,13 +152,6 @@ def do_ping(self, forced=False, callback: callable | None = None) -> None: if forced or USB.ping in self._callbacks: self._request_ping(callback) - def _request_info(self, callback: callable | None = None) -> None: - """Request info from node.""" - self._callback_NodeInfo = callback - _node_request = NodeInfoRequest(self._mac) - _node_request.priority = Priority.Low - self.message_sender(_node_request) - def _request_features(self, callback: callable | None = None) -> None: """Request supported features for this node.""" self._callback_NodeFeature = callback @@ -166,6 +159,13 @@ def _request_features(self, callback: callable | None = None) -> None: NodeFeaturesRequest(self._mac), ) + def _request_NodeInfo(self, callback: callable | None = None) -> None: + """Request info from node.""" + self._callback_NodeInfo = callback + _node_request = NodeInfoRequest(self._mac) + _node_request.priority = Priority.Low + self.message_sender(_node_request) + def _request_ping(self, callback: callable | None = None) -> None: """Ping node.""" self._callback_NodePing = callback diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 71cb355be..73eab1014 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -251,7 +251,8 @@ def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for PlugwiseCircle class.""" if not self.available: self.available = True - self._request_info() + if not isinstance(message, NodeInfoResponse): + self._request_NodeInfo() self._last_update = message.timestamp if isinstance(message, CirclePowerUsageResponse): self._process_CirclePowerUsageResponse(message) diff --git a/plugwise/nodes/sed.py b/plugwise/nodes/sed.py index 552013cdb..d6cfd4667 100644 --- a/plugwise/nodes/sed.py +++ b/plugwise/nodes/sed.py @@ -138,7 +138,7 @@ def _queue_request(self, message: PlugwiseRequest): ) # Overrule method from PlugwiseNode class - def _request_info(self, callback=None): + def _request_NodeInfo(self, callback=None): """Request info from node""" self._callback_NodeInfo = callback self._queue_request(NodeInfoRequest(self._mac)) From 4ca36713ec88c303bee6a99fe79bf0eedc94661d Mon Sep 17 00:00:00 2001 From: brefra Date: Thu, 6 Jan 2022 21:42:53 +0100 Subject: [PATCH 63/87] Rename _request_ping into _request_NodePing --- plugwise/nodes/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index ea76af927..bd4149f86 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -150,7 +150,7 @@ def rssi_out(self) -> int: def do_ping(self, forced=False, callback: callable | None = None) -> None: """Send network ping message to node.""" if forced or USB.ping in self._callbacks: - self._request_ping(callback) + self._request_NodePing(callback) def _request_features(self, callback: callable | None = None) -> None: """Request supported features for this node.""" @@ -166,7 +166,7 @@ def _request_NodeInfo(self, callback: callable | None = None) -> None: _node_request.priority = Priority.Low self.message_sender(_node_request) - def _request_ping(self, callback: callable | None = None) -> None: + def _request_NodePing(self, callback: callable | None = None) -> None: """Ping node.""" self._callback_NodePing = callback _request = NodePingRequest(self._mac) From dd11e53205258d124e2b7eefaba93d8133e7c054 Mon Sep 17 00:00:00 2001 From: brefra Date: Thu, 6 Jan 2022 21:53:36 +0100 Subject: [PATCH 64/87] Add more typing --- plugwise/nodes/__init__.py | 10 ++++++---- plugwise/nodes/circle_plus.py | 4 ++-- plugwise/stick.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index bd4149f86..93d92f9bf 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -37,7 +37,7 @@ def __init__(self, mac: str, address: int, message_sender: callable): self._mac = bytes(mac, encoding=UTF8_DECODE) self.message_sender = message_sender self._features = () - self._address = address + self._address: int = address self._callbacks = {} self._last_update: datetime | None = None self._available: bool = False @@ -50,7 +50,8 @@ def __init__(self, mac: str, address: int, message_sender: callable): self._hardware_version = None self._firmware_version = None self._relay_state = False - self._last_log_address = None + self._info_last_log_address: int | None = None + self._info_last_timestamp: datetime | None = None self._device_features = None # Local callback variables @@ -240,6 +241,7 @@ def _process_NodePingResponse(self, message: NodePingResponse) -> None: def _process_NodeInfoResponse(self, message: NodeInfoResponse) -> None: """Process content of 'NodeInfoResponse' message.""" + self._info_last_timestamp = message.timestamp if message.relay_state.serialize() == b"01": if not self._relay_state: self._relay_state = True @@ -251,8 +253,8 @@ def _process_NodeInfoResponse(self, message: NodeInfoResponse) -> None: self._hardware_version = message.hw_ver.value.decode(UTF8_DECODE) self._firmware_version = message.fw_ver.value self._node_type = message.node_type.value - if self._last_log_address != message.last_logaddr.value: - self._last_log_address = message.last_logaddr.value + if self._info_last_log_address != message.last_logaddr.value: + self._info_last_log_address = message.last_logaddr.value _LOGGER.debug("Node type = %s", self.hardware_model) if not self._battery_powered: _LOGGER.debug("Relay state = %s", str(self._relay_state)) diff --git a/plugwise/nodes/circle_plus.py b/plugwise/nodes/circle_plus.py index 89ca74b31..273fcc068 100644 --- a/plugwise/nodes/circle_plus.py +++ b/plugwise/nodes/circle_plus.py @@ -28,8 +28,8 @@ class PlugwiseCirclePlus(PlugwiseCircle): def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) - self._plugwise_nodes = {} - self._scan_response = {} + self._plugwise_nodes: dict(str, int) = {} + self._scan_response: dict(int, bool) = {} self._realtime_clock_offset = None self.get_real_time_clock(self.sync_realtime_clock) diff --git a/plugwise/stick.py b/plugwise/stick.py index 484071828..0cdea9afd 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -86,7 +86,7 @@ def __init__(self, port, callback=None): self._nodes_discovered = None self._nodes_not_discovered: list(str) = [] self._nodes_off_line = 0 - self._nodes_to_discover = {} + self._nodes_to_discover: dict(str, int) = {} self._port = port self._run_update_thread = False self._run_watchdog = None From 36ee7d69d3e52031855408d8e54ab44e95c09bea Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 12:30:27 +0100 Subject: [PATCH 65/87] Make features extendable --- plugwise/constants.py | 10 ++++++++++ plugwise/nodes/__init__.py | 8 +++++++- plugwise/nodes/circle.py | 41 ++++++++++++-------------------------- plugwise/nodes/scan.py | 9 ++------- plugwise/nodes/sense.py | 7 ++----- plugwise/nodes/switch.py | 8 ++------ 6 files changed, 36 insertions(+), 47 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index 4dd431d0b..f41092804 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -133,9 +133,19 @@ class USB(str, Enum): """USB property ID's.""" available = "available" + hour_cons = "energy_consumption_hour" + hour_prod = "energy_production_hour" + day_cons = "energy_consumption_day" + day_prod = "energy_production_day" + week_cons = "energy_consumption_week" + week_prod = "energy_production_week" humidity = "humidity" + interval_cons = "interval_consumption" + interval_prod = "interval_production" motion = "motion" ping = "ping" + power_1s = "power_1s" + power_8s = "power_8s" relay = "relay" switch = "switch" temperature = "temperature" diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 93d92f9bf..8be4f245a 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -22,6 +22,12 @@ from ..util import validate_mac, version_to_model _LOGGER = logging.getLogger(__name__) +FEATURES_NODE = ( + USB.available, + USB.ping, + USB.rssi_in, + USB.rssi_out, +) class PlugwiseNode: @@ -36,7 +42,7 @@ def __init__(self, mac: str, address: int, message_sender: callable): ) self._mac = bytes(mac, encoding=UTF8_DECODE) self.message_sender = message_sender - self._features = () + self._features: tuple(USB, ...) = FEATURES_NODE self._address: int = address self._callbacks = {} self._last_update: datetime | None = None diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 73eab1014..5d08eb90e 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -5,14 +5,6 @@ import logging from ..constants import ( - FEATURE_ENERGY_CONSUMPTION_TODAY, - FEATURE_POWER_CONSUMPTION_CURRENT_HOUR, - FEATURE_POWER_CONSUMPTION_PREVIOUS_HOUR, - FEATURE_POWER_CONSUMPTION_TODAY, - FEATURE_POWER_CONSUMPTION_YESTERDAY, - FEATURE_POWER_PRODUCTION_CURRENT_HOUR, - FEATURE_POWER_USE, - FEATURE_POWER_USE_LAST_8_SEC, MAX_TIME_DRIFT, MESSAGE_TIME_OUT, PULSES_PER_KW_SECOND, @@ -38,11 +30,16 @@ ) from ..nodes import PlugwiseNode -_FEATURES = ( - USB.available, - USB.ping, - USB.rssi_in, - USB.rssi_out, +FEATURES_CIRCLE = ( + USB.hour_cons, + USB.hour_prod, + USB.day_cons, + USB.day_prod, + USB.interval_cons, + USB.interval_prod, + USB.power_1s, + USB.power_8s, + USB.relay, ) _LOGGER = logging.getLogger(__name__) @@ -52,21 +49,6 @@ class PlugwiseCircle(PlugwiseNode): def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) - self._features = ( - FEATURE_ENERGY_CONSUMPTION_TODAY["id"], - FEATURE_PING["id"], - FEATURE_POWER_USE["id"], - FEATURE_POWER_USE_LAST_8_SEC["id"], - FEATURE_POWER_CONSUMPTION_CURRENT_HOUR["id"], - FEATURE_POWER_CONSUMPTION_PREVIOUS_HOUR["id"], - FEATURE_POWER_CONSUMPTION_TODAY["id"], - FEATURE_POWER_CONSUMPTION_YESTERDAY["id"], - FEATURE_POWER_PRODUCTION_CURRENT_HOUR["id"], - # FEATURE_POWER_PRODUCTION_PREVIOUS_HOUR["id"], - FEATURE_RSSI_IN["id"], - FEATURE_RSSI_OUT["id"], - FEATURE_RELAY["id"], - ) self._energy_consumption_today_reset = datetime.now().replace( hour=0, minute=0, second=0, microsecond=0 ) @@ -104,6 +86,9 @@ def __init__(self, mac: str, address: int, message_sender: callable): ) - datetime.utcnow().replace(minute=0, second=0, microsecond=0) self._clock_offset = None + # Supported features of node + self._features += FEATURES_CIRCLE + # Local callback variables self._callback_RelaySwitchedOn: callable | None = None self._callback_RelaySwitchedOff: callable | None = None diff --git a/plugwise/nodes/scan.py b/plugwise/nodes/scan.py index 27072d5d0..896b0532b 100644 --- a/plugwise/nodes/scan.py +++ b/plugwise/nodes/scan.py @@ -20,12 +20,7 @@ ) from ..nodes.sed import NodeSED -_FEATURES = ( - USB.motion, - USB.ping, - USB.rssi_in, - USB.rssi_out, -) +FEATURES_SCAN = (USB.motion,) _LOGGER = logging.getLogger(__name__) @@ -34,7 +29,7 @@ class PlugwiseScan(NodeSED): def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) - self._features = _FEATURES + self._features += FEATURES_SCAN self._motion_state = False self._motion_reset_timer = None self._daylight_mode = None diff --git a/plugwise/nodes/sense.py b/plugwise/nodes/sense.py index cb653c188..190fcc0b3 100644 --- a/plugwise/nodes/sense.py +++ b/plugwise/nodes/sense.py @@ -14,11 +14,8 @@ from ..nodes.sed import NodeSED _LOGGER = logging.getLogger(__name__) -_FEATURES = ( +FEATURES_SENSE = ( USB.humidity, - USB.ping, - USB.rssi_in, - USB.rssi_out, USB.temperature, ) @@ -28,7 +25,7 @@ class PlugwiseSense(NodeSED): def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) - self._features = _FEATURES + self._features += FEATURES_SENSE self._temperature = None self._humidity = None diff --git a/plugwise/nodes/switch.py b/plugwise/nodes/switch.py index d1c785ebd..626747f70 100644 --- a/plugwise/nodes/switch.py +++ b/plugwise/nodes/switch.py @@ -8,6 +8,7 @@ from ..nodes.sed import NodeSED _LOGGER = logging.getLogger(__name__) +FEATURES_SWITCH = (USB.switch,) class PlugwiseSwitch(NodeSED): @@ -15,12 +16,7 @@ class PlugwiseSwitch(NodeSED): def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) - self._features = ( - USB.ping, - USB.rssi_in, - USB.rssi_out, - USB.switch, - ) + self._features += FEATURES_SWITCH self._switch_state = False @property From 582e3f22b6294132a9d4bd239522aaeb1f5bb1b8 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 13:05:55 +0100 Subject: [PATCH 66/87] Update typing & comments to request and response --- plugwise/messages/__init__.py | 4 +- plugwise/messages/requests.py | 256 ++++++++++++++++++++++++--------- plugwise/messages/responses.py | 245 ++++++++++++++++++++----------- plugwise/stick.py | 4 +- 4 files changed, 354 insertions(+), 155 deletions(-) diff --git a/plugwise/messages/__init__.py b/plugwise/messages/__init__.py index 4f8d28efb..d28753778 100644 --- a/plugwise/messages/__init__.py +++ b/plugwise/messages/__init__.py @@ -10,7 +10,7 @@ class PlugwiseMessage: ID = b"0000" def __init__(self): - self.mac = "" + self.mac = None self.checksum = None self.args = [] @@ -18,7 +18,7 @@ def serialize(self): """Return message in a serialized format that can be sent out on wire.""" _args = b"".join(a.serialize() for a in self.args) msg = self.ID - if self.mac != "": + if self.mac is not None: msg += self.mac msg += _args self.checksum = self.calculate_checksum(msg) diff --git a/plugwise/messages/requests.py b/plugwise/messages/requests.py index 78722ea77..2848d484b 100644 --- a/plugwise/messages/requests.py +++ b/plugwise/messages/requests.py @@ -35,7 +35,7 @@ class Priority(int, Enum): class PlugwiseRequest(PlugwiseMessage): """Base class for request messages to be send from by USB-Stick.""" - def __init__(self, mac): + def __init__(self, mac: bytes | None) -> None: PlugwiseMessage.__init__(self) self.args = [] self.mac = mac @@ -131,25 +131,32 @@ def stick_state(self, state: bytes) -> None: class NodeNetworkInfoRequest(PlugwiseRequest): - """TODO: PublicNetworkInfoRequest + """ + Request network information - No arguments + Supported protocols : 1.0, 2.0 + Response message : NodeNetworkInfoResponse """ ID = b"0001" + def __init__(self) -> None: + # No MAC address required + super().__init__(None) + class CirclePlusConnectRequest(PlugwiseRequest): """ Request to connect a Circle+ to the Stick - Response message: CirclePlusConnectResponse + Supported protocols : 1.0, 2.0 + Response message : CirclePlusConnectResponse """ ID = b"0004" # This message has an exceptional format and therefore need to override the serialize method - def serialize(self): + def serialize(self) -> None: # This command has args: byte: key, byte: networkinfo.index, ulong: networkkey = 0 args = b"00000000000000000000" msg = self.ID + args + self.mac @@ -159,41 +166,42 @@ def serialize(self): class NodeAddRequest(PlugwiseRequest): """ - Inform node it is added to the Plugwise Network it to memory of Circle+ node + Add node to the Plugwise Network and add it to memory of Circle+ node - Response message: [acknowledge message] + Supported protocols : 1.0, 2.0 + Response message : NodeNetworkInfoResponse """ ID = b"0007" - def __init__(self, mac, accept: bool): + def __init__(self, mac: bytes, accept: bool) -> None: super().__init__(mac) accept_value = 1 if accept else 0 self.args.append(Int(accept_value, length=2)) # This message has an exceptional format (MAC at end of message) # and therefore a need to override the serialize method - def serialize(self): + def serialize(self) -> None: args = b"".join(a.serialize() for a in self.args) msg = self.ID + args + self.mac checksum = self.calculate_checksum(msg) return MESSAGE_HEADER + msg + checksum + MESSAGE_FOOTER -class NodeAllowJoiningRequest(PlugwiseRequest): +class CirclePlusAllowJoiningRequest(PlugwiseRequest): """ Enable or disable receiving joining request of unjoined nodes. - Circle+ node will respond with an acknowledge message + Circle+ node will respond - Response message: NodeAckLargeResponse + Supported protocols : 1.0, 2.0, 2.6 (has extra 'AllowThirdParty' field) + Response message : NodeAckResponse """ ID = b"0008" - def __init__(self, accept: bool): - super().__init__("") - # TODO: Make sure that '01' means enable, and '00' disable joining - val = 1 if accept else 0 + def __init__(self, enable: bool) -> None: + super().__init__(None) + val = 1 if enable else 0 self.args.append(Int(val, length=2)) @@ -201,12 +209,13 @@ class NodeResetRequest(PlugwiseRequest): """ TODO: Some kind of reset request - Response message: ??? + Supported protocols : 1.0, 2.0, 2.1 + Response message : """ ID = b"0009" - def __init__(self, mac, moduletype, timeout): + def __init__(self, mac: bytes, moduletype: int, timeout: int) -> None: super().__init__(mac) self.args += [ Int(moduletype, length=2), @@ -215,71 +224,162 @@ def __init__(self, mac, moduletype, timeout): class StickInitRequest(PlugwiseRequest): - """Initialize USB-Stick.""" + """ + Initialize USB-Stick. + + Supported protocols : 1.0, 2.0 + Response message : StickInitResponse + """ ID = b"000A" - Response = "StickInitResponse" - def __init__(self): + def __init__(self) -> None: """message for that initializes the Stick""" # init is the only request message that doesn't send MAC address - super().__init__("") + super().__init__(None) class NodeImagePrepareRequest(PlugwiseRequest): """ - TODO: PWEswImagePrepareRequestV1_0 + TODO: Some kind of request to prepare node for a firmware image. - Response message: TODO: + Supported protocols : 1.0, 2.0 + Response message : """ ID = b"000B" +class NodeImageValidateRequest(PlugwiseRequest): + """ + TODO: Some kind of request to validate a firmware image for a node. + + Supported protocols : 1.0, 2.0 + Response message : NodeImageValidationResponse + """ + + ID = b"000C" + + class NodePingRequest(PlugwiseRequest): - """Ping node.""" + """ + Ping node + + Supported protocols : 1.0, 2.0 + Response message : NodePingResponse + """ ID = b"000D" - Response = "NodePingResponse" + + +class NodeImageActivateRequest(PlugwiseRequest): + """ + TODO: Some kind of request to activate a firmware image for a node. + + Supported protocols : 1.0, 2.0 + Response message : + """ + + ID = b"000F" + + def __init__(self, mac: bytes, type: int, reset_delay: int) -> None: + super().__init__(mac) + _type = Int(type, 2) + _reset_delay = Int(reset_delay, 2) + self.args += [_type, _reset_delay] class CirclePowerUsageRequest(PlugwiseRequest): - """Request current power usage.""" + """ + Request current power usage. + + Supported protocols : 1.0, 2.0, 2.1, 2.3 + Response message : CirclePowerUsageResponse + """ ID = b"0012" - Response = "CirclePowerUsageResponse" + + +class CircleLogDataRequest(PlugwiseRequest): + """ + TODO: Some kind of request to get log data from a node. + Only supported at protocol version 1.0 ! + + + + + Supported protocols : 1.0 + Response message : CircleLogDataResponse + """ + + ID = b"0014" + + def __init__(self, mac: bytes, start: datetime, end: datetime) -> None: + super().__init__(mac) + + passed_days_start = start.day - 1 + month_minutes_start = ( + (passed_days_start * DAY_IN_MINUTES) + + (start.hour * HOUR_IN_MINUTES) + + start.minute + ) + from_abs = DateTime(start.year, start.month, month_minutes_start) + + passed_days_end = end.day - 1 + month_minutes_end = ( + (passed_days_end * DAY_IN_MINUTES) + + (end.hour * HOUR_IN_MINUTES) + + end.minute + ) + to_abs = DateTime(end.year, end.month, month_minutes_end) + + self.args += [from_abs, to_abs] class CircleClockSetRequest(PlugwiseRequest): - """Set internal clock of node.""" + """ + Set internal clock of node and flash address + + Supported protocols : 1.0, 2.0 + Response message : NodeResponse + """ ID = b"0016" - Response = "CirclePowerUsageResponse" - def __init__(self, mac, dt): + def __init__( + self, + mac: bytes, + dt: datetime, + flash_address: str = "FFFFFFFF", + protocol_version: str = "2.0", + ) -> None: super().__init__(mac) - passed_days = dt.day - 1 - month_minutes = ( - (passed_days * DAY_IN_MINUTES) + (dt.hour * HOUR_IN_MINUTES) + dt.minute - ) - this_date = DateTime(dt.year, dt.month, month_minutes) + if protocol_version == "1.0": + pass + # FIXME: Define "absoluteHour" variable + elif protocol_version == "2.0": + passed_days = dt.day - 1 + month_minutes = ( + (passed_days * DAY_IN_MINUTES) + (dt.hour * HOUR_IN_MINUTES) + dt.minute + ) + this_date = DateTime(dt.year, dt.month, month_minutes) this_time = Time(dt.hour, dt.minute, dt.second) day_of_week = Int(dt.weekday(), 2) - # FIXME: use LogAddr instead - log_buf_addr = String("FFFFFFFF", 8) + log_buf_addr = String(flash_address, 8) self.args += [this_date, log_buf_addr, this_time, day_of_week] -class CircleSwitchRelayRequest(PlugwiseRequest): +class CircleRelaySwitchRequest(PlugwiseRequest): """ - switches relay on/off + Request to switches relay on/off - Response message: NodeAckLargeResponse + Supported protocols : 1.0, 2.0 + Response message : NodeResponse """ ID = b"0017" - def __init__(self, mac, on): + def __init__(self, mac: bytes, on: bool) -> None: super().__init__(mac) val = 1 if on else 0 self.args.append(Int(val, length=2)) @@ -287,15 +387,16 @@ def __init__(self, mac, on): class CirclePlusScanRequest(PlugwiseRequest): """ - Get all linked Circle plugs from Circle+ - a Plugwise network can have 64 devices the node ID value has a range from 0 to 63 + Request all linked Circle plugs from Circle+ + a Plugwise network (Circle+) can have 64 devices the node ID value has a range from 0 to 63 - Response message: CirclePlusScanResponse + Supported protocols : 1.0, 2.0 + Response message : CirclePlusScanResponse """ ID = b"0018" - def __init__(self, mac, node_address): + def __init__(self, mac: bytes, node_address: int) -> None: super().__init__(mac) self.args.append(Int(node_address, length=2)) self.node_address = node_address @@ -306,12 +407,13 @@ class NodeRemoveRequest(PlugwiseRequest): Request node to be removed from Plugwise network by removing it from memory of Circle+ node. - Response message: NodeRemoveResponse + Supported protocols : 1.0, 2.0 + Response message : NodeRemoveResponse """ ID = b"001C" - def __init__(self, mac_circle_plus, mac_to_unjoined): + def __init__(self, mac_circle_plus: bytes, mac_to_unjoined: str) -> None: super().__init__(mac_circle_plus) self.args.append(String(mac_to_unjoined, length=16)) @@ -320,7 +422,8 @@ class NodeInfoRequest(PlugwiseRequest): """ Request status info of node - Response message: NodeInfoResponse + Supported protocols : 1.0, 2.0, 2.3 + Response message : NodeInfoResponse """ ID = b"0023" @@ -330,7 +433,8 @@ class CircleCalibrationRequest(PlugwiseRequest): """ Request power calibration settings of node - Response message: CircleCalibrationResponse + Supported protocols : 1.0, 2.0 + Response message : CircleCalibrationResponse """ ID = b"0026" @@ -338,9 +442,10 @@ class CircleCalibrationRequest(PlugwiseRequest): class CirclePlusRealTimeClockSetRequest(PlugwiseRequest): """ - Set real time clock of CirclePlus + Set real time clock of Circle+ - Response message: [Acknowledge message] + Supported protocols : 1.0, 2.0 + Response message : NodeResponse """ ID = b"0028" @@ -357,27 +462,36 @@ class CirclePlusRealTimeClockGetRequest(PlugwiseRequest): """ Request current real time clock of CirclePlus - Response message: CirclePlusRealTimeClockResponse + Supported protocols : 1.0, 2.0 + Response message : CirclePlusRealTimeClockResponse """ ID = b"0029" +# TODO : Insert +# +# ID = b"003B" = Get Schedule request +# ID = b"003C" = Set Schedule request + + class CircleClockGetRequest(PlugwiseRequest): """ Request current internal clock of node - Response message: CircleClockResponse + Supported protocols : 1.0, 2.0 + Response message : CircleClockResponse """ ID = b"003E" -class CircleEnableScheduleRequest(PlugwiseRequest): +class CircleActivateScheduleRequest(PlugwiseRequest): """ Request to switch Schedule on or off - Response message: TODO: + Supported protocols : 1.0, 2.0 + Response message : TODO: """ ID = b"0040" @@ -437,11 +551,11 @@ def __init__(self, group_mac, switch_state: bool): self.args.append(Int(val, length=2)) -class CircleEnergyCountersRequest(PlugwiseRequest): +class CircleEnergyLogsRequest(PlugwiseRequest): """ - Request energy usage counters storaged a given memory address + Request energy usage counters stored a given memory address - Response message: CircleEnergyCountersResponse + Response message: CircleEnergyLogsResponse """ ID = b"0048" @@ -514,6 +628,7 @@ def __init__( class NodeSelfRemoveRequest(PlugwiseRequest): """ + TODO: @@ -525,18 +640,18 @@ class NodeSelfRemoveRequest(PlugwiseRequest): ID = b"0051" -class NodeMeasureIntervalRequest(PlugwiseRequest): +class CircleMeasureIntervalRequest(PlugwiseRequest): """ - Configure the logging interval of power measurement in minutes + Configure the logging interval of energy measurement in minutes - Response message: TODO: + Response message: Ack message with ??? TODO: """ ID = b"0057" - def __init__(self, mac, usage, production): + def __init__(self, mac, consumption, production): super().__init__(mac) - self.args.append(Int(usage, length=4)) + self.args.append(Int(consumption, length=4)) self.args.append(Int(production, length=4)) @@ -631,17 +746,18 @@ def __init__(self, mac, interval): self.args.append(Int(interval, length=2)) -class CircleInitialRelaisStateRequest(PlugwiseRequest): +class CircleRelayInitStateRequest(PlugwiseRequest): """ - Get or set initial Relais state + Get or set initial relay state after power-up of Circle. - Response message: CircleInitialRelaisStateResponse + Supported protocols : 2.6 + Response message : CircleInitRelayStateResponse """ ID = b"0138" - def __init__(self, mac, configure: bool, relais_state: bool): + def __init__(self, mac: bytes, configure: bool, relay_state: bool) -> None: super().__init__(mac) set_or_get = Int(1 if configure else 0, length=2) - relais = Int(1 if relais_state else 0, length=2) - self.args += [set_or_get, relais] + relay = Int(1 if relay_state else 0, length=2) + self.args += [set_or_get, relay] diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index d7048a3a0..22b72b84b 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -30,10 +30,10 @@ ) REJOIN_RESPONSE_ID = b"FFFD" -AWAKE_RESPONSE = b"FFFE" -SWITCH_GROUP_RESPONSE = b"FFFF" +AWAKE_RESPONSE_ID = b"FFFE" +SWITCH_GROUP_RESPONSE_ID = b"FFFF" -SPECIAL_SEQ_IDS = (REJOIN_RESPONSE_ID, AWAKE_RESPONSE, SWITCH_GROUP_RESPONSE) +SPECIAL_SEQ_IDS = (REJOIN_RESPONSE_ID, AWAKE_RESPONSE_ID, SWITCH_GROUP_RESPONSE_ID) class StickResponseType(bytes, Enum): @@ -94,7 +94,7 @@ class PlugwiseResponse(PlugwiseMessage): Base class for response messages received by USB-Stick. """ - def __init__(self, format_size=None): + def __init__(self, format_size: String | None = None) -> None: super().__init__() self.format_size = format_size self.params = [] @@ -109,7 +109,7 @@ def __init__(self, format_size=None): else: self.len_correction = 0 - def deserialize(self, response): + def deserialize(self, response: bytes) -> None: self.timestamp = datetime.utcnow().replace(tzinfo=timezone.utc) if response[:4] != MESSAGE_HEADER: raise InvalidMessageHeader( @@ -150,14 +150,15 @@ def deserialize(self, response): msg += self.mac msg += _args - def _parse_params(self, response): + def _parse_params(self, response: bytes) -> None: for param in self.params: my_val = response[: len(param)] param.deserialize(my_val) response = response[len(my_val) :] return response - def __len__(self): + def __len__(self) -> int: + """Return the size of response message.""" arglen = sum(len(x) for x in self.params) return 34 + arglen + self.len_correction @@ -171,33 +172,37 @@ class StickResponse(PlugwiseResponse): ID = b"0000" - def __init__(self): + def __init__(self) -> None: super().__init__(STICK_MESSAGE_SIZE) class NodeResponse(PlugwiseResponse): """ - Acknowledge message with source MAC + Report status from node to a specific request - Response to: Any message + Supported protocols : 1.0, 2.0 + Response to requests: TODO: complete list + CircleClockSetRequest + CirclePlusRealTimeClockSetRequest """ ID = b"0000" - def __init__(self): + def __init__(self) -> None: super().__init__(NODE_MESSAGE_SIZE) -class CirclePlusQueryResponse(PlugwiseResponse): +class NodeNetworkInfoResponse(PlugwiseResponse): """ - TODO: + Report status of zigbee network - Response to : ??? + Supported protocols : 1.0, 2.0 + Response to request : NodeNetworkInfoRequest """ ID = b"0002" - def __init__(self): + def __init__(self) -> None: super().__init__() self.channel = String(None, length=2) self.source_mac_id = String(None, length=16) @@ -220,29 +225,31 @@ def __len__(self): arglen = sum(len(x) for x in self.params) return 18 + arglen - def deserialize(self, response): + def deserialize(self, response: bytes) -> None: super().deserialize(response) # Clear first two characters of mac ID, as they contain part of the short PAN-ID self.new_node_mac_id.value = b"00" + self.new_node_mac_id.value[2:] -class CirclePlusQueryEndResponse(PlugwiseResponse): +class NodeSpecificResponse(PlugwiseResponse): """ - TODO: - PWAckReplyV1_0 - + TODO: Report some sort of status from node + + PWAckReplyV1_0 + - Response to : ??? + Supported protocols : 1.0, 2.0 + Response to requests: Unknown: TODO """ ID = b"0003" - def __init__(self): + def __init__(self) -> None: super().__init__() self.status = Int(0, 4) self.params += [self.status] - def __len__(self): + def __len__(self) -> int: arglen = sum(len(x) for x in self.params) return 18 + arglen @@ -251,12 +258,13 @@ class CirclePlusConnectResponse(PlugwiseResponse): """ CirclePlus connected to the network - Response to : CirclePlusConnectRequest + Supported protocols : 1.0, 2.0 + Response to request : CirclePlusConnectRequest """ ID = b"0005" - def __init__(self): + def __init__(self) -> None: super().__init__() self.existing = Int(0, 2) self.allowed = Int(0, 2) @@ -269,9 +277,10 @@ def __len__(self): class NodeJoinAvailableResponse(PlugwiseResponse): """ - Message from an unjoined node to notify it is available to join a plugwise network + Request from Node to join a plugwise network - Response to : + Supported protocols : 1.0, 2.0 + Response to request : No request as every unjoined node is requesting to be added automatically """ ID = b"0006" @@ -279,18 +288,19 @@ class NodeJoinAvailableResponse(PlugwiseResponse): class NodePingResponse(PlugwiseResponse): """ - Ping response from node + Ping and RSSI (Received Signal Strength Indicator) response from node - - incomingLastHopRssiTarget (received signal strength indicator) - - lastHopRssiSource + - rssi_in : Incoming last hop RSSI target + - rssi_out : Last hop RSSI source - timediffInMs - Response to : NodePingRequest + Supported protocols : 1.0, 2.0 + Response to request : NodePingRequest """ ID = b"000E" - def __init__(self): + def __init__(self) -> None: super().__init__() self.rssi_in = Int(0, length=2) self.rssi_out = Int(0, length=2) @@ -302,6 +312,22 @@ def __init__(self): ] +class NodeImageValidationResponse(PlugwiseResponse): + """ + TODO: Some kind of response to validate a firmware image for a node. + + Supported protocols : 1.0, 2.0 + Response to request : NodeImageValidationRequest + """ + + ID = b"0010" + + def __init__(self) -> None: + super().__init__() + self.timestamp = UnixTimestamp(0) + self.params += [self.timestamp] + + class StickInitResponse(PlugwiseResponse): """ Returns the configuration and status of the USB-Stick @@ -309,17 +335,15 @@ class StickInitResponse(PlugwiseResponse): Optional: - circle_plus_mac - network_id + - TODO: Two unknown parameters - - - - - Response to: StickInitRequest + Supported protocols : 1.0, 2.0 + Response to request : StickInitRequest """ ID = b"0011" - def __init__(self): + def __init__(self) -> None: super().__init__() self.unknown1 = Int(0, length=2) self.network_is_online = Int(0, length=2) @@ -339,37 +363,63 @@ class CirclePowerUsageResponse(PlugwiseResponse): """ Returns power usage as impulse counters for several different timeframes - Response to : CirclePowerUsageRequest + Supported protocols : 1.0, 2.0, 2.1, 2.3 + Response to request : CirclePowerUsageRequest """ ID = b"0013" - def __init__(self): + def __init__(self, protocol_version: str = "2.3") -> None: super().__init__() self.pulse_1s = Int(0, 4) self.pulse_8s = Int(0, 4) - self.pulse_hour_consumed = Int(0, 8) - self.pulse_hour_produced = Int(0, 8) self.nanosecond_offset = Int(0, 4) - self.params += [ - self.pulse_1s, - self.pulse_8s, - self.pulse_hour_consumed, - self.pulse_hour_produced, - self.nanosecond_offset, - ] + self.params += [self.pulse_1s, self.pulse_8s] + if protocol_version == "2.3": + self.pulse_counter_consumed = Int(0, 8) + self.pulse_counter_produced = Int(0, 8) + self.params += [ + self.pulse_counter_consumed, + self.pulse_counter_produced, + ] + self.params += [self.nanosecond_offset] + + +class CircleLogDataResponse(PlugwiseResponse): + """ + TODO: Returns some kind of log data from a node. + Only supported at protocol version 1.0 ! + + + + + + + Supported protocols : 1.0 + Response to: CircleLogDataRequest + """ + + ID = b"0015" + + def __init__(self) -> None: + super().__init__() + self.stored_abs = DateTime() + self.powermeterinfo = Int(0, 8, False) + self.flashaddress = LogAddr(0, length=8) + self.params += [self.stored_abs, self.powermeterinfo, self.flashaddress] class CirclePlusScanResponse(PlugwiseResponse): """ - Returns the MAC of a registered node at the specified memory address + Returns the MAC of a registered node at the specified memory address of a Circle+ - Response to: CirclePlusScanRequest + Supported protocols : 1.0, 2.0 + Response to request : CirclePlusScanRequest """ ID = b"0019" - def __init__(self): + def __init__(self) -> None: super().__init__() self.node_mac = String(None, length=16) self.node_address = Int(0, 2, False) @@ -381,12 +431,13 @@ class NodeRemoveResponse(PlugwiseResponse): Returns conformation (or not) if node is removed from the Plugwise network by having it removed from the memory of the Circle+ - Response to: NodeRemoveRequest + Supported protocols : 1.0, 2.0 + Response to request : NodeRemoveRequest """ ID = b"001D" - def __init__(self): + def __init__(self) -> None: super().__init__() self.node_mac_id = String(None, length=16) self.status = Int(0, 2) @@ -397,24 +448,45 @@ class NodeInfoResponse(PlugwiseResponse): """ Returns the status information of Node - Response to: NodeInfoRequest + Supported protocols : 1.0, 2.0, 2.3 + Response to request : NodeInfoRequest """ ID = b"0024" - def __init__(self): + def __init__(self, protocol_version: str = "2.0") -> None: super().__init__() - self.datetime = DateTime() + self.last_logaddr = LogAddr(0, length=8) - self.relay_state = Int(0, length=2) + if protocol_version == "1.0": + pass + self.datetime = DateTime() # FIXME: Define "absoluteHour" variable + self.relay_state = Int(0, length=2) + self.params += [ + self.datetime, + self.last_logaddr, + self.relay_state, + ] + elif protocol_version == "2.0": + self.datetime = DateTime() + self.relay_state = Int(0, length=2) + self.params += [ + self.datetime, + self.last_logaddr, + self.relay_state, + ] + elif protocol_version == "2.3": + self.state_mask = Int(0, length=2) # FIXME: Define "Statemask" variable + self.params += [ + self.datetime, + self.last_logaddr, + self.state_mask, + ] self.hz = Int(0, length=2) self.hw_ver = String(None, length=12) self.fw_ver = UnixTimestamp(0) self.node_type = Int(0, length=2) self.params += [ - self.datetime, - self.last_logaddr, - self.relay_state, self.hz, self.hw_ver, self.fw_ver, @@ -424,14 +496,15 @@ def __init__(self): class CircleCalibrationResponse(PlugwiseResponse): """ - returns the calibration settings of node + Returns the calibration settings of node - Response to: CircleCalibrationRequest + Supported protocols : 1.0, 2.0 + Response to request : CircleCalibrationRequest """ ID = b"0027" - def __init__(self): + def __init__(self) -> None: super().__init__() self.gain_a = Float(0, 8) self.gain_b = Float(0, 8) @@ -444,12 +517,13 @@ class CirclePlusRealTimeClockResponse(PlugwiseResponse): """ returns the real time clock of CirclePlus node - Response to: CirclePlusRealTimeClockGetRequest + Supported protocols : 1.0, 2.0 + Response to request : CirclePlusRealTimeClockGetRequest """ ID = b"003A" - def __init__(self): + def __init__(self) -> None: super().__init__() self.time = RealClockTime() @@ -458,16 +532,22 @@ def __init__(self): self.params += [self.time, self.day_of_week, self.date] +# TODO : Insert +# +# ID = b"003D" = Schedule response + + class CircleClockResponse(PlugwiseResponse): """ Returns the current internal clock of Node - Response to: CircleClockGetRequest + Supported protocols : 1.0, 2.0 + Response to request : CircleClockGetRequest """ ID = b"003F" - def __init__(self): + def __init__(self) -> None: super().__init__() self.time = Time() self.day_of_week = Int(0, 2, False) @@ -476,12 +556,12 @@ def __init__(self): self.params += [self.time, self.day_of_week, self.unknown, self.unknown2] -class CircleEnergyCountersResponse(PlugwiseResponse): +class CircleEnergyLogsResponse(PlugwiseResponse): """ Returns historical energy usage of requested memory address Each response contains 4 energy counters at specified 1 hour timestamp - Response to: CircleEnergyCountersRequest + Response to: CircleEnergyLogsRequest """ ID = b"0049" @@ -616,40 +696,43 @@ def __init__(self): self.params += [self.humidity, self.temperature] -class CircleInitialRelaisStateResponse(PlugwiseResponse): +class CircleRelayInitStateResponse(PlugwiseResponse): """ - Returns the initial relais state. + Returns the configured relay state after power-up of Circle - Response to: CircleInitialRelaisStateRequest + Supported protocols : 2.6 + Response to request : CircleRelayInitStateRequest """ ID = b"0139" def __init__(self): super().__init__() - set_or_get = Int(0, length=2) - relais = Int(0, length=2) - self.params += [set_or_get, relais] + is_get = Int(0, length=2) + relay = Int(0, length=2) + self.params += [is_get, relay] id_to_message = { - b"0002": CirclePlusQueryResponse(), - b"0003": CirclePlusQueryEndResponse(), + b"0002": NodeNetworkInfoResponse(), + b"0003": NodeAckResponse(), b"0005": CirclePlusConnectResponse(), b"0006": NodeJoinAvailableResponse(), b"000E": NodePingResponse(), b"0011": StickInitResponse(), b"0013": CirclePowerUsageResponse(), + b"0015": CircleLogDataResponse(), b"0019": CirclePlusScanResponse(), b"001D": NodeRemoveResponse(), b"0024": NodeInfoResponse(), b"0027": CircleCalibrationResponse(), b"003A": CirclePlusRealTimeClockResponse(), b"003F": CircleClockResponse(), - b"0049": CircleEnergyCountersResponse(), + b"0049": CircleEnergyLogsResponse(), b"0060": NodeFeaturesResponse(), b"0100": NodeAckResponse(), b"0105": SenseReportResponse(), + b"0139": CircleRelayInitStateResponse(), } @@ -661,9 +744,9 @@ def get_message_response(message_id, length, seq_id): # First check for known sequence ID's if seq_id == REJOIN_RESPONSE_ID: return NodeRejoinResponse() - if seq_id == AWAKE_RESPONSE: + if seq_id == AWAKE_RESPONSE_ID: return NodeAwakeResponse() - if seq_id == SWITCH_GROUP_RESPONSE: + if seq_id == SWITCH_GROUP_RESPONSE_ID: return NodeSwitchGroupResponse() # No fixed sequence ID, continue at message ID diff --git a/plugwise/stick.py b/plugwise/stick.py index 0cdea9afd..8681c7664 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -30,7 +30,7 @@ ) from .messages.requests import ( NodeAddRequest, - NodeAllowJoiningRequest, + CirclePlusAllowJoiningRequest, NodeInfoRequest, NodePingRequest, NodeRemoveRequest, @@ -248,7 +248,7 @@ def allow_join_requests(self, enable: bool, accept: bool): Enable or disable Plugwise network Automatically accept new join request """ - self.msg_controller.send(NodeAllowJoiningRequest(enable)) + self.msg_controller.send(CirclePlusAllowJoiningRequest(enable)) if enable: self._accept_join_requests = accept else: From 73a27288e87103f4782bc2979e30bc4f22a96042 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 13:19:47 +0100 Subject: [PATCH 67/87] Full rewrite of energy - move energy related code to energy.py - make energy not depended to fixed (hour) log interval - allow changing of log interval - make energy collection more flexible to use --- plugwise/constants.py | 125 +--- plugwise/nodes/__init__.py | 2 +- plugwise/nodes/circle.py | 1124 ++++++++++++++----------------- plugwise/nodes/energy.py | 1278 ++++++++++++++++++++++++++++++++++++ plugwise/stick.py | 3 +- 5 files changed, 1775 insertions(+), 757 deletions(-) create mode 100644 plugwise/nodes/energy.py diff --git a/plugwise/constants.py b/plugwise/constants.py index f41092804..89905d460 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -1,5 +1,6 @@ """Plugwise Stick and Smile constants.""" +from datetime import datetime, timezone from enum import Enum # Copied homeassistant.consts @@ -30,10 +31,19 @@ ### Stick constants ### +LOCAL_TIMEZONE = datetime.now(timezone.utc).astimezone().tzinfo +DAY_IN_HOURS = 24 +WEEK_IN_HOURS = 168 + DAY_IN_MINUTES = 1440 HOUR_IN_MINUTES = 60 DAY_IN_SECONDS = 86400 +HOUR_IN_SECONDS = 3600 +MINUTE_IN_SECONDS = 60 + +SECOND_IN_NANOSECONDS = 1000000000 + UTF8_DECODE = "utf-8" # Serial connection settings for plugwise USB stick @@ -55,7 +65,11 @@ # plugwise year information is offset from y2k PLUGWISE_EPOCH = 2000 PULSES_PER_KW_SECOND = 468.9385193 + +# Energy log memory addresses LOGADDR_OFFSET = 278528 +LOGADDR_MAX = 65535 # TODO: Determine last log address, currently not used yet + # Default sleep between sending messages SLEEP_TIME = 0.1 @@ -177,117 +191,6 @@ class USB(str, Enum): SENSE_TEMPERATURE_MULTIPLIER = 175.72 SENSE_TEMPERATURE_OFFSET = 46.85 -# Stick device features -FEATURE_AVAILABLE = { - "id": "available", - "name": "Available", - "state": "available", - "unit": "state", -} -FEATURE_ENERGY_CONSUMPTION_TODAY = { - "id": "energy_consumption_today", - "name": "Energy consumption today", - "state": "Energy_consumption_today", - "unit": ENERGY_KILO_WATT_HOUR, -} -FEATURE_HUMIDITY = { - "id": "humidity", - "name": "Humidity", - "state": "humidity", - "unit": "%", -} -FEATURE_MOTION = { - "id": "motion", - "name": "Motion", - "state": "motion", - "unit": "state", -} -FEATURE_PING = { - "id": "ping", - "name": "Ping roundtrip", - "state": "ping", - "unit": TIME_MILLISECONDS, -} -FEATURE_POWER_USE = { - "id": "power_1s", - "name": "Power usage", - "state": "current_power_usage", - "unit": POWER_WATT, -} -FEATURE_POWER_USE_LAST_8_SEC = { - "id": "power_8s", - "name": "Power usage 8 seconds", - "state": "current_power_usage_8_sec", - "unit": POWER_WATT, -} -FEATURE_POWER_CONSUMPTION_CURRENT_HOUR = { - "id": "power_con_cur_hour", - "name": "Power consumption current hour", - "state": "power_consumption_current_hour", - "unit": ENERGY_KILO_WATT_HOUR, -} -FEATURE_POWER_CONSUMPTION_PREVIOUS_HOUR = { - "id": "power_con_prev_hour", - "name": "Power consumption previous hour", - "state": "power_consumption_previous_hour", - "unit": ENERGY_KILO_WATT_HOUR, -} -FEATURE_POWER_CONSUMPTION_TODAY = { - "id": "power_con_today", - "name": "Power consumption today", - "state": "power_consumption_today", - "unit": ENERGY_KILO_WATT_HOUR, -} -FEATURE_POWER_CONSUMPTION_YESTERDAY = { - "id": "power_con_yesterday", - "name": "Power consumption yesterday", - "state": "power_consumption_yesterday", - "unit": ENERGY_KILO_WATT_HOUR, -} -FEATURE_POWER_PRODUCTION_CURRENT_HOUR = { - "id": "power_prod_cur_hour", - "name": "Power production current hour", - "state": "power_production_current_hour", - "unit": ENERGY_KILO_WATT_HOUR, -} -FEATURE_POWER_PRODUCTION_PREVIOUS_HOUR = { - "id": "power_prod_prev_hour", - "name": "Power production previous hour", - "state": "power_production_previous_hour", - "unit": ENERGY_KILO_WATT_HOUR, -} -FEATURE_RELAY = { - "id": "relay", - "name": "Relay state", - "state": "relay_state", - "unit": "state", -} -FEATURE_SWITCH = { - "id": "switch", - "name": "Switch state", - "state": "switch_state", - "unit": "state", -} -FEATURE_TEMPERATURE = { - "id": "temperature", - "name": "Temperature", - "state": "temperature", - "unit": TEMP_CELSIUS, -} - -# TODO: Need to validate RSSI sensors -FEATURE_RSSI_IN = { - "id": "RSSI_in", - "name": "RSSI in", - "state": "rssi_in", - "unit": "Unknown", -} -FEATURE_RSSI_OUT = { - "id": "RSSI_out", - "name": "RSSI out", - "state": "rssi_out", - "unit": "Unknown", -} ### Smile constants ### diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 8be4f245a..1787b6779 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -55,7 +55,7 @@ def __init__(self, mac: str, address: int, message_sender: callable): self._node_type = None self._hardware_version = None self._firmware_version = None - self._relay_state = False + self._relay_state: bool = False self._info_last_log_address: int | None = None self._info_last_timestamp: datetime | None = None self._device_features = None diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 5d08eb90e..cb7d39568 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -5,30 +5,45 @@ import logging from ..constants import ( + DAY_IN_MINUTES, + DAY_IN_SECONDS, MAX_TIME_DRIFT, MESSAGE_TIME_OUT, - PULSES_PER_KW_SECOND, + SECOND_IN_NANOSECONDS, USB, ) from ..messages.requests import ( CircleCalibrationRequest, CircleClockGetRequest, CircleClockSetRequest, - CircleEnergyCountersRequest, + CircleEnergyLogsRequest, + CircleMeasureIntervalRequest, CirclePowerUsageRequest, - CircleSwitchRelayRequest, + CircleRelaySwitchRequest, Priority, ) from ..messages.responses import ( CircleCalibrationResponse, CircleClockResponse, - CircleEnergyCountersResponse, + CircleEnergyLogsResponse, CirclePowerUsageResponse, + NodeInfoResponse, NodeResponse, NodeResponseType, PlugwiseResponse, ) from ..nodes import PlugwiseNode +from ..nodes.energy import ( + Calibration, + CircleCalibration, + EnergyCollection, + PulseInterval, + PulseLog, + Pulses, + pulses_to_kws, +) + +_LOGGER = logging.getLogger(__name__) FEATURES_CIRCLE = ( USB.hour_cons, @@ -41,7 +56,12 @@ USB.power_8s, USB.relay, ) -_LOGGER = logging.getLogger(__name__) +ENERGY_COUNTER_IDS = ( + USB.hour_cons, + USB.hour_prod, + USB.day_cons, + USB.day_prod, +) class PlugwiseCircle(PlugwiseNode): @@ -49,45 +69,10 @@ class PlugwiseCircle(PlugwiseNode): def __init__(self, mac: str, address: int, message_sender: callable): super().__init__(mac, address, message_sender) - self._energy_consumption_today_reset = datetime.now().replace( - hour=0, minute=0, second=0, microsecond=0 - ) - self._energy_history_collecting = False - self._energy_history = {} - self._energy_last_collected_timestamp = datetime(2000, 1, 1) - self._energy_last_rollover_timestamp = datetime(2000, 1, 1) - self._energy_last_local_hour = datetime.now().hour - self._energy_last_populated_slot = 0 - self._energy_pulses_current_hour = None - self._energy_pulses_prev_hour = None - self._energy_rollover_day_started = False - self._energy_rollover_day_finished = True - self._energy_rollover_history_started = False - self._energy_rollover_history_finished = True - self._energy_rollover_hour_started = False - self._energy_rollover_hour_finished = True - self._energy_pulses_today_hourly = None - self._energy_pulses_today_now = None - self._energy_pulses_yesterday = None - self._new_relay_state = False - self._new_relay_stamp = datetime.utcnow() - timedelta(seconds=MESSAGE_TIME_OUT) - self._pulses_1s = None - self._pulses_8s = None - self._pulses_produced_1h = None - self.calibration = False - self._gain_a = None - self._gain_b = None - self._off_noise = None - self._off_tot = None - self._measures_power = True - self._last_log_collected = False - self.timezone_delta = datetime.now().replace( - minute=0, second=0, microsecond=0 - ) - datetime.utcnow().replace(minute=0, second=0, microsecond=0) - self._clock_offset = None # Supported features of node self._features += FEATURES_CIRCLE + self._measures_power: bool = True # Local callback variables self._callback_RelaySwitchedOn: callable | None = None @@ -97,111 +82,310 @@ def __init__(self, mac: str, address: int, message_sender: callable): self._callback_ClockAccepted: callable | None = None self._callback_CircleCalibration: callable | None = None self._callback_CirclePowerUsage: callable | None = None + self._callback_CircleMeasureIntervalConsumption: callable | None = None + self._callback_CircleMeasureIntervalProduction: callable | None = None + self._callback_CircleEnergyLogs: dict(int, callable) = {} - self.get_clock(self.sync_clock) - self._request_calibration() + # Clock settings + self._clock_offset = None + _utc = datetime.utcnow().replace(tzinfo=timezone.utc) - timedelta( + seconds=MESSAGE_TIME_OUT + ) + self._request_CircleClockGet(self.sync_clock) + + # Counters + self._pulses_1s: float | None = None + self._pulses_8s: float | None = None + self._pluses_consumed: int = 0 + self._pluses_produced: int = 0 + self._energy = EnergyCollection(ENERGY_COUNTER_IDS, self._log) + + # local log duration interval variables + self._log_interval_consumption: int | None = None + self._log_interval_consumption_request: int | None = None + self._log_interval_consumption_set: datetime = _utc + self._log_interval_production: int | None = None + self._log_interval_production_request: int | None = None + self._log_interval_production_set: datetime = _utc + + # Relay states + self._new_relay_state: bool = False + self._new_relay_set: datetime = _utc + + # Energy calibration & get initial energy logs afterwards + self._calibration: CircleCalibration | None = None + self._request_CircleCalibration(self.update_energy_log_collection) @property - def current_power_usage(self): + def power_1s(self) -> float: """ Returns power usage during the last second in Watts Based on last received power usage information """ - if self._pulses_1s is not None: - return self.pulses_to_kws(self._pulses_1s) * 1000 + if self._pulses_1s is not None and self._calibration is not None: + return pulses_to_kws(self._pulses_1s, self._calibration, 1) * 1000 return None @property - def current_power_usage_8_sec(self): + def power_8s(self) -> float: """ Returns power usage during the last 8 second in Watts Based on last received power usage information """ - if self._pulses_8s is not None: - return self.pulses_to_kws(self._pulses_8s, 8) * 1000 + if self._pulses_8s is not None and self._calibration is not None: + return pulses_to_kws(self._pulses_8s, self._calibration, 8) * 1000 return None @property - def energy_consumption_today(self) -> float: - """Returns total energy consumption since midnight in kWh""" - if self._energy_pulses_today_now is not None: - return self.pulses_to_kws(self._energy_pulses_today_now, 3600) - return None + def energy_consumption_hour(self) -> int | None: + """Returns energy consumption used this hour in kWh""" + return self._energy.counters[USB.hour_cons].energy @property - def energy_consumption_today_last_reset(self): - """Last reset of total energy consumption today""" - return self._energy_consumption_today_reset + def energy_consumption_hour_last_reset(self) -> datetime: + """Returns last reset of energy consumption used this hour""" + return self._energy.counters[USB.hour_cons].reset @property - def power_consumption_current_hour(self): - """ - Returns the power usage during this running hour in kWh - Based on last received power usage information - """ - if self._energy_pulses_current_hour is not None: - return self.pulses_to_kws(self._energy_pulses_current_hour, 3600) - return None + def energy_consumption_day(self) -> int | None: + """Returns energy consumption used today in kWh""" + return self._energy.counters[USB.day_cons].energy @property - def power_consumption_previous_hour(self): - """Returns power consumption during the previous hour in kWh""" - if self._energy_pulses_prev_hour is not None: - return self.pulses_to_kws(self._energy_pulses_prev_hour, 3600) - return None + def energy_consumption_day_last_reset(self) -> datetime: + """Returns last reset of energy consumption used today""" + return self._energy.counters[USB.day_cons].reset @property - def power_consumption_today(self): - """Total power consumption during today in kWh""" - if self._energy_pulses_today_hourly is not None: - return self.pulses_to_kws(self._energy_pulses_today_hourly, 3600) - return None + def energy_consumption_week(self) -> int | None: + """Returns energy consumption used this week in kWh""" + return self._energy.counters[USB.week_cons].energy @property - def power_consumption_yesterday(self): - """Total power consumption of yesterday in kWh""" - if self._energy_pulses_yesterday is not None: - return self.pulses_to_kws(self._energy_pulses_yesterday, 3600) - return None + def energy_consumption_week_last_reset(self) -> datetime: + """Returns last reset of energy consumption used today""" + return self._energy.counters[USB.week_cons].reset @property - def power_production_current_hour(self): - """ - Returns the power production during this running hour in kWh - Based on last received power usage information - """ - if self._pulses_produced_1h is not None: - return self.pulses_to_kws(self._pulses_produced_1h, 3600) - return None + def energy_production_hour(self) -> int | None: + """Returns energy production used this hour in kWh""" + return self._energy.counters[USB.hour_prod].energy + + @property + def energy_production_hour_last_reset(self) -> datetime: + """Returns last reset of energy production of this hour""" + return self._energy.counters[USB.hour_prod].reset + + @property + def energy_production_day(self) -> int | None: + """Returns energy production of today in kWh""" + return self._energy.counters[USB.day_prod].energy + + @property + def energy_production_day_last_reset(self) -> datetime: + """Returns last reset of energy production of today""" + return self._energy.counters[USB.day_prod].reset + + @property + def energy_production_week(self) -> int | None: + """Returns energy production of this week in kWh""" + return self._energy.counters[USB.week_prod].energy + + @property + def energy_production_week_last_reset(self) -> datetime: + """Returns last reset of energy production this week""" + return self._energy.counters[USB.week_prod].reset @property - def relay_state(self) -> bool: + def interval_consumption(self) -> int | None: + """Return interval (minutes) energy consumption is stored in local memory of Circle.""" + if self._log_interval_consumption_set + timedelta( + seconds=MESSAGE_TIME_OUT + ) > datetime.utcnow().replace(tzinfo=timezone.utc): + return self._log_interval_consumption + return self._energy.interval_consumption + + @interval_consumption.setter + def interval_consumption(self, consumption_interval: int) -> None: + """Request to change the energy collection interval in minutes.""" + assert ( + 1 <= consumption_interval <= DAY_IN_MINUTES + ), "Consumption interval value out of range (1-1440)" + _production_interval = self._log_interval_production + if _production_interval is None: + _production_interval = consumption_interval + self._log_interval_consumption_set = datetime.utcnow().replace( + tzinfo=timezone.utc + ) + self._log_interval_consumption_request = consumption_interval + self._request_CircleMeasureInterval(consumption_interval, _production_interval) + + @property + def interval_production(self) -> int | None: + """Return interval (minutes) energy production is stored in local memory of Circle.""" + if self._log_interval_production_set + timedelta( + seconds=MESSAGE_TIME_OUT + ) > datetime.utcnow().replace(tzinfo=timezone.utc): + return self._log_interval_production + return self._energy.interval_production + + @interval_production.setter + def interval_production(self, production_interval: int) -> None: + """Request to change the energy collection interval in minutes.""" + assert ( + 1 <= production_interval <= DAY_IN_MINUTES + ), "Production interval value out of range (1-1440)" + _consumption_interval = self._log_interval_consumption + if _consumption_interval is None: + _consumption_interval = production_interval + self._log_interval_production_set = datetime.utcnow().replace( + tzinfo=timezone.utc + ) + self._log_interval_production_request = production_interval + self._request_CircleMeasureInterval(_consumption_interval, production_interval) + + @property + def relay(self) -> bool: """ Return last known relay state or the new switch state by anticipating the acknowledge for new state is getting in before message timeout. """ - if ( - self._new_relay_stamp + timedelta(seconds=MESSAGE_TIME_OUT) - > datetime.utcnow() - ): + if self._new_relay_set + timedelta( + seconds=MESSAGE_TIME_OUT + ) > datetime.utcnow().replace(tzinfo=timezone.utc): return self._new_relay_state return self._relay_state - @relay_state.setter - def relay_state(self, state): + @relay.setter + def relay(self, state: bool) -> None: """Request the relay to switch state.""" - self._request_switch(state) + self._request_CircleRelaySwitch(state) self._new_relay_state = state - self._new_relay_stamp = datetime.utcnow() + self._new_relay_set = datetime.utcnow().replace(tzinfo=timezone.utc) if state != self._relay_state: - self.do_callback(FEATURE_RELAY["id"]) + self.do_callback(USB.relay) + + def update_power_usage(self) -> None: + """Request power usage and missing energy logs.""" + if self.available: + self._request_CirclePowerUsage() + + def update_energy_log_collection(self) -> None: + """Request missing energy log(s).""" + if not self.available: + return + _missing_addresses = self._energy.missing_log_addresses + + if _missing_addresses is not None: + if self._log: + _LOGGER.error( + "update_energy_log_collection for %s | Request missing | missing=%s, self._energy.next_log_timestamp=%s", + self.mac, + str(_missing_addresses), + str(self._energy.next_log_timestamp), + ) + for _address in _missing_addresses: + self._request_CircleEnergyLogs(_address) + else: + # Less than two full log addresses has been collected. Request logs stored at last 4 addresses + if self._info_last_timestamp > datetime.utcnow().replace( + tzinfo=timezone.utc + ) - timedelta(minutes=1): + # Recent node info, so do an initial request for last 10 log addresses + if self._log: + _LOGGER.error( + "update_energy_log_collection for %s | Request initial | _info_last_timestamp=%s, self._info_last_log_address=%s", + self.mac, + str(self._info_last_timestamp), + str(self._info_last_log_address), + ) + for _address in range( + self._info_last_log_address, + self._info_last_log_address - 11, + -1, + ): + self._request_CircleEnergyLogs(_address) + elif self._info_last_timestamp < datetime.utcnow().replace( + tzinfo=timezone.utc + ) - timedelta(minutes=15): + # node request older than 15 minutes, do node info request first + if self._log: + _LOGGER.error( + "update_energy_log_collection for %s | Request node info | _info_last_timestamp=%s, self._info_last_log_address=%s", + self.mac, + str(self._info_last_timestamp), + str(self._info_last_log_address), + ) + self._request_NodeInfo(self.update_energy_log_collection) + else: + if self._log: + _LOGGER.error( + "update_energy_log_collection for %s | Skip initial | _info_last_timestamp=%s", + self.mac, + str(self._info_last_timestamp), + ) + if self._log: + _LOGGER.error("update_energy_log_collection for %s | Finished", self.mac) - def _request_calibration(self, callback: callable | None = None) -> None: + def _request_CircleCalibration(self, callback: callable | None = None) -> None: """Request calibration info""" self._callback_CircleCalibration = callback self.message_sender(CircleCalibrationRequest(self._mac)) - def _request_switch( + def _request_CircleClockGet(self, callback: callable | None = None) -> None: + """get current datetime of internal clock of Circle.""" + self._callback_CircleClockResponse = callback + _clock_request = CircleClockGetRequest(self._mac) + _clock_request.priority = Priority.Low + self.message_sender(_clock_request) + + def _request_CircleClockSet(self, callback: callable | None = None) -> None: + """set internal clock of CirclePlus.""" + self._callback_ClockAccepted = callback + _clock_request = CircleClockSetRequest(self._mac, datetime.utcnow()) + _clock_request.priority = Priority.High + self.message_sender(_clock_request) + + def _request_CircleEnergyLogs( + self, address: int, callback: callable | None = None + ) -> None: + """Request energy counters for given memory address""" + if self._log: + _LOGGER.error( + "_request_CircleEnergyLogs for %s | address=%s", self.mac, str(address) + ) + if address not in self._energy.log_collected_addresses: + self._callback_CircleEnergyLogs[address] = callback + _request = CircleEnergyLogsRequest(self._mac, address) + _request.priority = Priority.Low + self.message_sender(_request) + if self._log: + _LOGGER.error( + "_request_CircleEnergyLogs for %s | SEND address=%s", + self.mac, + str(address), + ) + + def _request_CircleMeasureInterval( + self, + consumption: int, + production: int, + consumption_callback: callable | None = None, + production__callback: callable | None = None, + ) -> None: + """Request to change log measure intervals.""" + self._callback_CircleMeasureIntervalConsumption = consumption_callback + self._callback_CircleMeasureIntervalProduction = production__callback + self.message_sender( + CircleMeasureIntervalRequest(self._mac, consumption, production) + ) + + def _request_CirclePowerUsage(self, callback: callable | None = None) -> None: + """Request power usage and missing energy logs.""" + self._callback_CirclePowerUsage = callback + self.message_sender(CirclePowerUsageRequest(self._mac)) + + def _request_CircleRelaySwitch( self, state: bool, success_callback: callable | None = None, @@ -213,25 +397,10 @@ def _request_switch( else: self._callback_RelaySwitchedOff = success_callback self._callback_RelaySwitchFailed = failed_callback - _relay_request = CircleSwitchRelayRequest(self._mac, state) + _relay_request = CircleRelaySwitchRequest(self._mac, state) _relay_request.priority = Priority.High self.message_sender(_relay_request) - def request_power_update(self, callback: callable | None = None) -> None: - """Request power usage and update energy counters""" - if self._available: - self._callback_CirclePowerUsage = callback - self.message_sender(CirclePowerUsageRequest(self._mac)) - if len(self._energy_history) > 0: - # Request new energy counters if last one is more than one hour ago - if self._energy_last_collected_timestamp < datetime.utcnow().replace( - minute=0, second=0, microsecond=0 - ): - self.request_energy_counters() - else: - # No history collected yet, request energy history - self.request_energy_counters() - def message_for_node(self, message: PlugwiseResponse) -> None: """Process received messages for PlugwiseCircle class.""" if not self.available: @@ -245,13 +414,141 @@ def message_for_node(self, message: PlugwiseResponse) -> None: self._process_NodeResponse(message) elif isinstance(message, CircleCalibrationResponse): self._process_CircleCalibrationResponse(message) - elif isinstance(message, CircleEnergyCountersResponse): - self._process_CircleEnergyCountersResponse(message) + elif isinstance(message, CircleEnergyLogsResponse): + self._process_CircleEnergyLogsResponse(message) elif isinstance(message, CircleClockResponse): self._process_CircleClockResponse(message) + elif isinstance(message, NodeInfoResponse): + self._process_NodeInfoResponse(message) else: super().message_for_node(message) + def _process_CircleCalibrationResponse( + self, message: CircleCalibrationResponse + ) -> None: + """Store calibration properties""" + self._calibration: CircleCalibration = { + Calibration.GAIN_A: message.gain_a.value, + Calibration.GAIN_B: message.gain_b.value, + Calibration.OFF_NOISE: message.off_noise.value, + Calibration.OFF_TOT: message.off_tot.value, + } + # Forward calibration config to energy collection + self._energy.calibration = self._calibration + + if self._callback_CircleCalibration is not None: + self._callback_CircleCalibration() + self._callback_CircleCalibration = None + + def _process_CircleClockResponse(self, message: CircleClockResponse) -> None: + """Process content of 'CircleClockResponse' message.""" + _dt_of_circle = datetime.utcnow().replace( + hour=message.time.value.hour, + minute=message.time.value.minute, + second=message.time.value.second, + microsecond=0, + tzinfo=timezone.utc, + ) + clock_offset = message.timestamp.replace(microsecond=0) - _dt_of_circle + if clock_offset.days == -1: + self._clock_offset = clock_offset.seconds - DAY_IN_SECONDS + else: + self._clock_offset = clock_offset.seconds + _LOGGER.debug( + "Clock of node %s has drifted %s sec", + self.mac, + str(self._clock_offset), + ) + if self._callback_CircleClockResponse is not None: + self._callback_CircleClockResponse() + self._callback_CircleClockResponse = None + + def _process_CircleEnergyLogsResponse(self, message: CircleEnergyLogsResponse): + """ + Forward historical energy log information to energy counters + Each response message contains 4 log counters (slots) + of the energy pulses collected during the previous hour of given timestamp + """ + for _slot in range(4, 0, -1): + _log_timestamp = getattr(message, "logdate%d" % (_slot,)).value + _log_pulses = getattr(message, "pulses%d" % (_slot,)).value + if self._log: + _LOGGER.info( + "_process_CircleEnergyLogsResponse for %s | address=%s, slot=%s, timestamp=%s, pulses=%s", + self.mac, + str(message.logaddr.value), + str(_slot), + str(_log_timestamp), + str(_log_pulses), + ) + if _log_timestamp is not None: + _log_state: PulseLog = { + Pulses.address: message.logaddr.value, + Pulses.slot: _slot, + Pulses.timestamp: _log_timestamp, + Pulses.pulses: _log_pulses, + } + self._energy.log = _log_state + + # Update intervals + self._update_intervals() + + # Callback + if self._callback_CircleEnergyLogs.get(message.logaddr.value): + self._callback_CircleEnergyLogs[message.logaddr.value]() + del self._callback_CircleEnergyLogs[message.logaddr.value] + + def _process_CirclePowerUsageResponse( + self, message: CirclePowerUsageResponse + ) -> None: + """Process content of 'CirclePowerUsageResponse' message.""" + if self._calibration is None: + _LOGGER.warning( + "Received power update for %s before calibration information is known", + self.mac, + ) + self._request_CircleCalibration(self.update_power_usage) + return + # Power consumption last second + self._pulses_1s = self._correct_power_pulses( + message.pulse_1s.value, message.nanosecond_offset.value + ) + self.do_callback(USB.power_1s) + + # Power consumption last 8 seconds + self._pulses_8s = self._correct_power_pulses( + message.pulse_8s.value, message.nanosecond_offset.value + ) + self.do_callback(USB.power_8s) + + # Store change pulse values + _consumed = False + if self._pluses_consumed != message.pulse_counter_consumed.value: + self._pluses_consumed = message.pulse_counter_consumed.value + _consumed = True + _produced = False + if self._pluses_produced != message.pulse_counter_produced.value: + self._pluses_produced = message.pulse_counter_produced.value + _produced = True + + # Forward pulse interval counters to Energy Collection + _pulse_interval: PulseInterval = { + Pulses.timestamp: message.timestamp, + Pulses.consumption: message.pulse_counter_consumed.value, + Pulses.production: message.pulse_counter_produced.value, + } + self._energy.pulses = _pulse_interval + + # Counter update callback only if pulse value has changed + for _id in ENERGY_COUNTER_IDS: + if _consumed and self._energy.counters[_id].consumption: + self.do_callback(_id) + if _produced and not self._energy.counters[_id].consumption: + self.do_callback(_id) + if self._callback_CirclePowerUsage is not None: + self._callback_CirclePowerUsage() + self._callback_CirclePowerUsage = None + def _process_NodeResponse(self, message: NodeResponse) -> None: """Process content of 'NodeResponse' message.""" if message.ack_id == NodeResponseType.RelaySwitchedOn: @@ -290,518 +587,61 @@ def _process_NodeResponse(self, message: NodeResponse) -> None: if self._callback_ClockAccepted is not None: self._callback_ClockAccepted() self._callback_ClockAccepted = None + elif message.ack_id == NodeResponseType.PowerLogIntervalAccepted: + # Forward new intervals to energy collection + if self._log_interval_consumption_request is not None: + self._energy.interval_consumption = ( + self._log_interval_consumption_request + ) + if self._log_interval_production_request is not None: + self._energy.interval_production = self._log_interval_production_request + self._update_intervals() else: super()._process_NodeResponse(message) - def _process_CirclePowerUsageResponse( - self, message: CirclePowerUsageResponse - ) -> None: - """Process content of 'CirclePowerUsageResponse' message.""" - - # Sometimes the circle returns -1 for some of the pulse counters - # likely this means the circle measures very little power and is suffering from - # rounding errors. Zero these out. However, negative pulse values are valid - # for power producing appliances, like solar panels, so don't complain too loudly. - if not self.calibration: - _LOGGER.info( - "Received power update for %s before calibration information is known", - self.mac, - ) - self._request_calibration(self.request_power_update) - return - # Power consumption last second - if message.pulse_1s.value == -1: - message.pulse_1s.value = 0 - _LOGGER.debug( - "1 sec power pulse counter for node %s has value of -1, corrected to 0", - self.mac, - ) - self._pulses_1s = message.pulse_1s.value - if message.pulse_1s.value != 0: - if message.nanosecond_offset.value != 0: - pulses_1s = ( - message.pulse_1s.value - * (1000000000 + message.nanosecond_offset.value) - ) / 1000000000 - else: - pulses_1s = message.pulse_1s.value - self._pulses_1s = pulses_1s - else: - self._pulses_1s = 0 - self.do_callback(FEATURE_POWER_USE["id"]) - # Power consumption last 8 seconds - if message.pulse_8s.value == -1: - message.pulse_8s.value = 0 - _LOGGER.debug( - "8 sec power pulse counter for node %s has value of -1, corrected to 0", - self.mac, - ) - if message.pulse_8s.value != 0: - if message.nanosecond_offset.value != 0: - pulses_8s = ( - message.pulse_8s.value - * (1000000000 + message.nanosecond_offset.value) - ) / 1000000000 - else: - pulses_8s = message.pulse_8s.value - self._pulses_8s = pulses_8s - else: - self._pulses_8s = 0 - self.do_callback(FEATURE_POWER_USE_LAST_8_SEC["id"]) - # Power consumption current hour - if message.pulse_hour_consumed.value == -1: - _LOGGER.debug( - "1 hour consumption power pulse counter for node %s has value of -1, drop value", - self.mac, - ) - else: - self._update_energy_current_hour(message.pulse_hour_consumed.value) - # Power produced current hour - if message.pulse_hour_produced.value == -1: - message.pulse_hour_produced.value = 0 - _LOGGER.debug( - "1 hour power production pulse counter for node %s has value of -1, corrected to 0", - self.mac, - ) - if self._pulses_produced_1h != message.pulse_hour_produced.value: - self._pulses_produced_1h = message.pulse_hour_produced.value - self.do_callback(FEATURE_POWER_PRODUCTION_CURRENT_HOUR["id"]) - if self._callback_CirclePowerUsage is not None: - self._callback_CirclePowerUsage() - self._callback_CirclePowerUsage = None - - def _process_CircleCalibrationResponse( - self, message: CircleCalibrationResponse - ) -> None: - """Process content of 'CircleCalibrationResponse' message.""" - for calibration in ("gain_a", "gain_b", "off_noise", "off_tot"): - val = getattr(message, calibration).value - setattr(self, "_" + calibration, val) - self.calibration = True - - if self._callback_CircleCalibration is not None: - self._callback_CircleCalibration() - self._callback_CircleCalibration = None - - def pulses_to_kws(self, pulses, seconds=1): - """ - converts the amount of pulses to kWs using the calaboration offsets - """ - if pulses is None: - return None - if pulses == 0 or not self.calibration: - return 0.0 - pulses_per_s = pulses / float(seconds) - corrected_pulses = seconds * ( - ( - (((pulses_per_s + self._off_noise) ** 2) * self._gain_b) - + ((pulses_per_s + self._off_noise) * self._gain_a) - ) - + self._off_tot - ) - calc_value = corrected_pulses / PULSES_PER_KW_SECOND / seconds - # Fix minor miscalculations - if -0.001 < calc_value < 0.001: - calc_value = 0.0 - return calc_value - - def _collect_energy_pulses(self, start_utc: datetime, end_utc: datetime): - """Return energy pulses of given hours""" - - if start_utc == end_utc: - hours = 0 - else: - hours = int((end_utc - start_utc).seconds / 3600) - _energy_history_failed = False - _energy_pulses = 0 - for hour in range(0, hours + 1): - _log_timestamp = start_utc + timedelta(hours=hour) - if self._energy_history.get(_log_timestamp) is not None: - _energy_pulses += self._energy_history[_log_timestamp] - _LOGGER.debug( - "_collect_energy_pulses for %s | %s : %s, total = %s", - self.mac, - str(_log_timestamp), - str(self._energy_history[_log_timestamp]), - str(_energy_pulses), - ) - else: - _mem_address = self._energy_timestamp_memory_address(_log_timestamp) - _LOGGER.info( - "_collect_energy_pulses for %s at %s not found, request counter from memory %s (from mem=%s, slot=%s, timestamp=%s)", - self.mac, - str(_log_timestamp), - str(_mem_address), - str(self._last_log_address), - str(self._energy_last_populated_slot), - str(self._energy_last_collected_timestamp), + def _process_NodeInfoResponse(self, message: NodeInfoResponse) -> None: + """Process contents of 'NodeInfoResponse' message""" + _protocol_set = False + if self._protocol is None: + _protocol_set = True + super()._process_NodeInfoResponse(message) + if _protocol_set and self._protocol: + if self._protocol[1] == "2.6": + # Request the current configuration of relay state at power-up + _relay_init_request = CircleRelayInitStateRequest( + self._mac, False, False ) - self.request_energy_counters(_mem_address) - _energy_history_failed = True - # Validate all history values where present - if not _energy_history_failed: - return _energy_pulses - return None - - def _update_energy_current_hour(self, _pulses_cur_hour): - """Update energy consumption (pulses) of current hour""" - _LOGGER.info( - "_update_energy_current_hour for %s | counter = %s, update= %s", - self.mac, - str(self._energy_pulses_current_hour), - str(_pulses_cur_hour), - ) - _hour_rollover = False - if self._energy_pulses_current_hour is None: - self._energy_pulses_current_hour = _pulses_cur_hour - self.do_callback(FEATURE_POWER_CONSUMPTION_CURRENT_HOUR["id"]) - else: - if self._energy_pulses_current_hour != _pulses_cur_hour: - if self._energy_pulses_current_hour > _pulses_cur_hour: - _hour_rollover = True - self._energy_pulses_current_hour = _pulses_cur_hour - self.do_callback(FEATURE_POWER_CONSUMPTION_CURRENT_HOUR["id"]) - # Update today - self._update_energy_today_now(_hour_rollover, False, False) - - def _update_energy_today_now( - self, hour_rollover=False, history_rollover=False, day_rollover=False - ): - """Update energy consumption (pulses) of today up to now""" - - _pulses_today_now = None - - # Check for rollovers triggers - if hour_rollover and self._energy_rollover_hour_finished: - self._energy_rollover_hour_started = True - self._energy_rollover_hour_finished = False - if history_rollover and self._energy_rollover_history_finished: - self._energy_rollover_history_started = True - self._energy_rollover_history_finished = False - if day_rollover and self._energy_rollover_day_finished: - self._energy_rollover_day_started = True - self._energy_rollover_day_finished = False - # Set counter - if self._energy_rollover_hour_started: - if self._energy_rollover_history_started: - if self._energy_rollover_day_started: - # Day rollover, reset to only current hour - _pulses_today_now = self._energy_pulses_current_hour - self._energy_rollover_day_started = False - self._energy_rollover_day_finished = True - else: - # Hour rollover, reset to hour history with current hour - if ( - self._energy_pulses_today_hourly is None - or self._energy_pulses_current_hour is None - ): - _pulses_today_now = None - else: - _pulses_today_now = ( - self._energy_pulses_today_hourly - + self._energy_pulses_current_hour - ) - self._energy_rollover_hour_started = False - self._energy_rollover_hour_finished = True - self._energy_rollover_history_started = False - self._energy_rollover_history_finished = True - else: - # Wait for history_rollover, keep current counter - _pulses_today_now = None - else: - if self._energy_rollover_history_started: - # Wait for hour_rollover, keep current counter - _pulses_today_now = None - else: - # Regular update - if ( - self._energy_pulses_today_hourly is None - or self._energy_pulses_current_hour is None - ): - _pulses_today_now = None - else: - _pulses_today_now = ( - self._energy_pulses_today_hourly - + self._energy_pulses_current_hour - ) - if _pulses_today_now is None: - _LOGGER.info( - "_update_energy_today_now for %s | skip update, hour: %s=%s=%s, history: %s=%s=%s, day: %s=%s=%s", - self.mac, - str(hour_rollover), - str(self._energy_rollover_hour_started), - str(self._energy_rollover_hour_finished), - str(history_rollover), - str(self._energy_rollover_history_started), - str(self._energy_rollover_history_finished), - str(day_rollover), - str(self._energy_rollover_day_started), - str(self._energy_rollover_day_finished), - ) - else: - _LOGGER.info( - "_update_energy_today_now for %s | counter = %s, update= %s (%s + %s)", - self.mac, - str(self._energy_pulses_today_now), - str(_pulses_today_now), - str(self._energy_pulses_today_hourly), - str(self._energy_pulses_current_hour), - ) - if self._energy_pulses_today_now is None: - self._energy_pulses_today_now = _pulses_today_now - if self._energy_pulses_today_now is not None: - self.do_callback(FEATURE_ENERGY_CONSUMPTION_TODAY["id"]) - else: - if self._energy_pulses_today_now != _pulses_today_now: - self._energy_pulses_today_now = _pulses_today_now - self.do_callback(FEATURE_ENERGY_CONSUMPTION_TODAY["id"]) - - def _update_energy_previous_hour(self, prev_hour: datetime): - """Update energy consumption (pulses) of previous hour""" - _pulses_prev_hour = self._collect_energy_pulses(prev_hour, prev_hour) - _LOGGER.info( - "_update_energy_previous_hour for %s | counter = %s, update= %s, timestamp %s", - self.mac, - str(self._energy_pulses_yesterday), - str(_pulses_prev_hour), - str(prev_hour), - ) - if self._energy_pulses_prev_hour is None: - self._energy_pulses_prev_hour = _pulses_prev_hour - if self._energy_pulses_prev_hour is not None: - self.do_callback(FEATURE_POWER_CONSUMPTION_PREVIOUS_HOUR["id"]) - else: - if self._energy_pulses_prev_hour != _pulses_prev_hour: - self._energy_pulses_prev_hour = _pulses_prev_hour - self.do_callback(FEATURE_POWER_CONSUMPTION_PREVIOUS_HOUR["id"]) - - def _update_energy_yesterday( - self, start_yesterday: datetime, end_yesterday: datetime - ): - """Update energy consumption (pulses) of yesterday""" - _pulses_yesterday = self._collect_energy_pulses(start_yesterday, end_yesterday) - _LOGGER.debug( - "_update_energy_yesterday for %s | counter = %s, update= %s, range %s to %s", - self.mac, - str(self._energy_pulses_yesterday), - str(_pulses_yesterday), - str(start_yesterday), - str(end_yesterday), - ) - if self._energy_pulses_yesterday is None: - self._energy_pulses_yesterday = _pulses_yesterday - if self._energy_pulses_yesterday is not None: - self.do_callback(FEATURE_POWER_CONSUMPTION_YESTERDAY["id"]) - else: - if self._energy_pulses_yesterday != _pulses_yesterday: - self._energy_pulses_yesterday = _pulses_yesterday - self.do_callback(FEATURE_POWER_CONSUMPTION_YESTERDAY["id"]) - - def _update_energy_today_hourly(self, start_today: datetime, end_today: datetime): - """Update energy consumption (pulses) of today up to last hour""" - if start_today > end_today: - _pulses_today_hourly = 0 - else: - _pulses_today_hourly = self._collect_energy_pulses(start_today, end_today) - _LOGGER.info( - "_update_energy_today_hourly for %s | counter = %s, update= %s, range %s to %s", - self.mac, - str(self._energy_pulses_today_hourly), - str(_pulses_today_hourly), - str(start_today), - str(end_today), - ) - if self._energy_pulses_today_hourly is None: - self._energy_pulses_today_hourly = _pulses_today_hourly - if self._energy_pulses_today_hourly is not None: - self.do_callback(FEATURE_POWER_CONSUMPTION_TODAY["id"]) - else: - if self._energy_pulses_today_hourly != _pulses_today_hourly: - self._energy_pulses_today_hourly = _pulses_today_hourly - self.do_callback(FEATURE_POWER_CONSUMPTION_TODAY["id"]) - - def request_energy_counters( - self, log_address=None, callback: callable | None = None - ): - """Request power log of specified address""" - _LOGGER.debug( - "request_energy_counters for %s of address %s", self.mac, str(log_address) - ) - if log_address is None: - log_address = self._last_log_address - if log_address is not None: - if len(self._energy_history) > 48 or self._energy_history_collecting: - # Energy history already collected - if ( - log_address == self._last_log_address - and self._energy_last_populated_slot == 4 - ): - # Rollover of energy counter slot, get new memory address first - self._energy_last_populated_slot = 0 - self._request_info(self.request_energy_counters) - else: - # Request new energy counters - _log_request = CircleEnergyCountersRequest(self._mac, log_address) - _log_request.priority = Priority.Low - self.message_sender(_log_request) - else: - # Collect energy counters of today and yesterday - # Each request contains will return 4 hours, except last request - - # TODO: validate range of log_addresses - self._energy_history_collecting = True - for req_log_address in range(log_address - 13, log_address + 1): - _log_request = CircleEnergyCountersRequest( - self._mac, req_log_address - ) - _log_request.priority = Priority.Low - self.message_sender(_log_request) + _relay_init_request.priority = Priority.Low + self.message_sender(_relay_init_request) - def _process_CircleEnergyCountersResponse( - self, message: CircleEnergyCountersResponse - ) -> None: - """Process content of 'CircleEnergyCountersResponse' message.""" - - # Save historical energy information in local counters - # Each response message contains 4 log counters (slots) - # of the energy pulses collected during the previous hour of given timestamp - - if message.logaddr.value == self._last_log_address: - self._energy_last_populated_slot = 0 - # Collect energy history pulses from received log address - # Store pulse in self._energy_history using the timestamp in UTC as index - _utc_hour_timestamp = datetime.utcnow().replace( - minute=0, second=0, microsecond=0 - ) - _local_midnight_timestamp = datetime.now().replace( - hour=0, minute=0, second=0, microsecond=0 - ) - _local_hour = datetime.now().hour - _utc_midnight_timestamp = _utc_hour_timestamp - timedelta(hours=_local_hour) - _midnight_rollover = False - _history_rollover = False + def _update_intervals(self) -> None: + """Update interval features.""" - for _slot in range(1, 5): - _log_timestamp = getattr(message, "logdate%d" % (_slot,)).value - if _log_timestamp is None: - break - self._energy_history[_log_timestamp] = getattr( - message, "pulses%d" % (_slot,) - ).value - - # Store last populated _slot - if message.logaddr.value == self._last_log_address: - self._energy_last_populated_slot = _slot - # Store most recent timestamp of collected pulses - if self._energy_last_collected_timestamp < _log_timestamp: - self._energy_last_collected_timestamp = _log_timestamp - # Trigger history rollover - if ( - _log_timestamp == _utc_hour_timestamp - and self._energy_last_rollover_timestamp < _utc_hour_timestamp - ): - self._energy_last_rollover_timestamp = _utc_hour_timestamp - _history_rollover = True - _LOGGER.info( - "_process_CircleEnergyCountersResponse for %s | history rollover, reset date to %s", - self.mac, - str(_utc_hour_timestamp), - ) - # Trigger midnight rollover - if ( - _log_timestamp == _utc_midnight_timestamp - and self._energy_consumption_today_reset < _local_midnight_timestamp - ): - _LOGGER.info( - "_process_CircleEnergyCountersResponse for %s | midnight rollover, reset date to %s", - self.mac, - str(_local_midnight_timestamp), - ) - self._energy_consumption_today_reset = _local_midnight_timestamp - _midnight_rollover = True - # Reset energy collection progress + # Consumption feature if ( - self._energy_history_collecting - and len(self._energy_history) > 48 - and self._energy_last_collected_timestamp == _utc_hour_timestamp + self._log_interval_consumption is None + and self._energy.interval_consumption is not None ): - self._energy_last_rollover_timestamp = self._energy_last_collected_timestamp - self._energy_history_collecting = False - _history_rollover = False - _midnight_rollover = False + self._log_interval_consumption = self._energy.interval_consumption + self.do_callback(USB.interval_cons) else: - _LOGGER.info( - "_process_CircleEnergyCountersResponse for %s | collection not running, len=%s, timestamp:%s=%s", - self.mac, - str(len(self._energy_history)), - str(self._energy_last_collected_timestamp), - str(_utc_hour_timestamp), - ) - # Update energy counters - if not self._energy_history_collecting: - self._update_energy_previous_hour(_utc_hour_timestamp) - self._update_energy_today_hourly( - _utc_midnight_timestamp + timedelta(hours=1), - _utc_hour_timestamp, - ) - self._update_energy_yesterday( - _utc_midnight_timestamp - timedelta(hours=23), - _utc_midnight_timestamp, - ) - self._update_energy_today_now(False, _history_rollover, _midnight_rollover) - else: - _LOGGER.info( - "_process_CircleEnergyCountersResponse for %s | self._energy_history_collecting running", - self.mac, - str(_local_midnight_timestamp), - ) - # Cleanup energy history for more than 8 day's ago - _8_days_ago = datetime.utcnow().replace( - minute=0, second=0, microsecond=0 - ) - timedelta(days=8) - for log_timestamp in list(self._energy_history.keys()): - if log_timestamp < _8_days_ago: - del self._energy_history[log_timestamp] + if self._energy.interval_consumption is not None: + if self._log_interval_consumption != self._energy.interval_consumption: + self._log_interval_consumption = self._energy.interval_consumption + self.do_callback(USB.interval_cons) - def _process_CircleClockResponse(self, message: CircleClockResponse) -> None: - """Process content of 'CircleClockResponse' message.""" - log_date = datetime( - datetime.utcnow().year, - datetime.utcnow().month, - datetime.utcnow().day, - message.time.value.hour, - message.time.value.minute, - message.time.value.second, - ).replace(tzinfo=timezone.utc) - clock_offset = message.timestamp.replace(microsecond=0) - ( - log_date + self.timezone_delta - ) - if clock_offset.days == -1: - self._clock_offset = clock_offset.seconds - 86400 + # Production feature + if ( + self._log_interval_production is None + and self._energy.interval_production is not None + ): + self._log_interval_production = self._energy.interval_production + self.do_callback(USB.interval_prod) else: - self._clock_offset = clock_offset.seconds - _LOGGER.debug( - "Clock of node %s has drifted %s sec", - self.mac, - str(self._clock_offset), - ) - if self._callback_CircleClockResponse is not None: - self._callback_CircleClockResponse() - self._callback_CircleClockResponse = None - - def get_clock(self, callback: callable | None = None) -> None: - """get current datetime of internal clock of Circle.""" - self._callback_CircleClockResponse = callback - _clock_request = CircleClockGetRequest(self._mac) - _clock_request.priority = Priority.Low - self.message_sender(_clock_request) - - def set_clock(self, callback: callable | None = None) -> None: - """set internal clock of CirclePlus.""" - self._callback_ClockAccepted = callback - _clock_request = CircleClockSetRequest(self._mac, datetime.utcnow()) - _clock_request.priority = Priority.High - self.message_sender(_clock_request) + if self._energy.interval_production is not None: + if self._log_interval_production != self._energy.interval_production: + self._log_interval_production = self._energy.interval_production + self.do_callback(USB.interval_cons) def sync_clock(self, max_drift=0): """Resync clock of node if time has drifted more than MAX_TIME_DRIFT""" @@ -814,29 +654,25 @@ def sync_clock(self, max_drift=0): self.mac, str(self._clock_offset), ) - self.set_clock() + self._request_CircleClockSet() - def _energy_timestamp_memory_address(self, utc_timestamp: datetime): - """Return memory address for given energy counter timestamp""" - _utc_now_timestamp = datetime.utcnow().replace( - minute=0, second=0, microsecond=0 - ) - if utc_timestamp > _utc_now_timestamp: - return None - _seconds_offset = (_utc_now_timestamp - utc_timestamp).seconds - _hours_offset = _seconds_offset / 3600 - - _slot = self._energy_last_populated_slot - if _slot == 0: - _slot = 4 - _address = self._last_log_address - - # last known - _hours = 1 - while _hours <= _hours_offset: - _slot -= 1 - if _slot == 0: - _address -= 1 - _slot = 4 - _hours += 1 - return _address + def _correct_power_pulses(self, pulses: int, offset: int) -> float: + """Return correct pulses based on given measurement time offset (nanoseconds)""" + + # Sometimes the circle returns -1 for some of the pulse counters + # likely this means the circle measures very little power and is suffering from + # rounding errors. Zero these out. However, negative pulse values are valid + # for power producing appliances, like solar panels, so don't complain too loudly. + if pulses == -1: + _LOGGER.error( + "Power pulse counter for node %s has value of -1, corrected to 0", + self.mac, + ) + return 0 + if pulses != 0: + if offset != 0: + return ( + pulses * (SECOND_IN_NANOSECONDS + offset) + ) / SECOND_IN_NANOSECONDS + return pulses + return 0 diff --git a/plugwise/nodes/energy.py b/plugwise/nodes/energy.py new file mode 100644 index 000000000..813b235a2 --- /dev/null +++ b/plugwise/nodes/energy.py @@ -0,0 +1,1278 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from enum import Enum +import logging +from typing import TypedDict + +from ..constants import ( + DAY_IN_HOURS, + HOUR_IN_SECONDS, + LOCAL_TIMEZONE, + LOGADDR_MAX, + MINUTE_IN_SECONDS, + PULSES_PER_KW_SECOND, + USB, + WEEK_IN_HOURS, +) + +_LOGGER = logging.getLogger(__name__) + +ENERGY_COUNTERS = { + USB.hour_cons: { + "consumption": True, + "hours": 1, + }, + USB.hour_prod: { + "consumption": False, + "hours": 1, + }, + USB.day_cons: { + "consumption": True, + "hours": DAY_IN_HOURS, + }, + USB.day_prod: { + "consumption": False, + "hours": DAY_IN_HOURS, + }, + USB.week_cons: { + "consumption": True, + "hours": WEEK_IN_HOURS, + }, + USB.week_prod: { + "consumption": False, + "hours": WEEK_IN_HOURS, + }, +} +FEATURE_ENERGY_IDS = (USB.hour_cons, USB.hour_prod, USB.day_cons, USB.day_prod) +CONSUMED = True +PRODUCED = False + + +def calc_log_address(address: int, slot: int, offset: int) -> tuple: + """Calculate addess and slot for log based for specified offset""" + + # FIXME: Handle max address (max is currently unknown) to guard against address rollovers + if offset < 0: + while offset + slot < 1: + address -= 1 + offset += 4 + if offset > 0: + while offset + slot > 4: + address += 1 + offset -= 4 + return (address, slot + offset) + + +def pulses_to_kws( + pulses: int, calibration: CircleCalibration, seconds=1 +) -> float | None: + """ + converts the amount of pulses to kWs using the calaboration offsets + """ + if pulses == 0: + return 0.0 + if pulses < 0: + pulses = pulses * -1 + pulses_per_s = pulses / float(seconds) + corrected_pulses = seconds * ( + ( + ( + ((pulses_per_s + calibration[Calibration.OFF_NOISE]) ** 2) + * calibration[Calibration.GAIN_B] + ) + + ( + (pulses_per_s + calibration[Calibration.OFF_NOISE]) + * calibration[Calibration.GAIN_A] + ) + ) + + calibration[Calibration.OFF_TOT] + ) + calc_value = corrected_pulses / PULSES_PER_KW_SECOND / seconds + # Fix minor miscalculations + if -0.001 < calc_value < 0.001: + calc_value = 0.0 + return calc_value + + +class Calibration(str, Enum): + """Energy calibration strings.""" + + GAIN_A = "gain_a" + GAIN_B = "gain_b" + OFF_NOISE = "off_noise" + OFF_TOT = "off_tot" + + +class Pulses(str, Enum): + """USB Pulse strings.""" + + pulses = "pulses" + start = "start" + address = "address" + slot = "slot" + timestamp = "timestamp" + direction = "direction" + consumption = "consumption" + production = "production" + + +class CircleCalibration(TypedDict): + """Definition of a calibration for Plugwise devices (Circle, Stealth).""" + + gain_a: int + gain_b: int + off_noise: int + off_tot: int + + +class PulseStats(TypedDict): + """Pulse statistics at specific timestamp.""" + + timestamp: datetime + start: datetime + pulses: int | None + + +class PulseLogRecord(TypedDict): + """Historic pulse of specific timestamp.""" + + timestamp: datetime + pulses: int + direction: bool + + +class PulseLog(TypedDict): + """Raw energy pulses log.""" + + address: int + slot: int + timestamp: datetime + pulses: int + + +class PulseInterval(TypedDict): + """Raw energy pulses interval stats.""" + + timestamp: datetime + consumption: int + production: int + + +class EnergyCounter: + """ + Class to hold energy counter statistics. + """ + + def __init__(self, feature_id: USB, log=False) -> None: + """Initialize EnergyCounter class.""" + self._debug_log = log + self._calibration: CircleCalibration | None = None + self._consumption: bool = ENERGY_COUNTERS[feature_id]["consumption"] + self._energy: float | None = None + self._feature_id: USB = feature_id + self._last_update: datetime | None = None + self._statistics: PulseStats | None = None + self._reset: datetime = self._calc_reset(False) + self._next_reset: datetime = self._calc_reset(True) + + @property + def calibration(self) -> CircleCalibration | None: + """Return current energy calibration configration.""" + return self._calibration + + @calibration.setter + def calibration(self, calibration: CircleCalibration): + """Set energy calibration configuration.""" + self._calibration = calibration + + @property + def consumption(self) -> bool: + """ + Indicates if energy counter is consumption related. + True is consumption, False is production. + """ + return self._consumption + + @property + def energy(self) -> float | None: + """Total energy flow in kWh.""" + return self._energy + + @property + def statistics(self) -> PulseStats | None: + """Pulse statistics since last counter reset.""" + return self._statistics + + @statistics.setter + def statistics(self, statistics: PulseStats) -> None: + """Pulse statistics since last counter reset. """ + if self._statistics is None: + self._statistics = statistics + else: + if statistics[Pulses.timestamp] >= self._next_reset: + # Counter reset rollover + self._reset = self._calc_reset(False) + self._next_reset = self._calc_reset(True) + self._energy = None + else: + # Recalculate energy + if statistics[Pulses.pulses] is None or self._calibration is None: + _LOGGER.debug( + "EnergyCounter | statistics | _id=%s, pulses=%s, _calibration=%s", + str(self._feature_id), + str(statistics[Pulses.pulses]), + str(self._calibration), + ) + self._energy = None + else: + self._energy = pulses_to_kws( + statistics[Pulses.pulses], self._calibration, HOUR_IN_SECONDS + ) + + self._statistics = statistics + + @property + def reset(self) -> datetime: + """Last reset of energy counter in UTC.""" + return self._reset + + @property + def next_reset(self) -> datetime: + """Next reset of energy counter in UTC.""" + return self._next_reset + + @property + def expired(self) -> bool: + """Indicate if current energy counter reset is expired.""" + if self._next_reset < self._statistics[Pulses.timestamp]: + return False + return True + + @property + def last_update(self) -> datetime | None: + """Last update of energy counter.""" + return self._statistics[Pulses.timestamp] + + def _calc_reset(self, next_reset=True) -> datetime: + """Recalculate counter reset based on interval hours of counter and the local timezone.""" + if next_reset: + _offset = timedelta(hours=ENERGY_COUNTERS[self._feature_id]["hours"]) + else: + _offset = timedelta(hours=0) + + # Set reset to start of this hour in timezone aware timestamp + _reset = datetime.now().replace( + tzinfo=LOCAL_TIMEZONE, minute=0, second=0, microsecond=0 + ) + + # Set reset to start of day + if self._feature_id in (USB.day_cons, USB.day_prod): + _reset = _reset.replace(hour=0) + + # Respect weekday's + if self._feature_id in (USB.week_cons, USB.week_prod): + _offset = _offset - timedelta(days=_reset.weekday()) + + return _reset + _offset + + +class EnergyCollection: + """ + Class to store consumed and produced energy pulses of + the current interval and past (history log) intervals. + It calculates the consumed and produced energy (kWh) + Also calculates the interval duration + """ + + def __init__(self, energy_counter_ids: tuple[USB], log=False): + """Initialize EnergyCollection class.""" + + self._debug_log = log + self._energy_counter_ids = energy_counter_ids + self._calibration: CircleCalibration | None = None + self._counters: dict[USB, EnergyCounter] = self._initialize_counters() + + # Local pulse log related variables. + self._log: PulseLog | None = None + self._logs: dict[int, dict[int, PulseLogRecord]] | None = None + self._log_first: dict[bool, tuple(int, int, datetime) | None] = { + CONSUMED: None, + False: None, + } + self._log_second: dict[bool, tuple(int, int, datetime) | None] = { + CONSUMED: None, + False: None, + } + self._log_before_last: dict[bool, tuple(int, int, datetime) | None] = { + CONSUMED: None, + False: None, + } + self._log_last: dict[bool, tuple(int, int, datetime) | None] = { + CONSUMED: None, + False: None, + } + + self._log_consumption: bool = True + self._log_production: bool = False + + # Local pulse interval related variables. + self._pulses: PulseInterval | None = None + self._interval_pulses: dict[bool, int] = {CONSUMED: 0, False: 0} + self._interval: dict[bool, int | None] = {CONSUMED: None, False: None} + self._interval_cleanup: dict[bool, timedelta | None] = { + CONSUMED: None, + False: None, + } + self._interval_delta: dict[bool, timedelta | None] = { + CONSUMED: None, + False: None, + } + + self._max_counter_id: dict[bool, USB] = { + CONSUMED: self._get_max_counter_id(CONSUMED), + False: self._get_max_counter_id(False), + } + + # Local rollover states + self._rollover_interval_pulses: dict[bool, bool] = { + CONSUMED: False, + False: False, + } + self._rollover_interval_log: dict[bool, bool] = {CONSUMED: False, False: False} + + def _initialize_counters(self) -> dict[USB, EnergyCounter]: + """Setup counters and define max_counter_id.""" + _counters = {} + for _feature in self._energy_counter_ids: + _counters[_feature] = EnergyCounter(_feature, self._debug_log) + return _counters + + def _get_max_counter_id(self, consumption: bool) -> USB | None: + """Return counter id with largest duration""" + _max_duration = 0 + _id = None + for _feature in self._energy_counter_ids: + if ENERGY_COUNTERS[_feature][Pulses.consumption] == consumption: + if _max_duration < ENERGY_COUNTERS[_feature]["hours"]: + _max_duration = ENERGY_COUNTERS[_feature]["hours"] + _id = _feature + return _id + + @property + def calibration(self) -> CircleCalibration | None: + """Energy calibration configration.""" + return self._calibration + + @calibration.setter + def calibration(self, calibration: CircleCalibration): + """Energy calibration configuration.""" + self._calibration = calibration + + # Forward new calibration to each energy counter + for _id in self._counters: + self._counters[_id].calibration = calibration + + @property + def counters(self) -> dict[USB, EnergyCounter]: + """Statistics of all energy counters in kWh.""" + return self._counters + + @property + def interval_consumption(self) -> int | None: + """Interval in minutes between last consumption pulse logs.""" + return self._interval[CONSUMED] + + @interval_consumption.setter + def interval_consumption(self, consumption_interval: int) -> None: + """Set new interval in minutes.""" + if self._interval[CONSUMED] is not None: + if self._interval[CONSUMED] > consumption_interval: + self._interval_cleanup[CONSUMED] = timedelta( + minutes=self._interval[CONSUMED] + ) + self._interval[CONSUMED] = consumption_interval + self._interval_delta[CONSUMED] = timedelta(minutes=consumption_interval) + + @property + def interval_production(self) -> int | None: + """Interval in minutes between last production pulse logs.""" + return self._interval[PRODUCED] + + @interval_production.setter + def interval_production(self, production_interval: int) -> None: + """Set new interval in minutes.""" + if self._interval[PRODUCED] is not None: + if self._interval[PRODUCED] > production_interval: + self._interval_cleanup[PRODUCED] = timedelta( + minutes=self._interval[PRODUCED] + ) + self._interval[PRODUCED] = production_interval + self._interval_delta[PRODUCED] = timedelta(minutes=production_interval) + + @property + def log(self) -> PulseLog | None: + """Log of last collected pulses.""" + return self._log + + @log.setter + def log(self, pulse_log: PulseLog) -> None: + """Store last received PulseLog values.""" + + if self._debug_log: + _LOGGER.error( + "EnergyCollection | log.setter | START | address=%s, slot=%s, pulses=%s, timestamp=%s, duplicate=%s", + str(pulse_log[Pulses.address]), + str(pulse_log[Pulses.slot]), + str(pulse_log[Pulses.pulses]), + str(pulse_log[Pulses.timestamp]), + str( + self._log_exists(pulse_log[Pulses.address], pulse_log[Pulses.slot]) + ), + ) + + self._log = pulse_log + + # Only update if log information has not been collected before + if not self._log_exists(pulse_log[Pulses.address], pulse_log[Pulses.slot]): + _direction = CONSUMED + if pulse_log[Pulses.pulses] < 0: + _direction = False + self._log_production = True + _log: PulseLogRecord = { + Pulses.pulses: pulse_log[Pulses.pulses], + Pulses.timestamp: pulse_log[Pulses.timestamp], + Pulses.direction: _direction, + } + + # Add log record to local dict + if self._logs is None: + self._logs = {pulse_log[Pulses.address]: {pulse_log[Pulses.slot]: _log}} + elif self._logs.get(pulse_log[Pulses.address]) is None: + self._logs[pulse_log[Pulses.address]] = {pulse_log[Pulses.slot]: _log} + else: + self._logs[pulse_log[Pulses.address]][pulse_log[Pulses.slot]] = _log + + self._update_log_rollovers(_direction, pulse_log[Pulses.timestamp]) + self._update_log_states() + self._cleanup_logs() + self._update_interval_deltas(_direction) + self._update_counters(_direction) + + if self._debug_log: + _LOGGER.error( + "EnergyCollection | log.setter | FINISHED | address=%s, slot=%s, pulses=%s, timestamp=%s", + str(pulse_log[Pulses.address]), + str(pulse_log[Pulses.slot]), + str(pulse_log[Pulses.pulses]), + str(pulse_log[Pulses.timestamp]), + ) + + @property + def log_address_first(self) -> int | None: + """First known log address""" + if self._logs: + return min(self._logs.keys()) + return None + + @property + def log_address_last(self) -> int | None: + """Last known log address""" + if self._logs: + return max(self._logs.keys()) + return None + + @property + def log_collected_addresses(self) -> list[int]: + """List of collected log addresses with all slots populated.""" + _return_list = [] + if self._logs is not None: + for _address in self._logs.keys(): + if len(self._logs[_address]) == 4: + _return_list.append(_address) + return _return_list + + @property + def log_consumption_first(self) -> tuple(int, int, datetime) | None: + """Return tuple (address, slot, timestamp) of the oldest consumption log.""" + return self._log_first[CONSUMED] + + @property + def log_consumption_last(self) -> tuple(int, int, datetime) | None: + """Return tuple (address, slot, timestamp) of the most recent consumption log.""" + return self._log_last[CONSUMED] + + @property + def log_production_first(self) -> tuple(int, int, datetime) | None: + """Return tuple (address, slot, timestamp) of the oldest production log. Returns 'None' if unable to detect.""" + return self._log_first[PRODUCED] + + @property + def log_production_last(self) -> tuple(int, int, datetime) | None: + """Return tuple (address, slot, timestamp) of the most recent production log. Returns 'None' if unable to detect.""" + return self._log_last[PRODUCED] + + @property + def log_slot_first(self) -> int: + """First known slot""" + if (_address := self.log_address_first) is not None: + if self._logs[_address]: + return min(self._logs[_address].keys()) + return None + + @property + def log_slot_last(self) -> int: + """Last known slot""" + if (_address := self.log_address_last) is not None: + if self._logs[_address]: + return max(self._logs[_address].keys()) + return None + + def _next_log_timestamp(self, direction: bool) -> datetime | None: + """Return timestamp of next expected consumption log.""" + if ( + self._interval_delta[direction] is not None + and self._log_last[direction] is not None + ): + return self._log_last[direction][2] + self._interval_delta[direction] + return None + + @property + def next_log_timestamp(self) -> datetime | None: + """ + Return timestamp of next expected log. + Return None if we are unable to determine the next log. + """ + if (_next_consumption := self._next_log_timestamp(CONSUMED)) is not None: + if self._log_production: + if (_next_production := self._next_log_timestamp(False)) is not None: + if _next_consumption < _next_production: + return _next_consumption + else: + return _next_production + else: + return _next_consumption + else: + return _next_consumption + else: + if (_next_production := self._next_log_timestamp(False)) is not None: + return _next_production + return None + + @property + def missing_log_addresses(self) -> list[int] | None: + """ + List of any addres missing in current sequence. + Returns None if no logs are collected. + """ + if self._logs is None: + return None + if self._max_counter_id[CONSUMED] is None: + return None + if self._log_production and self._max_counter_id[PRODUCED] is None: + return None + if ( + self.log_address_first is None + or self.log_slot_first is None + or self.log_address_last is None + or self.log_slot_last is None + ): + return None + if self._log_last[CONSUMED] is None: + return None + if self._log_production and self._log_last[PRODUCED] is None: + return None + + # Collect any missing address in current range + _addresses = self._logs.keys() + _missing = [ + address + for address in range(min(_addresses), max(_addresses) + 1) + if address not in _addresses + ] + + # Add missing log addresses prior to first collected log + _before = self._counters[self._max_counter_id[CONSUMED]].reset + if ( + self._log_production + and _before > self._counters[self._max_counter_id[PRODUCED]].reset + ): + _before = self._counters[self._max_counter_id[PRODUCED]].reset + + for _address in self._missing_addresses_before(_before): + if _address not in _missing: + if self._debug_log: + _LOGGER.error( + "EnergyCollection | missing_log_addresses | Add before (%s) : %s", + str(_before), + str(_address), + ) + _missing.append(_address) + + # Add missing log addresses post to last collected log + for _address in self._missing_addresses_after(): + if _address not in _missing: + if self._debug_log: + _LOGGER.error( + "EnergyCollection | missing_log_addresses | Add after :%s", + str(_address), + ) + _missing.append(_address) + + return _missing + + @property + def pulses(self) -> PulseInterval | None: + """Pulses since last log reset.""" + return self._pulses + + @pulses.setter + def pulses(self, pulses: PulseInterval) -> None: + """Store last received PulseInterval values.""" + if self._debug_log: + _LOGGER.error( + "EnergyCollection | pulses.setter | START | consumption=%s, production=%s", + str(pulses[Pulses.consumption]), + str(pulses[Pulses.production]), + ) + self._pulses = pulses + self._update_interval_pulses( + pulses[Pulses.timestamp], pulses[Pulses.consumption], CONSUMED + ) + self._update_interval_pulses( + pulses[Pulses.timestamp], pulses[Pulses.production], False + ) + self._update_counters(CONSUMED) + self._update_counters(False) + + if self._debug_log: + _LOGGER.error( + "EnergyCollection | pulses.setter | FINISHED | consumption=%s, production=%s", + str(pulses[Pulses.consumption]), + str(pulses[Pulses.production]), + ) + + def _cleanup_logs(self) -> None: + """Delete expired collected logs""" + _keep_after = None + if self._interval_delta[CONSUMED] is not None: + if self._interval_cleanup[CONSUMED] is None: + _keep_after = ( + self._counters[self._max_counter_id[CONSUMED]].reset + - self._interval_delta[CONSUMED] + ) + else: + _keep_after = ( + self._counters[self._max_counter_id[CONSUMED]].reset + - self._interval_cleanup[CONSUMED] + ) + + if self._log_production and self._interval_delta[PRODUCED] is not None: + if self._interval_cleanup[PRODUCED] is None: + if ( + self._counters[self._max_counter_id[PRODUCED]].reset + - self._interval_delta[PRODUCED] + < _keep_after + ): + _keep_after = ( + self._counters[self._max_counter_id[PRODUCED]].reset + - self._interval_delta[PRODUCED] + ) + else: + if ( + self._counters[self._max_counter_id[PRODUCED]].reset + - self._interval_cleanup[PRODUCED] + < _keep_after + ): + _keep_after = ( + self._counters[self._max_counter_id[PRODUCED]].reset + - self._interval_cleanup[PRODUCED] + ) + + if _keep_after is not None: + # Do cleanup + for _address in list(self._logs): + for _slot in list(self._logs[_address]): + if self._logs[_address][_slot][Pulses.timestamp] < _keep_after: + self._logs[_address].pop(_slot) + if len(self._logs[_address]) == 0: + self._logs.pop(_address) + + def _calc_interval(self, direction: bool, recent: bool) -> timedelta | None: + """ + Returns the time interval between the two collected log for the most + recent (=True) logs based on their timestamps. + Returns None if logs are not available. + """ + if recent: + if ( + self._log_last[direction] is not None + and self._log_before_last[direction] is not None + ): + return ( + self._log_last[direction][2] - self._log_before_last[direction][2] + ) + else: + if ( + self._log_second[direction] is not None + and self._log_first[direction] is not None + ): + return self._log_second[direction][2] - self._log_first[direction][2] + return None + + def _calc_log_pulses(self, utc_start: datetime, direction: bool) -> int | None: + """Return total pulses out of logs.""" + _log_pulses = None + if self._logs is not None: + for _address in self._logs.keys(): + for _slot in self._logs[_address].keys(): + if self._logs[_address][_slot][Pulses.direction] == direction: + if self._logs[_address][_slot][Pulses.timestamp] > utc_start: + if _log_pulses is None: + _log_pulses = self._logs[_address][_slot][Pulses.pulses] + else: + _log_pulses += self._logs[_address][_slot][ + Pulses.pulses + ] + return _log_pulses + + def _calc_total_pulses(self, utc_start: datetime, direction: bool) -> int | None: + """Calculate total pulses from given point in time.""" + + # Intervalpulses have to be up-to-date to return usefull pulse value + if self._pulses is None: + return None + _log_pulses = None + + # Skip if rollover is active for either consumption or production + + if ( + self._rollover_interval_pulses[direction] + or self._rollover_interval_log[direction] + ): + if self._debug_log and direction: + _LOGGER.error( + "EnergyCollection | _calc_total_pulses | Skip | Rollover active: pulses=%s, log=%s", + str(self._rollover_interval_pulses[direction]), + str(self._rollover_interval_log[direction]), + ) + return None + else: + if self._log_last[direction] and utc_start >= self._log_last[direction][2]: + _log_pulses = 0 + + # Collect total pulses from logs + if _log_pulses is None: + _log_pulses = self._calc_log_pulses(utc_start, direction) + + if self._debug_log and direction: + _LOGGER.error( + "EnergyCollection | _calc_total_pulses | start=%s, _log_pulses=%s, _interval_pulses=%s, direction=%s", + str(utc_start), + str(_log_pulses), + str(self._interval_pulses[direction]), + str(direction), + ) + + if _log_pulses is not None and self._interval_pulses[direction] is not None: + return _log_pulses + self._interval_pulses[direction] + return None + + def _log_exists(self, _address: int, _slot: int) -> bool: + if self._logs is None or self._logs.get(_address) is None: + return False + if self._logs[_address].get(_slot) is None: + return False + return True + + def _missing_addresses_before(self, target: datetime) -> list[int]: + """Return list of any missing address(es) prior to given log timestamp.""" + _addresses = [] + + if self._logs is None: + return _addresses + + if self._log_first[CONSUMED] is None or self._log_second[CONSUMED] is None: + return _addresses + _consumption_delta = ( + self._log_second[CONSUMED][2] - self._log_first[CONSUMED][2] + ) + _consumption_ts = self._log_first[CONSUMED][2] + + if self._log_production: + # Take production logs too + if self._log_first[PRODUCED] is None or self._log_second[PRODUCED] is None: + return _addresses + _production_delta = ( + self._log_second[PRODUCED][2] - self._log_first[PRODUCED][2] + ) + _production_ts = self._log_first[PRODUCED][2] + + # Get first known address and slot to start with + if _consumption_ts < _production_ts: + _address = self._log_first[CONSUMED][0] + _slot = self._log_first[CONSUMED][1] + else: + _address = self._log_first[PRODUCED][0] + _slot = self._log_first[PRODUCED][1] + else: + _address = self._log_first[CONSUMED][0] + _slot = self._log_first[CONSUMED][1] + + while True: + _address, _slot = calc_log_address(_address, _slot, -1) + if self._log_production: + if (_production_ts - _production_delta) > ( + _consumption_ts - _consumption_delta + ): + _production_ts -= _production_delta + else: + _consumption_ts -= _consumption_delta + if _consumption_ts < target and _production_ts < target: + break + else: + # Only consumption + _consumption_ts -= _consumption_delta + if _consumption_ts < target: + break + if _address not in _addresses: + _addresses.append(_address) + + return _addresses + + def _missing_addresses_after(self) -> list[int]: + """Return list of any missing address(es) between given timestamp.""" + + _addresses = [] + if self._logs is None: + return _addresses + + if self._log_before_last[CONSUMED] is None or self._log_last[CONSUMED] is None: + return _addresses + _consumption_delta = ( + self._log_last[CONSUMED][2] - self._log_before_last[CONSUMED][2] + ) + if _consumption_delta < timedelta(minutes=1): + return _addresses + + _address = self._log_last[CONSUMED][0] + _slot = self._log_last[CONSUMED][1] + _consumption_ts = self._log_last[CONSUMED][2] + if self._log_production: + # Take production logs in account too + if ( + self._log_before_last[PRODUCED] is None + or self._log_last[PRODUCED] is None + ): + return _addresses + _production_delta = ( + self._log_last[PRODUCED][2] - self._log_before_last[PRODUCED][2] + ) + if _production_delta < timedelta(minutes=1): + return _address + _production_ts = self._log_last[PRODUCED][2] + if _consumption_ts > _production_ts: + _address = self._log_last[PRODUCED][0] + _slot = self._log_last[PRODUCED][1] + + _target = datetime.utcnow().replace(tzinfo=timezone.utc) + while True: + _address, _slot = calc_log_address(_address, _slot, 1) + if self._log_production: + if (_production_ts + _production_delta) < ( + _consumption_ts + _consumption_delta + ): + _production_ts += _production_delta + if _consumption_ts >= _target and _production_ts >= _target: + break + else: + _consumption_ts += _consumption_delta + if _consumption_ts >= _target: + break + if _address not in _addresses: + _addresses.append(_address) + + return _addresses + + def _update_counters(self, direction: bool) -> None: + """Forward new pulse statistics to given (consumption or production) energy counters.""" + if ( + not self._rollover_interval_pulses[direction] + and not self._rollover_interval_log[direction] + ): + for _id in self._counters: + + # Only update for given consumption or production + if ENERGY_COUNTERS[_id]["consumption"] == direction: + if self._update_counter(_id, direction): + # Possible counter rollover, retry using new timestamp + self._update_counter(_id, direction) + else: + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_counters | SKIP, rollover active, pulse=%s, log=%s", + str(self._rollover_interval_pulses[direction]), + str(self._rollover_interval_log[direction]), + ) + + def _update_counter(self, counter_id: USB, direction: bool) -> bool: + """ + Forward new pulse statistics to energy counter + Returns True if counter has been reset (rollover) while updating. + """ + + if ( + _tot_pulses := self._calc_total_pulses( + self._counters[counter_id].reset, direction + ) + ) is not None: + if self._debug_log and direction: + _LOGGER.error( + "EnergyCollection | _update_counter | id=%s, pulses=%s, start=%s", + str(counter_id), + str(_tot_pulses), + str(self._counters[counter_id].reset), + ) + _pulse_stats: PulseStats = { + Pulses.timestamp: self._pulses[Pulses.timestamp], + Pulses.start: self._counters[counter_id].reset, + Pulses.pulses: _tot_pulses, + } + self._counters[counter_id].statistics = _pulse_stats + if self._counters[counter_id].energy is None: + return True + else: + if self._debug_log and direction: + _LOGGER.error( + "EnergyCollection | _update_counter | _id=%s, _tot_pulses=None", + str(counter_id), + ) + return False + + def _update_interval_deltas(self, direction: bool) -> None: + """Update interval variables""" + + self._interval_delta[direction] = self._calc_interval(direction, True) + if self._interval_delta[direction] is not None: + self._interval[direction] = ( + self._interval_delta[direction].total_seconds() / MINUTE_IN_SECONDS + ) + + def _update_interval_pulses( + self, timestamp: datetime, pulses: int | None, direction: bool + ) -> None: + """Update local consumption pulse varables.""" + + if self._next_log_timestamp(direction) is None: + # Not enough logs collected yet, skip checking for rollover + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_interval_pulses | _next_interval | pulses.timestamp=%s, self._next_log_timestamp=%s, direction=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + str(direction), + ) + self._interval_pulses[direction] = pulses + self._update_counters(direction) + else: + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_interval_pulses | pulses=%s, _interval_consumption_pulses=%s, timestamp=%s, _next_interval=%s, _rollover_interval_consumption_pulses=%s, _rollover_consumption_log=%s", + str(pulses), + str(self._interval_pulses[direction]), + str(timestamp), + str(self._next_log_timestamp(direction)), + str(self._rollover_interval_pulses[direction]), + str(self._rollover_interval_log[direction]), + ) + if timestamp < self._next_log_timestamp(direction): + self._update_before_log(timestamp, pulses, direction) + else: + self._update_after_log(timestamp, pulses, direction) + + def _update_before_log( + self, timestamp: datetime, pulses: int | None, direction: bool + ) -> None: + """Process interval pulse update before next expected log""" + if ( + not self._rollover_interval_pulses[direction] + and not self._rollover_interval_log[direction] + ): + # Before expected rollover and no rollover is started, so we expect a normal increase of pulses. + # A decrease indicates a rollover prior to the expected interval timestamp. + if pulses < self._interval_pulses[direction]: + # Decrease of interval pulses => Trigger interval rollover + self._rollover_interval_pulses[direction] = True + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_before_log | Decrease => Trigger rollover | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + else: + # Increase of interval pulses => Regular update + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_before_log | Increase => Regular update | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + elif ( + self._rollover_interval_pulses[direction] + and not self._rollover_interval_log[direction] + ): + # Rollover for interval pulses is already started but no new log is received yet. + # We expect an increase of pulses as previously reset is active before. + if pulses < self._interval_pulses[direction]: + # Next decrease => Rollover already active + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_before_log | Decrease => Interval rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + else: + # Increase => Rollover already active + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_before_log | Increase => Interval rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + elif ( + not self._rollover_interval_pulses[direction] + and self._rollover_interval_log[direction] + ): + # Rollover for log is already started but rollover for interval not yet. + # We expect a decrease of pulses which completes the rollover. + if pulses < self._interval_pulses[direction]: + # Decrease => Finish rollover + self._rollover_interval_log[direction] = False + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_before_log | Decrease => Finish log rollover | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + else: + # Increase => Finish rollover + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_before_log | Increase => Log rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + else: + if self._debug_log: + _LOGGER.warning( + "EnergyCollection | _update_before_log | Unexpected state - %s,%s | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(self._rollover_interval_pulses[direction]), + str(self._rollover_interval_log[direction]), + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + + self._interval_pulses[direction] = pulses + + def _update_after_log( + self, timestamp: datetime, pulses: int | None, direction: bool + ) -> None: + if ( + not self._rollover_interval_pulses[direction] + and self._rollover_interval_log[direction] + ): + # Rollover for log is already started but rollover for interval not yet. + # We expect a decrease which finish log rollover. + if pulses < self._interval_pulses[direction]: + # Decrease => Finish rollover + self._rollover_interval_log[direction] = False + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_after_log | Decrease => Finish log rollover | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + else: + # Increase => Update + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_after_log | Increase => Log rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + elif ( + not self._rollover_interval_pulses[direction] + and not self._rollover_interval_log[direction] + ): + # After expected rollover and no log rollover is started + # A decrease indicates a rollover prior to the expected log comming in. + if pulses < self._interval_pulses[direction]: + # Decrease => Trigger rollover + self._rollover_interval_pulses[direction] = True + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_after_log | Decrease => Trigger rollover | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + else: + # Increase => Without rollover active + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_after_log | Increase => Without rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + elif ( + self._rollover_interval_pulses[direction] + and not self._rollover_interval_log[direction] + ): + # Interval rollover is started and we're after expected log rollover timestamp. + # As reset happend before, we expect a increase of pulses and wait for log rollover to happen. + if pulses < self._interval_pulses[direction]: + # Decrease => Rollover active + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_after_log | Decrease => Rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + else: + # Increase => Rollover active + self._interval_pulses[direction] = pulses + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_after_log | Increase => Rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + else: + self._rollover_interval_log[direction] = False + self._rollover_interval_pulses[direction] = False + if self._debug_log: + _LOGGER.warning( + "EnergyCollection | _update_after_log | Unexpected state reset both | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) + self._interval_pulses[direction] = pulses + + def _update_log_states(self) -> tuple(bool, bool): + """ + Update local variables for: first, second, before_last, last. + Correct the consumption or production state of collected logs too. + Each second log with same timestamp should be marked as production. + """ + self._log_first[CONSUMED] = None + self._log_second[CONSUMED] = None + self._log_before_last[CONSUMED] = None + self._log_last[CONSUMED] = None + self._log_first[PRODUCED] = None + self._log_second[PRODUCED] = None + self._log_before_last[PRODUCED] = None + self._log_last[PRODUCED] = None + + _prev_address = None + for _address in sorted(self._logs): + for _slot in sorted(self._logs[_address]): + if _prev_address is None: + _prev_address = _address + _prev_slot = _slot + _prev_timestamp = self._logs[_address][_slot][Pulses.timestamp] + # Set first log variable + if self._logs[_address][_slot][Pulses.direction]: + self._log_first[CONSUMED] = (_address, _slot, _prev_timestamp) + else: + self._log_first[PRODUCED] = (_address, _slot, _prev_timestamp) + else: + if (_address, _slot) == calc_log_address( + _prev_address, _prev_slot, 1 + ): + if ( + self._logs[_address][_slot][Pulses.timestamp] + == _prev_timestamp + ): + # Mark second energy log item with same timestamp as production + self._logs[_address][_slot][Pulses.direction] = PRODUCED + self._log_production = True + else: + if self._logs[_address][_slot][Pulses.pulses] > 0: + self._logs[_address][_slot][Pulses.direction] = CONSUMED + + _direction = self._logs[_address][_slot][Pulses.direction] + # Update local first & last log variables + # First and Second + if self._log_first[_direction] is None: + self._log_first[_direction] = ( + _address, + _slot, + self._logs[_address][_slot][Pulses.timestamp], + ) + elif self._log_second[_direction] is None: + self._log_second[_direction] = ( + _address, + _slot, + self._logs[_address][_slot][Pulses.timestamp], + ) + # Before last and last + if self._log_last[_direction] is not None: + self._log_before_last[_direction] = ( + self._log_last[_direction][0], + self._log_last[_direction][1], + self._log_last[_direction][2], + ) + self._log_last[_direction] = ( + _address, + _slot, + self._logs[_address][_slot][Pulses.timestamp], + ) + else: + self._log_last[_direction] = ( + _address, + _slot, + self._logs[_address][_slot][Pulses.timestamp], + ) + + _prev_address = _address + _prev_slot = _slot + _prev_timestamp = self._logs[_address][_slot][Pulses.timestamp] + + def _update_log_rollovers(self, direction: bool, timestamp: datetime) -> None: + """Handle log rollovers.""" + + _next_ts = self._next_log_timestamp(direction) + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_log_rollovers | START | direction=%s |cp=%s, cl=%s, pp=%s, pl=%s | check ts=%s >= next_ts=%s", + str(direction), + str(self._rollover_interval_pulses[CONSUMED]), + str(self._rollover_interval_log[CONSUMED]), + str(self._rollover_interval_pulses[PRODUCED]), + str(self._rollover_interval_log[PRODUCED]), + str(timestamp), + str(_next_ts), + ) + + if _next_ts is not None and timestamp >= _next_ts: + if ( + self._rollover_interval_pulses[direction] + and not self._rollover_interval_log[direction] + ): + # Finish interval rollover + self._rollover_interval_pulses[direction] = False + elif ( + not self._rollover_interval_pulses[direction] + and not self._rollover_interval_log[direction] + ): + # Start log rollover + self._rollover_interval_log[direction] = True + + if self._debug_log: + _LOGGER.error( + "EnergyCollection | _update_log_rollovers | FINISHED | direction=%s |cp=%s, cl=%s, pp=%s, pl=%s", + str(direction), + str(self._rollover_interval_pulses[CONSUMED]), + str(self._rollover_interval_log[CONSUMED]), + str(self._rollover_interval_pulses[PRODUCED]), + str(self._rollover_interval_log[PRODUCED]), + ) diff --git a/plugwise/stick.py b/plugwise/stick.py index 8681c7664..b01b51b9a 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -673,7 +673,8 @@ def _update_loop(self): and self._device_nodes[mac].measures_power ): # Request current power usage - self._device_nodes[mac].request_power_update() + self._device_nodes[mac].update_power_usage() + self._device_nodes[mac].update_energy_log_collection() # Sync internal clock of power measure nodes once a day if _sync_clock: self._device_nodes[mac].sync_clock() From 52150aef49d540051620b99c5297ddbd09700878 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 13:22:31 +0100 Subject: [PATCH 68/87] Add missing MESSAGE_RETRY import --- plugwise/nodes/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 1787b6779..7d173ce2b 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -4,7 +4,7 @@ from datetime import datetime import logging -from ..constants import USB, UTF8_DECODE +from ..constants import MESSAGE_RETRY, USB, UTF8_DECODE, NodeType from ..messages.requests import ( NodeFeaturesRequest, NodeInfoRequest, From 25a2d229553a6e85ad21db204794a8bf6856bf48 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 13:28:12 +0100 Subject: [PATCH 69/87] Reorder and cleanup do... methods --- plugwise/nodes/__init__.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 7d173ce2b..51eb36993 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -154,12 +154,25 @@ def rssi_out(self) -> int: return self._rssi_out return 0 - def do_ping(self, forced=False, callback: callable | None = None) -> None: + def do_callback(self, sensor: USB) -> None: + """Execute callbacks registered for specified callback type.""" + if sensor in self._callbacks: + for callback in self._callbacks[sensor]: + try: + callback(None) + # TODO: narrow exception + except Exception as err: # pylint: disable=broad-except + _LOGGER.error( + "Error while executing callback : %s", + err, + ) + + def do_ping(self, callback: callable | None = None) -> None: """Send network ping message to node.""" - if forced or USB.ping in self._callbacks: + if USB.ping in self._callbacks: self._request_NodePing(callback) - def _request_features(self, callback: callable | None = None) -> None: + def _request_NodeFeatures(self, callback: callable | None = None) -> None: """Request supported features for this node.""" self._callback_NodeFeature = callback self.message_sender( @@ -217,19 +230,6 @@ def unsubscribe_callback(self, callback: callable, sensor: str): if sensor in self._callbacks: self._callbacks[sensor].remove(callback) - def do_callback(self, sensor): - """Execute callbacks registered for specified callback type.""" - if sensor in self._callbacks: - for callback in self._callbacks[sensor]: - try: - callback(None) - # TODO: narrow exception - except Exception as err: # pylint: disable=broad-except - _LOGGER.error( - "Error while executing all callback : %s", - err, - ) - def _process_NodePingResponse(self, message: NodePingResponse) -> None: """Process content of 'NodePingResponse' message.""" if self._rssi_in != message.rssi_in.value: From 59e858d7bb962ad98cb61c89df85e977aeda10cf Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 13:32:03 +0100 Subject: [PATCH 70/87] Guard for none discovered node --- plugwise/stick.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/plugwise/stick.py b/plugwise/stick.py index b01b51b9a..b66b5e300 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -489,15 +489,16 @@ def _process_NodeInfoResponse(self, message: NodeInfoResponse): if mac in self._nodes_not_discovered: self._nodes_not_discovered.remove(mac) else: - _LOGGER.info( - "Node with mac %s discovered", - mac, - ) - self._append_node( - mac, - self._nodes_to_discover[mac], - message.node_type.value, - ) + if mac in self._nodes_to_discover: + _LOGGER.info( + "Node with mac %s discovered", + mac, + ) + self._append_node( + mac, + self._nodes_to_discover[mac], + message.node_type.value, + ) self._pass_message_to_node(message) if mac in self._callback_NodeInfo.keys(): From 31767e0a1ff81b07f00173f275c7eee72ef2d105 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 13:33:05 +0100 Subject: [PATCH 71/87] Correct comments --- plugwise/nodes/__init__.py | 2 +- plugwise/nodes/switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise/nodes/__init__.py b/plugwise/nodes/__init__.py index 51eb36993..6b78486bb 100644 --- a/plugwise/nodes/__init__.py +++ b/plugwise/nodes/__init__.py @@ -115,7 +115,7 @@ def hardware_version(self) -> str: @property def last_update(self) -> datetime: - """Return datetime of last received update.""" + """Return datetime of last received update in UTC.""" return self._last_update @property diff --git a/plugwise/nodes/switch.py b/plugwise/nodes/switch.py index 626747f70..bdbb489d1 100644 --- a/plugwise/nodes/switch.py +++ b/plugwise/nodes/switch.py @@ -25,7 +25,7 @@ def switch(self) -> bool: return self._switch_state def message_for_node(self, message: PlugwiseResponse) -> None: - """Process received messages for PlugwiseSense class.""" + """Process received messages for PlugwiseSwitch class.""" self.available = True self._last_update = message.timestamp if isinstance(message, NodeSwitchGroupResponse): From 31133b3dd44db555808b2b0065861120c8eb11d8 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 15:53:57 +0100 Subject: [PATCH 72/87] Cleanup logging --- plugwise/nodes/circle.py | 98 ++++------- plugwise/nodes/energy.py | 351 ++++++++++++++++----------------------- 2 files changed, 177 insertions(+), 272 deletions(-) diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index cb7d39568..9f9aa4627 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -98,7 +98,7 @@ def __init__(self, mac: str, address: int, message_sender: callable): self._pulses_8s: float | None = None self._pluses_consumed: int = 0 self._pluses_produced: int = 0 - self._energy = EnergyCollection(ENERGY_COUNTER_IDS, self._log) + self._energy = EnergyCollection(ENERGY_COUNTER_IDS) # local log duration interval variables self._log_interval_consumption: int | None = None @@ -277,55 +277,38 @@ def update_energy_log_collection(self) -> None: _missing_addresses = self._energy.missing_log_addresses if _missing_addresses is not None: - if self._log: - _LOGGER.error( - "update_energy_log_collection for %s | Request missing | missing=%s, self._energy.next_log_timestamp=%s", - self.mac, - str(_missing_addresses), - str(self._energy.next_log_timestamp), - ) for _address in _missing_addresses: self._request_CircleEnergyLogs(_address) - else: - # Less than two full log addresses has been collected. Request logs stored at last 4 addresses - if self._info_last_timestamp > datetime.utcnow().replace( - tzinfo=timezone.utc - ) - timedelta(minutes=1): - # Recent node info, so do an initial request for last 10 log addresses - if self._log: - _LOGGER.error( - "update_energy_log_collection for %s | Request initial | _info_last_timestamp=%s, self._info_last_log_address=%s", - self.mac, - str(self._info_last_timestamp), - str(self._info_last_log_address), - ) - for _address in range( - self._info_last_log_address, - self._info_last_log_address - 11, - -1, - ): - self._request_CircleEnergyLogs(_address) - elif self._info_last_timestamp < datetime.utcnow().replace( - tzinfo=timezone.utc - ) - timedelta(minutes=15): - # node request older than 15 minutes, do node info request first - if self._log: - _LOGGER.error( - "update_energy_log_collection for %s | Request node info | _info_last_timestamp=%s, self._info_last_log_address=%s", - self.mac, - str(self._info_last_timestamp), - str(self._info_last_log_address), - ) - self._request_NodeInfo(self.update_energy_log_collection) - else: - if self._log: - _LOGGER.error( - "update_energy_log_collection for %s | Skip initial | _info_last_timestamp=%s", - self.mac, - str(self._info_last_timestamp), - ) - if self._log: - _LOGGER.error("update_energy_log_collection for %s | Finished", self.mac) + return + + # Less than two full log addresses has been collected. Request logs stored at last 4 addresses + if self._info_last_timestamp > datetime.utcnow().replace( + tzinfo=timezone.utc + ) - timedelta(minutes=1): + # Recent node info, so do an initial request for last 10 log addresses + _LOGGER.debug( + "update_energy_log_collection for %s | Request initial | _info_last_timestamp=%s, self._info_last_log_address=%s", + self.mac, + str(self._info_last_timestamp), + str(self._info_last_log_address), + ) + for _address in range( + self._info_last_log_address, + self._info_last_log_address - 11, + -1, + ): + self._request_CircleEnergyLogs(_address) + elif self._info_last_timestamp < datetime.utcnow().replace( + tzinfo=timezone.utc + ) - timedelta(minutes=15): + # node request older than 15 minutes, do node info request first + _LOGGER.debug( + "update_energy_log_collection for %s | Request node info | _info_last_timestamp=%s, self._info_last_log_address=%s", + self.mac, + str(self._info_last_timestamp), + str(self._info_last_log_address), + ) + self._request_NodeInfo(self.update_energy_log_collection) def _request_CircleCalibration(self, callback: callable | None = None) -> None: """Request calibration info""" @@ -350,21 +333,11 @@ def _request_CircleEnergyLogs( self, address: int, callback: callable | None = None ) -> None: """Request energy counters for given memory address""" - if self._log: - _LOGGER.error( - "_request_CircleEnergyLogs for %s | address=%s", self.mac, str(address) - ) if address not in self._energy.log_collected_addresses: self._callback_CircleEnergyLogs[address] = callback _request = CircleEnergyLogsRequest(self._mac, address) _request.priority = Priority.Low self.message_sender(_request) - if self._log: - _LOGGER.error( - "_request_CircleEnergyLogs for %s | SEND address=%s", - self.mac, - str(address), - ) def _request_CircleMeasureInterval( self, @@ -472,15 +445,6 @@ def _process_CircleEnergyLogsResponse(self, message: CircleEnergyLogsResponse): for _slot in range(4, 0, -1): _log_timestamp = getattr(message, "logdate%d" % (_slot,)).value _log_pulses = getattr(message, "pulses%d" % (_slot,)).value - if self._log: - _LOGGER.info( - "_process_CircleEnergyLogsResponse for %s | address=%s, slot=%s, timestamp=%s, pulses=%s", - self.mac, - str(message.logaddr.value), - str(_slot), - str(_log_timestamp), - str(_log_pulses), - ) if _log_timestamp is not None: _log_state: PulseLog = { Pulses.address: message.logaddr.value, diff --git a/plugwise/nodes/energy.py b/plugwise/nodes/energy.py index 813b235a2..3050997e6 100644 --- a/plugwise/nodes/energy.py +++ b/plugwise/nodes/energy.py @@ -164,9 +164,8 @@ class EnergyCounter: Class to hold energy counter statistics. """ - def __init__(self, feature_id: USB, log=False) -> None: + def __init__(self, feature_id: USB) -> None: """Initialize EnergyCounter class.""" - self._debug_log = log self._calibration: CircleCalibration | None = None self._consumption: bool = ENERGY_COUNTERS[feature_id]["consumption"] self._energy: float | None = None @@ -285,10 +284,8 @@ class EnergyCollection: Also calculates the interval duration """ - def __init__(self, energy_counter_ids: tuple[USB], log=False): + def __init__(self, energy_counter_ids: tuple[USB]): """Initialize EnergyCollection class.""" - - self._debug_log = log self._energy_counter_ids = energy_counter_ids self._calibration: CircleCalibration | None = None self._counters: dict[USB, EnergyCounter] = self._initialize_counters() @@ -345,7 +342,7 @@ def _initialize_counters(self) -> dict[USB, EnergyCounter]: """Setup counters and define max_counter_id.""" _counters = {} for _feature in self._energy_counter_ids: - _counters[_feature] = EnergyCounter(_feature, self._debug_log) + _counters[_feature] = EnergyCounter(_feature) return _counters def _get_max_counter_id(self, consumption: bool) -> USB | None: @@ -418,18 +415,14 @@ def log(self) -> PulseLog | None: @log.setter def log(self, pulse_log: PulseLog) -> None: """Store last received PulseLog values.""" - - if self._debug_log: - _LOGGER.error( - "EnergyCollection | log.setter | START | address=%s, slot=%s, pulses=%s, timestamp=%s, duplicate=%s", - str(pulse_log[Pulses.address]), - str(pulse_log[Pulses.slot]), - str(pulse_log[Pulses.pulses]), - str(pulse_log[Pulses.timestamp]), - str( - self._log_exists(pulse_log[Pulses.address], pulse_log[Pulses.slot]) - ), - ) + _LOGGER.debug( + "EnergyCollection | log.setter | address=%s, slot=%s, pulses=%s, timestamp=%s, duplicate=%s", + str(pulse_log[Pulses.address]), + str(pulse_log[Pulses.slot]), + str(pulse_log[Pulses.pulses]), + str(pulse_log[Pulses.timestamp]), + str(self._log_exists(pulse_log[Pulses.address], pulse_log[Pulses.slot])), + ) self._log = pulse_log @@ -459,15 +452,6 @@ def log(self, pulse_log: PulseLog) -> None: self._update_interval_deltas(_direction) self._update_counters(_direction) - if self._debug_log: - _LOGGER.error( - "EnergyCollection | log.setter | FINISHED | address=%s, slot=%s, pulses=%s, timestamp=%s", - str(pulse_log[Pulses.address]), - str(pulse_log[Pulses.slot]), - str(pulse_log[Pulses.pulses]), - str(pulse_log[Pulses.timestamp]), - ) - @property def log_address_first(self) -> int | None: """First known log address""" @@ -601,22 +585,11 @@ def missing_log_addresses(self) -> list[int] | None: for _address in self._missing_addresses_before(_before): if _address not in _missing: - if self._debug_log: - _LOGGER.error( - "EnergyCollection | missing_log_addresses | Add before (%s) : %s", - str(_before), - str(_address), - ) _missing.append(_address) # Add missing log addresses post to last collected log for _address in self._missing_addresses_after(): if _address not in _missing: - if self._debug_log: - _LOGGER.error( - "EnergyCollection | missing_log_addresses | Add after :%s", - str(_address), - ) _missing.append(_address) return _missing @@ -629,12 +602,11 @@ def pulses(self) -> PulseInterval | None: @pulses.setter def pulses(self, pulses: PulseInterval) -> None: """Store last received PulseInterval values.""" - if self._debug_log: - _LOGGER.error( - "EnergyCollection | pulses.setter | START | consumption=%s, production=%s", - str(pulses[Pulses.consumption]), - str(pulses[Pulses.production]), - ) + _LOGGER.debug( + "EnergyCollection | pulses.setter | consumption=%s, production=%s", + str(pulses[Pulses.consumption]), + str(pulses[Pulses.production]), + ) self._pulses = pulses self._update_interval_pulses( pulses[Pulses.timestamp], pulses[Pulses.consumption], CONSUMED @@ -645,13 +617,6 @@ def pulses(self, pulses: PulseInterval) -> None: self._update_counters(CONSUMED) self._update_counters(False) - if self._debug_log: - _LOGGER.error( - "EnergyCollection | pulses.setter | FINISHED | consumption=%s, production=%s", - str(pulses[Pulses.consumption]), - str(pulses[Pulses.production]), - ) - def _cleanup_logs(self) -> None: """Delete expired collected logs""" _keep_after = None @@ -750,12 +715,11 @@ def _calc_total_pulses(self, utc_start: datetime, direction: bool) -> int | None self._rollover_interval_pulses[direction] or self._rollover_interval_log[direction] ): - if self._debug_log and direction: - _LOGGER.error( - "EnergyCollection | _calc_total_pulses | Skip | Rollover active: pulses=%s, log=%s", - str(self._rollover_interval_pulses[direction]), - str(self._rollover_interval_log[direction]), - ) + _LOGGER.debug( + "EnergyCollection | _calc_total_pulses | Skip | Rollover active: pulses=%s, log=%s", + str(self._rollover_interval_pulses[direction]), + str(self._rollover_interval_log[direction]), + ) return None else: if self._log_last[direction] and utc_start >= self._log_last[direction][2]: @@ -764,15 +728,13 @@ def _calc_total_pulses(self, utc_start: datetime, direction: bool) -> int | None # Collect total pulses from logs if _log_pulses is None: _log_pulses = self._calc_log_pulses(utc_start, direction) - - if self._debug_log and direction: - _LOGGER.error( - "EnergyCollection | _calc_total_pulses | start=%s, _log_pulses=%s, _interval_pulses=%s, direction=%s", - str(utc_start), - str(_log_pulses), - str(self._interval_pulses[direction]), - str(direction), - ) + _LOGGER.debug( + "EnergyCollection | _calc_total_pulses | start=%s, _log_pulses=%s, _interval_pulses=%s, direction=%s", + str(utc_start), + str(_log_pulses), + str(self._interval_pulses[direction]), + str(direction), + ) if _log_pulses is not None and self._interval_pulses[direction] is not None: return _log_pulses + self._interval_pulses[direction] @@ -908,12 +870,11 @@ def _update_counters(self, direction: bool) -> None: # Possible counter rollover, retry using new timestamp self._update_counter(_id, direction) else: - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_counters | SKIP, rollover active, pulse=%s, log=%s", - str(self._rollover_interval_pulses[direction]), - str(self._rollover_interval_log[direction]), - ) + _LOGGER.debug( + "EnergyCollection | _update_counters | SKIP, rollover active, pulse=%s, log=%s", + str(self._rollover_interval_pulses[direction]), + str(self._rollover_interval_log[direction]), + ) def _update_counter(self, counter_id: USB, direction: bool) -> bool: """ @@ -926,13 +887,12 @@ def _update_counter(self, counter_id: USB, direction: bool) -> bool: self._counters[counter_id].reset, direction ) ) is not None: - if self._debug_log and direction: - _LOGGER.error( - "EnergyCollection | _update_counter | id=%s, pulses=%s, start=%s", - str(counter_id), - str(_tot_pulses), - str(self._counters[counter_id].reset), - ) + _LOGGER.debug( + "EnergyCollection | _update_counter | id=%s, pulses=%s, start=%s", + str(counter_id), + str(_tot_pulses), + str(self._counters[counter_id].reset), + ) _pulse_stats: PulseStats = { Pulses.timestamp: self._pulses[Pulses.timestamp], Pulses.start: self._counters[counter_id].reset, @@ -942,11 +902,10 @@ def _update_counter(self, counter_id: USB, direction: bool) -> bool: if self._counters[counter_id].energy is None: return True else: - if self._debug_log and direction: - _LOGGER.error( - "EnergyCollection | _update_counter | _id=%s, _tot_pulses=None", - str(counter_id), - ) + _LOGGER.debug( + "EnergyCollection | _update_counter | _id=%s, _tot_pulses=None", + str(counter_id), + ) return False def _update_interval_deltas(self, direction: bool) -> None: @@ -965,26 +924,24 @@ def _update_interval_pulses( if self._next_log_timestamp(direction) is None: # Not enough logs collected yet, skip checking for rollover - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_interval_pulses | _next_interval | pulses.timestamp=%s, self._next_log_timestamp=%s, direction=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - str(direction), - ) + _LOGGER.debug( + "EnergyCollection | _update_interval_pulses | _next_interval | pulses.timestamp=%s, self._next_log_timestamp=%s, direction=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + str(direction), + ) self._interval_pulses[direction] = pulses self._update_counters(direction) else: - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_interval_pulses | pulses=%s, _interval_consumption_pulses=%s, timestamp=%s, _next_interval=%s, _rollover_interval_consumption_pulses=%s, _rollover_consumption_log=%s", - str(pulses), - str(self._interval_pulses[direction]), - str(timestamp), - str(self._next_log_timestamp(direction)), - str(self._rollover_interval_pulses[direction]), - str(self._rollover_interval_log[direction]), - ) + _LOGGER.debug( + "EnergyCollection | _update_interval_pulses | pulses=%s, _interval_consumption_pulses=%s, timestamp=%s, _next_interval=%s, _rollover_interval_consumption_pulses=%s, _rollover_consumption_log=%s", + str(pulses), + str(self._interval_pulses[direction]), + str(timestamp), + str(self._next_log_timestamp(direction)), + str(self._rollover_interval_pulses[direction]), + str(self._rollover_interval_log[direction]), + ) if timestamp < self._next_log_timestamp(direction): self._update_before_log(timestamp, pulses, direction) else: @@ -1003,20 +960,18 @@ def _update_before_log( if pulses < self._interval_pulses[direction]: # Decrease of interval pulses => Trigger interval rollover self._rollover_interval_pulses[direction] = True - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_before_log | Decrease => Trigger rollover | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - ) + _LOGGER.debug( + "EnergyCollection | _update_before_log | Decrease => Trigger rollover | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) else: # Increase of interval pulses => Regular update - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_before_log | Increase => Regular update | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - ) + _LOGGER.debug( + "EnergyCollection | _update_before_log | Increase => Regular update | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) elif ( self._rollover_interval_pulses[direction] and not self._rollover_interval_log[direction] @@ -1025,20 +980,18 @@ def _update_before_log( # We expect an increase of pulses as previously reset is active before. if pulses < self._interval_pulses[direction]: # Next decrease => Rollover already active - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_before_log | Decrease => Interval rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - ) + _LOGGER.debug( + "EnergyCollection | _update_before_log | Decrease => Interval rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) else: # Increase => Rollover already active - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_before_log | Increase => Interval rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - ) + _LOGGER.debug( + "EnergyCollection | _update_before_log | Increase => Interval rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) elif ( not self._rollover_interval_pulses[direction] and self._rollover_interval_log[direction] @@ -1048,29 +1001,26 @@ def _update_before_log( if pulses < self._interval_pulses[direction]: # Decrease => Finish rollover self._rollover_interval_log[direction] = False - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_before_log | Decrease => Finish log rollover | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - ) + _LOGGER.debug( + "EnergyCollection | _update_before_log | Decrease => Finish log rollover | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) else: # Increase => Finish rollover - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_before_log | Increase => Log rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - ) - else: - if self._debug_log: - _LOGGER.warning( - "EnergyCollection | _update_before_log | Unexpected state - %s,%s | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(self._rollover_interval_pulses[direction]), - str(self._rollover_interval_log[direction]), + _LOGGER.debug( + "EnergyCollection | _update_before_log | Increase => Log rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", str(timestamp), str(self._next_log_timestamp(direction)), ) + else: + _LOGGER.warning( + "EnergyCollection | _update_before_log | Unexpected state - %s,%s | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(self._rollover_interval_pulses[direction]), + str(self._rollover_interval_log[direction]), + str(timestamp), + str(self._next_log_timestamp(direction)), + ) self._interval_pulses[direction] = pulses @@ -1086,20 +1036,18 @@ def _update_after_log( if pulses < self._interval_pulses[direction]: # Decrease => Finish rollover self._rollover_interval_log[direction] = False - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_after_log | Decrease => Finish log rollover | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - ) + _LOGGER.debug( + "EnergyCollection | _update_after_log | Decrease => Finish log rollover | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) else: # Increase => Update - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_after_log | Increase => Log rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - ) + _LOGGER.debug( + "EnergyCollection | _update_after_log | Increase => Log rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) elif ( not self._rollover_interval_pulses[direction] and not self._rollover_interval_log[direction] @@ -1109,20 +1057,18 @@ def _update_after_log( if pulses < self._interval_pulses[direction]: # Decrease => Trigger rollover self._rollover_interval_pulses[direction] = True - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_after_log | Decrease => Trigger rollover | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - ) + _LOGGER.debug( + "EnergyCollection | _update_after_log | Decrease => Trigger rollover | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) else: # Increase => Without rollover active - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_after_log | Increase => Without rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - ) + _LOGGER.debug( + "EnergyCollection | _update_after_log | Increase => Without rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) elif ( self._rollover_interval_pulses[direction] and not self._rollover_interval_log[direction] @@ -1131,30 +1077,27 @@ def _update_after_log( # As reset happend before, we expect a increase of pulses and wait for log rollover to happen. if pulses < self._interval_pulses[direction]: # Decrease => Rollover active - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_after_log | Decrease => Rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - ) + _LOGGER.debug( + "EnergyCollection | _update_after_log | Decrease => Rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) else: # Increase => Rollover active self._interval_pulses[direction] = pulses - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_after_log | Increase => Rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", - str(timestamp), - str(self._next_log_timestamp(direction)), - ) - else: - self._rollover_interval_log[direction] = False - self._rollover_interval_pulses[direction] = False - if self._debug_log: - _LOGGER.warning( - "EnergyCollection | _update_after_log | Unexpected state reset both | pulses.timestamp=%s, self._next_log_timestamp=%s", + _LOGGER.debug( + "EnergyCollection | _update_after_log | Increase => Rollover active | pulses.timestamp=%s, self._next_log_timestamp=%s", str(timestamp), str(self._next_log_timestamp(direction)), ) + else: + self._rollover_interval_log[direction] = False + self._rollover_interval_pulses[direction] = False + _LOGGER.warning( + "EnergyCollection | _update_after_log | Unexpected state reset both | pulses.timestamp=%s, self._next_log_timestamp=%s", + str(timestamp), + str(self._next_log_timestamp(direction)), + ) self._interval_pulses[direction] = pulses def _update_log_states(self) -> tuple(bool, bool): @@ -1241,17 +1184,16 @@ def _update_log_rollovers(self, direction: bool, timestamp: datetime) -> None: """Handle log rollovers.""" _next_ts = self._next_log_timestamp(direction) - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_log_rollovers | START | direction=%s |cp=%s, cl=%s, pp=%s, pl=%s | check ts=%s >= next_ts=%s", - str(direction), - str(self._rollover_interval_pulses[CONSUMED]), - str(self._rollover_interval_log[CONSUMED]), - str(self._rollover_interval_pulses[PRODUCED]), - str(self._rollover_interval_log[PRODUCED]), - str(timestamp), - str(_next_ts), - ) + _LOGGER.debug( + "EnergyCollection | _update_log_rollovers | START | direction=%s | cp=%s, cl=%s, pp=%s, pl=%s | check ts=%s >= next_ts=%s", + str(direction), + str(self._rollover_interval_pulses[CONSUMED]), + str(self._rollover_interval_log[CONSUMED]), + str(self._rollover_interval_pulses[PRODUCED]), + str(self._rollover_interval_log[PRODUCED]), + str(timestamp), + str(_next_ts), + ) if _next_ts is not None and timestamp >= _next_ts: if ( @@ -1267,12 +1209,11 @@ def _update_log_rollovers(self, direction: bool, timestamp: datetime) -> None: # Start log rollover self._rollover_interval_log[direction] = True - if self._debug_log: - _LOGGER.error( - "EnergyCollection | _update_log_rollovers | FINISHED | direction=%s |cp=%s, cl=%s, pp=%s, pl=%s", - str(direction), - str(self._rollover_interval_pulses[CONSUMED]), - str(self._rollover_interval_log[CONSUMED]), - str(self._rollover_interval_pulses[PRODUCED]), - str(self._rollover_interval_log[PRODUCED]), - ) + _LOGGER.debug( + "EnergyCollection | _update_log_rollovers | FINISHED | direction=%s | cp=%s, cl=%s, pp=%s, pl=%s", + str(direction), + str(self._rollover_interval_pulses[CONSUMED]), + str(self._rollover_interval_log[CONSUMED]), + str(self._rollover_interval_pulses[PRODUCED]), + str(self._rollover_interval_log[PRODUCED]), + ) From dbd40d7f18aeb64628716dd583eab06c11f7c119 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 15:54:38 +0100 Subject: [PATCH 73/87] Fix mac validation at response messages --- plugwise/messages/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/messages/responses.py b/plugwise/messages/responses.py index 22b72b84b..6f9a33e0b 100644 --- a/plugwise/messages/responses.py +++ b/plugwise/messages/responses.py @@ -146,7 +146,7 @@ def deserialize(self, response: bytes) -> None: _args = b"".join(a.serialize() for a in self.args) msg = self.ID - if self.mac != "": + if self.mac is not None: msg += self.mac msg += _args From 2a45257eaffffff362d22779098275d18b6890c2 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 17:07:51 +0100 Subject: [PATCH 74/87] Remove duplicate logging --- plugwise/controller.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index 78f6c7865..7a9cae432 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -274,13 +274,6 @@ def _log_status_of_request(self, seq_id: bytes) -> None: str(seq_id), ) elif self._pending_request[seq_id].stick_state == StickResponseType.timeout: - if not isinstance(self._pending_request, NodePingRequest): - _LOGGER.warning( - "Stick 'time out' received for %s%s with seq_id=%s, retry request", - self._pending_request[seq_id].__class__.__name__, - _target, - str(seq_id), - ) _request = self._pending_request[seq_id] _request.stick_state = None self._pending_request[seq_id].finished = True From c28eb7fd2b8c5b9c0ff86a43c624591118a9f538 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 17:12:08 +0100 Subject: [PATCH 75/87] Apply formatting to CHANGELOG.md --- CHANGELOG.md | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afdd60b1c..7010743a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,33 +1,37 @@ # Changelog # v0.16.0 - Smile - Change output format, allowing full use of Core DataUpdateCoordintor in plugwise-beta - - Change from list- to dict-format for binary_sensors, sensors and switches - - Provide gateway-devices for Legacy Anna and Stretch - - Code-optimizations + +- Change from list- to dict-format for binary_sensors, sensors and switches +- Provide gateway-devices for Legacy Anna and Stretch +- Code-optimizations # v0.15.7 - Smile - Improve implementation of cooling-function-detection - - Anna: add two sensors related to automatic switching between heating and cooling and add a heating/cooling-mode active indication - - Adam: also provide a heating/cooling-mode active indication - - Fixing #171 - - Improved dependency handling (@dependabot) + +- Anna: add two sensors related to automatic switching between heating and cooling and add a heating/cooling-mode active indication +- Adam: also provide a heating/cooling-mode active indication +- Fixing #171 +- Improved dependency handling (@dependabot) # v0.15.6 - Smile - Various fixes and improvements - - Adam: collect `control_state` from master thermostats, allows showing the thermostat state as on the Plugwise App - - Adam: collect `allowed_modes` and look for `cooling`, indicating cooling capability being available - - Optimize code: use `_all_appliances()` once instead of 3 times, by updating/changing `single_master_thermostat()`, - - Protect several more variables, - - Change/improve how `illuminance` and `outdoor_temperature` are obtained, - - Use walrus operator where applicable, - - Various small code improvements, - - Add and adapt testcode - - Add testing for python 3.10, improve dependencies (github workflow) - - Bump aiohttp to 3.8.1, remove fixed dependencies + +- Adam: collect `control_state` from master thermostats, allows showing the thermostat state as on the Plugwise App +- Adam: collect `allowed_modes` and look for `cooling`, indicating cooling capability being available +- Optimize code: use `_all_appliances()` once instead of 3 times, by updating/changing `single_master_thermostat()`, +- Protect several more variables, +- Change/improve how `illuminance` and `outdoor_temperature` are obtained, +- Use walrus operator where applicable, +- Various small code improvements, +- Add and adapt testcode +- Add testing for python 3.10, improve dependencies (github workflow) +- Bump aiohttp to 3.8.1, remove fixed dependencies # v0.15.5 - Skipping, not released ## v0.15.4 - Smile - Bugfix: handle removed thermostats - - Recognize when a thermostat has been removed from a zone and don't show it in Core - - Rename Group Switch to Switchgroup, remove vendor name + +- Recognize when a thermostat has been removed from a zone and don't show it in Core +- Rename Group Switch to Switchgroup, remove vendor name ## v0.15.3 - Skipping, not released @@ -48,7 +52,7 @@ ## v0.14.1 - Smile: removing further `last_reset`s - - As per https://developers.home-assistant.io/blog/2021/08/16/state_class_total +- As per ## v0.14.0 - Smile: sensor-platform updates - 2021.9 compatible From d5eb125ad7b8d0b0a1cf6359fdcc96c2fd1f5cf4 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 17:29:20 +0100 Subject: [PATCH 76/87] Update CHANGELOG.md --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7010743a5..bcbaef53b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +# v0.17.x - Stick - Improve performance and energy monitoring + +- Improved timeout message handling based on response messages of USB-stick +- More frequent energy monitoring polling when Stick is idle +- Full rewrite of energy collection code. No dependency to fixed log interval. Should fix [#149]https://github.com/plugwise/plugwise-beta/issues/149 and [157]https://github.com/plugwise/plugwise-beta/issues/157 +- Added ability to change energy log interval +- Added full support for energy production [#39](https://github.com/plugwise/python-plugwise/issues/39) + # v0.16.0 - Smile - Change output format, allowing full use of Core DataUpdateCoordintor in plugwise-beta - Change from list- to dict-format for binary_sensors, sensors and switches From 64d4a629cdb0d3bf3a724e3387bef28bbb220ccd Mon Sep 17 00:00:00 2001 From: autoblack Date: Sun, 9 Jan 2022 16:30:26 +0000 Subject: [PATCH 77/87] fixup: USB_energy_refactor Python code reformatted using Black --- plugwise/nodes/energy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/nodes/energy.py b/plugwise/nodes/energy.py index 3050997e6..30bcd395c 100644 --- a/plugwise/nodes/energy.py +++ b/plugwise/nodes/energy.py @@ -205,7 +205,7 @@ def statistics(self) -> PulseStats | None: @statistics.setter def statistics(self, statistics: PulseStats) -> None: - """Pulse statistics since last counter reset. """ + """Pulse statistics since last counter reset.""" if self._statistics is None: self._statistics = statistics else: From 2eff0ff429080106a35153a8d176e58ba3c64538 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 17:40:18 +0100 Subject: [PATCH 78/87] Change logging to debug for expected timeouts --- plugwise/controller.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index 7a9cae432..d110eef1a 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -318,7 +318,17 @@ def _receive_timeout_loop(self): ) else: _target = "" - if self._pending_request[seq_id].retry_counter >= MESSAGE_RETRY: + if self._pending_request[seq_id].drop_at_timeout: + _LOGGER.debug( + "No response for %s%s while 'drop at timeout' is enabled => drop request (seq_id=%s, retry=%s, last try=%s, last stick_response=%s)", + self._pending_request[seq_id].__class__.__name__, + _target, + str(seq_id), + str(self._pending_request[seq_id].retry_counter), + str(self._pending_request[seq_id].send), + str(self._pending_request[seq_id].stick_response), + ) + elif self._pending_request[seq_id].retry_counter >= MESSAGE_RETRY: _LOGGER.warning( "No response for %s%s => drop request (seq_id=%s, retry=%s, last try=%s, last stick_response=%s)", self._pending_request[seq_id].__class__.__name__, From 6eee26f2aeecce7070ba9fcc75888c06057c7e29 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 17:48:16 +0100 Subject: [PATCH 79/87] Revert accidentally unrelated committed code --- plugwise/nodes/circle.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/plugwise/nodes/circle.py b/plugwise/nodes/circle.py index 9f9aa4627..77f6bb775 100644 --- a/plugwise/nodes/circle.py +++ b/plugwise/nodes/circle.py @@ -391,8 +391,6 @@ def message_for_node(self, message: PlugwiseResponse) -> None: self._process_CircleEnergyLogsResponse(message) elif isinstance(message, CircleClockResponse): self._process_CircleClockResponse(message) - elif isinstance(message, NodeInfoResponse): - self._process_NodeInfoResponse(message) else: super().message_for_node(message) @@ -563,21 +561,6 @@ def _process_NodeResponse(self, message: NodeResponse) -> None: else: super()._process_NodeResponse(message) - def _process_NodeInfoResponse(self, message: NodeInfoResponse) -> None: - """Process contents of 'NodeInfoResponse' message""" - _protocol_set = False - if self._protocol is None: - _protocol_set = True - super()._process_NodeInfoResponse(message) - if _protocol_set and self._protocol: - if self._protocol[1] == "2.6": - # Request the current configuration of relay state at power-up - _relay_init_request = CircleRelayInitStateRequest( - self._mac, False, False - ) - _relay_init_request.priority = Priority.Low - self.message_sender(_relay_init_request) - def _update_intervals(self) -> None: """Update interval features.""" From e6144e27c63b9212e3a6e1abbe18fa8d99d284a9 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 9 Jan 2022 18:13:26 +0100 Subject: [PATCH 80/87] Apply codespell --- plugwise/nodes/energy.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugwise/nodes/energy.py b/plugwise/nodes/energy.py index 30bcd395c..9cf243cde 100644 --- a/plugwise/nodes/energy.py +++ b/plugwise/nodes/energy.py @@ -50,7 +50,7 @@ def calc_log_address(address: int, slot: int, offset: int) -> tuple: - """Calculate addess and slot for log based for specified offset""" + """Calculate address and slot for log based for specified offset""" # FIXME: Handle max address (max is currently unknown) to guard against address rollovers if offset < 0: @@ -177,7 +177,7 @@ def __init__(self, feature_id: USB) -> None: @property def calibration(self) -> CircleCalibration | None: - """Return current energy calibration configration.""" + """Return current energy calibration configuration.""" return self._calibration @calibration.setter @@ -358,7 +358,7 @@ def _get_max_counter_id(self, consumption: bool) -> USB | None: @property def calibration(self) -> CircleCalibration | None: - """Energy calibration configration.""" + """Energy calibration configuration.""" return self._calibration @calibration.setter @@ -546,7 +546,7 @@ def next_log_timestamp(self) -> datetime | None: @property def missing_log_addresses(self) -> list[int] | None: """ - List of any addres missing in current sequence. + List of any address missing in current sequence. Returns None if no logs are collected. """ if self._logs is None: @@ -704,7 +704,7 @@ def _calc_log_pulses(self, utc_start: datetime, direction: bool) -> int | None: def _calc_total_pulses(self, utc_start: datetime, direction: bool) -> int | None: """Calculate total pulses from given point in time.""" - # Intervalpulses have to be up-to-date to return usefull pulse value + # Intervalpulses have to be up-to-date to return useful pulse value if self._pulses is None: return None _log_pulses = None @@ -920,7 +920,7 @@ def _update_interval_deltas(self, direction: bool) -> None: def _update_interval_pulses( self, timestamp: datetime, pulses: int | None, direction: bool ) -> None: - """Update local consumption pulse varables.""" + """Update local consumption pulse variables.""" if self._next_log_timestamp(direction) is None: # Not enough logs collected yet, skip checking for rollover @@ -1053,7 +1053,7 @@ def _update_after_log( and not self._rollover_interval_log[direction] ): # After expected rollover and no log rollover is started - # A decrease indicates a rollover prior to the expected log comming in. + # A decrease indicates a rollover prior to the expected log coming in. if pulses < self._interval_pulses[direction]: # Decrease => Trigger rollover self._rollover_interval_pulses[direction] = True @@ -1074,7 +1074,7 @@ def _update_after_log( and not self._rollover_interval_log[direction] ): # Interval rollover is started and we're after expected log rollover timestamp. - # As reset happend before, we expect a increase of pulses and wait for log rollover to happen. + # As reset happened before, we expect a increase of pulses and wait for log rollover to happen. if pulses < self._interval_pulses[direction]: # Decrease => Rollover active _LOGGER.debug( From 1e4b9875e1011d987c89982b6a9405019a8e4d22 Mon Sep 17 00:00:00 2001 From: brefra Date: Wed, 12 Jan 2022 22:02:36 +0100 Subject: [PATCH 81/87] Fix processing and logging of "drop_at_timeout" requests --- plugwise/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index d110eef1a..ac5c70f23 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -191,8 +191,8 @@ def _send_message_loop(self): time.sleep(SLEEP_TIME) timeout_counter += SLEEP_TIME - if _request.drop_at_timeout: - _LOGGER.error( + if not self._stick_response and _request.drop_at_timeout: + _LOGGER.info( "Stick does not respond to %s for %s, drop request as request is set to be dropped at timeout", _request.__class__.__name__, _request.target_mac, From b88d6f8975eed8bc9b3718babe9a41b53494ca3e Mon Sep 17 00:00:00 2001 From: brefra Date: Wed, 12 Jan 2022 22:04:02 +0100 Subject: [PATCH 82/87] Fix collecting missing production logs --- plugwise/nodes/energy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugwise/nodes/energy.py b/plugwise/nodes/energy.py index 9cf243cde..fec7d47f4 100644 --- a/plugwise/nodes/energy.py +++ b/plugwise/nodes/energy.py @@ -845,6 +845,8 @@ def _missing_addresses_after(self) -> list[int]: _consumption_ts + _consumption_delta ): _production_ts += _production_delta + else: + _consumption_ts += _consumption_delta if _consumption_ts >= _target and _production_ts >= _target: break else: From 10b6ec3e39305cab6a566491e970a33a2a26ce36 Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 16 Jan 2022 16:47:06 +0100 Subject: [PATCH 83/87] Remove unused node state --- plugwise/controller.py | 3 +-- plugwise/stick.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/plugwise/controller.py b/plugwise/controller.py index ac5c70f23..25b6183fb 100644 --- a/plugwise/controller.py +++ b/plugwise/controller.py @@ -49,13 +49,12 @@ class MessageRequest(TypedDict): class StickMessageController: """Handle connection and message sending and receiving""" - def __init__(self, port: str, message_processor, node_state): + def __init__(self, port: str, message_processor): """Initialize message controller""" self.connection = None self.discovery_finished = False self.init_callback = None self.message_processor = message_processor - self.node_state = node_state self.parser = PlugwiseParser(self.message_handler) self.port = port diff --git a/plugwise/stick.py b/plugwise/stick.py index b66b5e300..ff63ceb2f 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -154,7 +154,7 @@ def init_finished(): if not self.msg_controller: self.msg_controller = StickMessageController( - self.port, self.message_processor, self.node_state_updates + self.port, self.message_processor ) try: self.msg_controller.connect_to_stick() @@ -173,9 +173,7 @@ def init_finished(): def connect(self, callback=None): """Startup message controller and connect to stick.""" if not self.msg_controller: - self.msg_controller = StickMessageController( - self.port, self.message_processor, self.node_state_updates - ) + self.msg_controller = StickMessageController(self.port, self.message_processor) if self.msg_controller.connect_to_stick(callback): # update daemon self._run_update_thread = False From aea22dfcd6ee2aed75bb025b2966529daecac045 Mon Sep 17 00:00:00 2001 From: autoblack Date: Thu, 20 Jan 2022 19:48:16 +0000 Subject: [PATCH 84/87] fixup: USB_energy_refactor Python code reformatted using Black --- plugwise/stick.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise/stick.py b/plugwise/stick.py index ff63ceb2f..a8246c9ac 100644 --- a/plugwise/stick.py +++ b/plugwise/stick.py @@ -173,7 +173,9 @@ def init_finished(): def connect(self, callback=None): """Startup message controller and connect to stick.""" if not self.msg_controller: - self.msg_controller = StickMessageController(self.port, self.message_processor) + self.msg_controller = StickMessageController( + self.port, self.message_processor + ) if self.msg_controller.connect_to_stick(callback): # update daemon self._run_update_thread = False From 49693cba54030e5a58371093a8a8d2dba518ecac Mon Sep 17 00:00:00 2001 From: brefra Date: Thu, 20 Jan 2022 21:03:15 +0100 Subject: [PATCH 85/87] Bump to 0.16.2.a0 --- CHANGELOG.md | 2 +- plugwise/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcbaef53b..e3eb8fec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -# v0.17.x - Stick - Improve performance and energy monitoring +# v0.16.2.a0 - Stick - Improve performance and energy monitoring - Improved timeout message handling based on response messages of USB-stick - More frequent energy monitoring polling when Stick is idle diff --git a/plugwise/__init__.py b/plugwise/__init__.py index 3b2e0f2cc..78ff50790 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -1,6 +1,6 @@ """Plugwise module.""" -__version__ = "0.16.0" +__version__ = "0.16.2.a0" from plugwise.smile import Smile from plugwise.stick import Stick From 539d1bc05d33c6adcdd53443a050a892b0198443 Mon Sep 17 00:00:00 2001 From: brefra Date: Fri, 21 Jan 2022 21:42:00 +0100 Subject: [PATCH 86/87] Bump to 0.15.8 --- plugwise/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index 78ff50790..d2c3da0f3 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -1,6 +1,6 @@ """Plugwise module.""" -__version__ = "0.16.2.a0" +__version__ = "0.15.8" from plugwise.smile import Smile from plugwise.stick import Stick From d9cc0ea1782f968b0ee2fbba0f7127ff7f3b5fb6 Mon Sep 17 00:00:00 2001 From: brefra Date: Fri, 21 Jan 2022 23:30:55 +0100 Subject: [PATCH 87/87] Correct changelog --- CHANGELOG.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3eb8fec6..606d8ac01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,21 @@ # Changelog -# v0.16.2.a0 - Stick - Improve performance and energy monitoring - -- Improved timeout message handling based on response messages of USB-stick -- More frequent energy monitoring polling when Stick is idle -- Full rewrite of energy collection code. No dependency to fixed log interval. Should fix [#149]https://github.com/plugwise/plugwise-beta/issues/149 and [157]https://github.com/plugwise/plugwise-beta/issues/157 -- Added ability to change energy log interval -- Added full support for energy production [#39](https://github.com/plugwise/python-plugwise/issues/39) - # v0.16.0 - Smile - Change output format, allowing full use of Core DataUpdateCoordintor in plugwise-beta - Change from list- to dict-format for binary_sensors, sensors and switches - Provide gateway-devices for Legacy Anna and Stretch - Code-optimizations +# v0.15.8 - Stick - Improve performance and energy monitoring + +- Improved timeout message handling based on response messages of USB-stick +- More frequent energy monitoring polling when Stick is idle +- Full rewrite of energy collection code. No dependency to fixed log interval. Should fix + - [#149]https://github.com/plugwise/plugwise-beta/issues/149 + - [157]https://github.com/plugwise/plugwise-beta/issues/157 +- Added ability to change energy log interval +- Added full support for energy production [#39](https://github.com/plugwise/python-plugwise/issues/39) + # v0.15.7 - Smile - Improve implementation of cooling-function-detection - Anna: add two sensors related to automatic switching between heating and cooling and add a heating/cooling-mode active indication