Skip to content

Commit a63392c

Browse files
authored
Merge pull request #329 from plugwise/energy_log_cache
Line up energy_logs cache-content with energy_logs collection in memory
2 parents b7176ce + 62753d0 commit a63392c

File tree

3 files changed

+80
-107
lines changed

3 files changed

+80
-107
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.44.14 - 2025-08-31
4+
5+
- PR [329](https://github.com/plugwise/python-plugwise-usb/pull/329): Improve EnergyLogs caching: store only data from MAX_LOG_HOURS (24)
6+
37
## v0.44.13 - 2025-08-29
48

59
- PR [327](https://github.com/plugwise/python-plugwise-usb/pull/327): Improve code quality according to SonarCloud, simplify sed awake timer

plugwise_usb/nodes/circle.py

Lines changed: 75 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -401,9 +401,10 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09
401401
self._log_no_energy_stats_update()
402402
return None
403403

404-
# Always request last energy log records at initial startup
404+
# Always request the most recent energy log records at initial startup, check if the current
405+
# address is actually reported by the node even when all slots at that address are empty.
405406
if not self._last_energy_log_requested:
406-
self._last_energy_log_requested = await self.energy_log_update(
407+
self._last_energy_log_requested, _ = await self.energy_log_update(
407408
self._current_log_address, save_cache=False
408409
)
409410

@@ -416,9 +417,9 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09
416417
)
417418
return None
418419

419-
# Try collecting energy-stats for _current_log_address
420-
result = await self.energy_log_update(
421-
self._current_log_address, save_cache=True
420+
# Try collecting energy-stats from _current_log_address
421+
result, slots_empty_cur = await self.energy_log_update(
422+
self._current_log_address, save_cache=False
422423
)
423424
if not result:
424425
_LOGGER.debug(
@@ -428,18 +429,22 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09
428429
)
429430
return None
430431

431-
if self._current_log_address is not None:
432-
# Retry with previous log address as Circle node pointer to self._current_log_address
433-
# could be rolled over while the last log is at previous address/slot
434-
prev_log_address, _ = calc_log_address(self._current_log_address, 1, -4)
435-
result = await self.energy_log_update(prev_log_address, save_cache=True)
436-
if not result:
437-
_LOGGER.debug(
438-
"async_energy_update | %s | Log rollover | energy_log_update from address %s failed",
439-
self._mac_in_str,
440-
prev_log_address,
441-
)
442-
return None
432+
# Retry with previous log address as Circle node pointer to self._current_log_address
433+
# could be rolled over while the last log is at previous address
434+
prev_log_address, _ = calc_log_address(self._current_log_address, 1, -4)
435+
result, slots_empty_prev = await self.energy_log_update(
436+
prev_log_address, save_cache=False
437+
)
438+
if not result:
439+
_LOGGER.debug(
440+
"async_energy_update | %s | Log rollover | energy_log_update from address %s failed",
441+
self._mac_in_str,
442+
prev_log_address,
443+
)
444+
return None
445+
446+
if self._cache_enabled and (not slots_empty_cur or not slots_empty_prev):
447+
await self.save_cache()
443448

444449
if (
445450
missing_addresses := self._energy_counters.log_addresses_missing
@@ -453,7 +458,7 @@ async def energy_update(self) -> EnergyStatistics | None: # noqa: PLR0911 PLR09
453458
return self._energy_counters.energy_statistics
454459

455460
if len(missing_addresses) == 1:
456-
result = await self.energy_log_update(
461+
result, _ = await self.energy_log_update(
457462
missing_addresses[0], save_cache=True
458463
)
459464
if result:
@@ -515,9 +520,12 @@ async def _get_initial_energy_logs(self) -> None:
515520
max_addresses_to_collect, ceil(datetime.now(tz=UTC).hour / factor) + 1
516521
)
517522
log_address = self._current_log_address
523+
any_updates = False
518524
while total_addresses > 0:
519-
result = await self.energy_log_update(log_address, save_cache=False)
520-
if not result:
525+
result, slots_empty = await self.energy_log_update(
526+
log_address, save_cache=False
527+
)
528+
if result and slots_empty:
521529
# Stop initial log collection when an address contains no (None) or outdated data
522530
# Outdated data can indicate a EnergyLog address rollover: from address 6014 to 0
523531
_LOGGER.debug(
@@ -526,11 +534,12 @@ async def _get_initial_energy_logs(self) -> None:
526534
)
527535
break
528536

537+
any_updates |= not slots_empty
529538
log_address, _ = calc_log_address(log_address, 1, -4)
530539
total_addresses -= 1
531540

532-
if self._cache_enabled:
533-
await self._energy_log_records_save_to_cache()
541+
if self._cache_enabled and any_updates:
542+
await self.save_cache()
534543

535544
async def get_missing_energy_logs(self) -> None:
536545
"""Task to retrieve missing energy logs."""
@@ -554,8 +563,9 @@ async def get_missing_energy_logs(self) -> None:
554563
create_task(self.energy_log_update(address, save_cache=False))
555564
for address in missing_addresses
556565
]
566+
any_updates = False
557567
for idx, task in enumerate(tasks):
558-
result = await task
568+
result, slots_empty = await task
559569
# When an energy log collection task returns False, stop and cancel the remaining tasks
560570
if not result:
561571
to_cancel = tasks[idx + 1 :]
@@ -565,15 +575,25 @@ async def get_missing_energy_logs(self) -> None:
565575
await gather(*to_cancel, return_exceptions=True)
566576
break
567577

568-
if self._cache_enabled:
569-
await self._energy_log_records_save_to_cache()
578+
any_updates |= not slots_empty
579+
580+
if self._cache_enabled and any_updates:
581+
await self.save_cache()
570582

571583
async def energy_log_update(
572584
self, address: int | None, save_cache: bool = True
573-
) -> bool:
574-
"""Request energy logs and return True only when at least one recent, non-empty record was stored; otherwise return False."""
585+
) -> tuple[bool, bool]:
586+
"""Request energy logs from node and store them.
587+
588+
First bool: True when processing succeeded (records stored in memory, possibly all-empty);
589+
False only on transport or address errors.
590+
Second bool: slots_empty — True when all four slots at the address are empty or outdated;
591+
False when at least one recent, non-empty record was stored.
592+
"""
593+
result = False
594+
slots_empty = True
575595
if address is None:
576-
return False
596+
return result, slots_empty
577597

578598
_LOGGER.debug(
579599
"Requesting EnergyLogs from node %s address %s",
@@ -586,8 +606,9 @@ async def energy_log_update(
586606
"Retrieving EnergyLogs data from node %s failed",
587607
self._mac_in_str,
588608
)
589-
return False
609+
return result, slots_empty
590610

611+
result = True
591612
_LOGGER.debug("EnergyLogs from node %s, address=%s:", self._mac_in_str, address)
592613
await self._available_update_state(True, response.timestamp)
593614

@@ -600,31 +621,31 @@ async def energy_log_update(
600621
_LOGGER.debug(
601622
"In slot=%s: pulses=%s, timestamp=%s", _slot, log_pulses, log_timestamp
602623
)
603-
if (
604-
log_timestamp is None
605-
or log_pulses is None
606-
# Don't store an old log record; store an empty record instead
607-
or not self._check_timestamp_is_recent(address, _slot, log_timestamp)
608-
):
609-
self._energy_counters.add_empty_log(response.log_address, _slot)
610-
continue
611-
612-
cache_updated = await self._energy_log_record_update_state(
613-
response.log_address,
614-
_slot,
615-
log_timestamp.replace(tzinfo=UTC),
616-
log_pulses,
617-
import_only=True,
618-
)
624+
address = response.log_address
625+
if log_timestamp is None or log_pulses is None:
626+
self._energy_counters.add_empty_log(address, _slot)
627+
else:
628+
log_timestamp = log_timestamp.replace(tzinfo=UTC)
629+
if self._check_timestamp_is_recent(address, _slot, log_timestamp):
630+
self._energy_counters.add_pulse_log(
631+
address,
632+
_slot,
633+
log_timestamp,
634+
log_pulses,
635+
import_only=True,
636+
)
637+
cache_updated = True
619638

620639
self._energy_counters.update()
621-
if cache_updated and save_cache:
622-
_LOGGER.debug(
623-
"Saving energy record update to cache for %s", self._mac_in_str
624-
)
625-
await self.save_cache()
640+
if cache_updated:
641+
slots_empty = False
642+
await self._energy_log_records_save_to_cache()
643+
if save_cache:
644+
_LOGGER.debug("Saving energy cache for %s", self._mac_in_str)
645+
await self.save_cache()
646+
return result, slots_empty
626647

627-
return True
648+
return result, slots_empty
628649

629650
def _check_timestamp_is_recent(
630651
self, address: int, slot: int, timestamp: datetime
@@ -695,7 +716,7 @@ async def _energy_log_records_load_from_cache(self) -> bool:
695716
return True
696717

697718
async def _energy_log_records_save_to_cache(self) -> None:
698-
"""Save currently collected energy logs to cached file."""
719+
"""Update the in-memory energy log cache string (no file I/O)."""
699720
if not self._cache_enabled:
700721
return
701722

@@ -711,60 +732,8 @@ async def _energy_log_records_save_to_cache(self) -> None:
711732
f"{address}:{slot}:{ts.strftime('%Y-%m-%d-%H-%M-%S')}:{log.pulses}"
712733
)
713734
cached_logs = "|".join(records)
714-
_LOGGER.debug("Saving energy logrecords to cache for %s", self._mac_in_str)
735+
_LOGGER.debug("Updating in-memory energy log records for %s", self._mac_in_str)
715736
self._set_cache(CACHE_ENERGY_COLLECTION, cached_logs)
716-
# Persist new cache entries to disk immediately
717-
await self.save_cache(trigger_only=True)
718-
719-
async def _energy_log_record_update_state(
720-
self,
721-
address: int,
722-
slot: int,
723-
timestamp: datetime,
724-
pulses: int,
725-
import_only: bool = False,
726-
) -> bool:
727-
"""Process new energy log record. Returns true if record is new or changed."""
728-
self._energy_counters.add_pulse_log(
729-
address, slot, timestamp, pulses, import_only=import_only
730-
)
731-
if not self._cache_enabled:
732-
return False
733-
734-
log_cache_record = (
735-
f"{address}:{slot}:{timestamp.strftime('%Y-%m-%d-%H-%M-%S')}:{pulses}"
736-
)
737-
if (cached_logs := self._get_cache(CACHE_ENERGY_COLLECTION)) is not None:
738-
entries = cached_logs.split("|") if cached_logs else []
739-
if log_cache_record not in entries:
740-
_LOGGER.debug(
741-
"Adding logrecord (%s, %s) to cache of %s",
742-
str(address),
743-
str(slot),
744-
self._mac_in_str,
745-
)
746-
new_cache = (
747-
f"{log_cache_record}|{cached_logs}"
748-
if cached_logs
749-
else log_cache_record
750-
)
751-
self._set_cache(CACHE_ENERGY_COLLECTION, new_cache)
752-
await self.save_cache(trigger_only=True)
753-
return True
754-
755-
_LOGGER.debug(
756-
"Energy logrecord already present for %s, ignoring", self._mac_in_str
757-
)
758-
return False
759-
760-
_LOGGER.debug(
761-
"Cache is empty, adding new logrecord (%s, %s) for %s",
762-
str(address),
763-
str(slot),
764-
self._mac_in_str,
765-
)
766-
self._set_cache(CACHE_ENERGY_COLLECTION, log_cache_record)
767-
return True
768737

769738
@raise_not_loaded
770739
async def set_relay(self, state: bool) -> bool:
@@ -1187,7 +1156,7 @@ async def _relay_init_update_state(self, state: bool) -> None:
11871156
NodeFeature.RELAY_INIT, self._relay_config
11881157
)
11891158
_LOGGER.debug(
1190-
"Saving relay_init state update to cachefor %s", self._mac_in_str
1159+
"Saving relay_init state update to cache for %s", self._mac_in_str
11911160
)
11921161
await self.save_cache()
11931162

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "plugwise_usb"
7-
version = "0.44.13"
7+
version = "0.44.14"
88
license = "MIT"
99
keywords = ["home", "automation", "plugwise", "module", "usb"]
1010
classifiers = [

0 commit comments

Comments
 (0)