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

* Add service to set the AC schedule of renault vehicles

* Remove executable permission

* Applied review comments (use snapshot)

* Rewrote examples to not use JSON
This commit is contained in:
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": { "charge_set_schedules": {
"service": "mdi:calendar-clock" "service": "mdi:calendar-clock"
},
"ac_set_schedules": {
"service": "mdi:calendar-clock"
} }
} }
} }

View file

@ -167,6 +167,18 @@ class RenaultVehicleProxy:
"""Start vehicle ac.""" """Start vehicle ac."""
return await self._vehicle.set_ac_start(temperature, when) 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 @with_error_wrapping
async def get_charging_settings(self) -> models.KamereonVehicleChargingSettingsData: async def get_charging_settings(self) -> models.KamereonVehicleChargingSettingsData:
"""Get vehicle charging settings.""" """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_CANCEL = "ac_cancel"
SERVICE_AC_START = "ac_start" SERVICE_AC_START = "ac_start"
SERVICE_CHARGE_SET_SCHEDULES = "charge_set_schedules" 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: 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" "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: def get_vehicle_proxy(service_call_data: Mapping) -> RenaultVehicleProxy:
"""Get vehicle from service_call data.""" """Get vehicle from service_call data."""
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
@ -148,3 +200,9 @@ def setup_services(hass: HomeAssistant) -> None:
charge_set_schedules, charge_set_schedules,
schema=SERVICE_CHARGE_SET_SCHEDULES_SCHEMA, 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: device:
integration: renault 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: charge_set_schedules:
fields: fields:
vehicle: vehicle:
@ -35,31 +62,53 @@ charge_set_schedules:
device: device:
integration: renault integration: renault
schedules: schedules:
example: >- example:
[ - id: 1
{ activated: true
'id':1, monday:
'activated':true, startTime: "T12:00Z"
'monday':{'startTime':'T12:00Z','duration':15}, duration: 15
'tuesday':{'startTime':'T12:00Z','duration':15}, tuesday:
'wednesday':{'startTime':'T12:00Z','duration':15}, startTime: "T12:00Z"
'thursday':{'startTime':'T12:00Z','duration':15}, duration: 15
'friday':{'startTime':'T12:00Z','duration':15}, wednesday:
'saturday':{'startTime':'T12:00Z','duration':15}, startTime: "T12:00Z"
'sunday':{'startTime':'T12:00Z','duration':15} duration: 15
}, thursday:
{ startTime: "T12:00Z"
'id':2, duration: 15
'activated':false, friday:
'monday':{'startTime':'T12:00Z','duration':240}, startTime: "T12:00Z"
'tuesday':{'startTime':'T12:00Z','duration':240}, duration: 15
'wednesday':{'startTime':'T12:00Z','duration':240}, saturday:
'thursday':{'startTime':'T12:00Z','duration':240}, startTime: "T12:00Z"
'friday':{'startTime':'T12:00Z','duration':240}, duration: 15
'saturday':{'startTime':'T12:00Z','duration':240}, sunday:
'sunday':{'startTime':'T12:00Z','duration':240} 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 required: true
selector: selector:
object: object:

View file

@ -175,7 +175,7 @@
}, },
"ac_cancel": { "ac_cancel": {
"name": "Cancel A/C", "name": "Cancel A/C",
"description": "Canceles A/C on vehicle.", "description": "Cancels A/C on vehicle.",
"fields": { "fields": {
"vehicle": { "vehicle": {
"name": "Vehicle", "name": "Vehicle",
@ -196,6 +196,20 @@
"description": "Schedule details." "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 # 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] # name: test_service_set_charge_schedule[zoe_40]
list([ list([
dict({ dict({

View file

@ -7,7 +7,7 @@ from unittest.mock import patch
import pytest import pytest
from renault_api.exceptions import RenaultException from renault_api.exceptions import RenaultException
from renault_api.kamereon import schemas 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 syrupy import SnapshotAssertion
from homeassistant.components.renault.const import DOMAIN from homeassistant.components.renault.const import DOMAIN
@ -17,6 +17,7 @@ from homeassistant.components.renault.services import (
ATTR_VEHICLE, ATTR_VEHICLE,
ATTR_WHEN, ATTR_WHEN,
SERVICE_AC_CANCEL, SERVICE_AC_CANCEL,
SERVICE_AC_SET_SCHEDULES,
SERVICE_AC_START, SERVICE_AC_START,
SERVICE_CHARGE_SET_SCHEDULES, SERVICE_CHARGE_SET_SCHEDULES,
) )
@ -238,6 +239,101 @@ async def test_service_set_charge_schedule_multi(
assert mock_call_data[1].thursday.duration == 15 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( async def test_service_invalid_device_id(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> None: ) -> None: