diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index bbbfdaa6c..f80dd3598 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -210,8 +210,7 @@ class MotionConfig: class EnergyStatistics: """Energy statistics collection.""" - log_interval_consumption: int | None = None - log_interval_production: int | None = None + log_interval: int | None = None hour_consumption: float | None = None hour_consumption_reset: datetime | None = None day_consumption: float | None = None diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 7daa30265..5cdedb24f 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -2,7 +2,7 @@ from __future__ import annotations -from asyncio import Task, create_task +from asyncio import Task, create_task, gather from collections.abc import Awaitable, Callable from dataclasses import replace from datetime import UTC, datetime @@ -326,6 +326,7 @@ async def energy_update(self) -> EnergyStatistics | None: self.name, ) return None + # request node info update every 30 minutes. elif not self.skip_update(self._node_info, 1800): if await self.node_info_update() is None: @@ -419,6 +420,7 @@ async def energy_update(self) -> EnergyStatistics | None: "Skip creating task to update energy logs for node %s", self._mac_in_str, ) + if ( self._initialization_delay_expired is not None and datetime.now(tz=UTC) < self._initialization_delay_expired @@ -432,6 +434,7 @@ async def energy_update(self) -> EnergyStatistics | None: "Unable to return energy statistics for %s, collecting required data...", self.name, ) + return None async def get_missing_energy_logs(self) -> None: @@ -440,6 +443,7 @@ async def get_missing_energy_logs(self) -> None: self._energy_counters.update() if self._current_log_address is None: return None + if self._energy_counters.log_addresses_missing is None: _LOGGER.debug( "Start with initial energy request for the last 10 log addresses for node %s.", @@ -453,12 +457,13 @@ async def get_missing_energy_logs(self) -> None: log_address, _ = calc_log_address(log_address, 1, -4) total_addresses -= 1 - for task in log_update_tasks: - await task + await gather(*log_update_tasks) if self._cache_enabled: await self._energy_log_records_save_to_cache() + return + if self._energy_counters.log_addresses_missing is not None: _LOGGER.debug("Task created to get missing logs of %s", self._mac_in_str) if ( @@ -496,6 +501,7 @@ async def energy_log_update(self, address: int | None) -> bool: ) return False + _LOGGER.debug("EnergyLogs data from %s, address=%s", self._mac_in_str, address) await self._available_update_state(True, response.timestamp) energy_record_update = False @@ -504,7 +510,12 @@ async def energy_log_update(self, address: int | None) -> bool: # energy pulses collected during the previous hour of given timestamp for _slot in range(4, 0, -1): log_timestamp, log_pulses = response.log_data[_slot] - + _LOGGER.debug( + "In slot=%s: pulses=%s, timestamp=%s", + _slot, + log_pulses, + log_timestamp + ) if log_timestamp is None or log_pulses is None: self._energy_counters.add_empty_log(response.log_address, _slot) elif await self._energy_log_record_update_state( @@ -561,14 +572,17 @@ async def _energy_log_records_load_from_cache(self) -> bool: # Create task to retrieve remaining (missing) logs if self._energy_counters.log_addresses_missing is None: return False + if len(self._energy_counters.log_addresses_missing) > 0: if self._retrieve_energy_logs_task is not None: if not self._retrieve_energy_logs_task.done(): await self._retrieve_energy_logs_task + self._retrieve_energy_logs_task = create_task( self.get_missing_energy_logs() ) return False + return True async def _energy_log_records_save_to_cache(self) -> None: diff --git a/plugwise_usb/nodes/helpers/counter.py b/plugwise_usb/nodes/helpers/counter.py index f894d8244..486f86f43 100644 --- a/plugwise_usb/nodes/helpers/counter.py +++ b/plugwise_usb/nodes/helpers/counter.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime #, timedelta from enum import Enum, auto import logging from typing import Final @@ -30,8 +30,8 @@ class EnergyType(Enum): EnergyType.PRODUCTION_HOUR, EnergyType.CONSUMPTION_DAY, EnergyType.PRODUCTION_DAY, - EnergyType.CONSUMPTION_WEEK, - EnergyType.PRODUCTION_WEEK, + # EnergyType.CONSUMPTION_WEEK, + # EnergyType.PRODUCTION_WEEK, ) ENERGY_HOUR_COUNTERS: Final = ( EnergyType.CONSUMPTION_HOUR, @@ -49,12 +49,12 @@ class EnergyType(Enum): ENERGY_CONSUMPTION_COUNTERS: Final = ( EnergyType.CONSUMPTION_HOUR, EnergyType.CONSUMPTION_DAY, - EnergyType.CONSUMPTION_WEEK, + # EnergyType.CONSUMPTION_WEEK, ) ENERGY_PRODUCTION_COUNTERS: Final = ( EnergyType.PRODUCTION_HOUR, EnergyType.PRODUCTION_DAY, - EnergyType.PRODUCTION_WEEK, + # EnergyType.PRODUCTION_WEEK, ) _LOGGER = logging.getLogger(__name__) @@ -105,11 +105,8 @@ def add_pulse_stats( self, pulses_consumed: int, pulses_produced: int, timestamp: datetime ) -> None: """Add pulse statistics.""" - _LOGGER.debug( - "add_pulse_stats | consumed=%s, for %s", - str(pulses_consumed), - self._mac, - ) + _LOGGER.debug("add_pulse_stats for %s with timestamp=%s", self._mac, timestamp) + _LOGGER.debug("consumed=%s | produced=%s", pulses_consumed, pulses_produced) self._pulse_collection.update_pulse_counter( pulses_consumed, pulses_produced, timestamp ) @@ -123,12 +120,12 @@ def energy_statistics(self) -> EnergyStatistics: @property def consumption_interval(self) -> int | None: """Measurement interval for energy consumption.""" - return self._pulse_collection.log_interval_consumption + return self._pulse_collection.log_interval @property def production_interval(self) -> int | None: """Measurement interval for energy production.""" - return self._pulse_collection.log_interval_production + return self._pulse_collection.log_interval @property def log_addresses_missing(self) -> list[int] | None: @@ -157,11 +154,9 @@ def update(self) -> None: self._pulse_collection.recalculate_missing_log_addresses() if self._calibration is None: return - self._energy_statistics.log_interval_consumption = ( - self._pulse_collection.log_interval_consumption - ) - self._energy_statistics.log_interval_production = ( - self._pulse_collection.log_interval_production + + self._energy_statistics.log_interval = ( + self._pulse_collection.log_interval ) ( self._energy_statistics.hour_consumption, @@ -171,23 +166,24 @@ def update(self) -> None: self._energy_statistics.day_consumption, self._energy_statistics.day_consumption_reset, ) = self._counters[EnergyType.CONSUMPTION_DAY].update(self._pulse_collection) - ( - self._energy_statistics.week_consumption, - self._energy_statistics.week_consumption_reset, - ) = self._counters[EnergyType.CONSUMPTION_WEEK].update(self._pulse_collection) + # ( + # self._energy_statistics.week_consumption, + # self._energy_statistics.week_consumption_reset, + # ) = self._counters[EnergyType.CONSUMPTION_WEEK].update(self._pulse_collection) - ( - self._energy_statistics.hour_production, - self._energy_statistics.hour_production_reset, - ) = self._counters[EnergyType.PRODUCTION_HOUR].update(self._pulse_collection) - ( - self._energy_statistics.day_production, - self._energy_statistics.day_production_reset, - ) = self._counters[EnergyType.PRODUCTION_DAY].update(self._pulse_collection) - ( - self._energy_statistics.week_production, - self._energy_statistics.week_production_reset, - ) = self._counters[EnergyType.PRODUCTION_WEEK].update(self._pulse_collection) + if self._pulse_collection.production_logging: + ( + self._energy_statistics.hour_production, + self._energy_statistics.hour_production_reset, + ) = self._counters[EnergyType.PRODUCTION_HOUR].update(self._pulse_collection) + ( + self._energy_statistics.day_production, + self._energy_statistics.day_production_reset, + ) = self._counters[EnergyType.PRODUCTION_DAY].update(self._pulse_collection) + # ( + # self._energy_statistics.week_production, + # self._energy_statistics.week_production_reset, + # ) = self._counters[EnergyType.PRODUCTION_WEEK].update(self._pulse_collection) @property def timestamp(self) -> datetime | None: @@ -213,18 +209,21 @@ def __init__( self._mac = mac if energy_id not in ENERGY_COUNTERS: raise EnergyError(f"Invalid energy id '{energy_id}' for Energy counter") + self._calibration: EnergyCalibration | None = None self._duration = "hour" if energy_id in ENERGY_DAY_COUNTERS: self._duration = "day" - elif energy_id in ENERGY_WEEK_COUNTERS: - self._duration = "week" + #elif energy_id in ENERGY_WEEK_COUNTERS: + # self._duration = "week" + self._energy_id: EnergyType = energy_id self._is_consumption = True self._direction = "consumption" if self._energy_id in ENERGY_PRODUCTION_COUNTERS: self._direction = "production" self._is_consumption = False + self._last_reset: datetime | None = None self._last_update: datetime | None = None self._pulses: int | None = None @@ -259,9 +258,16 @@ def energy(self) -> float | None: """Total energy (in kWh) since last reset.""" if self._pulses is None or self._calibration is None: return None + if self._pulses == 0: return 0.0 - pulses_per_s = self._pulses / float(HOUR_IN_SECONDS) + + # Handle both positive and negative pulses values + negative = False + if self._pulses < 0: + negative = True + + pulses_per_s = abs(self._pulses) / float(HOUR_IN_SECONDS) corrected_pulses = HOUR_IN_SECONDS * ( ( ( @@ -276,8 +282,9 @@ def energy(self) -> float | None: + self._calibration.off_tot ) calc_value = corrected_pulses / PULSES_PER_KW_SECOND / HOUR_IN_SECONDS - # Guard for minor negative miscalculations - calc_value = max(calc_value, 0.0) + if negative: + calc_value = -calc_value + return calc_value @property @@ -299,14 +306,14 @@ def update( last_reset = last_reset.replace(minute=0, second=0, microsecond=0) elif self._energy_id in ENERGY_DAY_COUNTERS: last_reset = last_reset.replace(hour=0, minute=0, second=0, microsecond=0) - elif self._energy_id in ENERGY_WEEK_COUNTERS: - last_reset = last_reset - timedelta(days=last_reset.weekday()) - last_reset = last_reset.replace( - hour=0, - minute=0, - second=0, - microsecond=0, - ) + # elif self._energy_id in ENERGY_WEEK_COUNTERS: + # last_reset = last_reset - timedelta(days=last_reset.weekday()) + # last_reset = last_reset.replace( + # hour=0, + # minute=0, + # second=0, + # microsecond=0, + # ) pulses, last_update = pulse_collection.collected_pulses( last_reset, self._is_consumption @@ -319,10 +326,11 @@ def update( ) if pulses is None or last_update is None: return (None, None) + self._last_update = last_update self._last_reset = last_reset self._pulses = pulses energy = self.energy - _LOGGER.debug("energy=%s or last_update=%s", energy, last_update) + _LOGGER.debug("energy=%s on last_update=%s", energy, last_update) return (energy, last_reset) diff --git a/plugwise_usb/nodes/helpers/pulses.py b/plugwise_usb/nodes/helpers/pulses.py index a3c0f6511..1b46cf2c0 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -7,14 +7,15 @@ import logging from typing import Final -from ...constants import LOGADDR_MAX, MINUTE_IN_SECONDS, WEEK_IN_HOURS +from ...constants import LOGADDR_MAX, MINUTE_IN_SECONDS, DAY_IN_HOURS from ...exceptions import EnergyError _LOGGER = logging.getLogger(__name__) CONSUMED: Final = True PRODUCED: Final = False +PRODUCERS: tuple[str] = ("000D6F00029C32C7", "0098765432101234", "dummy") -MAX_LOG_HOURS = WEEK_IN_HOURS +MAX_LOG_HOURS = DAY_IN_HOURS def calc_log_address(address: int, slot: int, offset: int) -> tuple[int, int]: @@ -47,13 +48,12 @@ class PulseLogRecord: class PulseCollection: - """Store consumed and produced energy pulses of the current interval and past (history log) intervals.""" + """Store energy pulses of the current interval and past (history log) intervals.""" def __init__(self, mac: str) -> None: """Initialize PulseCollection class.""" self._mac = mac - self._log_interval_consumption: int | None = None - self._log_interval_production: int | None = None + self._log_interval: int | None = None self._last_log_address: int | None = None self._last_log_slot: int | None = None @@ -67,32 +67,29 @@ def __init__(self, mac: str) -> None: self._last_empty_log_address: int | None = None self._last_empty_log_slot: int | None = None - self._last_log_consumption_timestamp: datetime | None = None - self._last_log_consumption_address: int | None = None - self._last_log_consumption_slot: int | None = None - self._first_log_consumption_timestamp: datetime | None = None - self._first_log_consumption_address: int | None = None - self._first_log_consumption_slot: int | None = None - self._next_log_consumption_timestamp: datetime | None = None - - self._last_log_production_timestamp: datetime | None = None - self._last_log_production_address: int | None = None - self._last_log_production_slot: int | None = None - self._first_log_production_timestamp: datetime | None = None - self._first_log_production_address: int | None = None - self._first_log_production_slot: int | None = None - self._next_log_production_timestamp: datetime | None = None + self._last_log_timestamp: datetime | None = None + self._last_log_address: int | None = None + self._last_log_slot: int | None = None + self._first_log_timestamp: datetime | None = None + self._first_log_address: int | None = None + self._first_log_slot: int | None = None + self._next_log_timestamp: datetime | None = None + self._consumption_counter_reset = False + self._production_counter_reset = False self._rollover_consumption = False self._rollover_production = False self._logs: dict[int, dict[int, PulseLogRecord]] | None = None self._log_addresses_missing: list[int] | None = None - self._log_production: bool | None = None self._pulses_consumption: int | None = None self._pulses_production: int | None = None self._pulses_timestamp: datetime | None = None + self._log_production = False + if mac in PRODUCERS: + self._log_production = True + @property def collected_logs(self) -> int: """Total collected logs.""" @@ -124,11 +121,11 @@ def logs(self) -> dict[int, dict[int, PulseLogRecord]]: def last_log(self) -> tuple[int, int] | None: """Return address and slot of last imported log.""" if ( - self._last_log_consumption_address is None - or self._last_log_consumption_slot is None + self._last_log_address is None + or self._last_log_slot is None ): return None - return (self._last_log_consumption_address, self._last_log_consumption_slot) + return (self._last_log_address, self._last_log_slot) @property def production_logging(self) -> bool | None: @@ -136,14 +133,9 @@ def production_logging(self) -> bool | None: return self._log_production @property - def log_interval_consumption(self) -> int | None: - """Interval in minutes between last consumption pulse logs.""" - return self._log_interval_consumption - - @property - def log_interval_production(self) -> int | None: - """Interval in minutes between last production pulse logs.""" - return self._log_interval_production + def log_interval(self) -> int | None: + """Interval in minutes between the last two pulse logs.""" + return self._log_interval @property def log_rollover(self) -> bool: @@ -159,24 +151,34 @@ def collected_pulses( self, from_timestamp: datetime, is_consumption: bool ) -> tuple[int | None, datetime | None]: """Calculate total pulses from given timestamp.""" + _LOGGER.debug( + "collected_pulses 1 | %s | is_cons=%s, from_timestamp=%s", + self._mac, + is_consumption, + from_timestamp, + ) + _LOGGER.debug("collected_pulses 1a | _log_production=%s", self._log_production) + if not is_consumption and not self._log_production: + return (None, None) - # _LOGGER.debug("collected_pulses | %s | is_cons=%s, from=%s", self._mac, is_consumption, from_timestamp) + # !! consumption-pulses reset every hour - pulses just before rollover are lost? - if not is_consumption: - if self._log_production is None or not self._log_production: - return (None, None) + # !! production-pulses do not reset every hour but at the max counter value - double-check + # if this is correct the pulses lost at rollover can be calculated: + # max-counter - prev-value + counter after reset + # Is the below code (6 lines) correct? if is_consumption and self._rollover_consumption: - _LOGGER.debug("collected_pulses | %s | _rollover_consumption", self._mac) + _LOGGER.debug("collected_pulses 2 | %s | _rollover_consumption", self._mac) return (None, None) if not is_consumption and self._rollover_production: - _LOGGER.debug("collected_pulses | %s | _rollover_production", self._mac) + _LOGGER.debug("collected_pulses 2 | %s | _rollover_production", self._mac) return (None, None) if ( log_pulses := self._collect_pulses_from_logs(from_timestamp, is_consumption) ) is None: - _LOGGER.debug("collected_pulses | %s | log_pulses:None", self._mac) + _LOGGER.debug("collected_pulses 3 | %s | log_pulses:None", self._mac) return (None, None) pulses: int | None = None @@ -188,15 +190,22 @@ def collected_pulses( pulses = self._pulses_production timestamp = self._pulses_timestamp # _LOGGER.debug("collected_pulses | %s | pulses=%s", self._mac, pulses) - if pulses is None: _LOGGER.debug( - "collected_pulses | %s | is_consumption=%s, pulses=None", + "collected_pulses 4 | %s | is_consumption=%s, pulses=None", self._mac, is_consumption, ) return (None, None) - return (pulses + log_pulses, timestamp) + + _LOGGER.debug( + "collected_pulses 5 | pulses=%s | log_pulses=%s | consumption=%s at timestamp=%s", + pulses, + log_pulses, + is_consumption, + timestamp, + ) + return (abs(pulses + log_pulses), timestamp) def _collect_pulses_from_logs( self, from_timestamp: datetime, is_consumption: bool @@ -205,24 +214,17 @@ def _collect_pulses_from_logs( if self._logs is None: _LOGGER.debug("_collect_pulses_from_logs | %s | self._logs=None", self._mac) return None - if is_consumption: - if self._last_log_consumption_timestamp is None: - _LOGGER.debug( - "_collect_pulses_from_logs | %s | self._last_log_consumption_timestamp=None", - self._mac, - ) - return None - if from_timestamp > self._last_log_consumption_timestamp: - return 0 - else: - if self._last_log_production_timestamp is None: - _LOGGER.debug( - "_collect_pulses_from_logs | %s | self._last_log_production_timestamp=None", - self._mac, - ) - return None - if from_timestamp > self._last_log_production_timestamp: - return 0 + + if self._last_log_timestamp is None: + _LOGGER.debug( + "_collect_pulses_from_logs | %s | self._last_log_timestamp=None", + self._mac, + ) + return None + + if from_timestamp > self._last_log_timestamp: + return 0 + missing_logs = self._logs_missing(from_timestamp) if missing_logs is None or missing_logs: _LOGGER.debug( @@ -233,7 +235,6 @@ def _collect_pulses_from_logs( return None log_pulses = 0 - for log_item in self._logs.values(): for slot_item in log_item.values(): if ( @@ -241,14 +242,20 @@ def _collect_pulses_from_logs( and slot_item.timestamp > from_timestamp ): log_pulses += slot_item.pulses + return log_pulses def update_pulse_counter( self, pulses_consumed: int, pulses_produced: int, timestamp: datetime ) -> None: - """Update pulse counter.""" + """Update pulse counter, checking for rollover based on counter reset.""" self._pulses_timestamp = timestamp - self._update_rollover() + self._consumption_counter_reset = False + self._production_counter_reset = False + self._update_rollover(True) + if self._log_production: + self._update_rollover(False) + if not (self._rollover_consumption or self._rollover_production): # No rollover based on time, check rollover based on counter reset # Required for special cases like nodes which have been power off for several days @@ -256,79 +263,84 @@ def update_pulse_counter( self._pulses_consumption is not None and self._pulses_consumption > pulses_consumed ): - self._rollover_consumption = True + self._consumption_counter_reset = True + _LOGGER.debug( + "_consumption_counter_reset | self._pulses_consumption=%s > pulses_consumed=%s", + self._pulses_consumption, + pulses_consumed, + ) + if ( self._pulses_production is not None - and self._pulses_production > pulses_produced + and self._pulses_production < pulses_produced ): - self._rollover_production = True + self._production_counter_reset = True + _LOGGER.debug( + "_production_counter_reset | self._pulses_production=%s < pulses_produced=%s", + self._pulses_production, + pulses_produced, + ) + self._pulses_consumption = pulses_consumed self._pulses_production = pulses_produced - def _update_rollover(self) -> None: - """Update rollover states. Returns True if rollover is applicable.""" + def _update_rollover(self, consumption: bool) -> None: + """Update rollover states. + + When the last found timestamp is outside the interval `_last_log_timestamp` + to `_next_log_timestamp` the pulses should not be counted as part of the + ongoing collection-interval. + """ if self._log_addresses_missing is not None and self._log_addresses_missing: return + if ( self._pulses_timestamp is None - or self._last_log_consumption_timestamp is None - or self._next_log_consumption_timestamp is None + or self._last_log_timestamp is None + or self._next_log_timestamp is None ): # Unable to determine rollover return - if self._pulses_timestamp > self._next_log_consumption_timestamp: - self._rollover_consumption = True - _LOGGER.debug( - "_update_rollover | %s | set consumption rollover => pulses newer", - self._mac, - ) - elif self._pulses_timestamp < self._last_log_consumption_timestamp: - self._rollover_consumption = True - _LOGGER.debug( - "_update_rollover | %s | set consumption rollover => log newer", - self._mac, - ) - elif ( - self._last_log_consumption_timestamp - < self._pulses_timestamp - < self._next_log_consumption_timestamp - ): - if self._rollover_consumption: - _LOGGER.debug("_update_rollover | %s | reset consumption", self._mac) - self._rollover_consumption = False - else: - _LOGGER.debug("_update_rollover | %s | unexpected consumption", self._mac) - if not self._log_production: + if self._pulses_timestamp > self._next_log_timestamp: + if consumption: + self._rollover_consumption = True + _LOGGER.debug( + "_update_rollover | %s | set consumption rollover => pulses newer", + self._mac, + ) + else: + self._rollover_production = True + _LOGGER.debug( + "_update_rollover | %s | set production rollover => pulses newer", + self._mac, + ) return - if ( - self._last_log_production_timestamp is None - or self._next_log_production_timestamp is None - ): - # Unable to determine rollover + + if self._pulses_timestamp < self._last_log_timestamp: + if consumption: + self._rollover_consumption = True + _LOGGER.debug( + "_update_rollover | %s | set consumption rollover => log newer", + self._mac, + ) + else: + self._rollover_production = True + _LOGGER.debug( + "_update_rollover | %s | set production rollover => log newer", + self._mac, + ) return - if self._pulses_timestamp > self._next_log_production_timestamp: - self._rollover_production = True - _LOGGER.debug( - "_update_rollover | %s | set production rollover => pulses newer", - self._mac, - ) - elif self._pulses_timestamp < self._last_log_production_timestamp: - self._rollover_production = True - _LOGGER.debug( - "_update_rollover | %s | reset production rollover => log newer", - self._mac, - ) - elif ( - self._last_log_production_timestamp - < self._pulses_timestamp - < self._next_log_production_timestamp - ): + + # _last_log_timestamp <= _pulses_timestamp <= _next_log_timestamp + if consumption: + if self._rollover_consumption: + _LOGGER.debug("_update_rollover | %s | reset consumption rollover", self._mac) + self._rollover_consumption = False + else: if self._rollover_production: - _LOGGER.debug("_update_rollover | %s | reset production", self._mac) + _LOGGER.debug("_update_rollover | %s | reset production rollover", self._mac) self._rollover_production = False - else: - _LOGGER.debug("_update_rollover | %s | unexpected production", self._mac) 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.""" @@ -373,16 +385,27 @@ def add_log( import_only: bool = False, ) -> bool: """Store pulse log.""" - log_record = PulseLogRecord(timestamp, pulses, CONSUMED) + _LOGGER.debug( + "add_log | address=%s | slot=%s | timestamp=%s | pulses=%s | import_only=%s", + address, + slot, + timestamp, + pulses, + import_only, + ) + direction = CONSUMED + if self._log_production and pulses < 0: + direction = PRODUCED + + log_record = PulseLogRecord(timestamp, pulses, direction) if not self._add_log_record(address, slot, log_record): if not self._log_exists(address, slot): return False if address != self._last_log_address and slot != self._last_log_slot: return False - self._update_log_direction(address, slot, timestamp) self._update_log_references(address, slot) self._update_log_interval() - self._update_rollover() + self._update_rollover(direction) if not import_only: self.recalculate_missing_log_addresses() return True @@ -427,116 +450,37 @@ def _add_log_record( self._last_empty_log_slot = None return True - def _update_log_direction( - self, address: int, slot: int, timestamp: datetime - ) -> None: - """Update Energy direction of log record. - - Two subsequential logs with the same timestamp indicates the first - is consumption and second production. - """ - if self._logs is None: - return - - prev_address, prev_slot = calc_log_address(address, slot, -1) - if self._log_exists(prev_address, prev_slot): - if self._logs[prev_address][prev_slot].timestamp == timestamp: - # Given log is the second log with same timestamp, - # mark direction as production - self._logs[address][slot].is_consumption = False - self._logs[prev_address][prev_slot].is_consumption = True - self._log_production = True - elif self._log_production: - self._logs[address][slot].is_consumption = True - if self._logs[prev_address][prev_slot].is_consumption: - self._logs[prev_address][prev_slot].is_consumption = False - self._reset_log_references() - elif self._log_production is None: - self._log_production = False - - next_address, next_slot = calc_log_address(address, slot, 1) - if self._log_exists(next_address, next_slot): - if self._logs[next_address][next_slot].timestamp == timestamp: - # Given log is the first log with same timestamp, - # mark direction as production of next log - self._logs[address][slot].is_consumption = True - if self._logs[next_address][next_slot].is_consumption: - self._logs[next_address][next_slot].is_consumption = False - self._reset_log_references() - self._log_production = True - elif self._log_production: - self._logs[address][slot].is_consumption = False - self._logs[next_address][next_slot].is_consumption = True - elif self._log_production is None: - self._log_production = False - def _update_log_interval(self) -> None: """Update the detected log interval based on the most recent two logs.""" - if self._logs is None or self._log_production is None: + if self._logs is None: _LOGGER.debug( - "_update_log_interval | %s | _logs=%s, _log_production=%s", + "_update_log_interval fail | %s | _logs=%s", self._mac, self._logs, - self._log_production, ) return - last_cons_address, last_cons_slot = self._last_log_reference( - is_consumption=True - ) - if last_cons_address is None or last_cons_slot is None: + + last_address, last_slot = self._last_log_reference() + if last_address is None or last_slot is None: return - # Update interval of consumption - last_cons_timestamp = self._logs[last_cons_address][last_cons_slot].timestamp - address, slot = calc_log_address(last_cons_address, last_cons_slot, -1) - while self._log_exists(address, slot): - if self._logs[address][slot].is_consumption: - delta1: timedelta = ( - last_cons_timestamp - self._logs[address][slot].timestamp - ) - self._log_interval_consumption = int( - delta1.total_seconds() / MINUTE_IN_SECONDS - ) - break - if not self._log_production: - return - address, slot = calc_log_address(address, slot, -1) - if ( - self._log_interval_consumption is not None - and self._last_log_consumption_timestamp is not None - ): - self._next_log_consumption_timestamp = ( - self._last_log_consumption_timestamp - + timedelta(minutes=self._log_interval_consumption) + last_timestamp = self._logs[last_address][last_slot].timestamp + address, slot = calc_log_address(last_address, last_slot, -1) + if self._log_exists(address, slot): + delta: timedelta = ( + last_timestamp - self._logs[address][slot].timestamp + ) + self._log_interval = int( + delta.total_seconds() / MINUTE_IN_SECONDS ) - if not self._log_production: - return - # Update interval of production - last_prod_address, last_prod_slot = self._last_log_reference( - is_consumption=False - ) - if last_prod_address is None or last_prod_slot is None: - return - last_prod_timestamp = self._logs[last_prod_address][last_prod_slot].timestamp - address, slot = calc_log_address(last_prod_address, last_prod_slot, -1) - while self._log_exists(address, slot): - if not self._logs[address][slot].is_consumption: - delta2: timedelta = ( - last_prod_timestamp - self._logs[address][slot].timestamp - ) - self._log_interval_production = int( - delta2.total_seconds() / MINUTE_IN_SECONDS - ) - break - address, slot = calc_log_address(address, slot, -1) if ( - self._log_interval_production is not None - and self._last_log_production_timestamp is not None + self._log_interval is not None + and self._last_log_timestamp is not None ): - self._next_log_production_timestamp = ( - self._last_log_production_timestamp - + timedelta(minutes=self._log_interval_production) + self._next_log_timestamp = ( + self._last_log_timestamp + + timedelta(minutes=self._log_interval) ) def _log_exists(self, address: int, slot: int) -> bool: @@ -549,179 +493,84 @@ def _log_exists(self, address: int, slot: int) -> bool: return True def _update_last_log_reference( - self, address: int, slot: int, timestamp: datetime, is_consumption: bool + self, address: int, slot: int, timestamp: datetime ) -> None: """Update references to last (most recent) log record.""" if self._last_log_timestamp is None or self._last_log_timestamp < timestamp: self._last_log_address = address self._last_log_slot = slot self._last_log_timestamp = timestamp - elif self._last_log_timestamp == timestamp and not is_consumption: - self._last_log_address = address - self._last_log_slot = slot - self._last_log_timestamp = timestamp - - def _update_last_consumption_log_reference( - self, address: int, slot: int, timestamp: datetime - ) -> None: - """Update references to last (most recent) log consumption record.""" - if ( - self._last_log_consumption_timestamp is None - or self._last_log_consumption_timestamp <= timestamp - ): - self._last_log_consumption_timestamp = timestamp - self._last_log_consumption_address = address - self._last_log_consumption_slot = slot def _reset_log_references(self) -> None: """Reset log references.""" - self._last_log_consumption_address = None - self._last_log_consumption_slot = None - self._last_log_consumption_timestamp = None - self._first_log_consumption_address = None - self._first_log_consumption_slot = None - self._first_log_consumption_timestamp = None - self._last_log_production_address = None - self._last_log_production_slot = None - self._last_log_production_timestamp = None - self._first_log_production_address = None - self._first_log_production_slot = None - self._first_log_production_timestamp = None + self._last_log_address = None + self._last_log_slot = None + self._last_log_timestamp = None + self._first_log_address = None + self._first_log_slot = None + self._first_log_timestamp = None + if self._logs is None: return + for address in self._logs: for slot, log_record in self._logs[address].items(): - if log_record.is_consumption: - if self._last_log_consumption_timestamp is None: - self._last_log_consumption_timestamp = log_record.timestamp - if self._last_log_consumption_timestamp <= log_record.timestamp: - self._last_log_consumption_timestamp = log_record.timestamp - self._last_log_consumption_address = address - self._last_log_consumption_slot = slot - - if self._first_log_consumption_timestamp is None: - self._first_log_consumption_timestamp = log_record.timestamp - if self._first_log_consumption_timestamp >= log_record.timestamp: - self._first_log_consumption_timestamp = log_record.timestamp - self._first_log_consumption_address = address - self._first_log_consumption_slot = slot - else: - if self._last_log_production_timestamp is None: - self._last_log_production_timestamp = log_record.timestamp - if self._last_log_production_timestamp <= log_record.timestamp: - self._last_log_production_address = address - self._last_log_production_slot = slot - - if self._first_log_production_timestamp is None: - self._first_log_production_timestamp = log_record.timestamp - if self._first_log_production_timestamp > log_record.timestamp: - self._first_log_production_timestamp = log_record.timestamp - self._first_log_production_address = address - self._first_log_production_slot = slot - - def _update_last_production_log_reference( - self, address: int, slot: int, timestamp: datetime - ) -> None: - """Update references to last (most recent) log production record.""" - if ( - self._last_log_production_timestamp is None - or self._last_log_production_timestamp <= timestamp - ): - self._last_log_production_timestamp = timestamp - self._last_log_production_address = address - self._last_log_production_slot = slot + if self._last_log_timestamp is None: + self._last_log_timestamp = log_record.timestamp + if self._last_log_timestamp <= log_record.timestamp: + self._last_log_timestamp = log_record.timestamp + self._last_log_address = address + self._last_log_slot = slot + + if self._first_log_timestamp is None: + self._first_log_timestamp = log_record.timestamp + if self._first_log_timestamp >= log_record.timestamp: + self._first_log_timestamp = log_record.timestamp + self._first_log_address = address + self._first_log_slot = slot def _update_first_log_reference( - self, address: int, slot: int, timestamp: datetime, is_consumption: bool + self, address: int, slot: int, timestamp: datetime ) -> None: """Update references to first (oldest) log record.""" if self._first_log_timestamp is None or self._first_log_timestamp > timestamp: self._first_log_address = address self._first_log_slot = slot self._first_log_timestamp = timestamp - elif self._first_log_timestamp == timestamp and is_consumption: - self._first_log_address = address - self._first_log_slot = slot - self._first_log_timestamp = timestamp - - def _update_first_consumption_log_reference( - self, address: int, slot: int, timestamp: datetime - ) -> None: - """Update references to first (oldest) log consumption record.""" - if ( - self._first_log_consumption_timestamp is None - or self._first_log_consumption_timestamp >= timestamp - ): - self._first_log_consumption_timestamp = timestamp - self._first_log_consumption_address = address - self._first_log_consumption_slot = slot - - def _update_first_production_log_reference( - self, address: int, slot: int, timestamp: datetime - ) -> None: - """Update references to first (oldest) log production record.""" - if ( - self._first_log_production_timestamp is None - or self._first_log_production_timestamp >= timestamp - ): - self._first_log_production_timestamp = timestamp - self._first_log_production_address = address - self._first_log_production_slot = slot def _update_log_references(self, address: int, slot: int) -> None: """Update next expected log timestamps.""" if self._logs is None: return + log_time_stamp = self._logs[address][slot].timestamp - is_consumption = self._logs[address][slot].is_consumption # Update log references - self._update_first_log_reference(address, slot, log_time_stamp, is_consumption) - self._update_last_log_reference(address, slot, log_time_stamp, is_consumption) - - if is_consumption: - self._update_first_consumption_log_reference(address, slot, log_time_stamp) - self._update_last_consumption_log_reference(address, slot, log_time_stamp) - else: - # production - self._update_first_production_log_reference(address, slot, log_time_stamp) - self._update_last_production_log_reference(address, slot, log_time_stamp) + self._update_first_log_reference(address, slot, log_time_stamp) + self._update_last_log_reference(address, slot, log_time_stamp) @property def log_addresses_missing(self) -> list[int] | None: """Return the addresses of missing logs.""" return self._log_addresses_missing - def _last_log_reference( - self, is_consumption: bool | None = None - ) -> tuple[int | None, int | None]: + def _last_log_reference(self) -> tuple[int | None, int | None]: """Address and slot of last log.""" - if is_consumption is None: - return (self._last_log_address, self._last_log_slot) - if is_consumption: - return (self._last_log_consumption_address, self._last_log_consumption_slot) - return (self._last_log_production_address, self._last_log_production_slot) - - def _first_log_reference( - self, is_consumption: bool | None = None - ) -> tuple[int | None, int | None]: + return (self._last_log_address, self._last_log_slot) + + def _first_log_reference(self) -> tuple[int | None, int | None]: """Address and slot of first log.""" - if is_consumption is None: - return (self._first_log_address, self._first_log_slot) - if is_consumption: - return ( - self._first_log_consumption_address, - self._first_log_consumption_slot, - ) - return (self._first_log_production_address, self._first_log_production_slot) + return (self._first_log_address, self._first_log_slot) def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: """Calculate list of missing log addresses.""" if self._logs is None: self._log_addresses_missing = None return None + if self.collected_logs < 2: return None + last_address, last_slot = self._last_log_reference() if last_address is None or last_slot is None: _LOGGER.debug( @@ -791,16 +640,9 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: # Check if we are able to calculate log interval address, slot = calc_log_address(first_address, first_slot, -1) log_interval: int | None = None - if self._log_interval_consumption is not None: - log_interval = self._log_interval_consumption - elif self._log_interval_production is not None: - log_interval = self._log_interval_production - if ( - self._log_interval_production is not None - and log_interval is not None - and self._log_interval_production < log_interval - ): - log_interval = self._log_interval_production + if self._log_interval is not None: + log_interval = self._log_interval + if log_interval is None: return None @@ -850,53 +692,24 @@ def _missing_addresses_before( return addresses # default interval - calc_interval_cons = timedelta(hours=1) + calc_interval = timedelta(hours=1) if ( - self._log_interval_consumption is not None - and self._log_interval_consumption > 0 + self._log_interval is not None + and self._log_interval > 0 ): - # Use consumption interval - calc_interval_cons = timedelta(minutes=self._log_interval_consumption) - if self._log_interval_consumption == 0: + calc_interval = timedelta(minutes=self._log_interval) + if self._log_interval == 0: pass - if self._log_production is not True: - expected_timestamp = ( - self._logs[address][slot].timestamp - calc_interval_cons - ) - address, slot = calc_log_address(address, slot, -1) - while expected_timestamp > target and address > 0: - if address not in addresses: - addresses.append(address) - expected_timestamp -= calc_interval_cons - address, slot = calc_log_address(address, slot, -1) - else: - # Production logging active - calc_interval_prod = timedelta(hours=1) - if ( - self._log_interval_production is not None - and self._log_interval_production > 0 - ): - calc_interval_prod = timedelta(minutes=self._log_interval_production) - - expected_timestamp_cons = ( - self._logs[address][slot].timestamp - calc_interval_cons - ) - expected_timestamp_prod = ( - self._logs[address][slot].timestamp - calc_interval_prod - ) - + expected_timestamp = ( + self._logs[address][slot].timestamp - calc_interval + ) + address, slot = calc_log_address(address, slot, -1) + while expected_timestamp > target and address > 0: + if address not in addresses: + addresses.append(address) + expected_timestamp -= calc_interval address, slot = calc_log_address(address, slot, -1) - while ( - expected_timestamp_cons > target or expected_timestamp_prod > target - ) and address > 0: - if address not in addresses: - addresses.append(address) - if expected_timestamp_prod > expected_timestamp_cons: - expected_timestamp_prod -= calc_interval_prod - else: - expected_timestamp_cons -= calc_interval_cons - address, slot = calc_log_address(address, slot, -1) return addresses @@ -905,52 +718,25 @@ def _missing_addresses_after( ) -> list[int]: """Return list of any missing address(es) after given log timestamp.""" addresses: list[int] = [] - if self._logs is None: return addresses # default interval - calc_interval_cons = timedelta(hours=1) - if ( - self._log_interval_consumption is not None - and self._log_interval_consumption > 0 - ): - # Use consumption interval - calc_interval_cons = timedelta(minutes=self._log_interval_consumption) - - if self._log_production is not True: - expected_timestamp = ( - self._logs[address][slot].timestamp + calc_interval_cons - ) - address, slot = calc_log_address(address, slot, 1) - while expected_timestamp < target: - address, slot = calc_log_address(address, slot, 1) - expected_timestamp += timedelta(hours=1) - if address not in addresses: - addresses.append(address) - return addresses - - # Production logging active - calc_interval_prod = timedelta(hours=1) + calc_interval = timedelta(hours=1) if ( - self._log_interval_production is not None - and self._log_interval_production > 0 + self._log_interval is not None + and self._log_interval > 0 ): - calc_interval_prod = timedelta(minutes=self._log_interval_production) + calc_interval = timedelta(minutes=self._log_interval) - expected_timestamp_cons = ( - self._logs[address][slot].timestamp + calc_interval_cons - ) - expected_timestamp_prod = ( - self._logs[address][slot].timestamp + calc_interval_prod + expected_timestamp = ( + self._logs[address][slot].timestamp + calc_interval ) address, slot = calc_log_address(address, slot, 1) - while expected_timestamp_cons < target or expected_timestamp_prod < target: + while expected_timestamp < target: + address, slot = calc_log_address(address, slot, 1) + expected_timestamp += timedelta(hours=1) if address not in addresses: addresses.append(address) - if expected_timestamp_prod < expected_timestamp_cons: - expected_timestamp_prod += calc_interval_prod - else: - expected_timestamp_cons += calc_interval_cons - address, slot = calc_log_address(address, slot, 1) + return addresses diff --git a/pyproject.toml b/pyproject.toml index 8f118df71..fa911f412 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "v0.40.0a33" +version = "v0.40.0a54" license = {file = "LICENSE"} description = "Plugwise USB (Stick) module for Python 3." readme = "README.md" diff --git a/tests/test_usb.py b/tests/test_usb.py index da959c3a4..7ef39f108 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -909,8 +909,7 @@ async def fake_get_missing_energy_logs(address: int) -> None: # Test energy state without request assert stick.nodes["0098765432101234"].energy == pw_api.EnergyStatistics( - log_interval_consumption=None, - log_interval_production=None, + log_interval=None, hour_consumption=None, hour_consumption_reset=None, day_consumption=None, @@ -930,16 +929,15 @@ async def fake_get_missing_energy_logs(address: int) -> None: # Allow for background task to finish assert stick.nodes["0098765432101234"].energy == pw_api.EnergyStatistics( - log_interval_consumption=60, - log_interval_production=None, + log_interval=60, hour_consumption=0.0026868922443345974, hour_consumption_reset=utc_now.replace(minute=0, second=0, microsecond=0), day_consumption=None, day_consumption_reset=None, week_consumption=None, week_consumption_reset=None, - hour_production=None, - hour_production_reset=None, + hour_production=0.0, + hour_production_reset=utc_now.replace(minute=0, second=0, microsecond=0), day_production=None, day_production_reset=None, week_production=None, @@ -960,15 +958,14 @@ def test_pulse_collection_consumption( # Test consumption logs tst_consumption = pw_energy_pulses.PulseCollection(mac="0098765432101234") assert tst_consumption.log_addresses_missing is None - assert tst_consumption.production_logging is None + assert tst_consumption.production_logging # is None # Test consumption - Log import #1 # No missing addresses yet test_timestamp = fixed_this_hour tst_consumption.add_log(100, 1, test_timestamp, 1000) - assert tst_consumption.log_interval_consumption is None - assert tst_consumption.log_interval_production is None - assert tst_consumption.production_logging is None + assert tst_consumption.log_interval is None + assert tst_consumption.production_logging # is None assert tst_consumption.collected_pulses( test_timestamp, is_consumption=True ) == (None, None) @@ -979,9 +976,8 @@ def test_pulse_collection_consumption( # return intermediate missing addresses test_timestamp = fixed_this_hour - td(hours=17) tst_consumption.add_log(95, 4, test_timestamp, 1000) - assert tst_consumption.log_interval_consumption is None - assert tst_consumption.log_interval_production is None - assert tst_consumption.production_logging is None + assert tst_consumption.log_interval is None + assert tst_consumption.production_logging # is None assert tst_consumption.collected_pulses( test_timestamp, is_consumption=True ) == (None, None) @@ -992,9 +988,8 @@ def test_pulse_collection_consumption( # so 'production logging' should be marked as False now test_timestamp = fixed_this_hour - td(hours=18) tst_consumption.add_log(95, 3, test_timestamp, 1000) - assert tst_consumption.log_interval_consumption is None - assert tst_consumption.log_interval_production is None - assert not tst_consumption.production_logging + assert tst_consumption.log_interval is None + assert tst_consumption.production_logging assert tst_consumption.collected_pulses( test_timestamp, is_consumption=True ) == (None, None) @@ -1003,9 +998,8 @@ def test_pulse_collection_consumption( # Test consumption - Log import #4, no change test_timestamp = fixed_this_hour - td(hours=19) tst_consumption.add_log(95, 2, test_timestamp, 1000) - assert tst_consumption.log_interval_consumption is None - assert tst_consumption.log_interval_production is None - assert not tst_consumption.production_logging + assert tst_consumption.log_interval is None + assert tst_consumption.production_logging assert tst_consumption.collected_pulses( test_timestamp, is_consumption=True ) == (None, None) @@ -1015,27 +1009,24 @@ def test_pulse_collection_consumption( # Complete log import for address 95 so it must drop from missing list test_timestamp = fixed_this_hour - td(hours=20) tst_consumption.add_log(95, 1, test_timestamp, 1000) - assert tst_consumption.log_interval_consumption is None - assert tst_consumption.log_interval_production is None - assert not tst_consumption.production_logging + assert tst_consumption.log_interval is None + assert tst_consumption.production_logging assert tst_consumption.log_addresses_missing == [99, 98, 97, 96] # Test consumption - Log import #6 # Add before last log so interval of consumption must be determined test_timestamp = fixed_this_hour - td(hours=1) tst_consumption.add_log(99, 4, test_timestamp, 750) - assert tst_consumption.log_interval_consumption == 60 - assert tst_consumption.log_interval_production is None - assert not tst_consumption.production_logging + assert tst_consumption.log_interval == 60 + assert tst_consumption.production_logging assert tst_consumption.log_addresses_missing == [99, 98, 97, 96] assert tst_consumption.collected_pulses( fixed_this_hour, is_consumption=True ) == (None, None) tst_consumption.add_log(99, 3, fixed_this_hour - td(hours=2), 1111) - assert tst_consumption.log_interval_consumption == 60 - assert tst_consumption.log_interval_production is None - assert not tst_consumption.production_logging + assert tst_consumption.log_interval == 60 + assert tst_consumption.production_logging assert tst_consumption.log_addresses_missing == [99, 98, 97, 96] assert tst_consumption.collected_pulses( fixed_this_hour, is_consumption=True @@ -1049,7 +1040,7 @@ def test_pulse_collection_consumption( ) == (1234, pulse_update_1) assert tst_consumption.collected_pulses( fixed_this_hour, is_consumption=False - ) == (None, None) + ) == (0, pulse_update_1) assert tst_consumption.log_addresses_missing == [99, 98, 97, 96] # Test consumption - pulse update #2 @@ -1061,7 +1052,7 @@ def test_pulse_collection_consumption( ) == (2345, pulse_update_2) assert tst_consumption.collected_pulses( test_timestamp, is_consumption=False - ) == (None, None) + ) == (0, pulse_update_2) # Test consumption - pulses + log (address=100, slot=1) test_timestamp = fixed_this_hour - td(hours=1) @@ -1070,7 +1061,7 @@ def test_pulse_collection_consumption( ) == (2345 + 1000, pulse_update_2) assert tst_consumption.collected_pulses( test_timestamp, is_consumption=False - ) == (None, None) + ) == (0, pulse_update_2) assert tst_consumption.log_addresses_missing == [99, 98, 97, 96] # Test consumption - pulses + logs (address=100, slot=1 & address=99, slot=4) @@ -1080,7 +1071,7 @@ def test_pulse_collection_consumption( ) == (2345 + 1000 + 750, pulse_update_2) assert tst_consumption.collected_pulses( test_timestamp, is_consumption=False - ) == (None, None) + ) == (0, pulse_update_2) # Test consumption - pulses + missing logs test_timestamp = fixed_this_hour - td(hours=3) @@ -1090,8 +1081,8 @@ def test_pulse_collection_consumption( assert tst_consumption.collected_pulses( test_timestamp, is_consumption=False ) == (None, None) - assert not tst_consumption.log_rollover + # add missing logs test_timestamp = fixed_this_hour - td(hours=3) tst_consumption.add_log(99, 2, (fixed_this_hour - td(hours=3)), 1000) @@ -1128,7 +1119,7 @@ def test_pulse_collection_consumption( test_timestamp, is_consumption=True ) == (None, None) tst_consumption.add_log(100, 2, (fixed_this_hour + td(hours=1)), 2222) - assert not tst_consumption.log_rollover + assert tst_consumption.log_rollover assert tst_consumption.collected_pulses( test_timestamp, is_consumption=True ) == (45, pulse_update_3) @@ -1138,16 +1129,16 @@ def test_pulse_collection_consumption( # Test log rollover by updating log first before updating pulses tst_consumption.add_log(100, 3, (fixed_this_hour + td(hours=2)), 3333) - assert tst_consumption.log_rollover assert tst_consumption.collected_pulses( fixed_this_hour, is_consumption=True ) == (None, None) + assert tst_consumption.log_rollover pulse_update_4 = fixed_this_hour + td(hours=2, seconds=10) tst_consumption.update_pulse_counter(321, 0, pulse_update_4) - assert not tst_consumption.log_rollover assert tst_consumption.collected_pulses( fixed_this_hour, is_consumption=True ) == (2222 + 3333 + 321, pulse_update_4) + assert not tst_consumption.log_rollover @freeze_time(dt.now()) def test_pulse_collection_consumption_empty( @@ -1202,46 +1193,43 @@ def test_pulse_collection_production(self, monkeypatch: pytest.MonkeyPatch) -> N # Test consumption and production logs tst_production = pw_energy_pulses.PulseCollection(mac="0098765432101234") assert tst_production.log_addresses_missing is None - assert tst_production.production_logging is None + assert tst_production.production_logging # is None # Test consumption & production - Log import #1 - production # Missing addresses can not be determined yet test_timestamp = fixed_this_hour - td(hours=1) - tst_production.add_log(200, 2, test_timestamp, 2000) + tst_production.add_log(200, 2, test_timestamp, -2000) assert tst_production.log_addresses_missing is None - assert tst_production.production_logging is None + # assert tst_production.production_logging is None # Test consumption & production - Log import #2 - consumption # production must be enabled & intervals are unknown # Log at address 200 is known and expect production logs too - test_timestamp = fixed_this_hour - td(hours=1) + test_timestamp = fixed_this_hour - td(hours=2) tst_production.add_log(200, 1, test_timestamp, 1000) - assert tst_production.log_addresses_missing is None - assert tst_production.log_interval_consumption is None - assert tst_production.log_interval_production is None - assert tst_production.production_logging + missing_check = list(range(199, 157, -1)) + assert tst_production.log_addresses_missing == missing_check + assert tst_production.log_interval == 60 + # assert tst_production.production_logging # Test consumption & production - Log import #3 - production - # Interval of consumption is not yet available - test_timestamp = fixed_this_hour - td(hours=2) # type: ignore[unreachable] - tst_production.add_log(199, 4, test_timestamp, 4000) - missing_check = list(range(199, 157, -1)) + # Interval of consumption is available + test_timestamp = fixed_this_hour - td(hours=3) # type: ignore[unreachable] + tst_production.add_log(199, 4, test_timestamp, -4000) + # missing_check = list(range(199, 157, -1)) assert tst_production.log_addresses_missing == missing_check - assert tst_production.log_interval_consumption is None - assert tst_production.log_interval_production == 60 - assert tst_production.production_logging + assert tst_production.log_interval == 60 + # assert tst_production.production_logging # Test consumption & production - Log import #4 - # Interval of consumption is available - test_timestamp = fixed_this_hour - td(hours=2) + test_timestamp = fixed_this_hour - td(hours=4) tst_production.add_log(199, 3, test_timestamp, 3000) assert tst_production.log_addresses_missing == missing_check - assert tst_production.log_interval_consumption == 60 - assert tst_production.log_interval_production == 60 - assert tst_production.production_logging + assert tst_production.log_interval == 60 + # assert tst_production.production_logging pulse_update_1 = fixed_this_hour + td(minutes=5) - tst_production.update_pulse_counter(100, 50, pulse_update_1) + tst_production.update_pulse_counter(100, -50, pulse_update_1) assert tst_production.collected_pulses( fixed_this_hour, is_consumption=True ) == (100, pulse_update_1) @@ -1249,17 +1237,17 @@ def test_pulse_collection_production(self, monkeypatch: pytest.MonkeyPatch) -> N fixed_this_hour, is_consumption=False ) == (50, pulse_update_1) assert tst_production.collected_pulses( - fixed_this_hour - td(hours=1), is_consumption=True - ) == (100, pulse_update_1) + fixed_this_hour - td(hours=1), is_consumption=False + ) == (50, pulse_update_1) assert tst_production.collected_pulses( fixed_this_hour - td(hours=2), is_consumption=True - ) == (1000 + 100, pulse_update_1) + ) == (100, pulse_update_1) assert tst_production.collected_pulses( - fixed_this_hour - td(hours=1), is_consumption=False - ) == (50, pulse_update_1) + fixed_this_hour - td(hours=3), is_consumption=True + ) == (1000 + 100, pulse_update_1) assert tst_production.collected_pulses( - fixed_this_hour - td(hours=2), is_consumption=False - ) == (2000 + 50, pulse_update_1) + fixed_this_hour - td(hours=4), is_consumption=False + ) == (6000 + 50, pulse_update_1) _pulse_update = 0 @@ -1281,14 +1269,14 @@ def test_log_address_rollover(self, monkeypatch: pytest.MonkeyPatch) -> None: # test tst_pc = pw_energy_pulses.PulseCollection(mac="0098765432101234") tst_pc.add_log(2, 4, fixed_this_hour - td(hours=1), 0) # prod - tst_pc.add_log(2, 3, fixed_this_hour - td(hours=1), 23935) # con - tst_pc.add_log(2, 2, fixed_this_hour - td(hours=2), 0) # prod - tst_pc.add_log(2, 1, fixed_this_hour - td(hours=2), 10786) # con - # <-- logs 0 & 1 are missing for hours 3, 4, 5 & 6 --> - tst_pc.add_log(6015, 4, fixed_this_hour - td(hours=7), 0) - tst_pc.add_log(6015, 3, fixed_this_hour - td(hours=7), 11709) - tst_pc.add_log(6015, 2, fixed_this_hour - td(hours=8), 0) - tst_pc.add_log(6015, 1, fixed_this_hour - td(hours=8), 10382) + tst_pc.add_log(2, 3, fixed_this_hour - td(hours=2), 23935) # con + tst_pc.add_log(2, 2, fixed_this_hour - td(hours=3), 0) # prod + tst_pc.add_log(2, 1, fixed_this_hour - td(hours=4), 10786) # con + # <-- logs 0 & 1 are missing for hours 5, 6, 7 & 8 --> + tst_pc.add_log(6015, 4, fixed_this_hour - td(hours=9), 0) + tst_pc.add_log(6015, 3, fixed_this_hour - td(hours=10), 11709) + tst_pc.add_log(6015, 2, fixed_this_hour - td(hours=11), 0) + tst_pc.add_log(6015, 1, fixed_this_hour - td(hours=12), 10382) assert tst_pc.log_addresses_missing == [1, 0] def pulse_update(