Add service to set the AC schedule of renault vehicles ()

* Add service to set the AC schedule of renault vehicles

* Remove executable permission

* Applied review comments (use snapshot)

* Rewrote examples to not use JSON
pull/126309/head
vhkristof 2024-09-20 10:18:47 +02:00 committed by GitHub
parent dccdb71b2d
commit 1f1ce67209
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 618 additions and 28 deletions

View File

@ -72,6 +72,9 @@
},
"charge_set_schedules": {
"service": "mdi:calendar-clock"
},
"ac_set_schedules": {
"service": "mdi:calendar-clock"
}
}
}

View File

@ -167,6 +167,18 @@ class RenaultVehicleProxy:
"""Start vehicle ac."""
return await self._vehicle.set_ac_start(temperature, when)
@with_error_wrapping
async def get_hvac_settings(self) -> models.KamereonVehicleHvacSettingsData:
"""Get vehicle hvac settings."""
return await self._vehicle.get_hvac_settings()
@with_error_wrapping
async def set_hvac_schedules(
self, schedules: list[models.HvacSchedule]
) -> models.KamereonVehicleHvacScheduleActionData:
"""Set vehicle hvac schedules."""
return await self._vehicle.set_hvac_schedules(schedules)
@with_error_wrapping
async def get_charging_settings(self) -> models.KamereonVehicleChargingSettingsData:
"""Get vehicle charging settings."""

View File

