From 3eeb09ed61a88206458615f0bd00fc3d8a862987 Mon Sep 17 00:00:00 2001 From: Luc Viala Date: Mon, 3 Jan 2022 11:44:48 +0100 Subject: [PATCH 1/2] Add Dacia Spring Support * Add Dacia Spring charge pause/resume endpoint * Add tests --- docs/endpoints.rst | 6 ++ .../vehicle_actions.charge-pause-resume.rst | 14 ++++ src/renault_api/cli/charge/commands.py | 1 + src/renault_api/cli/charge/control.py | 34 +++++++++- src/renault_api/kamereon/__init__.py | 24 ++++++- src/renault_api/renault_session.py | 4 +- src/renault_api/renault_vehicle.py | 23 ++++++- tests/cli/test_vehicle_charge.py | 67 +++++++++++++++++++ tests/fixtures.py | 11 +++ .../charge-pause-resume.pause.json | 7 ++ .../charge-pause-resume.resume.json | 7 ++ tests/kamereon/test_kamereon.py | 24 +++++++ tests/test_renault_vehicle.py | 16 +++++ 13 files changed, 231 insertions(+), 7 deletions(-) create mode 100644 docs/endpoints/vehicle_actions.charge-pause-resume.rst create mode 100644 tests/fixtures/kamereon/vehicle_action/charge-pause-resume.pause.json create mode 100644 tests/fixtures/kamereon/vehicle_action/charge-pause-resume.resume.json diff --git a/docs/endpoints.rst b/docs/endpoints.rst index 09792647..7b15b307 100644 --- a/docs/endpoints.rst +++ b/docs/endpoints.rst @@ -39,3 +39,9 @@ Action endpoints .. include:: endpoints/vehicle_action.charging-start.rst .. include:: endpoints/vehicle_action.hvac-start.rst .. include:: endpoints/vehicle_action.hvac-schedule.rst + + +Specific Action endpoints +------------------------- + +.. include:: endpoints/vehicle_actions.charge-pause-resume.rst diff --git a/docs/endpoints/vehicle_actions.charge-pause-resume.rst b/docs/endpoints/vehicle_actions.charge-pause-resume.rst new file mode 100644 index 00000000..41d77fc2 --- /dev/null +++ b/docs/endpoints/vehicle_actions.charge-pause-resume.rst @@ -0,0 +1,14 @@ +charge/pause-resume +'''''''''''''''''''''' + +.. rst-class:: endpoint + +Base url: + ``/commerce/v1/accounts/{account_id}/kamereon/kcm/v1/vehicles/{vin}/charge/pause-resume`` + +Sample return: + .. literalinclude:: /../tests/fixtures/kamereon/vehicle_action/charge-pause-resume.resume.json + :language: JSON + + .. literalinclude:: /../tests/fixtures/kamereon/vehicle_action/charge-pause-resume.pause.json + :language: JSON diff --git a/src/renault_api/cli/charge/commands.py b/src/renault_api/cli/charge/commands.py index 3e3bd193..b677f118 100644 --- a/src/renault_api/cli/charge/commands.py +++ b/src/renault_api/cli/charge/commands.py @@ -13,6 +13,7 @@ def charge() -> None: charge.add_command(control.start) +charge.add_command(control.stop) charge.add_command(control.mode) charge.add_command(history.history) charge.add_command(history.sessions) diff --git a/src/renault_api/cli/charge/control.py b/src/renault_api/cli/charge/control.py index 9a4eaee3..338d6ed5 100644 --- a/src/renault_api/cli/charge/control.py +++ b/src/renault_api/cli/charge/control.py @@ -47,5 +47,37 @@ async def start( vehicle = await renault_vehicle.get_vehicle( websession=websession, ctx_data=ctx_data ) - response = await vehicle.set_charge_start() + await vehicle.get_details() + if ( + vehicle._vehicle_details + and vehicle._vehicle_details.get_model_code() == "XBG1VE" + ): + response = await vehicle.set_charge_pause_resume("resume") + else: + response = await vehicle.set_charge_start() click.echo(response.raw_data) + + +@click.command() +@click.pass_obj +@helpers.coro_with_websession +async def stop( + ctx_data: Dict[str, Any], + *, + websession: aiohttp.ClientSession, +) -> None: + """Stop charge.""" + vehicle = await renault_vehicle.get_vehicle( + websession=websession, ctx_data=ctx_data + ) + await vehicle.get_details() + if ( + vehicle._vehicle_details + and vehicle._vehicle_details.get_model_code() == "XBG1VE" + ): + response = await vehicle.set_charge_pause_resume("pause") + click.echo(response.raw_data) + else: + response = None + click.echo("Function unavailable") + exit(1) diff --git a/src/renault_api/kamereon/__init__.py b/src/renault_api/kamereon/__init__.py index d6654f95..73f58430 100644 --- a/src/renault_api/kamereon/__init__.py +++ b/src/renault_api/kamereon/__init__.py @@ -38,6 +38,7 @@ "charging-start": {"version": 1, "type": "ChargingStart"}, "hvac-schedule": {"version": 2, "type": "HvacSchedule"}, "hvac-start": {"version": 1, "type": "HvacStart"}, + "pause-resume": {"version": 1, "type": "ChargePauseResume"}, } @@ -62,6 +63,14 @@ def get_car_adapter_url(root_url: str, account_id: str, version: int, vin: str) return f"{account_url}/kamereon/kca/car-adapter/v{version}/cars/{vin}" +def get_kcm_car_adapter_url( + root_url: str, account_id: str, version: int, vin: str +) -> str: + """Get the url to the kcm car adapter.""" + account_url = get_account_url(root_url, account_id) + return f"{account_url}/kamereon/kcm/v{version}/vehicles/{vin}" + + def get_contracts_url(root_url: str, account_id: str, vin: str) -> str: """Get the url to the car contracts.""" account_url = get_account_url(root_url, account_id) @@ -311,17 +320,28 @@ async def set_vehicle_action( vin: str, endpoint: str, attributes: Dict[str, Any], + actions: str = "actions", endpoint_version: Optional[int] = None, data_type: Optional[Dict[str, Any]] = None, ) -> models.KamereonVehicleDataResponse: - """POST to /v{endpoint_version}/cars/{vin}/actions/{endpoint}.""" + """POST to /v{endpoint_version}/cars/{vin}/{actions}/{endpoint}.""" car_adapter_url = get_car_adapter_url( root_url=root_url, account_id=account_id, version=endpoint_version or _get_endpoint_version(ACTION_ENDPOINTS[endpoint]), vin=vin, ) - url = f"{car_adapter_url}/actions/{endpoint}" + + if actions == "charge": + car_adapter_url = get_kcm_car_adapter_url( + root_url=root_url, + account_id=account_id, + version=endpoint_version + or _get_endpoint_version(ACTION_ENDPOINTS[endpoint]), + vin=vin, + ) + + url = f"{car_adapter_url}/{actions}/{endpoint}" params = {"country": country} json = { "data": { diff --git a/src/renault_api/renault_session.py b/src/renault_api/renault_session.py index 401557f4..e4bc3e7d 100644 --- a/src/renault_api/renault_session.py +++ b/src/renault_api/renault_session.py @@ -251,8 +251,9 @@ async def set_vehicle_action( vin: str, endpoint: str, attributes: Dict[str, Any], + actions: str = "actions", ) -> models.KamereonVehicleDataResponse: - """POST to /v{endpoint_version}/cars/{vin}/actions/{endpoint}.""" + """POST to /v{endpoint_version}/cars/{vin}/{actions}/{endpoint}.""" return await kamereon.set_vehicle_action( websession=self._websession, root_url=await self._get_kamereon_root_url(), @@ -261,6 +262,7 @@ async def set_vehicle_action( country=await self._get_country(), account_id=account_id, vin=vin, + actions=actions, endpoint=endpoint, attributes=attributes, ) diff --git a/src/renault_api/renault_vehicle.py b/src/renault_api/renault_vehicle.py index ad4e3d55..afe5561a 100644 --- a/src/renault_api/renault_vehicle.py +++ b/src/renault_api/renault_vehicle.py @@ -489,13 +489,30 @@ async def set_charge_mode( async def set_charge_start(self) -> models.KamereonVehicleChargingStartActionData: """Start vehicle charge.""" # await self.warn_on_method("set_charge_start") - attributes = {"action": "start"} - response = await self.session.set_vehicle_action( account_id=self.account_id, vin=self.vin, endpoint="charging-start", - attributes=attributes, + attributes={"action": "start"}, + ) + return cast( + models.KamereonVehicleChargingStartActionData, + response.get_attributes( + schemas.KamereonVehicleChargingStartActionDataSchema + ), + ) + + async def set_charge_pause_resume( + self, action: str + ) -> models.KamereonVehicleChargingStartActionData: + """Start vehicle charge.""" + # await self.warn_on_method("set_charge_start") + response = await self.session.set_vehicle_action( + account_id=self.account_id, + vin=self.vin, + endpoint="pause-resume", + attributes={"action": action}, + actions="charge", ) return cast( models.KamereonVehicleChargingStartActionData, diff --git a/tests/cli/test_vehicle_charge.py b/tests/cli/test_vehicle_charge.py index ea444d44..86600f25 100644 --- a/tests/cli/test_vehicle_charge.py +++ b/tests/cli/test_vehicle_charge.py @@ -431,6 +431,9 @@ def test_charging_settings_deactivate( def test_charging_start(mocked_responses: aioresponses, cli_runner: CliRunner) -> None: """It exits with a status code of zero.""" initialise_credential_store(include_account_id=True, include_vin=True) + + # RENAULT + fixtures.inject_get_vehicle_details(mocked_responses, "zoe_40.1.json") url = fixtures.inject_set_charging_start(mocked_responses, "start") result = cli_runner.invoke(__main__.main, "charge start") @@ -446,3 +449,67 @@ def test_charging_start(mocked_responses: aioresponses, cli_runner: CliRunner) - request: RequestCall = mocked_responses.requests[("POST", URL(url))][0] assert expected_json == request.kwargs["json"] assert expected_output == result.output + + +def test_charging_renault_stop( + mocked_responses: aioresponses, cli_runner: CliRunner +) -> None: + """It exits with a status code of zero.""" + initialise_credential_store(include_account_id=True, include_vin=True) + + # RENAULT + fixtures.inject_get_vehicle_details(mocked_responses, "zoe_40.1.json") + fixtures.inject_set_charge_pause_resume(mocked_responses, "pause") + + result = cli_runner.invoke(__main__.main, "charge stop") + assert result.exit_code == 1, result.exception + + +def test_charging_dacia_start( + mocked_responses: aioresponses, cli_runner: CliRunner +) -> None: + """It exits with a status code of zero.""" + initialise_credential_store(include_account_id=True, include_vin=True) + + # DACIA + fixtures.inject_get_vehicle_details(mocked_responses, "spring.1.json") + url = fixtures.inject_set_charge_pause_resume(mocked_responses, "resume") + + result = cli_runner.invoke(__main__.main, "charge start") + assert result.exit_code == 0, result.exception + + expected_json = { + "data": { + "attributes": {"action": "resume"}, + "type": "ChargePauseResume", + } + } + expected_output = "{'action': 'resume'}\n" + request: RequestCall = mocked_responses.requests[("POST", URL(url))][0] + assert expected_json == request.kwargs["json"] + assert expected_output == result.output + + +def test_charging_dacia_stop( + mocked_responses: aioresponses, cli_runner: CliRunner +) -> None: + """It exits with a status code of zero.""" + initialise_credential_store(include_account_id=True, include_vin=True) + + # DACIA + fixtures.inject_get_vehicle_details(mocked_responses, "spring.1.json") + url = fixtures.inject_set_charge_pause_resume(mocked_responses, "pause") + + result = cli_runner.invoke(__main__.main, "charge stop") + assert result.exit_code == 0, result.exception + + expected_json = { + "data": { + "attributes": {"action": "pause"}, + "type": "ChargePauseResume", + } + } + expected_output = "{'action': 'pause'}\n" + request: RequestCall = mocked_responses.requests[("POST", URL(url))][0] + assert expected_json == request.kwargs["json"] + assert expected_output == result.output diff --git a/tests/fixtures.py b/tests/fixtures.py index cdfbf134..c2ccf4b2 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -23,6 +23,7 @@ KAMEREON_BASE_URL = f"{TEST_KAMEREON_URL}/commerce/v1" ACCOUNT_PATH = f"accounts/{TEST_ACCOUNT_ID}" ADAPTER_PATH = f"{ACCOUNT_PATH}/kamereon/kca/car-adapter/v1/cars/{TEST_VIN}" +KCM_ADAPTER_PATH = f"{ACCOUNT_PATH}/kamereon/kcm/v1/vehicles/{TEST_VIN}" ADAPTER2_PATH = f"{ACCOUNT_PATH}/kamereon/kca/car-adapter/v2/cars/{TEST_VIN}" @@ -375,6 +376,16 @@ def inject_set_charge_schedule(mocked_responses: aioresponses, result: str) -> s ) +def inject_set_charge_pause_resume(mocked_responses: aioresponses, result: str) -> str: + """Inject sample charge-pause-resume.""" + urlpath = f"{KCM_ADAPTER_PATH}/charge/pause-resume?{DEFAULT_QUERY_STRING}" + return inject_action( + mocked_responses, + urlpath, + f"vehicle_action/charge-pause-resume.{result}.json", + ) + + def inject_set_charging_start(mocked_responses: aioresponses, result: str) -> str: """Inject sample charge-mode.""" urlpath = f"{ADAPTER_PATH}/actions/charging-start?{DEFAULT_QUERY_STRING}" diff --git a/tests/fixtures/kamereon/vehicle_action/charge-pause-resume.pause.json b/tests/fixtures/kamereon/vehicle_action/charge-pause-resume.pause.json new file mode 100644 index 00000000..f9b0368a --- /dev/null +++ b/tests/fixtures/kamereon/vehicle_action/charge-pause-resume.pause.json @@ -0,0 +1,7 @@ +{ + "data": { + "type": "ChargePauseResume", + "id": "guid", + "attributes": { "action": "pause" } + } +} diff --git a/tests/fixtures/kamereon/vehicle_action/charge-pause-resume.resume.json b/tests/fixtures/kamereon/vehicle_action/charge-pause-resume.resume.json new file mode 100644 index 00000000..e1012e3c --- /dev/null +++ b/tests/fixtures/kamereon/vehicle_action/charge-pause-resume.resume.json @@ -0,0 +1,7 @@ +{ + "data": { + "type": "ChargePauseResume", + "id": "guid", + "attributes": { "action": "resume" } + } +} diff --git a/tests/kamereon/test_kamereon.py b/tests/kamereon/test_kamereon.py index f0efa17d..6ae28ea0 100644 --- a/tests/kamereon/test_kamereon.py +++ b/tests/kamereon/test_kamereon.py @@ -92,3 +92,27 @@ async def test_set_vehicle_action( request: RequestCall = mocked_responses.requests[("POST", URL(url))][0] assert expected_json == request.kwargs["json"] + + +@pytest.mark.asyncio +async def test_set_vehicle_dacia_action( + websession: aiohttp.ClientSession, mocked_responses: aioresponses +) -> None: + """Test set_vehicle_action.""" + url = fixtures.inject_set_hvac_start(mocked_responses, "cancel") + assert await kamereon.set_vehicle_action( + websession=websession, + root_url=TEST_KAMEREON_URL, + api_key=TEST_KAMEREON_APIKEY, + gigya_jwt=fixtures.get_jwt(), + country=TEST_COUNTRY, + account_id=TEST_ACCOUNT_ID, + vin=TEST_VIN, + endpoint="hvac-start", + attributes={"action": "cancel"}, + ) + + expected_json = {"data": {"type": "HvacStart", "attributes": {"action": "cancel"}}} + + request: RequestCall = mocked_responses.requests[("POST", URL(url))][0] + assert expected_json == request.kwargs["json"] diff --git a/tests/test_renault_vehicle.py b/tests/test_renault_vehicle.py index 9c107ce8..6a996a7e 100644 --- a/tests/test_renault_vehicle.py +++ b/tests/test_renault_vehicle.py @@ -333,6 +333,22 @@ async def test_set_charge_schedules( assert expected_json == request.kwargs["json"] +@pytest.mark.asyncio +async def test_set_charge_resume( + vehicle: RenaultVehicle, mocked_responses: aioresponses +) -> None: + """Test set_charge_resume.""" + url = fixtures.inject_set_charge_pause_resume(mocked_responses, "resume") + + expected_json = { + "data": {"type": "ChargePauseResume", "attributes": {"action": "resume"}} + } + + assert await vehicle.set_charge_pause_resume("resume") + request: RequestCall = mocked_responses.requests[("POST", URL(url))][0] + assert expected_json == request.kwargs["json"] + + @pytest.mark.asyncio async def test_set_charge_start( vehicle: RenaultVehicle, mocked_responses: aioresponses From 7eae9a170f6249c259749687f8888b9fb0061e7e Mon Sep 17 00:00:00 2001 From: Luc Viala Date: Sat, 29 Jan 2022 09:23:20 +0100 Subject: [PATCH 2/2] Add specific method for kcm endpoint * Modify tests * Modify Kamereon model Signed-off-by: Luc Viala --- src/renault_api/cli/charge/control.py | 14 ++++---------- src/renault_api/kamereon/models.py | 10 ++++++++++ tests/kamereon/test_kamereon_vehicles.py | 11 +++++++++++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/renault_api/cli/charge/control.py b/src/renault_api/cli/charge/control.py index 338d6ed5..afccea93 100644 --- a/src/renault_api/cli/charge/control.py +++ b/src/renault_api/cli/charge/control.py @@ -47,11 +47,8 @@ async def start( vehicle = await renault_vehicle.get_vehicle( websession=websession, ctx_data=ctx_data ) - await vehicle.get_details() - if ( - vehicle._vehicle_details - and vehicle._vehicle_details.get_model_code() == "XBG1VE" - ): + details = await vehicle.get_details() + if details.get_model_code() == "XBG1VE": response = await vehicle.set_charge_pause_resume("resume") else: response = await vehicle.set_charge_start() @@ -70,11 +67,8 @@ async def stop( vehicle = await renault_vehicle.get_vehicle( websession=websession, ctx_data=ctx_data ) - await vehicle.get_details() - if ( - vehicle._vehicle_details - and vehicle._vehicle_details.get_model_code() == "XBG1VE" - ): + details = await vehicle.get_details() + if details.get_model_code() == "XBG1VE": response = await vehicle.set_charge_pause_resume("pause") click.echo(response.raw_data) else: diff --git a/src/renault_api/kamereon/models.py b/src/renault_api/kamereon/models.py index 821e160d..35d74352 100644 --- a/src/renault_api/kamereon/models.py +++ b/src/renault_api/kamereon/models.py @@ -62,6 +62,7 @@ }, "XBG1VE": { # DACIA SPRING "support-endpoint-hvac-status": True, + "pause-resume-via-kcm": True, # Pause/Resume is for charging actions }, } @@ -219,6 +220,15 @@ def warns_on_method(self, method: str) -> Optional[str]: ) return None # pragma: no cover + def uses_endpoint_via_kcm(self, endpoint: str) -> bool: + """Return True if model uses endpoint via kcm.""" + # Default to False for unknown vehicles + if self.model and self.model.code: + return VEHICLE_SPECIFICATIONS.get(self.model.code, {}).get( + f"{endpoint}-via-kcm", False + ) + return False # pragma: no cover + @dataclass class KamereonVehiclesLink(BaseModel): diff --git a/tests/kamereon/test_kamereon_vehicles.py b/tests/kamereon/test_kamereon_vehicles.py index 26eb7677..e2b70bf6 100644 --- a/tests/kamereon/test_kamereon_vehicles.py +++ b/tests/kamereon/test_kamereon_vehicles.py @@ -18,6 +18,7 @@ "uses_fuel": True, "supports-hvac-status": False, "supports-location": True, + "charging-endpoints-uses-kcm": False, }, "captur_ii.2.json": { "get_brand_label": "RENAULT", @@ -29,6 +30,7 @@ "uses_fuel": True, "supports-hvac-status": False, "supports-location": True, + "charging-endpoints-uses-kcm": False, }, "duster.1.json": { "get_brand_label": "RENAULT", @@ -40,6 +42,7 @@ "uses_fuel": True, "supports-hvac-status": True, "supports-location": True, + "charging-endpoints-uses-kcm": False, }, "twingo_ze.1.json": { "get_brand_label": "RENAULT", @@ -51,6 +54,7 @@ "uses_fuel": False, "supports-hvac-status": False, "supports-location": True, + "charging-endpoints-uses-kcm": False, }, "zoe_40.1.json": { "get_brand_label": "RENAULT", @@ -62,6 +66,7 @@ "uses_fuel": False, "supports-hvac-status": True, "supports-location": False, + "charging-endpoints-uses-kcm": False, }, "zoe_40.2.json": { "get_brand_label": "RENAULT", @@ -73,6 +78,7 @@ "uses_fuel": False, "supports-hvac-status": True, "supports-location": False, + "charging-endpoints-uses-kcm": False, }, "zoe_50.1.json": { "get_brand_label": "RENAULT", @@ -84,6 +90,7 @@ "uses_fuel": False, "supports-hvac-status": False, "supports-location": True, + "charging-endpoints-uses-kcm": False, }, "spring.1.json": { "get_brand_label": "DACIA", @@ -95,6 +102,7 @@ "uses_fuel": False, "supports-hvac-status": True, "supports-location": True, + "charging-endpoints-uses-kcm": True, }, } @@ -143,5 +151,8 @@ def test_vehicles_response(filename: str) -> None: "hvac-status" ), "supports-location": vehicle_details.supports_endpoint("location"), + "charging-endpoints-uses-kcm": vehicle_details.uses_endpoint_via_kcm( + "pause-resume" + ), } assert EXPECTED_SPECS[os.path.basename(filename)] == generated_specs