From 94765dd5050232950145eb83f2a742fa6ba5932f Mon Sep 17 00:00:00 2001 From: rikroe Date: Sun, 19 Feb 2023 09:39:22 +0100 Subject: [PATCH] Add charge start/stop remote service --- bimmer_connected/const.py | 1 + bimmer_connected/vehicle/remote_services.py | 33 +++++++++++-- test/test_remote_services.py | 51 +++++++++++++++++++-- 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/bimmer_connected/const.py b/bimmer_connected/const.py index 87c3044f..5a3c26a2 100644 --- a/bimmer_connected/const.py +++ b/bimmer_connected/const.py @@ -73,6 +73,7 @@ class Regions(str, Enum): VEHICLE_CHARGING_BASE_URL = "/eadrax-crccs/v1/vehicles/{vin}" VEHICLE_CHARGING_SETTINGS_SET_URL = VEHICLE_CHARGING_BASE_URL + "/charging-settings" VEHICLE_CHARGING_PROFILE_SET_URL = VEHICLE_CHARGING_BASE_URL + "/charging-profile" +VEHICLE_CHARGING_START_STOP_URL = VEHICLE_CHARGING_BASE_URL + "/{service_type}" VEHICLE_IMAGE_URL = "/eadrax-ics/v3/presentation/vehicles/{vin}/images?carView={view}" VEHICLE_POI_URL = "/eadrax-dcs/v1/send-to-car/send-to-car" diff --git a/bimmer_connected/vehicle/remote_services.py b/bimmer_connected/vehicle/remote_services.py index 1657acf9..701830aa 100644 --- a/bimmer_connected/vehicle/remote_services.py +++ b/bimmer_connected/vehicle/remote_services.py @@ -13,6 +13,7 @@ REMOTE_SERVICE_URL, VEHICLE_CHARGING_PROFILE_SET_URL, VEHICLE_CHARGING_SETTINGS_SET_URL, + VEHICLE_CHARGING_START_STOP_URL, VEHICLE_POI_URL, ) from bimmer_connected.models import ChargingSettings, PointOfInterest, StrEnum @@ -23,6 +24,7 @@ ChargingMode, ChargingPreferences, ) +from bimmer_connected.vehicle.fuel_and_battery import ChargingState if TYPE_CHECKING: from bimmer_connected.vehicle import MyBMWVehicle @@ -46,6 +48,7 @@ class ExecutionState(StrEnum): DELIVERED = "DELIVERED" EXECUTED = "EXECUTED" ERROR = "ERROR" + IGNORED = "IGNORED" UNKNOWN = "UNKNOWN" @@ -58,7 +61,8 @@ class Services(StrEnum): DOOR_UNLOCK = "door-unlock" HORN = "horn-blow" AIR_CONDITIONING = "climate-now" - CHARGE_NOW = "CHARGE_NOW" + CHARGE_START = "start-charging" + CHARGE_STOP = "stop-charging" CHARGING_SETTINGS = "CHARGING_SETTINGS" CHARGING_PROFILE = "CHARGING_PROFILE" SEND_POI = "SEND_POI" @@ -69,6 +73,8 @@ class Services(StrEnum): Services.CHARGING_SETTINGS: VEHICLE_CHARGING_SETTINGS_SET_URL, Services.CHARGING_PROFILE: VEHICLE_CHARGING_PROFILE_SET_URL, Services.SEND_POI: VEHICLE_POI_URL, + Services.CHARGE_START: VEHICLE_CHARGING_START_STOP_URL, + Services.CHARGE_STOP: VEHICLE_CHARGING_START_STOP_URL, } CHARGING_MODE_TO_CHARGING_PREFERENCE = { @@ -179,9 +185,30 @@ async def trigger_remote_horn(self) -> RemoteServiceStatus: raise ValueError(f"Vehicle does not support remote service {repr(Services.HORN)}.") return await self.trigger_remote_service(Services.HORN) - async def trigger_charge_now(self) -> RemoteServiceStatus: + async def trigger_charge_start(self) -> RemoteServiceStatus: """Trigger the vehicle to start charging.""" - return await self.trigger_remote_service(Services.CHARGE_NOW, refresh=True) + if not self._vehicle.is_remote_charge_start_enabled: + raise ValueError(f"Vehicle does not support remote service {repr(Services.CHARGE_START)}.") + + if not self._vehicle.fuel_and_battery.is_charger_connected: + _LOGGER.warning("Charger not connected, cannot start charging.") + return RemoteServiceStatus({"eventStatus": "IGNORED"}) + + return await self.trigger_remote_service(Services.CHARGE_START, refresh=True) + + async def trigger_charge_stop(self) -> RemoteServiceStatus: + """Trigger the vehicle to start charging.""" + if not self._vehicle.is_remote_charge_stop_enabled: + raise ValueError(f"Vehicle does not support remote service {repr(Services.CHARGE_STOP)}.") + + if not self._vehicle.fuel_and_battery.is_charger_connected: + _LOGGER.warning("Charger not connected, cannot stop charging.") + return RemoteServiceStatus({"eventStatus": "IGNORED"}) + if self._vehicle.fuel_and_battery.charging_status != ChargingState.CHARGING: + _LOGGER.warning("Vehicle not charging, cannot stop charging.") + return RemoteServiceStatus({"eventStatus": "IGNORED"}) + + return await self.trigger_remote_service(Services.CHARGE_STOP, refresh=True) async def trigger_remote_air_conditioning(self) -> RemoteServiceStatus: """Trigger the air conditioning to start.""" diff --git a/test/test_remote_services.py b/test/test_remote_services.py index 89f617a0..c36d28c5 100644 --- a/test/test_remote_services.py +++ b/test/test_remote_services.py @@ -26,7 +26,7 @@ from bimmer_connected.vehicle import remote_services from bimmer_connected.vehicle.remote_services import ExecutionState, RemoteServiceStatus -from . import RESPONSE_DIR, VIN_F31, VIN_G01, VIN_G26, VIN_I01_NOREX, load_response +from . import RESPONSE_DIR, VIN_F31, VIN_G01, VIN_G26, VIN_I01_NOREX, VIN_I20, load_response from .test_account import account_mock, get_mocked_account _RESPONSE_INITIATED = RESPONSE_DIR / "remote_services" / "eadrax_service_initiated.json" @@ -101,6 +101,9 @@ def remote_services_mock(): router.post(path__regex=r"/eadrax-vrccs/v3/presentation/remote-commands/.+/.+$").mock( side_effect=service_trigger_sideeffect ) + router.post(path__regex=r"/eadrax-crccs/v1/vehicles/.+/(start|stop)-charging$").mock( + side_effect=service_trigger_sideeffect + ) router.post(path__regex=r"/eadrax-crccs/v1/vehicles/.+/charging-settings$").mock( side_effect=service_trigger_sideeffect ) @@ -140,7 +143,8 @@ def test_states(): "VEHICLE_FINDER": {"call": "trigger_remote_vehicle_finder", "refresh": False}, "HORN_BLOW": {"call": "trigger_remote_horn", "refresh": False}, "SEND_POI": {"call": "trigger_send_poi", "refresh": False, "args": [POI_DATA]}, - "CHARGE_NOW": {"call": "trigger_charge_now", "refresh": True}, + "CHARGE_START": {"call": "trigger_charge_start", "refresh": True}, + "CHARGE_STOP": {"call": "trigger_charge_stop", "refresh": True}, "CHARGING_SETTINGS": {"call": "trigger_charging_settings_update", "refresh": True, "kwargs": CHARGING_SETTINGS}, } @@ -152,7 +156,7 @@ async def test_trigger_remote_services(): """Test executing a remote light flash.""" account = await get_mocked_account() - vehicle = account.get_vehicle(VIN_G26) + vehicle = account.get_vehicle(VIN_I20) for service in ALL_SERVICES.values(): with mock.patch( @@ -253,7 +257,7 @@ async def test_set_charging_profile(): @remote_services_mock() @pytest.mark.asyncio -async def test_vehicles_without_enabled(): +async def test_vehicles_without_enabled_services(): """Test setting the charging profile on a car.""" account = await get_mocked_account() @@ -265,12 +269,49 @@ async def test_vehicles_without_enabled(): for service in ALL_SERVICES.values(): with pytest.raises(ValueError): - await getattr(vehicle.remote_services, service["call"])( *service.get("args", []), **service.get("kwargs", {}) ) +@remote_services_mock() +@pytest.mark.asyncio +async def test_trigger_charge_start_stop_warnings(caplog): + """Test if warnings are produced correctly with the charge start/stop services.""" + + account = await get_mocked_account() + vehicle = account.get_vehicle(VIN_I20) + + fixture_not_connected = { + **vehicle.data["state"]["electricChargingState"], + "chargingStatus": "INVALID", + "isChargerConnected": False, + } + vehicle.update_state(vehicle.data, {"state": {"electricChargingState": fixture_not_connected}}) + + result = await vehicle.remote_services.trigger_charge_start() + assert result.state == ExecutionState.IGNORED + assert len([r for r in caplog.records if r.levelname == "WARNING" and "Charger not connected" in r.message]) == 1 + caplog.clear() + + result = await vehicle.remote_services.trigger_charge_stop() + assert result.state == ExecutionState.IGNORED + assert len([r for r in caplog.records if r.levelname == "WARNING" and "Charger not connected" in r.message]) == 1 + caplog.clear() + + fixture_connected_not_charging = { + **vehicle.data["state"]["electricChargingState"], + "chargingStatus": "WAITING_FOR_CHARGING", + "isChargerConnected": True, + } + vehicle.update_state(vehicle.data, {"state": {"electricChargingState": fixture_connected_not_charging}}) + + result = await vehicle.remote_services.trigger_charge_stop() + assert result.state == ExecutionState.IGNORED + assert len([r for r in caplog.records if r.levelname == "WARNING" and "Vehicle not charging" in r.message]) == 1 + caplog.clear() + + @remote_services_mock() @pytest.mark.asyncio async def test_get_remote_position():