Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
persistent. Existing retained STATE values on the broker are not converted
into retained `/set` commands.

* Republish the effective Total Battery Capacity to its state topic right after
the user updates the HA number. The `_set` handler used to only mutate the
in-memory override and rely on the next vehicle poll to refresh the shared
sensor topic, leaving the HA sensor stuck on the previous (often hardcoded
per-model default) value while the number widget already showed the new
setting. A payload of `0` re-publishes the per-model default via
`real_battery_capacity`.

## 0.11.0

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,18 @@ async def handle_typed_payload(self, payload: float) -> CommandProcessingResult:
LOG.info("Setting Total Battery Capacity to %f", payload)
self.vehicle_state.update_battery_capacity(payload)

# The HA number and sensor entities share the same state topic.
# Republish the effective capacity locally so the sensor reflects
# the change immediately instead of waiting for the next vehicle poll
# (payload of 0 falls back to the per-model default in real_battery_capacity).
effective_capacity = self.vehicle_state.vehicle.real_battery_capacity
if effective_capacity is not None and effective_capacity > 0:
self.publisher.publish_float(
self.vehicle_state.get_topic(
mqtt_topics.DRIVETRAIN_TOTAL_BATTERY_CAPACITY
),
effective_capacity,
)

# No need to force a refresh
return RESULT_DO_NOTHING
33 changes: 33 additions & 0 deletions tests/handlers/test_vehicle_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ async def test_payload_structure(self) -> None:
f"{VEHICLE_PREFIX}/{mqtt_topics.DRIVETRAIN_TOTAL_BATTERY_CAPACITY}"
f"/{mqtt_topics.RESULT_SUFFIX}"
)
TOTAL_BATTERY_CAPACITY_STATE_TOPIC = (
f"{VEHICLE_PREFIX}/{mqtt_topics.DRIVETRAIN_TOTAL_BATTERY_CAPACITY}"
)


class TestRetainedReplay(unittest.IsolatedAsyncioTestCase):
Expand Down Expand Up @@ -328,14 +331,44 @@ async def test_non_retained_force_still_applied(self) -> None:
async def test_retained_battery_capacity_replays_to_vehicle_info(self) -> None:
handler, pub = _build()
vehicle_state = cast("MagicMock", handler.vehicle_state)
vehicle_state.vehicle.real_battery_capacity = 50.0

await handler.handle_mqtt_command(
topic=TOTAL_BATTERY_CAPACITY_SET_TOPIC, payload="50.0", retained=True
)

vehicle_state.update_battery_capacity.assert_called_once_with(50.0)
pub.publish_float.assert_any_call(TOTAL_BATTERY_CAPACITY_STATE_TOPIC, 50.0)
pub.publish_str.assert_any_call(TOTAL_BATTERY_CAPACITY_RESULT_TOPIC, "Success")

async def test_battery_capacity_zero_payload_publishes_model_default(self) -> None:
"""Payload `0` clears the override; the per-model default is republished."""
handler, pub = _build()
vehicle_state = cast("MagicMock", handler.vehicle_state)
# update_battery_capacity(0) clears the override; real_battery_capacity then
# falls back to the per-model default (e.g. 64 kWh for an MG4 NMC).
vehicle_state.vehicle.real_battery_capacity = 64.0

await handler.handle_mqtt_command(
topic=TOTAL_BATTERY_CAPACITY_SET_TOPIC, payload="0", retained=False
)

vehicle_state.update_battery_capacity.assert_called_once_with(0.0)
pub.publish_float.assert_any_call(TOTAL_BATTERY_CAPACITY_STATE_TOPIC, 64.0)

async def test_battery_capacity_skips_publish_when_no_default(self) -> None:
"""When `real_battery_capacity` returns None (unknown model), skip the publish."""
handler, pub = _build()
vehicle_state = cast("MagicMock", handler.vehicle_state)
vehicle_state.vehicle.real_battery_capacity = None

await handler.handle_mqtt_command(
topic=TOTAL_BATTERY_CAPACITY_SET_TOPIC, payload="0", retained=False
)

vehicle_state.update_battery_capacity.assert_called_once_with(0.0)
pub.publish_float.assert_not_called()

async def test_retained_action_command_dropped_at_dispatcher(self) -> None:
"""Retained `/set` for an action-bearing command is dropped at the dispatcher.

Expand Down