@ -66,10 +66,43 @@ SERVICE_CHARGE_SET_SCHEDULES_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend(
}
)
SERVICE_AC_SET_SCHEDULE_DAY_SCHEMA = vol.Schema(
{
vol.Required("readyAtTime"): cv.string,
}
)
SERVICE_AC_SET_SCHEDULE_SCHEMA = vol.Schema(
{
vol.Required("id"): cv.positive_int,
vol.Optional("activated"): cv.boolean,
vol.Optional("monday"): vol.Any(None, SERVICE_AC_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("tuesday"): vol.Any(None, SERVICE_AC_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("wednesday"): vol.Any(None, SERVICE_AC_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("thursday"): vol.Any(None, SERVICE_AC_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("friday"): vol.Any(None, SERVICE_AC_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("saturday"): vol.Any(None, SERVICE_AC_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("sunday"): vol.Any(None, SERVICE_AC_SET_SCHEDULE_DAY_SCHEMA),
}
)
SERVICE_AC_SET_SCHEDULES_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend(
{
vol.Required(ATTR_SCHEDULES): vol.All(
cv.ensure_list, [SERVICE_AC_SET_SCHEDULE_SCHEMA]
),
}
)
SERVICE_AC_CANCEL = "ac_cancel"
SERVICE_AC_START = "ac_start"
SERVICE_CHARGE_SET_SCHEDULES = "charge_set_schedules"
SERVICES = [SERVICE_AC_CANCEL, SERVICE_AC_START, SERVICE_CHARGE_SET_SCHEDULES]
SERVICE_AC_SET_SCHEDULES = "ac_set_schedules"
SERVICES = [
SERVICE_AC_CANCEL,
SERVICE_AC_START,
SERVICE_CHARGE_SET_SCHEDULES,
SERVICE_AC_SET_SCHEDULES,
]
def setup_services(hass: HomeAssistant) -> None:
@ -111,6 +144,25 @@ def setup_services(hass: HomeAssistant) -> None:
"It may take some time before these changes are reflected in your vehicle"
)
async def ac_set_schedules(service_call: ServiceCall) -> None:
"""Set A/C schedules."""
schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES]
proxy = get_vehicle_proxy(service_call.data)
hvac_schedules = await proxy.get_hvac_settings()
for schedule in schedules:
hvac_schedules.update(schedule)
if TYPE_CHECKING:
assert hvac_schedules.schedules is not None
LOGGER.debug("HVAC set schedules attempt: %s", schedules)
result = await proxy.set_hvac_schedules(hvac_schedules.schedules)
LOGGER.debug("HVAC set schedules result: %s", result)
LOGGER.debug(
"It may take some time before these changes are reflected in your vehicle"
)
def get_vehicle_proxy(service_call_data: Mapping) -> RenaultVehicleProxy:
"""Get vehicle from service_call data."""
device_registry = dr.async_get(hass)
@ -148,3 +200,9 @@ def setup_services(hass: HomeAssistant) -> None:
charge_set_schedules,
schema=SERVICE_CHARGE_SET_SCHEDULES_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_AC_SET_SCHEDULES,
ac_set_schedules,
schema=SERVICE_AC_SET_SCHEDULES_SCHEMA,
)

View File

@ -27,6 +27,33 @@ ac_cancel:
device:
integration: renault
ac_set_schedules:
fields:
vehicle:
required: true
selector:
device:
integration: renault
schedules:
example:
- id: 1
activated: false
- id: 2
activated: true
monday:
readyAtTime: "T20:45Z"
sunday:
readyAtTime: "T20:45Z"
- id: 3
activated: false
- id: 4
activated: false
- id: 5
activated: false
required: true
selector:
object:
charge_set_schedules:
fields:
vehicle:
@ -35,31 +62,53 @@ charge_set_schedules:
device:
integration: renault
schedules:
example: >-
[
{
'id':1,
'activated':true,
'monday':{'startTime':'T12:00Z','duration':15},
'tuesday':{'startTime':'T12:00Z','duration':15},
'wednesday':{'startTime':'T12:00Z','duration':15},
'thursday':{'startTime':'T12:00Z','duration':15},
'friday':{'startTime':'T12:00Z','duration':15},
'saturday':{'startTime':'T12:00Z','duration':15},
'sunday':{'startTime':'T12:00Z','duration':15}
},
{
'id':2,
'activated':false,
'monday':{'startTime':'T12:00Z','duration':240},
'tuesday':{'startTime':'T12:00Z','duration':240},
'wednesday':{'startTime':'T12:00Z','duration':240},
'thursday':{'startTime':'T12:00Z','duration':240},
'friday':{'startTime':'T12:00Z','duration':240},
'saturday':{'startTime':'T12:00Z','duration':240},
'sunday':{'startTime':'T12:00Z','duration':240}
},
]
example:
- id: 1
activated: true
monday:
startTime: "T12:00Z"
duration: 15
tuesday:
startTime: "T12:00Z"
duration: 15
wednesday:
startTime: "T12:00Z"
duration: 15
thursday:
startTime: "T12:00Z"
duration: 15
friday:
startTime: "T12:00Z"
duration: 15
saturday:
startTime: "T12:00Z"
duration: 15
sunday:
startTime: "T12:00Z"
duration: 15
- id: 2
activated: true
monday:
startTime: "T12:00Z"
duration: 240
tuesday:
startTime: "T12:00Z"
duration: 240
wednesday:
startTime: "T12:00Z"
duration: 240
thursday:
startTime: "T12:00Z"
duration: 240
friday:
startTime: "T12:00Z"
duration: 240
saturday:
startTime: "T12:00Z"
duration: 240
sunday:
startTime: "T12:00Z"
duration: 240
required: true
selector:
object:

View File

@ -175,7 +175,7 @@
},
"ac_cancel": {
"name": "Cancel A/C",
"description": "Canceles A/C on vehicle.",
"description": "Cancels A/C on vehicle.",
"fields": {
"vehicle": {
"name": "Vehicle",
@ -196,6 +196,20 @@
"description": "Schedule details."
}
}
},
"ac_set_schedules": {
"name": "Update A/C schedule",
"description": "Updates A/C schedule on vehicle.",
"fields": {
"vehicle": {
"name": "Vehicle",
"description": "[%key:component::renault::services::ac_start::fields::vehicle::description%]"
},
"schedules": {
"name": "Schedules",
"description": "[%key:component::renault::services::charge_set_schedules::fields::schedules::description%]"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"data": {
"type": "HvacSchedule",
"id": "guid",
"attributes": {
"schedules": [
{
"id": 1,
"activated": true,
"tuesday": { "readyAtTime": "T04:30Z" },
"wednesday": { "readyAtTime": "T22:30Z" },
"thursday": { "readyAtTime": "T22:00Z" },
"friday": { "readyAtTime": "T23:30Z" },
"saturday": { "readyAtTime": "T18:30Z" },
"sunday": { "readyAtTime": "T12:45Z" }
}
]
}
}
}

View File

@ -0,0 +1,41 @@
{
"data": {
"type": "Car",
"id": "VF1AAAAA555777999",
"attributes": {
"dateTime": "2020-12-24T20:00:00.000Z",
"mode": "scheduled",
"schedules": [
{
"id": 1,
"activated": false
},
{
"id": 2,
"activated": true,
"wednesday": { "readyAtTime": "T15:15Z" },
"friday": { "readyAtTime": "T15:15Z" }
},
{
"id": 3,
"activated": false,
"monday": { "readyAtTime": "T23:30Z" },
"tuesday": { "readyAtTime": "T23:30Z" },
"wednesday": { "readyAtTime": "T23:30Z" },
"thursday": { "readyAtTime": "T23:30Z" },
"friday": { "readyAtTime": "T23:30Z" },
"saturday": { "readyAtTime": "T23:30Z" },
"sunday": { "readyAtTime": "T23:30Z" }
},
{
"id": 4,
"activated": false
},
{
"id": 5,
"activated": false
}
]
}
}
}

View File

@ -1,4 +1,301 @@
# serializer version: 1
# name: test_service_set_ac_schedule[zoe_40]
list([
dict({
'activated': False,
'friday': None,
'id': 1,
'monday': None,
'raw_data': dict({
'activated': False,
'id': 1,
}),
'saturday': None,
'sunday': None,
'thursday': None,
'tuesday': None,
'wednesday': None,
}),
dict({
'activated': True,
'friday': dict({
'raw_data': dict({
'readyAtTime': 'T15:15Z',
}),
'readyAtTime': 'T15:15Z',
}),
'id': 2,
'monday': None,
'raw_data': dict({
'activated': True,
'friday': dict({
'readyAtTime': 'T15:15Z',
}),
'id': 2,
'wednesday': dict({
'readyAtTime': 'T15:15Z',
}),
}),
'saturday': None,
'sunday': None,
'thursday': None,
'tuesday': None,
'wednesday': dict({
'raw_data': dict({
'readyAtTime': 'T15:15Z',
}),
'readyAtTime': 'T15:15Z',
}),
}),
dict({
'activated': False,
'friday': dict({
'raw_data': dict({
'readyAtTime': 'T23:30Z',
}),
'readyAtTime': 'T23:30Z',
}),
'id': 3,
'monday': dict({
'raw_data': dict({
'readyAtTime': 'T23:30Z',
}),
'readyAtTime': 'T23:30Z',
}),
'raw_data': dict({
'activated': False,
'friday': dict({
'readyAtTime': 'T23:30Z',
}),
'id': 3,
'monday': dict({
'readyAtTime': 'T23:30Z',
}),
'saturday': dict({
'readyAtTime': 'T23:30Z',
}),
'sunday': dict({
'readyAtTime': 'T23:30Z',
}),
'thursday': dict({
'readyAtTime': 'T23:30Z',
}),
'tuesday': dict({
'readyAtTime': 'T23:30Z',
}),
'wednesday': dict({
'readyAtTime': 'T23:30Z',
}),
}),
'saturday': dict({
'raw_data': dict({
'readyAtTime': 'T23:30Z',
}),
'readyAtTime': 'T23:30Z',
}),
'sunday': dict({
'raw_data': dict({
'readyAtTime': 'T23:30Z',
}),
'readyAtTime': 'T23:30Z',
}),
'thursday': dict({
'raw_data': dict({
'readyAtTime': 'T23:30Z',
}),
'readyAtTime': 'T23:30Z',
}),
'tuesday': dict({
'raw_data': dict({
'readyAtTime': 'T23:30Z',
}),
'readyAtTime': 'T23:30Z',
}),
'wednesday': dict({
'raw_data': dict({
'readyAtTime': 'T23:30Z',
}),
'readyAtTime': 'T23:30Z',
}),
}),
dict({
'activated': False,
'friday': None,
'id': 4,
'monday': None,
'raw_data': dict({
'activated': False,
'id': 4,
}),
'saturday': None,
'sunday': None,
'thursday': None,
'tuesday': None,
'wednesday': None,
}),
dict({
'activated': False,
'friday': None,
'id': 5,
'monday': None,
'raw_data': dict({
'activated': False,
'id': 5,
}),
'saturday': None,
'sunday': None,
'thursday': None,
'tuesday': None,
'wednesday': None,
}),
])
# ---
# name: test_service_set_ac_schedule_multi[zoe_40]
list([
dict({
'activated': False,
'friday': None,
'id': 1,
'monday': None,
'raw_data': dict({
'activated': False,
'id': 1,
}),
'saturday': None,
'sunday': None,
'thursday': None,
'tuesday': None,
'wednesday': None,
}),
dict({
'activated': True,
'friday': dict({
'raw_data': dict({
'readyAtTime': 'T15:15Z',
}),
'readyAtTime': 'T15:15Z',
}),
'id': 2,
'monday': None,
'raw_data': dict({
'activated': True,
'friday': dict({
'readyAtTime': 'T15:15Z',
}),
'id': 2,
'wednesday': dict({
'readyAtTime': 'T15:15Z',
}),
}),
'saturday': None,
'sunday': None,
'thursday': None,
'tuesday': None,
'wednesday': dict({
'raw_data': dict({
'readyAtTime': 'T15:15Z',
}),
'readyAtTime': 'T15:15Z',
}),
}),
dict({
'activated': True,
'friday': dict({
'raw_data': dict({
'readyAtTime': 'T12:00Z',
}),
'readyAtTime': 'T12:00Z',
}),
'id': 3,
'monday': dict({
'raw_data': dict({
'readyAtTime': 'T12:00Z',
}),
'readyAtTime': 'T12:00Z',
}),
'raw_data': dict({
'activated': False,
'friday': dict({
'readyAtTime': 'T23:30Z',
}),
'id': 3,
'monday': dict({
'readyAtTime': 'T23:30Z',
}),
'saturday': dict({
'readyAtTime': 'T23:30Z',
}),
'sunday': dict({
'readyAtTime': 'T23:30Z',
}),
'thursday': dict({
'readyAtTime': 'T23:30Z',
}),
'tuesday': dict({
'readyAtTime': 'T23:30Z',
}),
'wednesday': dict({
'readyAtTime': 'T23:30Z',
}),
}),
'saturday': dict({
'raw_data': dict({
'readyAtTime': 'T12:00Z',
}),
'readyAtTime': 'T12:00Z',
}),
'sunday': dict({
'raw_data': dict({
'readyAtTime': 'T12:00Z',
}),
'readyAtTime': 'T12:00Z',
}),
'thursday': dict({
'raw_data': dict({
'readyAtTime': 'T23:30Z',
}),
'readyAtTime': 'T23:30Z',
}),
'tuesday': dict({
'raw_data': dict({
'readyAtTime': 'T12:00Z',
}),
'readyAtTime': 'T12:00Z',
}),
'wednesday': None,
}),
dict({
'activated': False,
'friday': None,
'id': 4,
'monday': None,
'raw_data': dict({
'activated': False,
'id': 4,
}),
'saturday': None,
'sunday': None,
'thursday': None,
'tuesday': None,
'wednesday': None,
}),
dict({
'activated': False,
'friday': None,
'id': 5,
'monday': None,
'raw_data': dict({
'activated': False,
'id': 5,
}),
'saturday': None,
'sunday': None,
'thursday': None,
'tuesday': None,
'wednesday': None,
}),
])
# ---
# name: test_service_set_charge_schedule[zoe_40]
list([
dict({

View File

@ -7,7 +7,7 @@ from unittest.mock import patch
import pytest
from renault_api.exceptions import RenaultException
from renault_api.kamereon import schemas
from renault_api.kamereon.models import ChargeSchedule
from renault_api.kamereon.models import ChargeSchedule, HvacSchedule
from syrupy import SnapshotAssertion
from homeassistant.components.renault.const import DOMAIN
@ -17,6 +17,7 @@ from homeassistant.components.renault.services import (
ATTR_VEHICLE,
ATTR_WHEN,
SERVICE_AC_CANCEL,
SERVICE_AC_SET_SCHEDULES,
SERVICE_AC_START,
SERVICE_CHARGE_SET_SCHEDULES,
)
@ -238,6 +239,101 @@ async def test_service_set_charge_schedule_multi(
assert mock_call_data[1].thursday.duration == 15
async def test_service_set_ac_schedule(
hass: HomeAssistant, config_entry: ConfigEntry, snapshot: SnapshotAssertion
) -> None:
"""Test that service invokes renault_api with correct data."""
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
schedules = {"id": 2}
data = {
ATTR_VEHICLE: get_device_id(hass),
ATTR_SCHEDULES: schedules,
}
with (
patch(
"renault_api.renault_vehicle.RenaultVehicle.get_hvac_settings",
return_value=schemas.KamereonVehicleDataResponseSchema.loads(
load_fixture("renault/hvac_settings.json")
).get_attributes(schemas.KamereonVehicleHvacSettingsDataSchema),
),
patch(
"renault_api.renault_vehicle.RenaultVehicle.set_hvac_schedules",
return_value=(
schemas.KamereonVehicleHvacScheduleActionDataSchema.loads(
load_fixture("renault/action.set_ac_schedules.json")
)
),
) as mock_action,
):
await hass.services.async_call(
DOMAIN, SERVICE_AC_SET_SCHEDULES, service_data=data, blocking=True
)
assert len(mock_action.mock_calls) == 1
mock_call_data: list[ChargeSchedule] = mock_action.mock_calls[0][1][0]
assert mock_call_data == snapshot
async def test_service_set_ac_schedule_multi(
hass: HomeAssistant, config_entry: ConfigEntry, snapshot: SnapshotAssertion
) -> None:
"""Test that service invokes renault_api with correct data."""
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
schedules = [
{
"id": 3,
"activated": True,
"monday": {"readyAtTime": "T12:00Z"},
"tuesday": {"readyAtTime": "T12:00Z"},
"wednesday": None,
"friday": {"readyAtTime": "T12:00Z"},
"saturday": {"readyAtTime": "T12:00Z"},
"sunday": {"readyAtTime": "T12:00Z"},
},
{"id": 4},
]
data = {
ATTR_VEHICLE: get_device_id(hass),
ATTR_SCHEDULES: schedules,
}
with (
patch(
"renault_api.renault_vehicle.RenaultVehicle.get_hvac_settings",
return_value=schemas.KamereonVehicleDataResponseSchema.loads(
load_fixture("renault/hvac_settings.json")
).get_attributes(schemas.KamereonVehicleHvacSettingsDataSchema),
),
patch(
"renault_api.renault_vehicle.RenaultVehicle.set_hvac_schedules",
return_value=(
schemas.KamereonVehicleHvacScheduleActionDataSchema.loads(
load_fixture("renault/action.set_ac_schedules.json")
)
),
) as mock_action,
):
await hass.services.async_call(
DOMAIN, SERVICE_AC_SET_SCHEDULES, service_data=data, blocking=True
)
assert len(mock_action.mock_calls) == 1
mock_call_data: list[HvacSchedule] = mock_action.mock_calls[0][1][0]
assert mock_call_data == snapshot
# Schedule is activated now
assert mock_call_data[2].activated is True
# Monday updated with new values
assert mock_call_data[2].monday.readyAtTime == "T12:00Z"
# Wednesday has original values cleared
assert mock_call_data[2].wednesday is None
# Thursday keeps original values
assert mock_call_data[2].thursday.readyAtTime == "T23:30Z"
async def test_service_invalid_device_id(
hass: HomeAssistant, config_entry: ConfigEntry
) -> None: