Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Dacia Spring Support #459

Closed
wants to merge 2 commits into from
Closed
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
6 changes: 6 additions & 0 deletions docs/endpoints.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 14 additions & 0 deletions docs/endpoints/vehicle_actions.charge-pause-resume.rst
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/renault_api/cli/charge/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 27 additions & 1 deletion src/renault_api/cli/charge/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,31 @@ async def start(
vehicle = await renault_vehicle.get_vehicle(
websession=websession, ctx_data=ctx_data
)
response = await vehicle.set_charge_start()
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()
Comment on lines +50 to +54
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to keep the original method (simply set_charge_start) and move the logic there.

Suggested change
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()
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
)
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:
response = None
click.echo("Function unavailable")
exit(1)
Comment on lines +70 to +77
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to simply create a new function against the vehicle, and make the check there (you can raise NotImplementedException for the other vehicles)

Suggested change
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:
response = None
click.echo("Function unavailable")
exit(1)
response = await vehicle.set_charge_stop()
click.echo(response.raw_data)

24 changes: 22 additions & 2 deletions src/renault_api/kamereon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
}


Expand All @@ -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)
Expand Down Expand Up @@ -311,17 +320,28 @@ async def set_vehicle_action(
vin: str,
endpoint: str,
attributes: Dict[str, Any],
actions: str = "actions",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add an argument in the middle of the list, it becomes a breaking change.
You have to add it at the bottom of the list to make it non-breaking (even with a default value).

I suggest:

    attributes: Dict[str, Any],
    endpoint_version: Optional[int] = None,
    data_type: Optional[Dict[str, Any]] = None,
    *,
    car_adapter_type: str = "kca"
    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":
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to use a separate (new) argument to decide on the car_adapter_url:

    if car_adapter_type == "kcm":
        car_adapter_url = get_kcm_car_adapter_url(...
    else:
        car_adapter_url = get_car_adapter_url(...

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": {
Expand Down
10 changes: 10 additions & 0 deletions src/renault_api/kamereon/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
},
"XBG1VE": { # DACIA SPRING
"support-endpoint-hvac-status": True,
"pause-resume-via-kcm": True, # Pause/Resume is for charging actions
},
}

Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are adding tests for this, so no need to exclude coverage:

Suggested change
return False # pragma: no cover
return False



@dataclass
class KamereonVehiclesLink(BaseModel):
Expand Down
4 changes: 3 additions & 1 deletion src/renault_api/renault_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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,
)
23 changes: 20 additions & 3 deletions src/renault_api/renault_vehicle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment on lines +505 to +507
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename to set_charge_stop. Then inside the function you check if uses_endpoint_via_kcm, and send the correct request (pause or raise NotImplemented).

Suggested change
async def set_charge_pause_resume(
self, action: str
) -> models.KamereonVehicleChargingStartActionData:
async def set_charge_stop(self) -> models.KamereonVehicleChargingStartActionData:

You can do the same in set_charge_start, which should check if uses_endpoint_via_kcm, and send the correct request (kca start, or kcm resume).

"""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,
Expand Down
67 changes: 67 additions & 0 deletions tests/cli/test_vehicle_charge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
11 changes: 11 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"


Expand Down Expand Up @@ -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}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"data": {
"type": "ChargePauseResume",
"id": "guid",
"attributes": { "action": "pause" }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"data": {
"type": "ChargePauseResume",
"id": "guid",
"attributes": { "action": "resume" }
}
}
24 changes: 24 additions & 0 deletions tests/kamereon/test_kamereon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Loading