mirror of
https://github.com/home-assistant/core.git
synced 2025-04-27 02:37:50 +00:00
Add services to Renault integration (#54820)
* Add services * Add tests * Cleanup async * Fix pylint * Update services.yaml * Add extra schema validation * Rename constants * Simplify code * Move constants * Fix pylint * Cleanup constants * Drop charge_set_mode as moved to select platform * Only register the services if no config entry has registered them yet * Replace VIN with device selector to select vehicle * Update logging * Adjust type checking * Use a shared base SERVICE_VEHICLE_SCHEMA * Add selectors for ac_start (temperature/when) * Add object selector for charge_set_schedules service
This commit is contained in:
parent
46159c3f18
commit
02b7356596
@ -8,6 +8,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
|||||||
|
|
||||||
from .const import CONF_LOCALE, DOMAIN, PLATFORMS
|
from .const import CONF_LOCALE, DOMAIN, PLATFORMS
|
||||||
from .renault_hub import RenaultHub
|
from .renault_hub import RenaultHub
|
||||||
|
from .services import SERVICE_AC_START, setup_services, unload_services
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||||
@ -30,6 +31,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
|
|
||||||
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
|
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
|
||||||
|
|
||||||
|
if not hass.services.has_service(DOMAIN, SERVICE_AC_START):
|
||||||
|
setup_services(hass)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -41,5 +45,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
|
|
||||||
if unload_ok:
|
if unload_ok:
|
||||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||||
|
if not hass.data[DOMAIN]:
|
||||||
|
unload_services(hass)
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
@ -76,6 +76,11 @@ class RenaultVehicleProxy:
|
|||||||
"""Return a device description for device registry."""
|
"""Return a device description for device registry."""
|
||||||
return self._device_info
|
return self._device_info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vehicle(self) -> RenaultVehicle:
|
||||||
|
"""Return the underlying vehicle."""
|
||||||
|
return self._vehicle
|
||||||
|
|
||||||
async def async_initialise(self) -> None:
|
async def async_initialise(self) -> None:
|
||||||
"""Load available coordinators."""
|
"""Load available coordinators."""
|
||||||
self.coordinators = {
|
self.coordinators = {
|
||||||
|
165
homeassistant/components/renault/services.py
Normal file
165
homeassistant/components/renault/services.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
"""Support for Renault services."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
from types import MappingProxyType
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .renault_hub import RenaultHub
|
||||||
|
from .renault_vehicle import RenaultVehicleProxy
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ATTR_SCHEDULES = "schedules"
|
||||||
|
ATTR_TEMPERATURE = "temperature"
|
||||||
|
ATTR_VEHICLE = "vehicle"
|
||||||
|
ATTR_WHEN = "when"
|
||||||
|
|
||||||
|
SERVICE_VEHICLE_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_VEHICLE): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SERVICE_AC_START_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_TEMPERATURE): cv.positive_float,
|
||||||
|
vol.Optional(ATTR_WHEN): cv.datetime,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required("startTime"): cv.string,
|
||||||
|
vol.Required("duration"): cv.positive_int,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SERVICE_CHARGE_SET_SCHEDULE_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required("id"): cv.positive_int,
|
||||||
|
vol.Optional("activated"): cv.boolean,
|
||||||
|
vol.Optional("monday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
|
||||||
|
vol.Optional("tuesday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
|
||||||
|
vol.Optional("wednesday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
|
||||||
|
vol.Optional("thursday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
|
||||||
|
vol.Optional("friday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
|
||||||
|
vol.Optional("saturday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
|
||||||
|
vol.Optional("sunday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SERVICE_CHARGE_SET_SCHEDULES_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_SCHEDULES): vol.All(
|
||||||
|
cv.ensure_list, [SERVICE_CHARGE_SET_SCHEDULE_SCHEMA]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SERVICE_AC_CANCEL = "ac_cancel"
|
||||||
|
SERVICE_AC_START = "ac_start"
|
||||||
|
SERVICE_CHARGE_SET_SCHEDULES = "charge_set_schedules"
|
||||||
|
SERVICE_CHARGE_START = "charge_start"
|
||||||
|
SERVICES = [
|
||||||
|
SERVICE_AC_CANCEL,
|
||||||
|
SERVICE_AC_START,
|
||||||
|
SERVICE_CHARGE_SET_SCHEDULES,
|
||||||
|
SERVICE_CHARGE_START,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def setup_services(hass: HomeAssistant) -> None:
|
||||||
|
"""Register the Renault services."""
|
||||||
|
|
||||||
|
async def ac_cancel(service_call: ServiceCall) -> None:
|
||||||
|
"""Cancel A/C."""
|
||||||
|
proxy = get_vehicle_proxy(service_call.data)
|
||||||
|
|
||||||
|
LOGGER.debug("A/C cancel attempt")
|
||||||
|
result = await proxy.vehicle.set_ac_stop()
|
||||||
|
LOGGER.debug("A/C cancel result: %s", result)
|
||||||
|
|
||||||
|
async def ac_start(service_call: ServiceCall) -> None:
|
||||||
|
"""Start A/C."""
|
||||||
|
temperature: float = service_call.data[ATTR_TEMPERATURE]
|
||||||
|
when: datetime | None = service_call.data.get(ATTR_WHEN)
|
||||||
|
proxy = get_vehicle_proxy(service_call.data)
|
||||||
|
|
||||||
|
LOGGER.debug("A/C start attempt: %s / %s", temperature, when)
|
||||||
|
result = await proxy.vehicle.set_ac_start(temperature, when)
|
||||||
|
LOGGER.debug("A/C start result: %s", result.raw_data)
|
||||||
|
|
||||||
|
async def charge_set_schedules(service_call: ServiceCall) -> None:
|
||||||
|
"""Set charge schedules."""
|
||||||
|
schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES]
|
||||||
|
proxy = get_vehicle_proxy(service_call.data)
|
||||||
|
charge_schedules = await proxy.vehicle.get_charging_settings()
|
||||||
|
for schedule in schedules:
|
||||||
|
charge_schedules.update(schedule)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert charge_schedules.schedules is not None
|
||||||
|
LOGGER.debug("Charge set schedules attempt: %s", schedules)
|
||||||
|
result = await proxy.vehicle.set_charge_schedules(charge_schedules.schedules)
|
||||||
|
LOGGER.debug("Charge set schedules result: %s", result)
|
||||||
|
LOGGER.debug(
|
||||||
|
"It may take some time before these changes are reflected in your vehicle"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def charge_start(service_call: ServiceCall) -> None:
|
||||||
|
"""Start charge."""
|
||||||
|
proxy = get_vehicle_proxy(service_call.data)
|
||||||
|
|
||||||
|
LOGGER.debug("Charge start attempt")
|
||||||
|
result = await proxy.vehicle.set_charge_start()
|
||||||
|
LOGGER.debug("Charge start result: %s", result)
|
||||||
|
|
||||||
|
def get_vehicle_proxy(service_call_data: MappingProxyType) -> RenaultVehicleProxy:
|
||||||
|
"""Get vehicle from service_call data."""
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
device_id = service_call_data[ATTR_VEHICLE]
|
||||||
|
device_entry = device_registry.async_get(device_id)
|
||||||
|
if device_entry is None:
|
||||||
|
raise ValueError(f"Unable to find device with id: {device_id}")
|
||||||
|
|
||||||
|
proxy: RenaultHub
|
||||||
|
for proxy in hass.data[DOMAIN].values():
|
||||||
|
for vin, vehicle in proxy.vehicles.items():
|
||||||
|
if (DOMAIN, vin) in device_entry.identifiers:
|
||||||
|
return vehicle
|
||||||
|
raise ValueError(f"Unable to find vehicle with VIN: {device_entry.identifiers}")
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_AC_CANCEL,
|
||||||
|
ac_cancel,
|
||||||
|
schema=SERVICE_VEHICLE_SCHEMA,
|
||||||
|
)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_AC_START,
|
||||||
|
ac_start,
|
||||||
|
schema=SERVICE_AC_START_SCHEMA,
|
||||||
|
)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_CHARGE_SET_SCHEDULES,
|
||||||
|
charge_set_schedules,
|
||||||
|
schema=SERVICE_CHARGE_SET_SCHEDULES_SCHEMA,
|
||||||
|
)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_CHARGE_START,
|
||||||
|
charge_start,
|
||||||
|
schema=SERVICE_VEHICLE_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def unload_services(hass: HomeAssistant) -> None:
|
||||||
|
"""Unload Renault services."""
|
||||||
|
for service in SERVICES:
|
||||||
|
hass.services.async_remove(DOMAIN, service)
|
88
homeassistant/components/renault/services.yaml
Normal file
88
homeassistant/components/renault/services.yaml
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
ac_start:
|
||||||
|
description: Start A/C on vehicle.
|
||||||
|
fields:
|
||||||
|
vehicle:
|
||||||
|
name: Vehicle
|
||||||
|
description: The vehicle to send the command to.
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
integration: renault
|
||||||
|
temperature:
|
||||||
|
description: Target A/C temperature in °C.
|
||||||
|
example: "21"
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 15
|
||||||
|
max: 25
|
||||||
|
step: 0.5
|
||||||
|
unit_of_measurement: °C
|
||||||
|
when:
|
||||||
|
description: Timestamp for the start of the A/C (optional - defaults to now).
|
||||||
|
example: "2020-05-01T17:45:00"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
|
||||||
|
ac_cancel:
|
||||||
|
description: Cancel A/C on vehicle.
|
||||||
|
fields:
|
||||||
|
vehicle:
|
||||||
|
name: Vehicle
|
||||||
|
description: The vehicle to send the command to.
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
integration: renault
|
||||||
|
|
||||||
|
charge_set_schedules:
|
||||||
|
description: Update charge schedule on vehicle.
|
||||||
|
fields:
|
||||||
|
vehicle:
|
||||||
|
name: Vehicle
|
||||||
|
description: The vehicle to send the command to.
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
integration: renault
|
||||||
|
schedules:
|
||||||
|
description: Schedule details.
|
||||||
|
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}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
object:
|
||||||
|
|
||||||
|
charge_start:
|
||||||
|
description: Start charge on vehicle.
|
||||||
|
fields:
|
||||||
|
vehicle:
|
||||||
|
name: Vehicle
|
||||||
|
description: The vehicle to send the command to.
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
integration: renault
|
269
tests/components/renault/test_services.py
Normal file
269
tests/components/renault/test_services.py
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
"""Tests for Renault sensors."""
|
||||||
|
from datetime import datetime
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from renault_api.kamereon import schemas
|
||||||
|
from renault_api.kamereon.models import ChargeSchedule
|
||||||
|
|
||||||
|
from homeassistant.components.renault.const import DOMAIN
|
||||||
|
from homeassistant.components.renault.services import (
|
||||||
|
ATTR_SCHEDULES,
|
||||||
|
ATTR_TEMPERATURE,
|
||||||
|
ATTR_VEHICLE,
|
||||||
|
ATTR_WHEN,
|
||||||
|
SERVICE_AC_CANCEL,
|
||||||
|
SERVICE_AC_START,
|
||||||
|
SERVICE_CHARGE_SET_SCHEDULES,
|
||||||
|
SERVICE_CHARGE_START,
|
||||||
|
SERVICES,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_IDENTIFIERS,
|
||||||
|
ATTR_MANUFACTURER,
|
||||||
|
ATTR_MODEL,
|
||||||
|
ATTR_NAME,
|
||||||
|
ATTR_SW_VERSION,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
|
from . import setup_renault_integration_simple, setup_renault_integration_vehicle
|
||||||
|
|
||||||
|
from tests.common import load_fixture
|
||||||
|
from tests.components.renault.const import MOCK_VEHICLES
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_id(hass: HomeAssistant) -> str:
|
||||||
|
"""Get device_id."""
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
identifiers = {(DOMAIN, "VF1AAAAA555777999")}
|
||||||
|
device = device_registry.async_get_device(identifiers)
|
||||||
|
return device.id
|
||||||
|
|
||||||
|
|
||||||
|
async def test_service_registration(hass: HomeAssistant):
|
||||||
|
"""Test entry setup and unload."""
|
||||||
|
with patch("homeassistant.components.renault.PLATFORMS", []):
|
||||||
|
config_entry = await setup_renault_integration_simple(hass)
|
||||||
|
|
||||||
|
# Check that all services are registered.
|
||||||
|
for service in SERVICES:
|
||||||
|
assert hass.services.has_service(DOMAIN, service)
|
||||||
|
|
||||||
|
# Unload the entry
|
||||||
|
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||||
|
|
||||||
|
# Check that all services are un-registered.
|
||||||
|
for service in SERVICES:
|
||||||
|
assert not hass.services.has_service(DOMAIN, service)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_service_set_ac_cancel(hass: HomeAssistant):
|
||||||
|
"""Test that service invokes renault_api with correct data."""
|
||||||
|
await setup_renault_integration_vehicle(hass, "zoe_40")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
ATTR_VEHICLE: get_device_id(hass),
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"renault_api.renault_vehicle.RenaultVehicle.set_ac_stop",
|
||||||
|
return_value=(
|
||||||
|
schemas.KamereonVehicleHvacStartActionDataSchema.loads(
|
||||||
|
load_fixture("renault/action.set_ac_stop.json")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
) as mock_action:
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_AC_CANCEL, service_data=data, blocking=True
|
||||||
|
)
|
||||||
|
assert len(mock_action.mock_calls) == 1
|
||||||
|
assert mock_action.mock_calls[0][1] == ()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_service_set_ac_start_simple(hass: HomeAssistant):
|
||||||
|
"""Test that service invokes renault_api with correct data."""
|
||||||
|
await setup_renault_integration_vehicle(hass, "zoe_40")
|
||||||
|
|
||||||
|
temperature = 13.5
|
||||||
|
data = {
|
||||||
|
ATTR_VEHICLE: get_device_id(hass),
|
||||||
|
ATTR_TEMPERATURE: temperature,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"renault_api.renault_vehicle.RenaultVehicle.set_ac_start",
|
||||||
|
return_value=(
|
||||||
|
schemas.KamereonVehicleHvacStartActionDataSchema.loads(
|
||||||
|
load_fixture("renault/action.set_ac_start.json")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
) as mock_action:
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_AC_START, service_data=data, blocking=True
|
||||||
|
)
|
||||||
|
assert len(mock_action.mock_calls) == 1
|
||||||
|
assert mock_action.mock_calls[0][1] == (temperature, None)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_service_set_ac_start_with_date(hass: HomeAssistant):
|
||||||
|
"""Test that service invokes renault_api with correct data."""
|
||||||
|
await setup_renault_integration_vehicle(hass, "zoe_40")
|
||||||
|
|
||||||
|
temperature = 13.5
|
||||||
|
when = datetime(2025, 8, 23, 17, 12, 45)
|
||||||
|
data = {
|
||||||
|
ATTR_VEHICLE: get_device_id(hass),
|
||||||
|
ATTR_TEMPERATURE: temperature,
|
||||||
|
ATTR_WHEN: when,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"renault_api.renault_vehicle.RenaultVehicle.set_ac_start",
|
||||||
|
return_value=(
|
||||||
|
schemas.KamereonVehicleHvacStartActionDataSchema.loads(
|
||||||
|
load_fixture("renault/action.set_ac_start.json")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
) as mock_action:
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_AC_START, service_data=data, blocking=True
|
||||||
|
)
|
||||||
|
assert len(mock_action.mock_calls) == 1
|
||||||
|
assert mock_action.mock_calls[0][1] == (temperature, when)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_service_set_charge_schedule(hass: HomeAssistant):
|
||||||
|
"""Test that service invokes renault_api with correct data."""
|
||||||
|
await setup_renault_integration_vehicle(hass, "zoe_40")
|
||||||
|
|
||||||
|
schedules = {"id": 2}
|
||||||
|
data = {
|
||||||
|
ATTR_VEHICLE: get_device_id(hass),
|
||||||
|
ATTR_SCHEDULES: schedules,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"renault_api.renault_vehicle.RenaultVehicle.get_charging_settings",
|
||||||
|
return_value=schemas.KamereonVehicleDataResponseSchema.loads(
|
||||||
|
load_fixture("renault/charging_settings.json")
|
||||||
|
).get_attributes(schemas.KamereonVehicleChargingSettingsDataSchema),
|
||||||
|
), patch(
|
||||||
|
"renault_api.renault_vehicle.RenaultVehicle.set_charge_schedules",
|
||||||
|
return_value=(
|
||||||
|
schemas.KamereonVehicleHvacStartActionDataSchema.loads(
|
||||||
|
load_fixture("renault/action.set_charge_schedules.json")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
) as mock_action:
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_CHARGE_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_action.mock_calls[0][1] == (mock_call_data,)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_service_set_charge_schedule_multi(hass: HomeAssistant):
|
||||||
|
"""Test that service invokes renault_api with correct data."""
|
||||||
|
await setup_renault_integration_vehicle(hass, "zoe_40")
|
||||||
|
|
||||||
|
schedules = [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"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": 3},
|
||||||
|
]
|
||||||
|
data = {
|
||||||
|
ATTR_VEHICLE: get_device_id(hass),
|
||||||
|
ATTR_SCHEDULES: schedules,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"renault_api.renault_vehicle.RenaultVehicle.get_charging_settings",
|
||||||
|
return_value=schemas.KamereonVehicleDataResponseSchema.loads(
|
||||||
|
load_fixture("renault/charging_settings.json")
|
||||||
|
).get_attributes(schemas.KamereonVehicleChargingSettingsDataSchema),
|
||||||
|
), patch(
|
||||||
|
"renault_api.renault_vehicle.RenaultVehicle.set_charge_schedules",
|
||||||
|
return_value=(
|
||||||
|
schemas.KamereonVehicleHvacStartActionDataSchema.loads(
|
||||||
|
load_fixture("renault/action.set_charge_schedules.json")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
) as mock_action:
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_CHARGE_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_action.mock_calls[0][1] == (mock_call_data,)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_service_set_charge_start(hass: HomeAssistant):
|
||||||
|
"""Test that service invokes renault_api with correct data."""
|
||||||
|
await setup_renault_integration_vehicle(hass, "zoe_40")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
ATTR_VEHICLE: get_device_id(hass),
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"renault_api.renault_vehicle.RenaultVehicle.set_charge_start",
|
||||||
|
return_value=(
|
||||||
|
schemas.KamereonVehicleHvacStartActionDataSchema.loads(
|
||||||
|
load_fixture("renault/action.set_charge_start.json")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
) as mock_action:
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_CHARGE_START, service_data=data, blocking=True
|
||||||
|
)
|
||||||
|
assert len(mock_action.mock_calls) == 1
|
||||||
|
assert mock_action.mock_calls[0][1] == ()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_service_invalid_device_id(hass: HomeAssistant):
|
||||||
|
"""Test that service fails with ValueError if device_id not found in registry."""
|
||||||
|
await setup_renault_integration_vehicle(hass, "zoe_40")
|
||||||
|
|
||||||
|
data = {ATTR_VEHICLE: "VF1AAAAA555777999"}
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_AC_CANCEL, service_data=data, blocking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_service_invalid_device_id2(hass: HomeAssistant):
|
||||||
|
"""Test that service fails with ValueError if device_id not found in vehicles."""
|
||||||
|
config_entry = await setup_renault_integration_vehicle(hass, "zoe_40")
|
||||||
|
|
||||||
|
extra_vehicle = MOCK_VEHICLES["captur_phev"]["expected_device"]
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
device_registry.async_get_or_create(
|
||||||
|
config_entry_id=config_entry.entry_id,
|
||||||
|
identifiers=extra_vehicle[ATTR_IDENTIFIERS],
|
||||||
|
manufacturer=extra_vehicle[ATTR_MANUFACTURER],
|
||||||
|
name=extra_vehicle[ATTR_NAME],
|
||||||
|
model=extra_vehicle[ATTR_MODEL],
|
||||||
|
sw_version=extra_vehicle[ATTR_SW_VERSION],
|
||||||
|
)
|
||||||
|
device_id = device_registry.async_get_device(extra_vehicle[ATTR_IDENTIFIERS]).id
|
||||||
|
|
||||||
|
data = {ATTR_VEHICLE: device_id}
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN, SERVICE_AC_CANCEL, service_data=data, blocking=True
|
||||||
|
)
|
7
tests/fixtures/renault/action.set_ac_start.json
vendored
Normal file
7
tests/fixtures/renault/action.set_ac_start.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"type": "HvacStart",
|
||||||
|
"id": "guid",
|
||||||
|
"attributes": { "action": "start", "targetTemperature": 21.0 }
|
||||||
|
}
|
||||||
|
}
|
7
tests/fixtures/renault/action.set_ac_stop.json
vendored
Normal file
7
tests/fixtures/renault/action.set_ac_stop.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"type": "HvacStart",
|
||||||
|
"id": "guid",
|
||||||
|
"attributes": { "action": "cancel" }
|
||||||
|
}
|
||||||
|
}
|
38
tests/fixtures/renault/action.set_charge_schedules.json
vendored
Normal file
38
tests/fixtures/renault/action.set_charge_schedules.json
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"type": "ChargeSchedule",
|
||||||
|
"id": "guid",
|
||||||
|
"attributes": {
|
||||||
|
"schedules": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"activated": true,
|
||||||
|
"tuesday": {
|
||||||
|
"startTime": "T04:30Z",
|
||||||
|
"duration": 420
|
||||||
|
},
|
||||||
|
"wednesday": {
|
||||||
|
"startTime": "T22:30Z",
|
||||||
|
"duration": 420
|
||||||
|
},
|
||||||
|
"thursday": {
|
||||||
|
"startTime": "T22:00Z",
|
||||||
|
"duration": 420
|
||||||
|
},
|
||||||
|
"friday": {
|
||||||
|
"startTime": "T23:30Z",
|
||||||
|
"duration": 480
|
||||||
|
},
|
||||||
|
"saturday": {
|
||||||
|
"startTime": "T18:30Z",
|
||||||
|
"duration": 120
|
||||||
|
},
|
||||||
|
"sunday": {
|
||||||
|
"startTime": "T12:45Z",
|
||||||
|
"duration": 45
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
tests/fixtures/renault/action.set_charge_start.json
vendored
Normal file
7
tests/fixtures/renault/action.set_charge_start.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"type": "ChargingStart",
|
||||||
|
"id": "guid",
|
||||||
|
"attributes": { "action": "start" }
|
||||||
|
}
|
||||||
|
}
|
87
tests/fixtures/renault/charging_settings.json
vendored
Normal file
87
tests/fixtures/renault/charging_settings.json
vendored
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"type": "Car",
|
||||||
|
"id": "VF1AAAAA555777999",
|
||||||
|
"attributes": {
|
||||||
|
"mode": "scheduled",
|
||||||
|
"schedules": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"activated": true,
|
||||||
|
"monday": {
|
||||||
|
"startTime": "T00:00Z",
|
||||||
|
"duration": 450
|
||||||
|
},
|
||||||
|
"tuesday": {
|
||||||
|
"startTime": "T00:00Z",
|
||||||
|
"duration": 450
|
||||||
|
},
|
||||||
|
"wednesday": {
|
||||||
|
"startTime": "T00:00Z",
|
||||||
|
"duration": 450
|
||||||
|
},
|
||||||
|
"thursday": {
|
||||||
|
"startTime": "T00:00Z",
|
||||||
|
"duration": 450
|
||||||
|
},
|
||||||
|
"friday": {
|
||||||
|
"startTime": "T00:00Z",
|
||||||
|
"duration": 450
|
||||||
|
},
|
||||||
|
"saturday": {
|
||||||
|
"startTime": "T00:00Z",
|
||||||
|
"duration": 450
|
||||||
|
},
|
||||||
|
"sunday": {
|
||||||
|
"startTime": "T00:00Z",
|
||||||
|
"duration": 450
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"activated": true,
|
||||||
|
"monday": {
|
||||||
|
"startTime": "T23:30Z",
|
||||||
|
"duration": 15
|
||||||
|
},
|
||||||
|
"tuesday": {
|
||||||
|
"startTime": "T23:30Z",
|
||||||
|
"duration": 15
|
||||||
|
},
|
||||||
|
"wednesday": {
|
||||||
|
"startTime": "T23:30Z",
|
||||||
|
"duration": 15
|
||||||
|
},
|
||||||
|
"thursday": {
|
||||||
|
"startTime": "T23:30Z",
|
||||||
|
"duration": 15
|
||||||
|
},
|
||||||
|
"friday": {
|
||||||
|
"startTime": "T23:30Z",
|
||||||
|
"duration": 15
|
||||||
|
},
|
||||||
|
"saturday": {
|
||||||
|
"startTime": "T23:30Z",
|
||||||
|
"duration": 15
|
||||||
|
},
|
||||||
|
"sunday": {
|
||||||
|
"startTime": "T23:30Z",
|
||||||
|
"duration": 15
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"activated": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"activated": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"activated": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user