Skip to content

Commit 854f641

Browse files
nanomadclaude
andauthored
fix: deduplicate pollingPhase MQTT publishes (#436)
* fix: deduplicate pollingPhase MQTT publishes __publish_polling_phase() was called on every polling cycle via should_refresh(), publishing the phase to MQTT even when unchanged. Track the current phase and skip redundant publishes, matching the existing deduplication pattern in set_refresh_mode(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add A->B->A transition test for polling phase dedup Verifies that the deduplication correctly re-publishes a phase after an intervening different phase (OFF -> FORCE -> OFF publishes 3 times). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f1b0ebf commit 854f641

3 files changed

Lines changed: 45 additions & 0 deletions

File tree

src/vehicle.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def __init__(
130130
self.charge_polling_min_percent = charge_polling_min_percent
131131
self.refresh_mode = RefreshMode.OFF
132132
self.previous_refresh_mode = RefreshMode.OFF
133+
self.__polling_phase: PollingPhase | None = None
133134
self.__remote_ac_temp: int | None = None
134135
self.__remote_ac_running: bool = False
135136
self.__remote_heated_seats_front_left_level: int = 0
@@ -710,6 +711,9 @@ def get_topic(self, sub_topic: str) -> str:
710711
return f"{self.mqtt_vin_prefix}/{sub_topic}"
711712

712713
def __publish_polling_phase(self, phase: PollingPhase) -> None:
714+
if self.__polling_phase == phase:
715+
return
716+
self.__polling_phase = phase
713717
self.publisher.publish_str(
714718
self.get_topic(mqtt_topics.REFRESH_POLLING_PHASE), phase.value
715719
)

tests/mocks/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ class MessageCapturingConsolePublisher(ConsolePublisher):
1515
def __init__(self, configuration: Configuration) -> None:
1616
super().__init__(configuration)
1717
self.map: dict[str, Any] = {}
18+
self.publish_count: dict[str, int] = {}
1819

1920
@override
2021
def internal_publish(self, key: str, value: Any) -> None:
2122
self.map[key] = value
23+
self.publish_count[key] = self.publish_count.get(key, 0) + 1
2224
LOG.debug(f"{key}: {value}")

tests/test_vehicle_state.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,45 @@ def test_charging_stop_triggers_after_shutdown_grace(self) -> None:
486486
PollingPhase.AFTER_SHUTDOWN.value,
487487
)
488488

489+
def test_polling_phase_not_republished_when_unchanged(self) -> None:
490+
"""Calling should_refresh() twice with the same state should only publish the phase once."""
491+
self.vehicle_state.configure_missing()
492+
self.vehicle_state.set_refresh_mode(RefreshMode.OFF, "test")
493+
self.publisher.map.clear()
494+
self.publisher.publish_count.clear()
495+
496+
self.vehicle_state.should_refresh()
497+
self.vehicle_state.should_refresh()
498+
499+
phase_topic = self.get_topic(mqtt_topics.REFRESH_POLLING_PHASE)
500+
assert self.publisher.publish_count.get(phase_topic, 0) == 1
501+
502+
def test_polling_phase_republished_after_transition_back(self) -> None:
503+
"""A->B->A transition must publish all three phases (not suppress the return to A)."""
504+
self.vehicle_state.configure_missing()
505+
# Start with OFF (phase A)
506+
self.vehicle_state.set_refresh_mode(RefreshMode.OFF, "test")
507+
self.publisher.map.clear()
508+
self.publisher.publish_count.clear()
509+
510+
# Phase A: OFF
511+
self.vehicle_state.should_refresh()
512+
phase_topic = self.get_topic(mqtt_topics.REFRESH_POLLING_PHASE)
513+
assert self.publisher.map[phase_topic] == PollingPhase.OFF.value
514+
assert self.publisher.publish_count[phase_topic] == 1
515+
516+
# Phase B: FORCE (one-shot, reverts to PERIODIC)
517+
self.vehicle_state.set_refresh_mode(RefreshMode.FORCE, "test")
518+
self.vehicle_state.should_refresh()
519+
assert self.publisher.map[phase_topic] == PollingPhase.FORCE.value
520+
assert self.publisher.publish_count[phase_topic] == 2
521+
522+
# Phase A again: back to OFF
523+
self.vehicle_state.set_refresh_mode(RefreshMode.OFF, "test")
524+
self.vehicle_state.should_refresh()
525+
assert self.publisher.map[phase_topic] == PollingPhase.OFF.value
526+
assert self.publisher.publish_count[phase_topic] == 3
527+
489528
@staticmethod
490529
def get_topic(sub_topic: str) -> str:
491530
return f"/vehicles/{VIN}/{sub_topic}"

0 commit comments

Comments
 (0)