diff --git a/plugwise_usb/api.py b/plugwise_usb/api.py index bbbfdaa6c..ce9cdbf38 100644 --- a/plugwise_usb/api.py +++ b/plugwise_usb/api.py @@ -216,14 +216,10 @@ class EnergyStatistics: hour_consumption_reset: datetime | None = None day_consumption: float | None = None day_consumption_reset: datetime | None = None - week_consumption: float | None = None - week_consumption_reset: datetime | None = None hour_production: float | None = None hour_production_reset: datetime | None = None day_production: float | None = None day_production_reset: datetime | None = None - week_production: float | None = None - week_production_reset: datetime | None = None class PlugwiseNode(Protocol): diff --git a/plugwise_usb/nodes/circle.py b/plugwise_usb/nodes/circle.py index 7daa30265..dde3c5098 100644 --- a/plugwise_usb/nodes/circle.py +++ b/plugwise_usb/nodes/circle.py @@ -440,6 +440,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.", @@ -447,20 +448,17 @@ async def get_missing_energy_logs(self) -> None: ) total_addresses = 11 log_address = self._current_log_address - log_update_tasks = [] while total_addresses > 0: - log_update_tasks.append(self.energy_log_update(log_address)) + await self.energy_log_update(log_address) log_address, _ = calc_log_address(log_address, 1, -4) total_addresses -= 1 - for task in log_update_tasks: - await task - 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) + + _LOGGER.debug("Task created to get missing logs of %s", self._mac_in_str) if ( missing_addresses := self._energy_counters.log_addresses_missing ) is not None: @@ -472,8 +470,12 @@ async def get_missing_energy_logs(self) -> None: ) missing_addresses = sorted(missing_addresses, reverse=True) - for address in missing_addresses: - await self.energy_log_update(address) + tasks = [ + create_task(self.energy_log_update(address)) + for address in missing_addresses + ] + for task in tasks: + await task if self._cache_enabled: await self._energy_log_records_save_to_cache() @@ -496,6 +498,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 +507,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( @@ -515,9 +523,11 @@ async def energy_log_update(self, address: int | None) -> bool: import_only=True, ): energy_record_update = True + self._energy_counters.update() if energy_record_update: await self.save_cache() + return True async def _energy_log_records_load_from_cache(self) -> bool: diff --git a/plugwise_usb/nodes/helpers/counter.py b/plugwise_usb/nodes/helpers/counter.py index f894d8244..a8d08a8ea 100644 --- a/plugwise_usb/nodes/helpers/counter.py +++ b/plugwise_usb/nodes/helpers/counter.py @@ -21,8 +21,6 @@ class EnergyType(Enum): PRODUCTION_HOUR = auto() CONSUMPTION_DAY = auto() PRODUCTION_DAY = auto() - CONSUMPTION_WEEK = auto() - PRODUCTION_WEEK = auto() ENERGY_COUNTERS: Final = ( @@ -30,8 +28,6 @@ class EnergyType(Enum): EnergyType.PRODUCTION_HOUR, EnergyType.CONSUMPTION_DAY, EnergyType.PRODUCTION_DAY, - EnergyType.CONSUMPTION_WEEK, - EnergyType.PRODUCTION_WEEK, ) ENERGY_HOUR_COUNTERS: Final = ( EnergyType.CONSUMPTION_HOUR, @@ -41,20 +37,13 @@ class EnergyType(Enum): EnergyType.CONSUMPTION_DAY, EnergyType.PRODUCTION_DAY, ) -ENERGY_WEEK_COUNTERS: Final = ( - EnergyType.CONSUMPTION_WEEK, - EnergyType.PRODUCTION_WEEK, -) - ENERGY_CONSUMPTION_COUNTERS: Final = ( EnergyType.CONSUMPTION_HOUR, EnergyType.CONSUMPTION_DAY, - EnergyType.CONSUMPTION_WEEK, ) ENERGY_PRODUCTION_COUNTERS: Final = ( EnergyType.PRODUCTION_HOUR, EnergyType.PRODUCTION_DAY, - EnergyType.PRODUCTION_WEEK, ) _LOGGER = logging.getLogger(__name__) @@ -105,11 +94,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 ) @@ -160,9 +146,6 @@ def update(self) -> None: 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.hour_consumption, self._energy_statistics.hour_consumption_reset, @@ -171,23 +154,18 @@ 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.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.log_interval_production = ( + self._pulse_collection.log_interval_production + ) + ( + 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) @property def timestamp(self) -> datetime | None: @@ -211,14 +189,13 @@ def __init__( ) -> None: """Initialize energy counter based on energy id.""" self._mac = mac + self._midnight_reset_passed = False 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" self._energy_id: EnergyType = energy_id self._is_consumption = True self._direction = "consumption" @@ -259,9 +236,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 +260,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 @@ -297,16 +282,21 @@ def update( last_reset = datetime.now(tz=LOCAL_TIMEZONE) if self._energy_id in ENERGY_HOUR_COUNTERS: 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, - ) + if self._energy_id in ENERGY_DAY_COUNTERS: + # Postpone the last_reset time-changes at day-end until a device pulsecounter resets + if last_reset.hour == 0 and ( + not pulse_collection.pulse_counter_reset + and not self._midnight_reset_passed + ): + last_reset = (last_reset - timedelta(days=1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ) + else: + if last_reset.hour == 0 and pulse_collection.pulse_counter_reset: + self._midnight_reset_passed = True + if last_reset.hour == 1 and self._midnight_reset_passed: + self._midnight_reset_passed = False + 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 @@ -324,5 +314,6 @@ def update( 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..81de2ca1a 100644 --- a/plugwise_usb/nodes/helpers/pulses.py +++ b/plugwise_usb/nodes/helpers/pulses.py @@ -7,14 +7,14 @@ 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 -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]: @@ -83,6 +83,8 @@ def __init__(self, mac: str) -> None: self._first_log_production_slot: int | None = None self._next_log_production_timestamp: datetime | None = None + self._cons_pulsecounter_reset = False + self._prod_pulsecounter_reset = False self._rollover_consumption = False self._rollover_production = False @@ -155,39 +157,35 @@ def last_update(self) -> datetime | None: """Return timestamp of last update.""" return self._pulses_timestamp + @property + def pulse_counter_reset(self) -> bool: + """Return a pulse_counter reset.""" + return self._cons_pulsecounter_reset or self._prod_pulsecounter_reset + 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 | %s | is_cons=%s, from=%s", self._mac, is_consumption, from_timestamp) - + _LOGGER.debug( + "collected_pulses | %s | from_timestamp=%s | is_cons=%s | _log_production=%s", + self._mac, + from_timestamp, + is_consumption, + self._log_production + ) if not is_consumption: if self._log_production is None or not self._log_production: return (None, None) - if is_consumption and self._rollover_consumption: - _LOGGER.debug("collected_pulses | %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) - 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) - return (None, None) - pulses: int | None = None timestamp: datetime | None = None if is_consumption and self._pulses_consumption is not None: pulses = self._pulses_consumption timestamp = self._pulses_timestamp + if not is_consumption and self._pulses_production is not None: pulses = self._pulses_production timestamp = self._pulses_timestamp - # _LOGGER.debug("collected_pulses | %s | pulses=%s", self._mac, pulses) if pulses is None: _LOGGER.debug( @@ -196,7 +194,23 @@ def collected_pulses( is_consumption, ) return (None, None) - return (pulses + log_pulses, timestamp) + + if ( + log_pulses := self._collect_pulses_from_logs(from_timestamp, is_consumption) + ) is None: + _LOGGER.debug("collected_pulses | %s | log_pulses:None", self._mac) + return (None, None) + + _LOGGER.debug( + "collected_pulses | pulses=%s | log_pulses=%s | consumption=%s at timestamp=%s", + pulses, + log_pulses, + is_consumption, + timestamp, + ) + + # Always return positive values for energy_statistics + return (abs(pulses + log_pulses), timestamp) def _collect_pulses_from_logs( self, from_timestamp: datetime, is_consumption: bool @@ -205,6 +219,8 @@ def _collect_pulses_from_logs( if self._logs is None: _LOGGER.debug("_collect_pulses_from_logs | %s | self._logs=None", self._mac) return None + + timestamp: datetime | None = None if is_consumption: if self._last_log_consumption_timestamp is None: _LOGGER.debug( @@ -212,8 +228,8 @@ def _collect_pulses_from_logs( self._mac, ) return None - if from_timestamp > self._last_log_consumption_timestamp: - return 0 + + timestamp = self._last_log_consumption_timestamp else: if self._last_log_production_timestamp is None: _LOGGER.debug( @@ -221,8 +237,9 @@ def _collect_pulses_from_logs( self._mac, ) return None - if from_timestamp > self._last_log_production_timestamp: - return 0 + + timestamp = self._last_log_production_timestamp + missing_logs = self._logs_missing(from_timestamp) if missing_logs is None or missing_logs: _LOGGER.debug( @@ -233,102 +250,132 @@ def _collect_pulses_from_logs( return None log_pulses = 0 - - for log_item in self._logs.values(): + for log_item in self.logs.values(): for slot_item in log_item.values(): if ( slot_item.is_consumption == is_consumption and slot_item.timestamp > from_timestamp ): log_pulses += slot_item.pulses + + _LOGGER.debug( + "_collect_pulses_from_logs | log_pulses=%s | is_consumption=%s | from %s to %s", + log_pulses, + is_consumption, + from_timestamp, + timestamp, + ) return log_pulses def update_pulse_counter( self, pulses_consumed: int, pulses_produced: int, timestamp: datetime ) -> None: - """Update pulse counter.""" + """Update pulse counter. + + Both device consumption and production counters reset after the beginning of a new hour. + """ + self._cons_pulsecounter_reset = False + self._prod_pulsecounter_reset = False self._pulses_timestamp = timestamp self._update_rollover() + if ( + self._pulses_consumption is not None + and self._pulses_consumption > pulses_consumed + ): + self._cons_pulsecounter_reset = True + _LOGGER.debug("update_pulse_counter | consumption pulses reset") + + if ( + self._pulses_production is not None + and self._pulses_production < pulses_produced + ): + self._prod_pulsecounter_reset = True + _LOGGER.debug("update_pulse_counter | production pulses reset") + + # No rollover based on time, set rollover based on counter reset + # Required for special cases like nodes which have been powered off for several days 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 - if ( - self._pulses_consumption is not None - and self._pulses_consumption > pulses_consumed - ): + if self._cons_pulsecounter_reset: + _LOGGER.debug("update_pulse_counter | rollover consumption") self._rollover_consumption = True - if ( - self._pulses_production is not None - and self._pulses_production > pulses_produced - ): + + if self._prod_pulsecounter_reset: + _LOGGER.debug("update_pulse_counter | rollover production") self._rollover_production = True + self._pulses_consumption = pulses_consumed self._pulses_production = pulses_produced + _LOGGER.debug( + "update_pulse_counter | consumption pulses=%s | production pulses=%s", + self._pulses_consumption, + self._pulses_production, + ) def _update_rollover(self) -> None: - """Update rollover states. Returns True if rollover is applicable.""" + """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 - ): - # 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, + + self._rollover_consumption = self._detect_rollover( + self._rollover_consumption, + self._last_log_consumption_timestamp, + self._next_log_consumption_timestamp, + ) + if self._log_production: + self._rollover_production = self._detect_rollover( + self._rollover_production, + self._last_log_production_timestamp, + self._next_log_production_timestamp, + False, ) - 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: - return + def _detect_rollover( + self, + rollover: bool, + last_log_timestamp: datetime | None, + next_log_timestamp: datetime | None, + is_consumption=True, + ) -> bool: + """Helper function for _update_rollover().""" + if ( - self._last_log_production_timestamp is None - or self._next_log_production_timestamp is None - ): - # Unable to determine rollover - 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 + self._pulses_timestamp is not None + and last_log_timestamp is not None + and next_log_timestamp is not None ): - if self._rollover_production: - _LOGGER.debug("_update_rollover | %s | reset production", self._mac) - self._rollover_production = False - else: - _LOGGER.debug("_update_rollover | %s | unexpected production", self._mac) + direction = "consumption" + if not is_consumption: + direction = "production" + + if self._pulses_timestamp > next_log_timestamp: + _LOGGER.debug( + "_update_rollover | %s | set %s rollover => pulses newer", + self._mac, + direction, + ) + return True + + if self._pulses_timestamp < last_log_timestamp: + _LOGGER.debug( + "_update_rollover | %s | set %s rollover => log newer", + self._mac, + direction, + ) + return True + + if last_log_timestamp <= self._pulses_timestamp <= next_log_timestamp: + if rollover: + _LOGGER.debug( + "_update_rollover | %s | reset %s rollover", + self._mac, + direction + ) + return False 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,11 +420,16 @@ def add_log( import_only: bool = False, ) -> bool: """Store pulse log.""" - log_record = PulseLogRecord(timestamp, pulses, CONSUMED) + 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: + _LOGGER.debug("add_log | address-slot already exists") return False self._update_log_direction(address, slot, timestamp) self._update_log_references(address, slot) @@ -385,6 +437,14 @@ def add_log( self._update_rollover() if not import_only: self.recalculate_missing_log_addresses() + + _LOGGER.debug( + "add_log | pulses=%s | address=%s | slot= %s |time:%s", + pulses, + address, + slot, + timestamp, + ) return True def recalculate_missing_log_addresses(self) -> None: @@ -403,15 +463,19 @@ def _add_log_record( if self._logs is None: self._logs = {address: {slot: log_record}} return True + if self._log_exists(address, slot): return False + # Drop useless log records when we have at least 4 logs if self.collected_logs > 4 and log_record.timestamp < ( datetime.now(tz=UTC) - timedelta(hours=MAX_LOG_HOURS) ): return False + if self._logs.get(address) is None: self._logs[address] = {slot: log_record} + self._logs[address][slot] = log_record if ( address == self._first_empty_log_address @@ -419,12 +483,14 @@ def _add_log_record( ): self._first_empty_log_address = None self._first_empty_log_slot = None + if ( address == self._last_empty_log_address and slot == self._last_empty_log_slot ): self._last_empty_log_address = None self._last_empty_log_slot = None + return True def _update_log_direction( @@ -480,6 +546,7 @@ def _update_log_interval(self) -> None: self._log_production, ) return + last_cons_address, last_cons_slot = self._last_log_reference( is_consumption=True ) @@ -498,8 +565,7 @@ def _update_log_interval(self) -> None: 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 @@ -609,6 +675,7 @@ def _reset_log_references(self) -> None: 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_timestamp = log_record.timestamp self._last_log_production_address = address self._last_log_production_slot = slot @@ -672,20 +739,19 @@ 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 + log_timestamp = 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) + self._update_first_log_reference(address, slot, log_timestamp, is_consumption) + self._update_last_log_reference(address, slot, log_timestamp, 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_consumption_log_reference(address, slot, log_timestamp) + self._update_last_consumption_log_reference(address, slot, log_timestamp) + elif self._log_production: + self._update_first_production_log_reference(address, slot, log_timestamp) + self._update_last_production_log_reference(address, slot, log_timestamp) @property def log_addresses_missing(self) -> list[int] | None: @@ -720,8 +786,10 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | None: 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( @@ -795,12 +863,14 @@ def _logs_missing(self, from_timestamp: datetime) -> list[int] | 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 log_interval is None: return None @@ -860,7 +930,7 @@ def _missing_addresses_before( if self._log_interval_consumption == 0: pass - if self._log_production is not True: + if not self._log_production: expected_timestamp = ( self._logs[address][slot].timestamp - calc_interval_cons ) @@ -918,7 +988,7 @@ def _missing_addresses_after( # Use consumption interval calc_interval_cons = timedelta(minutes=self._log_interval_consumption) - if self._log_production is not True: + if not self._log_production: expected_timestamp = ( self._logs[address][slot].timestamp + calc_interval_cons ) diff --git a/pyproject.toml b/pyproject.toml index 8f118df71..7467082cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise_usb" -version = "v0.40.0a33" -license = {file = "LICENSE"} -description = "Plugwise USB (Stick) module for Python 3." -readme = "README.md" +version = "v0.40.0a89" keywords = ["home", "automation", "plugwise", "module", "usb"] classifiers = [ "Development Status :: 5 - Production/Stable", diff --git a/tests/test_usb.py b/tests/test_usb.py index da959c3a4..d10ac122f 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -915,14 +915,10 @@ async def fake_get_missing_energy_logs(address: int) -> None: hour_consumption_reset=None, day_consumption=None, day_consumption_reset=None, - week_consumption=None, - week_consumption_reset=None, hour_production=None, hour_production_reset=None, day_production=None, day_production_reset=None, - week_production=None, - week_production_reset=None, ) # energy_update is not complete and should return none utc_now = dt.now(UTC) @@ -936,14 +932,10 @@ async def fake_get_missing_energy_logs(address: int) -> None: 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, day_production=None, day_production_reset=None, - week_production=None, - week_production_reset=None, ) await stick.disconnect() @@ -1118,36 +1110,47 @@ def test_pulse_collection_consumption( tst_consumption.add_log(94, 1, (fixed_this_hour - td(hours=24)), 1000) assert tst_consumption.collected_logs == 24 + # Test rollover by updating pulses before log record + pulse_update_3 = fixed_this_hour + td(hours=0, minutes=0, seconds=30) + tst_consumption.update_pulse_counter(2500, 0, pulse_update_3) assert not tst_consumption.log_rollover - pulse_update_3 = fixed_this_hour + td(hours=1, seconds=3) - tst_consumption.update_pulse_counter(45, 0, pulse_update_3) - assert tst_consumption.log_rollover - test_timestamp = fixed_this_hour + td(hours=1, seconds=5) assert tst_consumption.collected_pulses( 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 + ) == (2500 + 1111 + 1000 + 750, pulse_update_3) + pulse_update_4 = fixed_this_hour + td(hours=1, minutes=1, seconds=3) + tst_consumption.update_pulse_counter(45, 0, pulse_update_4) + assert tst_consumption.log_rollover + test_timestamp = fixed_this_hour + td(hours=1, minutes=1, seconds=4) assert tst_consumption.collected_pulses( test_timestamp, is_consumption=True - ) == (45, pulse_update_3) + ) == (45, pulse_update_4) + tst_consumption.add_log(100, 2, (fixed_this_hour + td(hours=1, minutes=1, seconds=5)), 2222) + assert tst_consumption.log_rollover + # Test collection of the last full hour assert tst_consumption.collected_pulses( fixed_this_hour, is_consumption=True - ) == (45 + 2222, pulse_update_3) + ) == (45 + 2222, pulse_update_4) + pulse_update_5 = fixed_this_hour + td(hours=1, minutes=1, seconds=18) + test_timestamp_2 = fixed_this_hour + td(hours=1, minutes=1, seconds=20) + tst_consumption.update_pulse_counter(145, 0, pulse_update_5) + # Test collection of the last new hour + assert tst_consumption.collected_pulses( + test_timestamp_2, is_consumption=True + ) == (145, pulse_update_5) # 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) - pulse_update_4 = fixed_this_hour + td(hours=2, seconds=10) - tst_consumption.update_pulse_counter(321, 0, pulse_update_4) + ) == (145 + 2222 + 3333, pulse_update_5) + pulse_update_6 = fixed_this_hour + td(hours=2, seconds=10) + tst_consumption.update_pulse_counter(321, 0, pulse_update_6) assert not tst_consumption.log_rollover assert tst_consumption.collected_pulses( fixed_this_hour, is_consumption=True - ) == (2222 + 3333 + 321, pulse_update_4) + ) == (2222 + 3333 + 321, pulse_update_6) @freeze_time(dt.now()) def test_pulse_collection_consumption_empty(