HEX
Server: Apache/2.4.63 (Unix)
System: Linux Synopilou92 4.4.302+ #72806 SMP Mon Jul 21 23:16:00 CST 2025 x86_64
User: pilou92 (1026)
PHP: 8.0.30
Disabled: NONE
Upload Files
File: /volume1/docker/homeassistant2025/custom_components/uconnect/services.py
from typing import cast

from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import ServiceCall, callback, HomeAssistant
from homeassistant.helpers import device_registry

from py_uconnect.command import *

from .const import DOMAIN
from .coordinator import UconnectDataUpdateCoordinator

SERVICE_UPDATE = "update"

SERVICE_ENGINE_ON = "engine_on"
SERVICE_ENGINE_OFF = "engine_off"
SERVICE_COMFORT_ON = "comfort_on"
SERVICE_COMFORT_OFF = "comfort_off"
SERVICE_HVAC_ON = "hvac_on"
SERVICE_HVAC_OFF = "hvac_off"
SERVICE_PRECOND_ON = "precond_on"
SERVICE_PRECOND_OFF = "precond_off"
SERVICE_LIGHTS_HORN = "lights_horn"
SERVICE_LIGHTS = "lights"
SERVICE_DOORS_UNLOCK = "doors_unlock"
SERVICE_DOORS_LOCK = "doors_lock"
SERVICE_TRUNK_UNLOCK = "trunk_unlock"
SERVICE_TRUNK_LOCK = "trunk_lock"
SERVICE_LIFTGATE_UNLOCK = "liftgate_unlock"
SERVICE_LIFTGATE_LOCK = "liftgate_lock"
SERVICE_CHARGE = "charge_now"
SERVICE_DEEP_REFRESH = "deep_refresh"
SERVICE_REFRESH_LOCATION = "refresh_location"

SUPPORTED_SERVICES = [
    SERVICE_UPDATE,
    SERVICE_ENGINE_ON,
    SERVICE_ENGINE_OFF,
    SERVICE_COMFORT_ON,
    SERVICE_COMFORT_OFF,
    SERVICE_HVAC_ON,
    SERVICE_HVAC_OFF,
    SERVICE_PRECOND_ON,
    SERVICE_PRECOND_OFF,
    SERVICE_LIGHTS_HORN,
    SERVICE_LIGHTS,
    SERVICE_DOORS_UNLOCK,
    SERVICE_DOORS_LOCK,
    SERVICE_TRUNK_UNLOCK,
    SERVICE_TRUNK_LOCK,
    SERVICE_LIFTGATE_UNLOCK,
    SERVICE_LIFTGATE_LOCK,
    SERVICE_CHARGE,
    SERVICE_DEEP_REFRESH,
    SERVICE_REFRESH_LOCATION,
]

SERVICES_COMMANDS: dict[str, Command] = {
    SERVICE_ENGINE_ON: COMMAND_ENGINE_ON,
    SERVICE_ENGINE_OFF: COMMAND_ENGINE_OFF,
    SERVICE_COMFORT_ON: COMMAND_COMFORT_ON,
    SERVICE_COMFORT_OFF: COMMAND_COMFORT_OFF,
    SERVICE_HVAC_ON: COMMAND_HVAC_ON,
    SERVICE_HVAC_OFF: COMMAND_HVAC_OFF,
    SERVICE_PRECOND_ON: COMMAND_PRECOND_ON,
    SERVICE_PRECOND_OFF: COMMAND_PRECOND_OFF,
    SERVICE_LIGHTS_HORN: COMMAND_LIGHTS_HORN,
    SERVICE_LIGHTS: COMMAND_LIGHTS,
    SERVICE_DOORS_UNLOCK: COMMAND_DOORS_UNLOCK,
    SERVICE_DOORS_LOCK: COMMAND_DOORS_LOCK,
    SERVICE_TRUNK_UNLOCK: COMMAND_TRUNK_UNLOCK,
    SERVICE_TRUNK_LOCK: COMMAND_TRUNK_LOCK,
    SERVICE_LIFTGATE_UNLOCK: COMMAND_LIFTGATE_UNLOCK,
    SERVICE_LIFTGATE_LOCK: COMMAND_LIFTGATE_LOCK,
    SERVICE_CHARGE: COMMAND_CHARGE,
    SERVICE_DEEP_REFRESH: COMMAND_DEEP_REFRESH,
    SERVICE_REFRESH_LOCATION: COMMAND_REFRESH_LOCATION,
}


@callback
def async_setup_services(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
    """Set up services for Uconnect"""

    async def async_call_service(call: ServiceCall):
        coordinator: UconnectDataUpdateCoordinator = _get_coordinator_from_device(
            hass, call
        )
        vin = _get_vin_from_device(hass, call)

        if call.service != SERVICE_UPDATE:
            cmd = SERVICES_COMMANDS[call.service]
            if not cmd.name in coordinator.client.vehicles[vin].supported_commands:
                raise Exception(f"Service {call.service} is not supported by this car")

            await coordinator.async_command(vin, cmd)
        else:
            await coordinator.async_refresh()

    coordinator: UconnectDataUpdateCoordinator = hass.data[DOMAIN][
        config_entry.unique_id
    ]

    # Get a set of all supported commands from all cars
    supported_commands = set()
    for v in coordinator.client.vehicles.values():
        for cmd in v.supported_commands:
            if cmd in COMMANDS_BY_NAME:
                supported_commands.add(cmd)

    for service in SUPPORTED_SERVICES:
        # Create the service only if it's supported by at least one car
        if (
            service == SERVICE_UPDATE
            or SERVICES_COMMANDS[service].name in supported_commands
        ):
            hass.services.async_register(DOMAIN, service, async_call_service)

    return True


@callback
def async_unload_services(hass) -> None:
    for service in SUPPORTED_SERVICES:
        hass.services.async_remove(DOMAIN, service)


def _get_vin_from_device(hass: HomeAssistant, call: ServiceCall) -> str:
    coordinators = list(hass.data[DOMAIN].keys())

    if len(coordinators) == 1:
        coordinator: UconnectDataUpdateCoordinator = hass.data[DOMAIN][coordinators[0]]
        vehicles = coordinator.client.get_vehicles()
        if len(vehicles) == 1:
            return list(vehicles.keys())[0]

    device_entry = device_registry.async_get(hass).async_get(call.data[ATTR_DEVICE_ID])

    for entry in device_entry.identifiers:
        if entry[0] == DOMAIN:
            vin = entry[1]

    return vin


def _get_coordinator_from_device(
    hass: HomeAssistant, call: ServiceCall
) -> UconnectDataUpdateCoordinator:
    coordinators = list(hass.data[DOMAIN].keys())

    if len(coordinators) == 1:
        return hass.data[DOMAIN][coordinators[0]]

    device_entry = device_registry.async_get(hass).async_get(call.data[ATTR_DEVICE_ID])
    config_entry_ids = device_entry.config_entries
    config_entry_id = next(
        (
            config_entry_id
            for config_entry_id in config_entry_ids
            if cast(
                ConfigEntry,
                hass.config_entries.async_get_entry(config_entry_id),
            ).domain
            == DOMAIN
        ),
        None,
    )
    config_entry_unique_id = hass.config_entries.async_get_entry(
        config_entry_id
    ).unique_id

    return hass.data[DOMAIN][config_entry_unique_id]