diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index 33e7828fd..c4f35c826 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -260,6 +260,7 @@ class EnergyStatistics: hour_production_reset: datetime | None = None day_production: float | None = None day_production_reset: datetime | None = None + current_logaddress: int | None = None @dataclass(frozen=True) diff --git a/plugwise_usb/messages/requests.py b/plugwise_usb/messages/requests.py index a57a9860d..22bf0fb00 100644 --- a/plugwise_usb/messages/requests.py +++ b/plugwise_usb/messages/requests.py @@ -738,7 +738,7 @@ def __init__( mac: bytes, dt: datetime, protocol_version: float, - reset: bool = False, + reset: int | bool = False, ) -> None: """Initialize CircleLogDataRequest message object.""" super().__init__(send_fn, mac) @@ -754,7 +754,14 @@ def __init__( 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) - if reset: + if isinstance(reset, int): + self._args += [ + this_date, + LogAddr(reset, 8, False), + this_time, + day_of_week, + ] + elif reset: self._args += [ this_date, LogAddr(0, 8, False), diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 98e93fdc9..0a6ba9da8 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -27,6 +27,7 @@ from ..constants import ( DAY_IN_HOURS, DEFAULT_CONS_INTERVAL, + LOGADDR_MAX, MAX_TIME_DRIFT, MINIMAL_POWER_UPDATE, NO_PRODUCTION_INTERVAL, @@ -1030,26 +1031,11 @@ async def node_info_update( node_info = await node_request.send() if node_info is None: + _LOGGER.debug("No response for node_info_update() for %s", self.mac) + await self._available_update_state(False) return None await super().node_info_update(node_info) - await self._relay_update_state( - node_info.relay_state, timestamp=node_info.timestamp - ) - if self._current_log_address is not None and ( - self._current_log_address > node_info.current_logaddress_pointer - or self._current_log_address == 1 - ): - # Rollover of log address - _LOGGER.debug( - "Rollover log address from %s into %s for node %s", - self._current_log_address, - node_info.current_logaddress_pointer, - self._mac_in_str, - ) - - if self._current_log_address != node_info.current_logaddress_pointer: - self._current_log_address = node_info.current_logaddress_pointer return self._node_info @@ -1059,14 +1045,29 @@ async def update_node_details( ) -> bool: """Process new node info and return true if all fields are updated.""" if node_info.relay_state is not None: - self._relay_state = replace( - self._relay_state, - state=node_info.relay_state, - timestamp=node_info.timestamp, + await self._relay_update_state( + node_info.relay_state, timestamp=node_info.timestamp + ) + + if ( + node_info.current_logaddress_pointer is not None + and self._current_log_address is not None + and ( + self._current_log_address < node_info.current_logaddress_pointer + or self._current_log_address == 1 + ) + ): + # Rollover of log address + _LOGGER.debug( + "Rollover log address from %s into %s for node %s", + self._current_log_address, + node_info.current_logaddress_pointer, + self._mac_in_str, ) if node_info.current_logaddress_pointer is not None: self._current_log_address = node_info.current_logaddress_pointer + self._energy_counters.set_current_logaddres(self._current_log_address) return await super().update_node_details(node_info) @@ -1363,3 +1364,71 @@ async def energy_reset_request(self) -> None: "Node info update after energy-reset successful for %s", self._mac_in_str, ) + + async def energy_logaddr_setrequest(self, logaddr: int) -> None: + """Set the logaddress to a specific value.""" + if self._node_protocols is None: + raise NodeError("Unable to energy-reset when protocol version is unknown") + + if logaddr < 1 or logaddr >= LOGADDR_MAX: + raise ValueError("Set logaddress out of range for {self._mac_in_str}") + request = CircleClockSetRequest( + self._send, + self._mac_in_bytes, + datetime.now(tz=UTC), + self._node_protocols.max, + logaddr, + ) + if (response := await request.send()) is None: + raise NodeError(f"Logaddress set for {self._mac_in_str} failed") + + if response.ack_id != NodeResponseType.CLOCK_ACCEPTED: + raise MessageError( + f"Unexpected NodeResponseType {response.ack_id!r} received as response to CircleClockSetRequest" + ) + + _LOGGER.warning("Logaddress set for Node %s successful", self._mac_in_str) + + # Follow up by an energy-intervals (re)set + interval_request = CircleMeasureIntervalRequest( + self._send, + self._mac_in_bytes, + DEFAULT_CONS_INTERVAL, + NO_PRODUCTION_INTERVAL, + ) + if (interval_response := await interval_request.send()) is None: + raise NodeError("No response for CircleMeasureIntervalRequest") + + if ( + interval_response.response_type + != NodeResponseType.POWER_LOG_INTERVAL_ACCEPTED + ): + raise MessageError( + f"Unknown NodeResponseType '{interval_response.response_type.name}' received" + ) + _LOGGER.warning("Resetting energy intervals to default (= consumption only)") + + # Clear the cached energy_collection + if self._cache_enabled: + self._set_cache(CACHE_ENERGY_COLLECTION, "") + _LOGGER.warning( + "Energy-collection cache cleared successfully, updating cache for %s", + self._mac_in_str, + ) + await self.save_cache() + + # Clear PulseCollection._logs + self._energy_counters.reset_pulse_collection() + _LOGGER.warning("Resetting pulse-collection") + + # Request a NodeInfo update + if await self.node_info_update() is None: + _LOGGER.warning( + "Node info update failed after energy-reset for %s", + self._mac_in_str, + ) + else: + _LOGGER.warning( + "Node info update after energy-reset successful for %s", + self._mac_in_str, + ) diff --git a/plugwise_usb/nodes/helpers/counter.py b/plugwise_usb/nodes/helpers/counter.py index 2caf6828d..b02c87ee3 100644 --- a/plugwise_usb/nodes/helpers/counter.py +++ b/plugwise_usb/nodes/helpers/counter.py @@ -57,6 +57,7 @@ def __init__(self, mac: str) -> None: self._mac = mac self._calibration: EnergyCalibration | None = None self._counters: dict[EnergyType, EnergyCounter] = {} + self._current_logaddress: int | None = None for energy_type in ENERGY_COUNTERS: self._counters[energy_type] = EnergyCounter(energy_type, mac) self._pulse_collection = PulseCollection(mac) @@ -67,6 +68,10 @@ def collected_logs(self) -> int: """Total collected logs.""" return self._pulse_collection.collected_logs + def set_current_logaddres(self, address: int) -> None: + """Update current logaddress value.""" + self._current_logaddress = address + def add_empty_log(self, address: int, slot: int) -> None: """Add empty energy log record to mark any start of beginning of energy log collection.""" self._pulse_collection.add_empty_log(address, slot) @@ -127,6 +132,11 @@ def log_addresses_missing(self) -> list[int] | None: """Return list of addresses of energy logs.""" return self._pulse_collection.log_addresses_missing + @property + def current_logaddress(self) -> int | None: + """Return current registered logaddress value.""" + return self._current_logaddress + @property def log_rollover(self) -> bool: """Indicate if new log is required due to rollover.""" @@ -149,6 +159,7 @@ def update(self) -> None: self._pulse_collection.recalculate_missing_log_addresses() if self._calibration is None: return + self._energy_statistics.current_logaddress = self._current_logaddress self._energy_statistics.log_interval_consumption = ( self._pulse_collection.log_interval_consumption ) diff --git a/pyproject.toml b/pyproject.toml index ec276e74c..74f271f9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "0.45.0" +version = "0.45.1a0" license = "MIT" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ diff --git a/tests/test_usb.py b/tests/test_usb.py index d0a139908..c9be68005 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -947,6 +947,7 @@ async def fake_get_missing_energy_logs(address: int) -> None: hour_production_reset=None, day_production=None, day_production_reset=None, + current_logaddress=20, ) # energy_update is not complete and should return none utc_now = dt.now(UTC) @@ -964,6 +965,7 @@ async def fake_get_missing_energy_logs(address: int) -> None: hour_production_reset=None, day_production=None, day_production_reset=None, + current_logaddress=20, ) await stick.disconnect()