From e3ed307895ae694d863cab1dadaf2f1a686a258c Mon Sep 17 00:00:00 2001 From: Philipp Waller <1090452+philippwaller@users.noreply.github.com> Date: Fri, 4 Oct 2024 01:55:43 +0200 Subject: [PATCH 1/4] feat: add "Preclimate" switch --- custom_components/mbapi2020/const.py | 15 +++++++++++++++ custom_components/mbapi2020/switch.py | 25 +++++++++++++++++++------ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/custom_components/mbapi2020/const.py b/custom_components/mbapi2020/const.py index ee54ceb2..7d1b2048 100644 --- a/custom_components/mbapi2020/const.py +++ b/custom_components/mbapi2020/const.py @@ -1320,6 +1320,21 @@ None, None, ], + "preheat": [ + "Preclimate", + None, # Deprecated: DO NOT USE + "precond", + "precondStatus", + "value", + None, + None, + "mdi:hvac", + None, + False, + None, + None, + None, + ], } SENSORS_POLL = { diff --git a/custom_components/mbapi2020/switch.py b/custom_components/mbapi2020/switch.py index fef93ee3..3b6983e7 100644 --- a/custom_components/mbapi2020/switch.py +++ b/custom_components/mbapi2020/switch.py @@ -4,6 +4,7 @@ https://github.com/ReneNulschDE/mbapi2020/ """ from __future__ import annotations +import asyncio from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -56,17 +57,29 @@ async def async_setup_entry( class MercedesMESwitch(MercedesMeEntity, SwitchEntity, RestoreEntity): - """Representation of a Sensor.""" + """Representation of a Mercedes Me Switch.""" async def async_turn_on(self, **kwargs): - """Turn a device component on.""" - await getattr(self._coordinator.client, self._internal_name + "_start")(self._vin) + """Turn the device component on.""" + await getattr(self._coordinator.client, f"{self._internal_name}_start")(self._vin) + await self._async_wait_for_state(True) async def async_turn_off(self, **kwargs): - """Turn a device component off.""" - await getattr(self._coordinator.client, self._internal_name + "_stop")(self._vin) + """Turn the device component off.""" + await getattr(self._coordinator.client, f"{self._internal_name}_stop")(self._vin) + await self._async_wait_for_state(False) + + async def _async_wait_for_state(self, desired_state, max_attempts=30, delay=1): + """Wait until the device reaches the desired state.""" + for _ in range(max_attempts): + current_state = self._get_car_value(self._feature_name, self._object_name, self._attrib_name, False) + if current_state == desired_state: + break + await asyncio.sleep(delay) + else: + pass @property def is_on(self): - """Return true if device is locked.""" + """Return True if the device is on.""" return self._get_car_value(self._feature_name, self._object_name, self._attrib_name, False) From ec66072e4f3ac57bac31a7a426b39a1acf2196a6 Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:31:55 +0200 Subject: [PATCH 2/4] Add capability check to switch --- custom_components/mbapi2020/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/mbapi2020/const.py b/custom_components/mbapi2020/const.py index 7d1b2048..606651e9 100644 --- a/custom_components/mbapi2020/const.py +++ b/custom_components/mbapi2020/const.py @@ -1326,7 +1326,7 @@ "precond", "precondStatus", "value", - None, + "ZEV_PRECONDITIONING_START", None, "mdi:hvac", None, From 48d0ada11eedf00372ebe3c912cc475c1b8929ec Mon Sep 17 00:00:00 2001 From: Philipp Waller <1090452+philippwaller@users.noreply.github.com> Date: Sun, 6 Oct 2024 23:55:51 +0200 Subject: [PATCH 3/4] refactor: introduce new classes for flexible entity configuration --- custom_components/mbapi2020/__init__.py | 106 +++++-- custom_components/mbapi2020/binary_sensor.py | 2 +- custom_components/mbapi2020/button.py | 2 +- custom_components/mbapi2020/const.py | 37 +-- custom_components/mbapi2020/device_tracker.py | 2 +- custom_components/mbapi2020/helper.py | 6 + custom_components/mbapi2020/lock.py | 2 +- custom_components/mbapi2020/sensor.py | 4 +- custom_components/mbapi2020/switch.py | 279 +++++++++++++++--- 9 files changed, 338 insertions(+), 102 deletions(-) diff --git a/custom_components/mbapi2020/__init__.py b/custom_components/mbapi2020/__init__.py index 72e82bdd..31e47bb4 100644 --- a/custom_components/mbapi2020/__init__.py +++ b/custom_components/mbapi2020/__init__.py @@ -3,12 +3,12 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass from datetime import datetime import time +from typing import Protocol import aiohttp -import voluptuous as vol - from custom_components.mbapi2020.car import Car, CarAttribute, RcpOptions from custom_components.mbapi2020.const import ( ATTR_MB_MANUFACTURER, @@ -24,9 +24,16 @@ from custom_components.mbapi2020.errors import WebsocketError from custom_components.mbapi2020.helper import LogHelper as loghelper from custom_components.mbapi2020.services import setup_services +import voluptuous as vol + from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType @@ -216,6 +223,46 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): return unload_ok +class CapabilityCheckFunc(Protocol): + """Protocol for a callable that checks if a capability is available for a given car.""" + + def __call__(self, car: Car) -> bool: + """Check if the capability is available for the specified car.""" + + +@dataclass(frozen=True) +class MercedesMeEntityConfig: + """Configuration class for MercedesMe entities.""" + + id: str + entity_name: str + feature_name: str + object_name: str + attribute_name: str + + attributes: list[str] | None = None + icon: str | None = None + device_class: str | None = None + entity_category: EntityCategory | None = None + + capability_check: CapabilityCheckFunc | None = None + + def __repr__(self) -> str: + """Return a string representation of the MercedesMeEntityConfig instance.""" + return ( + f"{self.__class__.__name__}(" + f"internal_name={self.id!r}, " + f"entity_name={self.entity_name!r}, " + f"feature_name={self.feature_name!r}, " + f"object_name={self.object_name!r}, " + f"attribute_name={self.attribute_name!r}, " + f"capability_check={self.capability_check!r}, " + f"attributes={self.attributes!r}, " + f"device_class={self.device_class!r}, " + f"icon={self.icon!r}, " + f"entity_category={self.entity_category!r})" + ) + class MercedesMeEntity(CoordinatorEntity[MBAPI2020DataUpdateCoordinator], Entity): """Entity class for MercedesMe devices.""" @@ -223,12 +270,12 @@ class MercedesMeEntity(CoordinatorEntity[MBAPI2020DataUpdateCoordinator], Entity def __init__( self, - internal_name, - sensor_config, - vin, + internal_name: str, + config: list | MercedesMeEntityConfig, + vin: str, coordinator: MBAPI2020DataUpdateCoordinator, should_poll: bool = False, - ): + ) -> None: """Initialize the MercedesMe entity.""" super().__init__(coordinator) @@ -236,15 +283,35 @@ def __init__( self._coordinator = coordinator self._vin = vin self._internal_name = internal_name - self._sensor_config = sensor_config + self._sensor_config = config self._state = None - self._sensor_name = sensor_config[scf.DISPLAY_NAME.value] - self._internal_unit = sensor_config[scf.UNIT_OF_MEASUREMENT.value] - self._feature_name = sensor_config[scf.OBJECT_NAME.value] - self._object_name = sensor_config[scf.ATTRIBUTE_NAME.value] - self._attrib_name = sensor_config[scf.VALUE_FIELD_NAME.value] - self._flip_result = sensor_config[scf.FLIP_RESULT.value] + + # Temporary workaround: If PR get's approved, all entity types should be migrated to the new config classes + if isinstance(config, MercedesMeEntityConfig): + self._sensor_name = config.entity_name + self._internal_unit = None + self._feature_name = config.feature_name + self._object_name = config.object_name + self._attrib_name = config.attribute_name + self._flip_result = False + self._attr_device_class = config.device_class + self._attr_icon = config.icon + self._attr_state_class = None + self._attr_entity_category = config.entity_category + self._attributes = config.attributes + else: + self._sensor_name = config[scf.DISPLAY_NAME.value] + self._internal_unit = config[scf.UNIT_OF_MEASUREMENT.value] + self._feature_name = config[scf.OBJECT_NAME.value] + self._object_name = config[scf.ATTRIBUTE_NAME.value] + self._attrib_name = config[scf.VALUE_FIELD_NAME.value] + self._flip_result = config[scf.FLIP_RESULT.value] + self._attr_device_class = self._sensor_config[scf.DEVICE_CLASS.value] + self._attr_icon = self._sensor_config[scf.ICON.value] + self._attr_state_class = self._sensor_config[scf.STATE_CLASS.value] + self._attr_entity_category = self._sensor_config[scf.ENTITY_CATEGORY.value] + self._attributes = self._sensor_config[scf.EXTENDED_ATTRIBUTE_LIST.value] self._car = self._coordinator.client.cars[self._vin] self._use_chinese_location_data: bool = self._coordinator.config_entry.options.get( @@ -253,12 +320,9 @@ def __init__( self._name = f"{self._car.licenseplate} {self._sensor_name}" - self._attr_device_class = self._sensor_config[scf.DEVICE_CLASS.value] self._attr_device_info = {"identifiers": {(DOMAIN, self._vin)}} - self._attr_entity_category = self._sensor_config[scf.ENTITY_CATEGORY.value] - self._attr_icon = self._sensor_config[scf.ICON.value] self._attr_should_poll = should_poll - self._attr_state_class = self._sensor_config[scf.STATE_CLASS.value] + self._attr_native_unit_of_measurement = self.unit_of_measurement self._attr_translation_key = self._internal_name.lower() self._attr_unique_id = slugify(f"{self._vin}_{self._internal_name}") @@ -287,8 +351,8 @@ def extra_state_attributes(self): if value: state[item] = value if item != "timestamp" else datetime.fromtimestamp(int(value)) - if self._sensor_config[scf.EXTENDED_ATTRIBUTE_LIST.value] is not None: - for attrib in sorted(self._sensor_config[scf.EXTENDED_ATTRIBUTE_LIST.value]): + if self._attributes is not None: + for attrib in sorted(self._attributes): if "." in attrib: object_name = attrib.split(".")[0] attrib_name = attrib.split(".")[1] @@ -331,6 +395,8 @@ def unit_of_measurement(self): ) return reported_unit + if isinstance(self._sensor_config, MercedesMeEntityConfig): + return None return self._sensor_config[scf.UNIT_OF_MEASUREMENT.value] def update(self): diff --git a/custom_components/mbapi2020/binary_sensor.py b/custom_components/mbapi2020/binary_sensor.py index 6c7d91d2..08dba047 100644 --- a/custom_components/mbapi2020/binary_sensor.py +++ b/custom_components/mbapi2020/binary_sensor.py @@ -40,7 +40,7 @@ async def async_setup_entry( ): device = MercedesMEBinarySensor( internal_name=key, - sensor_config=value, + config=value, vin=car.finorvin, coordinator=coordinator, ) diff --git a/custom_components/mbapi2020/button.py b/custom_components/mbapi2020/button.py index e00fa95c..da0d4a2c 100644 --- a/custom_components/mbapi2020/button.py +++ b/custom_components/mbapi2020/button.py @@ -38,7 +38,7 @@ async def async_setup_entry( ): device = MercedesMEButton( internal_name=key, - sensor_config=value, + config=value, vin=car.finorvin, coordinator=coordinator, ) diff --git a/custom_components/mbapi2020/const.py b/custom_components/mbapi2020/const.py index 606651e9..f843966a 100644 --- a/custom_components/mbapi2020/const.py +++ b/custom_components/mbapi2020/const.py @@ -57,6 +57,10 @@ UPDATE_INTERVAL = timedelta(seconds=300) +STATE_CONFIRMATION_DURATION = ( + 60 # Duration to wait for state confirmation of interactive entitiess in seconds +) + DEFAULT_CACHE_PATH = "custom_components/mbapi2020/messages" DEFAULT_DOWNLOAD_PATH = "custom_components/mbapi2020/resources" DEFAULT_LOCALE = "en-GB" @@ -1304,39 +1308,6 @@ ], } -SWITCHES = { - "auxheat": [ - "AuxHeat", - None, # Deprecated: DO NOT USE - "auxheat", - "auxheatActive", - "value", - "AUXHEAT_START", - {}, - None, - None, - False, - None, - None, - None, - ], - "preheat": [ - "Preclimate", - None, # Deprecated: DO NOT USE - "precond", - "precondStatus", - "value", - "ZEV_PRECONDITIONING_START", - None, - "mdi:hvac", - None, - False, - None, - None, - None, - ], -} - SENSORS_POLL = { "geofencing_violation": [ "Geofencing Violation", diff --git a/custom_components/mbapi2020/device_tracker.py b/custom_components/mbapi2020/device_tracker.py index 28d5fb54..55eed1f3 100644 --- a/custom_components/mbapi2020/device_tracker.py +++ b/custom_components/mbapi2020/device_tracker.py @@ -42,7 +42,7 @@ async def async_setup_entry( for key, value in sorted(DEVICE_TRACKER.items()): device = MercedesMEDeviceTracker( internal_name=key, - sensor_config=value, + config=value, vin=car.finorvin, coordinator=coordinator, ) diff --git a/custom_components/mbapi2020/helper.py b/custom_components/mbapi2020/helper.py index 35ae13f8..722dc019 100644 --- a/custom_components/mbapi2020/helper.py +++ b/custom_components/mbapi2020/helper.py @@ -229,3 +229,9 @@ def default(self, o) -> Union[str, dict]: # noqa: D102 retval.update({p: getattr(o, p) for p in get_class_property_names(o)}) return {k: v for k, v in retval.items() if k not in JSON_EXPORT_IGNORED_KEYS} return str(o) + +def check_capabilities(car, required_capabilities): + """Check if the car has the required capabilities.""" + return all( + car.features.get(capability) is True for capability in required_capabilities + ) diff --git a/custom_components/mbapi2020/lock.py b/custom_components/mbapi2020/lock.py index ed22a091..5bc180d3 100644 --- a/custom_components/mbapi2020/lock.py +++ b/custom_components/mbapi2020/lock.py @@ -44,7 +44,7 @@ async def async_setup_entry( ): device = MercedesMELock( internal_name=key, - sensor_config=value, + config=value, vin=car.finorvin, coordinator=coordinator, ) diff --git a/custom_components/mbapi2020/sensor.py b/custom_components/mbapi2020/sensor.py index 7d593d8f..7bc011ef 100644 --- a/custom_components/mbapi2020/sensor.py +++ b/custom_components/mbapi2020/sensor.py @@ -50,7 +50,7 @@ async def async_setup_entry( ): device = MercedesMESensor( internal_name=key, - sensor_config=value, + config=value, vin=car.finorvin, coordinator=coordinator, ) @@ -74,7 +74,7 @@ async def async_setup_entry( ): device = MercedesMESensorPoll( internal_name=key, - sensor_config=value, + config=value, vin=car.finorvin, coordinator=coordinator, should_poll=True, diff --git a/custom_components/mbapi2020/switch.py b/custom_components/mbapi2020/switch.py index 3b6983e7..7ca8ea71 100644 --- a/custom_components/mbapi2020/switch.py +++ b/custom_components/mbapi2020/switch.py @@ -3,19 +3,36 @@ For more details about this component, please refer to the documentation at https://github.com/ReneNulschDE/mbapi2020/ """ + from __future__ import annotations -import asyncio +from dataclasses import dataclass +from typing import Protocol + +from config.custom_components.mbapi2020 import MercedesMeEntity, MercedesMeEntityConfig from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.restore_state import RestoreEntity -from . import MercedesMeEntity -from .const import CONF_FT_DISABLE_CAPABILITY_CHECK, DOMAIN, LOGGER, SWITCHES, SensorConfigFields as scf +from .const import ( + CONF_FT_DISABLE_CAPABILITY_CHECK, + DOMAIN, + LOGGER, + STATE_CONFIRMATION_DURATION, +) from .coordinator import MBAPI2020DataUpdateCoordinator -from .helper import LogHelper as loghelper +from .helper import LogHelper as loghelper, check_capabilities + + +async def async_turn_on_preheat(self: MercedesMESwitch, **kwargs) -> None: + """Turn on preheat.""" + if self._car.features.get("precondNow"): + await self._coordinator.client.preheat_start(self._vin) + else: + await self._coordinator.client.preheat_start_immediate(self._vin) async def async_setup_entry( @@ -23,63 +40,239 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Setups the switch platform.""" + """Set up the switch platform for Mercedes ME.""" - coordinator: MBAPI2020DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: MBAPI2020DataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] if not coordinator.client.cars: - LOGGER.info("No Cars found.") + LOGGER.info("No cars found during the switch creation process") return - sensor_list = [] + entities: list[MercedesMESwitch] = [] + skip_capability_check = config_entry.options.get( + CONF_FT_DISABLE_CAPABILITY_CHECK, False + ) + for car in coordinator.client.cars.values(): - for key, value in sorted(SWITCHES.items()): - if ( - value[scf.CAPABILITIES_LIST.value] is None - or config_entry.options.get(CONF_FT_DISABLE_CAPABILITY_CHECK, False) is True - or car.features.get(value[scf.CAPABILITIES_LIST.value], False) is True - ): - device = MercedesMESwitch( - internal_name=key, - sensor_config=value, - vin=car.finorvin, - coordinator=coordinator, + car_vin_masked = loghelper.Mask_VIN(car.finorvin) + + for config in SWITCH_CONFIGS: + capability_check = getattr(config, "capability_check", None) + if capability_check is None: + LOGGER.error( + "Missing capability check for switch config '%s'. Skipping", + config.id, + ) + continue + + if not skip_capability_check and not capability_check(car): + LOGGER.debug( + "Car '%s' does not support feature '%s'. Skipping", + car_vin_masked, + config.id, + ) + continue + + try: + entity = MercedesMESwitch( + config=config, vin=car.finorvin, coordinator=coordinator + ) + entities.append(entity) + LOGGER.debug( + "Created switch entity for car '%s': Internal Name='%s', Entity Name='%s'", + car_vin_masked, + config.id, + config.entity_name, ) - LOGGER.info( - "Created Switch for car %s - feature %s check: %s", - loghelper.Mask_VIN(car.finorvin), - value[5], - car.features.get(value[5]), + except Exception as e: + LOGGER.error( + "Error creating switch entity '%s' for car '%s': %s", + config.id, + car_vin_masked, + str(e), ) - sensor_list.append(device) - async_add_entities(sensor_list, True) + async_add_entities(entities) + + +class SwitchTurnOn(Protocol): + """Protocol for a callable that asynchronously turns on a MercedesME switch.""" + + async def __call__(self, **kwargs) -> None: + """Asynchronously turn on the switch.""" + + +class SwitchTurnOff(Protocol): + """Protocol for a callable that asynchronously turns off a MercedesME switch.""" + + async def __call__(self, **kwargs) -> None: + """Asynchronously turn off the switch.""" + + +class SwitchIsOn(Protocol): + """Protocol for a callable that checks if a MercedesME switch is on.""" + + def __call__(self) -> bool: + """Check if the switch is currently on.""" + + +@dataclass(frozen=True) +class MercedesMeSwitchEntityConfig(MercedesMeEntityConfig): + """Configuration class for MercedesMe switch entities.""" + + turn_on: SwitchTurnOn | None = None + turn_off: SwitchTurnOff | None = None + is_on: SwitchIsOn | None = None + + def __post_init__(self): + """Post-initialization checks to ensure required fields are set.""" + if self.capability_check is None: + raise ValueError(f"capability_check is required for {self.__class__.__name__}") + if self.turn_on is None: + raise ValueError(f"turn_on is required for {self.__class__.__name__}") + if self.turn_off is None: + raise ValueError(f"turn_off is required for {self.__class__.__name__}") + + def __repr__(self) -> str: + """Return a string representation of the MercedesMeSwitchEntityConfig instance.""" + return ( + f"{self.__class__.__name__}(" + f"internal_name={self.id!r}, " + f"entity_name={self.entity_name!r}, " + f"feature_name={self.feature_name!r}, " + f"object_name={self.object_name!r}, " + f"attribute_name={self.attribute_name!r}, " + f"capability_check={self.capability_check!r}, " + f"attributes={self.attributes!r}, " + f"device_class={self.device_class!r}, " + f"icon={self.icon!r}, " + f"entity_category={self.entity_category!r}, " + f"turn_on={self.turn_on!r}, " + f"turn_off={self.turn_off!r}, " + f"is_on={self.is_on!r})" + ) class MercedesMESwitch(MercedesMeEntity, SwitchEntity, RestoreEntity): """Representation of a Mercedes Me Switch.""" - async def async_turn_on(self, **kwargs): + def __init__(self, config: MercedesMeSwitchEntityConfig, vin, coordinator) -> None: + """Initialize the switch with methods for handling on/off commands.""" + self._turn_on_method = config.turn_on + self._turn_off_method = config.turn_off + self._is_on_method = config.is_on + + # Initialize command tracking variables + self._expected_state = None # True for on, False for off, or None + self._state_confirmation_duration = STATE_CONFIRMATION_DURATION + self._confirmation_handle = None + + super().__init__(config.id, config, vin, coordinator) + + async def async_turn_on(self, **kwargs: dict) -> None: """Turn the device component on.""" - await getattr(self._coordinator.client, f"{self._internal_name}_start")(self._vin) - await self._async_wait_for_state(True) + await self._async_handle_state_change(state=True, **kwargs) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: dict) -> None: """Turn the device component off.""" - await getattr(self._coordinator.client, f"{self._internal_name}_stop")(self._vin) - await self._async_wait_for_state(False) - - async def _async_wait_for_state(self, desired_state, max_attempts=30, delay=1): - """Wait until the device reaches the desired state.""" - for _ in range(max_attempts): - current_state = self._get_car_value(self._feature_name, self._object_name, self._attrib_name, False) - if current_state == desired_state: - break - await asyncio.sleep(delay) + await self._async_handle_state_change(state=False, **kwargs) + + async def _async_handle_state_change(self, state: bool, **kwargs) -> None: + """Handle changing the device state and manage confirmation duration.""" + # Set the expected state based on the desired state + self._expected_state = state + + # Execute the appropriate method based on the desired state + if state: + await self._turn_on_method(self, **kwargs) else: - pass + await self._turn_off_method(self, **kwargs) + + # Cancel previous confirmation if any + if self._confirmation_handle: + self._confirmation_handle() + + # Schedule state reset after confirmation duration + self._confirmation_handle = async_call_later( + self.hass, self._state_confirmation_duration, self._reset_expected_state + ) + + # Update the UI + self.async_write_ha_state() + + async def _reset_expected_state(self, _): + """Reset the expected state after confirmation duration and update the state.""" + self._expected_state = None + self._confirmation_handle = None + self.async_write_ha_state() @property - def is_on(self): + def is_on(self) -> bool: """Return True if the device is on.""" - return self._get_car_value(self._feature_name, self._object_name, self._attrib_name, False) + actual_state = self._get_actual_state() + + if self._expected_state is not None: + if actual_state == self._expected_state: + # Expected state reached, cancel confirmation duration + if self._confirmation_handle: + self._confirmation_handle() + self._confirmation_handle = None + self._expected_state = None + else: + # Return expected state during the confirmation duration + return self._expected_state + + return actual_state + + def _get_actual_state(self) -> bool: + """Return the actual state of the device.""" + if self._is_on_method: + return self._is_on_method() + return self._default_is_on() + + def _default_is_on(self) -> bool: + """Provide default implementation for determining the 'on' state.""" + return self._get_car_value( + self._feature_name, + self._object_name, + self._attrib_name, + default_value=False, + ) + + @property + def assumed_state(self) -> bool: + """Return True if the state is being assumed during the confirmation duration.""" + return self._expected_state is not None + +SWITCH_CONFIGS: list[MercedesMeSwitchEntityConfig] = [ + MercedesMeSwitchEntityConfig( + id="preheat", + entity_name="Preclimate", + feature_name="precond", + object_name="precondStatus", + attribute_name="value", + icon="mdi:hvac", + capability_check=lambda car: check_capabilities( + car, ["ZEV_PRECONDITIONING_START", "ZEV_PRECONDITIONING_STOP"] + ), + turn_on=async_turn_on_preheat, + turn_off=lambda self, **kwargs: self._coordinator.client.preheat_stop(self._vin), + + ), + MercedesMeSwitchEntityConfig( + id="auxheat", + entity_name="Auxiliary Heating", + feature_name="auxheat", + object_name="auxheatActive", + attribute_name="value", + icon="mdi:hvac", + capability_check=lambda car: check_capabilities( + car, ["AUXHEAT_START", "AUXHEAT_STOP"] + ), + turn_on=lambda self, **kwargs: self._coordinator.client.auxheat_start(self._vin), + turn_off=lambda self, **kwargs: self._coordinator.client.auxheat_stop(self._vin), + + ), +] From 7dcaf9504dce3eb70ae64620c4dc5a97a64a69f3 Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:42:31 +0200 Subject: [PATCH 4/4] Fix imports in switch.py --- custom_components/mbapi2020/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/mbapi2020/switch.py b/custom_components/mbapi2020/switch.py index 7ca8ea71..44dedae3 100644 --- a/custom_components/mbapi2020/switch.py +++ b/custom_components/mbapi2020/switch.py @@ -9,7 +9,6 @@ from dataclasses import dataclass from typing import Protocol -from config.custom_components.mbapi2020 import MercedesMeEntity, MercedesMeEntityConfig from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -17,6 +16,7 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.restore_state import RestoreEntity +from . import MercedesMeEntity, MercedesMeEntityConfig from .const import ( CONF_FT_DISABLE_CAPABILITY_CHECK, DOMAIN,