diff --git a/custom_components/mbapi2020/__init__.py b/custom_components/mbapi2020/__init__.py index 7ef7a787..5529d291 100644 --- a/custom_components/mbapi2020/__init__.py +++ b/custom_components/mbapi2020/__init__.py @@ -3,10 +3,11 @@ from __future__ import annotations import asyncio +from collections.abc import Callable, Coroutine from dataclasses import dataclass from datetime import datetime import time -from typing import Protocol +from typing import Any import aiohttp import voluptuous as vol @@ -26,12 +27,13 @@ from custom_components.mbapi2020.errors import WebsocketError from custom_components.mbapi2020.helper import LogHelper as loghelper from custom_components.mbapi2020.services import setup_services +from homeassistant.components.switch import SwitchEntityDescription 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.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify @@ -107,7 +109,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): try: capabilities = await coordinator.client.webapi.get_car_capabilities_commands(vin) hass.async_add_executor_job( - coordinator.client.write_debug_json_output, capabilities, f"ca-{loghelper.Mask_VIN(vin)}-", True + coordinator.client.write_debug_json_output, + capabilities, + f"ca-{loghelper.Mask_VIN(vin)}-", + True, ) if capabilities: for feature in capabilities.get("commands"): @@ -116,7 +121,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): capabilityInformation = feature.get("capabilityInformation", None) if capabilityInformation and len(capabilityInformation) > 0: features[feature.get("capabilityInformation")[0]] = bool(feature.get("isAvailable")) - except aiohttp.ClientError: # For some cars a HTTP401 is raised when asking for capabilities, see github issue #83 # We just ignore the capabilities @@ -134,7 +138,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): rcp_supported_settings = await coordinator.client.webapi.get_car_rcp_supported_settings(vin) if rcp_supported_settings: hass.async_add_executor_job( - coordinator.client.write_debug_json_output, rcp_supported_settings, "rcs" + coordinator.client.write_debug_json_output, + rcp_supported_settings, + "rcs", ) if rcp_supported_settings.get("data"): if rcp_supported_settings.get("data").get("attributes"): @@ -159,7 +165,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): setting_result = await coordinator.client.webapi.get_car_rcp_settings(vin, setting) if setting_result is not None: hass.async_add_executor_job( - coordinator.client.write_debug_json_output, setting_result, f"rcs_{setting}" + coordinator.client.write_debug_json_output, + setting_result, + f"rcs_{setting}", ) current_car = Car(vin) @@ -231,45 +239,12 @@ 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: +@dataclass(frozen=True, kw_only=True) +class MercedesMeEntityDescription(SwitchEntityDescription): """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})" - ) + check_capability_fn: Callable[[Car], Callable[[], Coroutine[Any, Any, bool]]] class MercedesMeEntity(CoordinatorEntity[MBAPI2020DataUpdateCoordinator], Entity): @@ -280,38 +255,31 @@ class MercedesMeEntity(CoordinatorEntity[MBAPI2020DataUpdateCoordinator], Entity def __init__( self, internal_name: str, - config: list | MercedesMeEntityConfig, + config: list | EntityDescription, vin: str, coordinator: MBAPI2020DataUpdateCoordinator, should_poll: bool = False, ) -> None: """Initialize the MercedesMe entity.""" - super().__init__(coordinator) self._hass = coordinator.hass self._coordinator = coordinator self._vin = vin self._internal_name = internal_name self._sensor_config = config + self._car = self._coordinator.client.cars[self._vin] + self._feature_name = None + self._object_name = None + self._attrib_name = None + self._flip_result = False self._state = None # 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 + if isinstance(config, EntityDescription): self._attributes = config.attributes + self.entity_description = config 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] @@ -321,25 +289,23 @@ def __init__( 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( - CONF_ENABLE_CHINA_GCJ_02, False - ) - - self._name = f"{self._car.licenseplate} {self._sensor_name}" + self._attr_native_unit_of_measurement = self.unit_of_measurement + self._use_chinese_location_data: bool = self._coordinator.config_entry.options.get( + CONF_ENABLE_CHINA_GCJ_02, False + ) + self._attr_translation_key = self._internal_name.lower() + self._attr_name = config[scf.DISPLAY_NAME.value] + self._name = f"{self._car.licenseplate} {config[scf.DISPLAY_NAME.value]}" self._attr_device_info = {"identifiers": {(DOMAIN, self._vin)}} self._attr_should_poll = should_poll - - 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}") - self._attr_name = self._sensor_name + + super().__init__(coordinator) def device_retrieval_status(self): """Return the retrieval_status of the sensor.""" - if self._sensor_name == "Car": + if self._internal_name == "car": return "VALID" return self._get_car_value(self._feature_name, self._object_name, "retrievalstatus", "error") @@ -404,15 +370,27 @@ def unit_of_measurement(self): ) return reported_unit - if isinstance(self._sensor_config, MercedesMeEntityConfig): + if isinstance(self._sensor_config, EntityDescription): return None return self._sensor_config[scf.UNIT_OF_MEASUREMENT.value] def update(self): """Get the latest data and updates the states.""" + if not self.enabled: + return + + if isinstance(self._sensor_config, EntityDescription): + try: + self._mercedes_me_update() + except Exception as err: + LOGGER.error("Error while updating entity %s: %s", self.name, err) + else: + self._state = self._get_car_value(self._feature_name, self._object_name, self._attrib_name, "error") + self.async_write_ha_state() - self._state = self._get_car_value(self._feature_name, self._object_name, self._attrib_name, "error") - self.async_write_ha_state() + def _mercedes_me_update(self) -> None: + """Update Mercedes Me entity.""" + raise NotImplementedError def _get_car_value(self, feature, object_name, attrib_name, default_value): value = None diff --git a/custom_components/mbapi2020/car.py b/custom_components/mbapi2020/car.py index 86d54e06..d7d5f16e 100644 --- a/custom_components/mbapi2020/car.py +++ b/custom_components/mbapi2020/car.py @@ -295,6 +295,10 @@ def publish_updates(self): for callback in self._update_listeners: callback() + def check_capabilities(self, required_capabilities: list[str]) -> bool: + """Check if the car has the required capabilities.""" + return all(self.features.get(capability) is True for capability in required_capabilities) + @dataclass(init=False) class Tires: diff --git a/custom_components/mbapi2020/client.py b/custom_components/mbapi2020/client.py index 68677897..fcef8c04 100644 --- a/custom_components/mbapi2020/client.py +++ b/custom_components/mbapi2020/client.py @@ -1261,6 +1261,13 @@ async def preheat_start_immediate(self, vin: str): await self.websocket.call(message.SerializeToString()) LOGGER.info("End preheat_start_immediate for vin %s", loghelper.Mask_VIN(vin)) + async def preheat_start_universal(self,vin: str) -> None: + """Turn on preheat universally for any car model.""" + if self._is_car_feature_available(vin, "precondNow"): + await self.preheat_start(vin) + else: + await self.preheat_start_immediate(vin) + async def preheat_start_departure_time(self, vin: str, departure_time: int): """Send a preconditioning start by time command to the car.""" LOGGER.info("Start preheat_start_departure_time for vin %s", loghelper.Mask_VIN(vin)) diff --git a/custom_components/mbapi2020/helper.py b/custom_components/mbapi2020/helper.py index c9905e0d..35ae13f8 100644 --- a/custom_components/mbapi2020/helper.py +++ b/custom_components/mbapi2020/helper.py @@ -229,8 +229,3 @@ 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/switch.py b/custom_components/mbapi2020/switch.py index 86332326..27cfe6d4 100644 --- a/custom_components/mbapi2020/switch.py +++ b/custom_components/mbapi2020/switch.py @@ -6,159 +6,72 @@ from __future__ import annotations +from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Protocol +from typing import Any -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription 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, MercedesMeEntityConfig +from . import MercedesMeEntity, MercedesMeEntityDescription +from .car import Car from .const import CONF_FT_DISABLE_CAPABILITY_CHECK, DOMAIN, LOGGER, STATE_CONFIRMATION_DURATION from .coordinator import MBAPI2020DataUpdateCoordinator -from .helper import LogHelper as loghelper, check_capabilities +from .helper import LogHelper as loghelper -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( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the switch platform for Mercedes ME.""" - - coordinator: MBAPI2020DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - - if not coordinator.client.cars: - LOGGER.info("No cars found during the switch creation process") - return - - entities: list[MercedesMESwitch] = [] - skip_capability_check = config_entry.options.get(CONF_FT_DISABLE_CAPABILITY_CHECK, False) - - for car in coordinator.client.cars.values(): - 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, - ) - except Exception as e: - LOGGER.error( - "Error creating switch entity '%s' for car '%s': %s", - config.id, - car_vin_masked, - str(e), - ) - - 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.""" - +@dataclass(frozen=True, kw_only=True) +class MercedesMeSwitchEntityDescription(MercedesMeEntityDescription, SwitchEntityDescription): + """Configuration class for MercedesMe switch entities.""" -class SwitchIsOn(Protocol): - """Protocol for a callable that checks if a MercedesME switch is on.""" + is_on_fn: Callable[[MercedesMeSwitch], Callable[[], Coroutine[Any, Any, bool]]] + turn_on_fn: Callable[[MercedesMeSwitch], Callable[[], Coroutine[Any, Any, None]]] + turn_off_fn: Callable[[MercedesMeSwitch], Callable[[], Coroutine[Any, Any, None]]] - def __call__(self) -> bool: - """Check if the switch is currently on.""" +SWITCH_DESCRIPTIONS: list[MercedesMeSwitchEntityDescription] = [ + MercedesMeSwitchEntityDescription( + key="precond", + translation_key="precond", + icon="mdi:hvac", + is_on_fn=lambda self: self._get_car_value("precond", "precondStatus", "value", default_value=False), + turn_on_fn=lambda self, **kwargs: self._coordinator.client.preheat_start_universal(self._vin), + turn_off_fn=lambda self, **kwargs: self._coordinator.client.preheat_stop(self._vin), + check_capability_fn=lambda car: car.check_capabilities( + ["ZEV_PRECONDITIONING_START", "ZEV_PRECONDITIONING_STOP"] + ), + ), + MercedesMeSwitchEntityDescription( + key="auxheat", + translation_key="auxheat", + icon="mdi:hvac", + is_on_fn=lambda self: self._get_car_value("auxheat", "auxheatActive", "value", default_value=False), + turn_on_fn=lambda self, **kwargs: self._coordinator.client.auxheat_start(self._vin), + turn_off_fn=lambda self, **kwargs: self._coordinator.client.auxheat_stop(self._vin), + check_capability_fn=lambda car: car.check_capabilities(["AUXHEAT_START", "AUXHEAT_STOP"]), + ), +] -@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): +class MercedesMeSwitch(MercedesMeEntity, SwitchEntity, RestoreEntity): """Representation of a Mercedes Me Switch.""" - def __init__(self, config: MercedesMeSwitchEntityConfig, vin, coordinator) -> None: + _entity_description: MercedesMeSwitchEntityDescription + + def __init__(self, description: MercedesMeSwitchEntityDescription, 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 + self._entity_description = description # 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) + super().__init__(description.key, description, vin, coordinator) async def async_turn_on(self, **kwargs: dict) -> None: """Turn the device component on.""" @@ -173,34 +86,51 @@ async def _async_handle_state_change(self, state: bool, **kwargs) -> None: # 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: - 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() + try: + # Execute the appropriate method and handle any exceptions + if state: + await self._entity_description.turn_on_fn(self, **kwargs) + else: + await self._entity_description.turn_off_fn(self, **kwargs) + + # Cancel any existing confirmation handle + 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 + ) + + except Exception as e: + # Log the error and reset state if needed + LOGGER.error( + "Error changing state to %s for entity '%s': %s", + "on" if state else "off", + self._entity_description.translation_key, + str(e), + ) + self._expected_state = None + if self._confirmation_handle: + self._confirmation_handle() + self._confirmation_handle = None + self.async_write_ha_state() async def _reset_expected_state(self, _): """Reset the expected state after confirmation duration and update the state.""" + self._attr_is_on = not self._expected_state self._expected_state = None self._confirmation_handle = None self.async_write_ha_state() - @property - def is_on(self) -> bool: - """Return True if the device is on.""" - actual_state = self._get_actual_state() + def _mercedes_me_update(self) -> None: + """Update Mercedes Me entity.""" + try: + actual_state = self._entity_description.is_on_fn(self) + except Exception as e: + LOGGER.error("Error getting actual state for %s: %s", self.name, str(e)) + self._attr_available = False + return if self._expected_state is not None: if actual_state == self._expected_state: @@ -211,52 +141,58 @@ def is_on(self) -> bool: 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, - ) + self._attr_is_on = self._expected_state + else: + self._attr_is_on = actual_state + self.async_write_ha_state() @property def assumed_state(self) -> bool: - """Return True if the state is being assumed during the confirmation duration.""" + """Return True if the state is assumed.""" 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), - ), -] +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the switch platform for Mercedes Me.""" + + coordinator: MBAPI2020DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + skip_capability_check: bool = config_entry.options.get(CONF_FT_DISABLE_CAPABILITY_CHECK, False) + + def check_capability(car: Car, description: MercedesMeSwitchEntityDescription) -> bool: + """Check if the car supports the necessary capability for the given feature description.""" + if not skip_capability_check and not description.check_capability_fn(car): + vin_masked = loghelper.Mask_VIN(car.finorvin) + LOGGER.debug( + "Skipping feature '%s' for VIN '%s' due to lack of required capability", description.key, vin_masked + ) + return False + return True + + def create_entity(description: MercedesMeSwitchEntityDescription, car: Car) -> MercedesMeSwitch | None: + """Create a MercedesMeSwitch entity for the car based on the given description.""" + vin_masked = loghelper.Mask_VIN(car.finorvin) + try: + entity = MercedesMeSwitch(description, car.finorvin, coordinator) + LOGGER.debug("Created switch entity for VIN: '%s', feature: '%s'", vin_masked, description.key) + except Exception as e: + LOGGER.error( + "Error creating switch entity for VIN: '%s', feature: '%s'. Exception:", + vin_masked, + description.key, + exc_info=True, + ) + return None + else: + return entity + + entities: list[MercedesMeSwitch] = [ + entity + for car in coordinator.client.cars.values() # Iterate over all cars + for description in SWITCH_DESCRIPTIONS # Iterate over all feature descriptions + if check_capability(car, description) # Check if the car supports the feature + and (entity := create_entity(description, car)) # Create the entity if possible + ] + + async_add_entities(entities) diff --git a/custom_components/mbapi2020/translations/de.json b/custom_components/mbapi2020/translations/de.json index d88d4152..e1804e0f 100644 --- a/custom_components/mbapi2020/translations/de.json +++ b/custom_components/mbapi2020/translations/de.json @@ -48,4 +48,4 @@ } } } -} +} \ No newline at end of file diff --git a/custom_components/mbapi2020/translations/en.json b/custom_components/mbapi2020/translations/en.json index 4fc236e6..01986f27 100755 --- a/custom_components/mbapi2020/translations/en.json +++ b/custom_components/mbapi2020/translations/en.json @@ -689,6 +689,14 @@ "3": "Deflation" } } + }, + "switch": { + "auxheat": { + "name": "Auxiliary Heating" + }, + "precond": { + "name": "Pre-entry climate control" + } } }, "selector": {