From 271deaaa74606d4cbac2b64ea352c10cb106c0ba Mon Sep 17 00:00:00 2001 From: Mitch Date: Tue, 18 Jan 2022 22:27:44 +0100 Subject: [PATCH] Added all the missing sensors and a camera that pulls image from api --- .../volkswagen_we_connect_id/__init__.py | 7 +- .../volkswagen_we_connect_id/binary_sensor.py | 162 +++++++++++++++++ .../volkswagen_we_connect_id/button.py | 7 +- .../volkswagen_we_connect_id/camera.py | 93 ++++++++++ .../volkswagen_we_connect_id/manifest.json | 4 +- .../volkswagen_we_connect_id/sensor.py | 165 +++++++++--------- 6 files changed, 344 insertions(+), 94 deletions(-) create mode 100644 custom_components/volkswagen_we_connect_id/binary_sensor.py create mode 100644 custom_components/volkswagen_we_connect_id/camera.py diff --git a/custom_components/volkswagen_we_connect_id/__init__.py b/custom_components/volkswagen_we_connect_id/__init__.py index 97749b7..56241ca 100644 --- a/custom_components/volkswagen_we_connect_id/__init__.py +++ b/custom_components/volkswagen_we_connect_id/__init__.py @@ -9,13 +9,15 @@ from weconnect.elements.vehicle import Vehicle from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -PLATFORMS: list[str] = ["sensor", "button"] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.CAMERA] + _LOGGER = logging.getLogger(__name__) @@ -240,7 +242,7 @@ class VolkswagenIDBaseEntity(Entity): def __init__( self, - vehicle, + vehicle: weconnect.Vehicle, data, we_connect: weconnect.WeConnect, coordinator: CoordinatorEntity, @@ -257,6 +259,7 @@ def __init__( self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"vw{vehicle.vin}")}, manufacturer="Volkswagen", + model=f"{vehicle.model}", # format because of the ID.3/ID.4 names. name=f"Volkswagen {vehicle.nickname}", ) diff --git a/custom_components/volkswagen_we_connect_id/binary_sensor.py b/custom_components/volkswagen_we_connect_id/binary_sensor.py new file mode 100644 index 0000000..92c254f --- /dev/null +++ b/custom_components/volkswagen_we_connect_id/binary_sensor.py @@ -0,0 +1,162 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +import logging +from typing import cast + +from weconnect import weconnect +from weconnect.elements.plug_status import PlugStatus +from weconnect.elements.window_heating_status import WindowHeatingStatus + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import VolkswagenIDBaseEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class VolkswagenIdBinaryEntityDescription(BinarySensorEntityDescription): + """Describes Volkswagen ID binary sensor entity.""" + + local_address: str | None = None + on_value: None = None + + +SENSORS: tuple[VolkswagenIdBinaryEntityDescription, ...] = ( + VolkswagenIdBinaryEntityDescription( + key="climatisationWithoutExternalPower", + name="Climatisation Without External Power", + local_address="/climatisation/climatisationSettings/climatisationWithoutExternalPower", + on_value=True, + ), + VolkswagenIdBinaryEntityDescription( + key="climatizationAtUnlock", + name="Climatisation At Unlock", + local_address="/climatisation/climatisationSettings/climatizationAtUnlock", + on_value=True, + ), + VolkswagenIdBinaryEntityDescription( + key="zoneFrontLeftEnabled", + name="Zone Front Left Enabled", + local_address="/climatisation/climatisationSettings/zoneFrontLeftEnabled", + on_value=True, + ), + VolkswagenIdBinaryEntityDescription( + key="zoneFrontRightEnabled", + name="Zone Front Right Enabled", + local_address="/climatisation/climatisationSettings/zoneFrontRightEnabled", + on_value=True, + ), + VolkswagenIdBinaryEntityDescription( + key="windowHeatingEnabled", + name="Window Heating Enabled", + local_address="/climatisation/climatisationSettings/windowHeatingEnabled", + on_value=True, + ), + VolkswagenIdBinaryEntityDescription( + key="frontWindowHeatingState", + name="Front Window Heating State", + local_address="/climatisation/windowHeatingStatus/windows/front/windowHeatingState", + on_value=WindowHeatingStatus.Window.WindowHeatingState.ON, + ), + VolkswagenIdBinaryEntityDescription( + key="rearWindowHeatingState", + name="Rear Window Heating State", + local_address="/climatisation/windowHeatingStatus/windows/rear/windowHeatingState", + on_value=WindowHeatingStatus.Window.WindowHeatingState.ON, + ), + VolkswagenIdBinaryEntityDescription( + key="autoUnlockPlugWhenCharged", + name="Auto Unlock Plug When Charged", + local_address="/charging/chargingSettings/autoUnlockPlugWhenCharged", + on_value=True, + ), + VolkswagenIdBinaryEntityDescription( + key="plugConnectionState", + name="Plug Connection State", + local_address="/charging/plugStatus/plugConnectionState", + device_class=BinarySensorDeviceClass.PLUG, + on_value=PlugStatus.PlugConnectionState.CONNECTED, + ), + VolkswagenIdBinaryEntityDescription( + key="plugLockState", + name="Plug Lock State", + local_address="/charging/plugStatus/plugLockState", + device_class=BinarySensorDeviceClass.LOCK, + on_value=PlugStatus.PlugLockState.LOCKED, + ), +) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add sensors for passed config_entry in HA.""" + we_connect: weconnect.WeConnect + we_connect = hass.data[DOMAIN][config_entry.entry_id] + vehicles = hass.data[DOMAIN][config_entry.entry_id + "_vehicles"] + + async def async_update_data(): + await hass.async_add_executor_job(we_connect.update) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="volkswagen_we_connect_id_sensors", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=30), + ) + + entities: list[VolkswagenIDSensor] = [] + for vin, vehicle in vehicles.items(): + for sensor in SENSORS: + entities.append( + VolkswagenIDSensor( + vehicle, + sensor, + we_connect, + coordinator, + ) + ) + if entities: + async_add_entities(entities) + + +class VolkswagenIDSensor(VolkswagenIDBaseEntity, BinarySensorEntity): + """Representation of a VolkswagenID vehicle sensor.""" + + entity_description: VolkswagenIdBinaryEntityDescription + + def __init__( + self, + vehicle: weconnect.Vehicle, + sensor: VolkswagenIdBinaryEntityDescription, + we_connect: weconnect.WeConnect, + coordinator: DataUpdateCoordinator, + ) -> None: + """Initialize VolkswagenID vehicle sensor.""" + super().__init__(vehicle, sensor, we_connect, coordinator) + + self.entity_description = sensor + self._coordinator = coordinator + self._attr_name = f"Volkswagen ID {vehicle.nickname} {sensor.name}" + self._attr_unique_id = f"{vehicle.vin}-{sensor.key}" + self._data = f"/vehicles/{vehicle.vin}{sensor.local_address}" + + @property + def is_on(self) -> bool: + """Return true if sensor is on.""" + + state = self._we_connect.getByAddressString(self._data) + return state == self.entity_description.on_value diff --git a/custom_components/volkswagen_we_connect_id/button.py b/custom_components/volkswagen_we_connect_id/button.py index 18fb513..d5d3b88 100644 --- a/custom_components/volkswagen_we_connect_id/button.py +++ b/custom_components/volkswagen_we_connect_id/button.py @@ -1,5 +1,9 @@ """TOLO Sauna Button controls.""" +from weconnect import weconnect +from weconnect.elements.control_operation import ControlOperation +from weconnect.elements.vehicle import Vehicle + from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -7,9 +11,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VolkswagenIDBaseEntity, set_climatisation -from weconnect import weconnect -from weconnect.elements.control_operation import ControlOperation -from weconnect.elements.vehicle import Vehicle from .const import DOMAIN diff --git a/custom_components/volkswagen_we_connect_id/camera.py b/custom_components/volkswagen_we_connect_id/camera.py new file mode 100644 index 0000000..8ddd616 --- /dev/null +++ b/custom_components/volkswagen_we_connect_id/camera.py @@ -0,0 +1,93 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +from datetime import timedelta +import io +import logging + +from PIL import Image +from weconnect import weconnect + +from homeassistant.components.camera import Camera +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add sensors for passed config_entry in HA.""" + we_connect: weconnect.WeConnect + we_connect = hass.data[DOMAIN][config_entry.entry_id] + vehicles = hass.data[DOMAIN][config_entry.entry_id + "_vehicles"] + + async def async_update_data(): + await hass.async_add_executor_job(we_connect.update) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="volkswagen_we_connect_id_sensors", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(minutes=60), + ) + + cameras: list[VolkswagenIDCamera] = [] + for vin, vehicle in vehicles.items(): + cameras.append( + VolkswagenIDCamera( + vehicle, + we_connect, + coordinator, + ) + ) + + if cameras: + async_add_entities(cameras) + + +class VolkswagenIDCamera(Camera): + # Implement one of these methods. + + def __init__( + self, + vehicle: weconnect.Vehicle, + we_connect: weconnect.WeConnect, + coordinator: DataUpdateCoordinator, + ) -> None: + """Initialize VolkswagenID camera.""" + + Camera.__init__(self) + + self._attr_name = f"Volkswagen ID {vehicle.nickname} Image" + self._attr_unique_id = f"{vehicle.vin}-Image" + + self._coordinator = coordinator + self._we_connect = we_connect + self._data = f"/vehicles/{vehicle.vin}/pictures/car" + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"vw{vehicle.vin}")}, + manufacturer="Volkswagen", + model=f"{vehicle.model}", # format because of the ID.3/ID.4 names. + name=f"Volkswagen {vehicle.nickname}", + ) + + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: + """Return image response.""" + try: + image = self._we_connect.getByAddressString(self._data).value + imgByteArr = io.BytesIO() + image.save(imgByteArr, format=image.format) + imgByteArr = imgByteArr.getvalue() + return imgByteArr + + except FileNotFoundError: + _LOGGER.warning("Could not read camera %s image from file: %s") + return None diff --git a/custom_components/volkswagen_we_connect_id/manifest.json b/custom_components/volkswagen_we_connect_id/manifest.json index e9383d5..869dc10 100644 --- a/custom_components/volkswagen_we_connect_id/manifest.json +++ b/custom_components/volkswagen_we_connect_id/manifest.json @@ -3,12 +3,12 @@ "name": "Volkswagen We Connect ID", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/volkswagen_we_connect_id", - "requirements": ["weconnect==0.32.1", "ascii_magic==1.6"], + "requirements": ["weconnect==0.33.0", "ascii_magic==1.6"], "ssdp": [], "zeroconf": [], "homekit": {}, "dependencies": [], "codeowners": ["@mitch-dc"], "iot_class": "cloud_polling", - "version": "0.1" + "version": "0.3" } diff --git a/custom_components/volkswagen_we_connect_id/sensor.py b/custom_components/volkswagen_we_connect_id/sensor.py index a1e68c7..f812cbd 100644 --- a/custom_components/volkswagen_we_connect_id/sensor.py +++ b/custom_components/volkswagen_we_connect_id/sensor.py @@ -1,12 +1,14 @@ """Platform for sensor integration.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging from typing import cast +from weconnect import weconnect + from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from . import VolkswagenIDBaseEntity from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER, @@ -19,100 +21,122 @@ TEMP_FAHRENHEIT, TIME_MINUTES, ) +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from . import VolkswagenIDBaseEntity from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -CLIMASTATUS_SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription(key="climatisationState", name="Climatisation State"), - SensorEntityDescription( + +@dataclass +class VolkswagenIdEntityDescription(SensorEntityDescription): + """Describes Volkswagen ID sensor entity.""" + + local_address: str | None = None + + +SENSORS: tuple[VolkswagenIdEntityDescription, ...] = ( + VolkswagenIdEntityDescription( + key="climatisationState", + name="Climatisation State", + local_address="/climatisation/climatisationStatus/climatisationState", + ), + VolkswagenIdEntityDescription( key="remainingClimatisationTime_min", name="Remaining Climatisation Time", native_unit_of_measurement=TIME_MINUTES, + local_address="/climatisation/climatisationStatus/remainingClimatisationTime_min", ), -) - -CLIMASETTINGS_SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( + VolkswagenIdEntityDescription( key="targetTemperature_C", name="Target Temperature C", device_class=DEVICE_CLASS_TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, + local_address="/climatisation/climatisationSettings/targetTemperature_C", ), - SensorEntityDescription( + VolkswagenIdEntityDescription( key="targetTemperature_F", name="Target Temperature F", device_class=DEVICE_CLASS_TEMPERATURE, native_unit_of_measurement=TEMP_FAHRENHEIT, + local_address="/climatisation/climatisationSettings/targetTemperature_F", ), -) - - -CHARGESTATUS_SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( + VolkswagenIdEntityDescription( + key="unitInCar", + name="Unit In car", + local_address="/climatisation/climatisationSettings/unitInCar", + ), + VolkswagenIdEntityDescription( key="chargingState", name="Charging State", icon="mdi:ev-station", + local_address="/charging/chargingStatus/chargingState", ), - SensorEntityDescription( + VolkswagenIdEntityDescription( key="remainingChargingTimeToComplete_min", name="Remaining Charging Time", native_unit_of_measurement=TIME_MINUTES, + local_address="/charging/chargingStatus/remainingChargingTimeToComplete_min", + ), + VolkswagenIdEntityDescription( + key="chargeMode", + name="Charging Mode", + icon="mdi:ev-station", + local_address="/charging/chargingStatus/chargeMode", ), - SensorEntityDescription( + VolkswagenIdEntityDescription( key="chargePower_kW", name="Charge Power", native_unit_of_measurement=POWER_KILO_WATT, device_class=DEVICE_CLASS_POWER, + local_address="/charging/chargingStatus/chargePower_kW", ), - SensorEntityDescription( + VolkswagenIdEntityDescription( key="chargeRate_kmph", name="Charge Rate", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, device_class=DEVICE_CLASS_POWER, + local_address="/charging/chargingStatus/chargeRate_kmph", ), -) - -CHARGESETTINGS_SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription(key="maxChargeCurrentAC", name="Charge Current AC"), - SensorEntityDescription( + VolkswagenIdEntityDescription( + key="maxChargeCurrentAC", + name="Charge Current AC", + local_address="/charging/chargingSettings/maxChargeCurrentAC", + ), + VolkswagenIdEntityDescription( key="targetSOC_pct", name="Target State of Charge", device_class=DEVICE_CLASS_BATTERY, native_unit_of_measurement=PERCENTAGE, + local_address="/charging/chargingSettings/targetSOC_pct", ), -) - -BATTERYSTATUS_SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( + VolkswagenIdEntityDescription( key="currentSOC_pct", name="State of Charge", device_class=DEVICE_CLASS_BATTERY, native_unit_of_measurement=PERCENTAGE, + local_address="/charging/batteryStatus/currentSOC_pct", ), - SensorEntityDescription( + VolkswagenIdEntityDescription( name="Range", key="cruisingRangeElectric_km", native_unit_of_measurement=LENGTH_KILOMETERS, + local_address="/charging/batteryStatus/cruisingRangeElectric_km", ), ) async def async_setup_entry(hass, config_entry, async_add_entities): """Add sensors for passed config_entry in HA.""" - weConnect = hass.data[DOMAIN][config_entry.entry_id] + we_connect: weconnect.WeConnect + we_connect = hass.data[DOMAIN][config_entry.entry_id] vehicles = hass.data[DOMAIN][config_entry.entry_id + "_vehicles"] - # await hass.async_add_executor_job(weConnect.update) async def async_update_data(): - """Fetch data from API endpoint. - This is the place to pre-process the data to lookup tables - so entities can quickly look up their data. - """ - await hass.async_add_executor_job(weConnect.update) + await hass.async_add_executor_job(we_connect.update) coordinator = DataUpdateCoordinator( hass, @@ -125,54 +149,13 @@ async def async_update_data(): ) entities: list[VolkswagenIDSensor] = [] - for vin, vehicle in vehicles.items(): # weConnect.vehicles.items(): - for sensor in CLIMASTATUS_SENSORS: - entities.append( - VolkswagenIDSensor( - vehicle, - sensor, - vehicle.domains["climatisation"]["climatisationStatus"], - weConnect, - coordinator, - ) - ) - for sensor in CLIMASETTINGS_SENSORS: + for vin, vehicle in vehicles.items(): + for sensor in SENSORS: entities.append( VolkswagenIDSensor( vehicle, sensor, - vehicle.domains["climatisation"]["climatisationSettings"], - weConnect, - coordinator, - ) - ) - for sensor in CHARGESTATUS_SENSORS: - entities.append( - VolkswagenIDSensor( - vehicle, - sensor, - vehicle.domains["charging"]["chargingStatus"], - weConnect, - coordinator, - ) - ) - for sensor in CHARGESETTINGS_SENSORS: - entities.append( - VolkswagenIDSensor( - vehicle, - sensor, - vehicle.domains["charging"]["chargingSettings"], - weConnect, - coordinator, - ) - ) - for sensor in BATTERYSTATUS_SENSORS: - entities.append( - VolkswagenIDSensor( - vehicle, - sensor, - vehicle.domains["charging"]["batteryStatus"], - weConnect, + we_connect, coordinator, ) ) @@ -183,23 +166,31 @@ async def async_update_data(): class VolkswagenIDSensor(VolkswagenIDBaseEntity, SensorEntity): """Representation of a VolkswagenID vehicle sensor.""" - entity_description: SensorEntityDescription + entity_description: VolkswagenIdEntityDescription - def __init__(self, vehicle, description, data, we_connect, coordinator) -> None: + def __init__( + self, + vehicle: weconnect.Vehicle, + sensor: VolkswagenIdEntityDescription, + we_connect: weconnect.WeConnect, + coordinator: DataUpdateCoordinator, + ) -> None: """Initialize VolkswagenID vehicle sensor.""" - super().__init__(vehicle, data, we_connect, coordinator) - self.entity_description = description + super().__init__(vehicle, sensor, we_connect, coordinator) + + self.entity_description = sensor self._coordinator = coordinator - self._attr_name = f"Volkswagen ID {vehicle.nickname} {description.name}" - self._attr_unique_id = f"{vehicle.vin}-{description.key}" - self._attr_native_unit_of_measurement = description.native_unit_of_measurement + self._attr_name = f"Volkswagen ID {vehicle.nickname} {sensor.name}" + self._attr_unique_id = f"{vehicle.vin}-{sensor.key}" + self._attr_native_unit_of_measurement = sensor.native_unit_of_measurement + self._data = f"/vehicles/{vehicle.vin}{sensor.local_address}" @property def native_value(self) -> StateType: """Return the state.""" - state = getattr(self._data, self.entity_description.key).value + state = self._we_connect.getByAddressString(self._data) - if hasattr(state, "value"): + while hasattr(state, "value"): state = state.value return cast(StateType, state)