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
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ def topic(cls) -> str:
async def handle_true(self) -> None:
LOG.info(f"Vehicle {self.vin} charging cable will be locked")
await self.saic_api.control_charging_port_lock(self.vin, unlock=False)
# Some models never report the lock state, so publish the commanded one.
# Models that do report it overwrite it on the forced refresh.
self.vehicle_state.update_charging_cable_lock(locked=True)

@override
async def handle_false(self) -> None:
LOG.info(f"Vehicle {self.vin} charging cable will be unlocked")
await self.saic_api.control_charging_port_lock(self.vin, unlock=True)
self.vehicle_state.update_charging_cable_lock(locked=False)

@override
async def _get_action_result(self, _action_result: None) -> CommandProcessingResult:
Expand Down
7 changes: 6 additions & 1 deletion src/status_publisher/charge/chrg_mgmt_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,12 @@ def publish(self, charge_mgmt_data: ChrgMgmtData) -> ChrgMgmtDataProcessingResul

self._transform_and_publish(
topic=mqtt_topics.DRIVETRAIN_CHARGING_CABLE_LOCK,
value=charge_mgmt_data.charging_port_locked,
# Some models (e.g. MG ZS EV) do not report the lock state at all
value=(
charge_mgmt_data.charging_port_locked
if charge_mgmt_data.ccuEleccLckCtrlDspCmd is not None
else None
),
transform=int_to_bool,
)

Expand Down
5 changes: 5 additions & 0 deletions src/vehicle.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,11 @@ def set_is_charging(self, is_charging: bool) -> None:
self.get_topic(mqtt_topics.DRIVETRAIN_CHARGING), self.is_charging
)

def update_charging_cable_lock(self, *, locked: bool) -> None:
self.publisher.publish_bool(
self.get_topic(mqtt_topics.DRIVETRAIN_CHARGING_CABLE_LOCK), locked
)

def handle_vehicle_status(
self, vehicle_status: VehicleStatusResp
) -> VehicleStatusRespProcessingResult:
Expand Down
49 changes: 49 additions & 0 deletions tests/handlers/command/test_drivetrain_charging_cable_lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from __future__ import annotations

import unittest
from unittest.mock import AsyncMock, MagicMock

import pytest

from handlers.command.base import RESULT_REFRESH_AND_CLEAR
from handlers.command.drivetrain.drivetrain_charging_cable_lock import (
DrivetrainChargingCableLockCommand,
)


class TestDrivetrainChargingCableLockCommand(unittest.IsolatedAsyncioTestCase):
async def test_lock_publishes_locked_state(self) -> None:
vehicle_state = MagicMock()
saic_api = AsyncMock()
handler = DrivetrainChargingCableLockCommand(saic_api, vehicle_state)

result = await handler.handle("true")

assert result == RESULT_REFRESH_AND_CLEAR
saic_api.control_charging_port_lock.assert_called_once_with(
vehicle_state.vin, unlock=False
)
vehicle_state.update_charging_cable_lock.assert_called_once_with(locked=True)

async def test_unlock_publishes_unlocked_state(self) -> None:
vehicle_state = MagicMock()
saic_api = AsyncMock()
handler = DrivetrainChargingCableLockCommand(saic_api, vehicle_state)

await handler.handle("false")

saic_api.control_charging_port_lock.assert_called_once_with(
vehicle_state.vin, unlock=True
)
vehicle_state.update_charging_cable_lock.assert_called_once_with(locked=False)

async def test_failed_command_does_not_publish_state(self) -> None:
vehicle_state = MagicMock()
saic_api = AsyncMock()
saic_api.control_charging_port_lock.side_effect = RuntimeError("boom")
handler = DrivetrainChargingCableLockCommand(saic_api, vehicle_state)

with pytest.raises(RuntimeError):
await handler.handle("true")

vehicle_state.update_charging_cable_lock.assert_not_called()
20 changes: 20 additions & 0 deletions tests/test_vehicle_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ def _charge_data_availability(self) -> str | None:
topic = f"/vehicles/{VIN}/{mqtt_topics.DRIVETRAIN_CHARGING_DATA_AVAILABLE}"
return self.publisher.map.get(topic)

def _cable_lock_topic(self) -> str:
return f"/vehicles/{VIN}/{mqtt_topics.DRIVETRAIN_CHARGING_CABLE_LOCK}"

def test_cable_lock_published_when_reported(self) -> None:
self.vehicle_state.handle_charge_status(get_mock_charge_management_data_resp())
assert self.publisher.map[self._cable_lock_topic()] is True

def test_cable_lock_not_published_when_not_reported(self) -> None:
resp = get_mock_charge_management_data_resp()
assert resp.chrgMgmtData is not None
resp.chrgMgmtData.ccuEleccLckCtrlDspCmd = None
self.vehicle_state.handle_charge_status(resp)
assert self._cable_lock_topic() not in self.publisher.map

def test_update_charging_cable_lock_publishes_commanded_state(self) -> None:
self.vehicle_state.update_charging_cable_lock(locked=True)
assert self.publisher.map[self._cable_lock_topic()] is True
self.vehicle_state.update_charging_cable_lock(locked=False)
assert self.publisher.map[self._cable_lock_topic()] is False

def _vehicle_availability(self) -> str | None:
return self.publisher.map.get(f"/vehicles/{VIN}/{mqtt_topics.AVAILABLE}")

Expand Down
Loading