Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new switch "Preclimate" for hybrid and electric vehicles #269

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 86 additions & 20 deletions custom_components/mbapi2020/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -216,35 +223,95 @@ 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."""

_attr_has_entity_name = True

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)

self._hass = coordinator.hass
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(
Expand All @@ -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}")
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion custom_components/mbapi2020/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async def async_setup_entry(
):
device = MercedesMEBinarySensor(
internal_name=key,
sensor_config=value,
config=value,
vin=car.finorvin,
coordinator=coordinator,
)
Expand Down
2 changes: 1 addition & 1 deletion custom_components/mbapi2020/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ async def async_setup_entry(
):
device = MercedesMEButton(
internal_name=key,
sensor_config=value,
config=value,
vin=car.finorvin,
coordinator=coordinator,
)
Expand Down
22 changes: 4 additions & 18 deletions custom_components/mbapi2020/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1304,24 +1308,6 @@
],
}

SWITCHES = {
"auxheat": [
"AuxHeat",
None, # Deprecated: DO NOT USE
"auxheat",
"auxheatActive",
"value",
"AUXHEAT_START",
{},
None,
None,
False,
None,
None,
None,
],
}

SENSORS_POLL = {
"geofencing_violation": [
"Geofencing Violation",
Expand Down
2 changes: 1 addition & 1 deletion custom_components/mbapi2020/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
6 changes: 6 additions & 0 deletions custom_components/mbapi2020/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
2 changes: 1 addition & 1 deletion custom_components/mbapi2020/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async def async_setup_entry(
):
device = MercedesMELock(
internal_name=key,
sensor_config=value,
config=value,
vin=car.finorvin,
coordinator=coordinator,
)
Expand Down
4 changes: 2 additions & 2 deletions custom_components/mbapi2020/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async def async_setup_entry(
):
device = MercedesMESensor(
internal_name=key,
sensor_config=value,
config=value,
vin=car.finorvin,
coordinator=coordinator,
)
Expand All @@ -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,
Expand Down
Loading
Loading