diff --git a/custom_components/luxtronik/__init__.py b/custom_components/luxtronik/__init__.py index ae2208b..6ae3ef3 100644 --- a/custom_components/luxtronik/__init__.py +++ b/custom_components/luxtronik/__init__.py @@ -1,10 +1,12 @@ """Support for Luxtronik heatpump controllers.""" # region Imports +from dataclasses import dataclass + from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.typing import ConfigType from luxtronik import LOGGER as LuxLogger @@ -34,6 +36,12 @@ LuxLogger.setLevel(level="WARNING") # endregion Constants +@dataclass +class LuxtronikEntityDescription(EntityDescription): + """Class describing Luxtronik entities.""" + + luxtronik_key: str = "" + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up from config entry.""" diff --git a/custom_components/luxtronik/const.py b/custom_components/luxtronik/const.py index 40d1cd0..42803d5 100644 --- a/custom_components/luxtronik/const.py +++ b/custom_components/luxtronik/const.py @@ -8,31 +8,19 @@ import homeassistant.helpers.config_validation as cv import voluptuous as vol +from homeassistant.components.sensor import (STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntityDescription) +from homeassistant.const import (CONF_HOST, CONF_PORT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, + PRESSURE_BAR, TEMP_CELSIUS, TEMP_KELVIN, + TIME_HOURS, TIME_SECONDS, Platform) from homeassistant.helpers.entity import EntityCategory -from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, - SensorEntityDescription, -) -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - DEVICE_CLASS_FREQUENCY, - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_TIMESTAMP, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - PERCENTAGE, - POWER_WATT, - PRESSURE_BAR, - TEMP_CELSIUS, - TEMP_KELVIN, - TIME_HOURS, - TIME_SECONDS, -) # endregion Imports @@ -41,7 +29,14 @@ LOGGER: Final[logging.Logger] = logging.getLogger(__package__) -PLATFORMS: list[str] = ["sensor", "binary_sensor", "climate", "number", "switch"] +PLATFORMS: list[str] = [ + Platform.SENSOR, + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.NUMBER, + Platform.SWITCH, + Platform.UPDATE, +] # endregion Constants Main # region Conf @@ -255,6 +250,10 @@ class LuxMode(Enum): 'parameters.ID_Einst_MK2Typ_akt', 'parameters.ID_Einst_MK3Typ_akt'] +DOWNLOAD_PORTAL_URL: Final = ( + "https://www.heatpump24.com/software/fetchSoftware.php?softwareID=" +) + class LuxMkTypes(Enum): off: Final = 0 discharge: Final = 1 diff --git a/custom_components/luxtronik/diagnostics.py b/custom_components/luxtronik/diagnostics.py index 2554fd2..49d855a 100644 --- a/custom_components/luxtronik/diagnostics.py +++ b/custom_components/luxtronik/diagnostics.py @@ -14,8 +14,6 @@ from homeassistant.helpers import device_registry from luxtronik import Luxtronik as Lux -from .const import CONF_COORDINATOR, DOMAIN - TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} diff --git a/custom_components/luxtronik/sensor.py b/custom_components/luxtronik/sensor.py index 6c43f78..5d860d8 100644 --- a/custom_components/luxtronik/sensor.py +++ b/custom_components/luxtronik/sensor.py @@ -629,18 +629,6 @@ async def async_setup_entry( ), ] - if luxtronik.get_value("visibilities.ID_Visi_Temp_Solarkoll") > 0: - entities += [ - LuxtronikSensor( - luxtronik, - device_info_domestic_water, - "calculations.ID_WEB_Temperatur_TSK", - "solar_collector_temperature", - f"Solar {text_collector}", - "mdi:solar-panel-large", - entity_category=None, - ), - ] # Temp. disabled: # solar_present = luxtronik.detect_solar_present() # if solar_present: diff --git a/custom_components/luxtronik/update.py b/custom_components/luxtronik/update.py new file mode 100644 index 0000000..f0f00b6 --- /dev/null +++ b/custom_components/luxtronik/update.py @@ -0,0 +1,187 @@ +"""Luxtronik Update platform.""" +from __future__ import annotations + +import re +import threading +from dataclasses import dataclass +from datetime import datetime, timedelta +from typing import Final + +import requests +from homeassistant.components.update import (ENTITY_ID_FORMAT, UpdateEntity, + UpdateEntityDescription) +from homeassistant.components.update.const import UpdateEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import Throttle + +from . import LuxtronikDevice, LuxtronikEntityDescription +from .const import (DOMAIN, DOWNLOAD_PORTAL_URL, LOGGER, + LUX_MODELS_AlphaInnotec, LUX_MODELS_Novelan, + LUX_MODELS_Other) + +MIN_TIME_BETWEEN_UPDATES: Final = timedelta(hours=1) + + +@dataclass +class LuxtronikUpdateEntityDescription( + LuxtronikEntityDescription, UpdateEntityDescription +): + """Class describing Luxtronik update entities.""" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Luxtronik update platform.""" + + LOGGER.debug("Setting up Luxtronik update entity") + luxtronik_device: LuxtronikDevice = hass.data.get(DOMAIN) + luxtronik_device.read() + data: dict = config_entry.data + + description = LuxtronikUpdateEntityDescription( + luxtronik_key="calculations.ID_WEB_SoftStand", + key="firmware", + entity_category=EntityCategory.CONFIG, + ) + update_entity = LuxtronikUpdateEntity( + entry=config_entry, luxtronik_device=luxtronik_device, description=description, device_info=hass.data[f"{DOMAIN}_DeviceInfo"] + ) + entities = [update_entity] + + async_add_entities(entities) + + +class LuxtronikUpdateEntity(UpdateEntity): + """Representation of Luxtronik.""" + + _attr_title = "Luxtronik Firmware Version" + _attr_supported_features: UpdateEntityFeature = UpdateEntityFeature.RELEASE_NOTES + __firmware_version_available = None + __firmware_version_available_last_request = None + + def __init__( + self, + entry: ConfigEntry, + luxtronik_device: LuxtronikDevice, + description: LuxtronikUpdateEntityDescription, + device_info + ) -> None: + """Initialize the Luxtronik.""" + super().__init__() + self.entity_description = description + self.luxtronik_device = luxtronik_device + # self.coordinator = coordinator + self._attr_device_info = device_info + # self._attr_unique_id = f"tuya.{device.id}" + self.luxtronik_key = description.luxtronik_key + + self._attr_name = "Luxtronik Firmware" + luxtronik_device.read() + # self._attr_state = luxtronik_device.get_value(description.luxtronik_key) + prefix = DOMAIN + self.entity_id = ENTITY_ID_FORMAT.format( + f"{prefix}_{description.key}" + ) + self._attr_unique_id = self.entity_id + self._request_available_firmware_version() + + @property + def installed_version(self) -> str: + """Return the current app version.""" + # return self._attr_state + return self.luxtronik_device.get_value(self.entity_description.luxtronik_key) + + @property + def latest_version(self) -> str: + """Return if there is an update.""" + if self.__firmware_version_available is None: + self._request_available_firmware_version() + return None + return self.__firmware_version_available[:len(self.installed_version)] + + def release_notes(self) -> str | None: + release_url = self._get_manufacturer_firmware_url_by_model( + self.luxtronik_device.get_value("calculations.ID_WEB_Code_WP_akt") + ) + download_id = self._get_firmware_download_id(self.installed_version) + download_url = f"{DOWNLOAD_PORTAL_URL}{download_id}" + return f'Firmware Download PortalDirect Download

alpha innotec doesn\'t provide a changelog.
Please contact support for more information.' + + def _get_firmware_download_id(self, installed_version: str) -> int | None: + """Return the heatpump firmware id for the download portal.""" + if installed_version.startswith("V1."): + return 0 + elif installed_version.startswith("V2."): + return 1 + elif installed_version.startswith("V3."): + return 2 + elif installed_version.startswith("V4."): + return 3 + elif installed_version.startswith("F1."): + return 4 + elif installed_version.startswith("WWB1."): + return 5 + elif installed_version.startswith("smo"): + return 6 + return None + + # @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self) -> None: + """Update sensor values.""" + # self._attr_state = self.luxtronik_device.get_value(self.entity_description.luxtronik_key) + if ( + self.__firmware_version_available_last_request is None + or self.__firmware_version_available_last_request + < datetime.utcnow().timestamp() - 3600 + ): + self._request_available_firmware_version() + + def _request_available_firmware_version(self) -> None: + def do_request_available_firmware_version(self, download_id: int): + if download_id is None: + self.__firmware_version_available = STATE_UNAVAILABLE + return + try: + response = requests.get( + f"{DOWNLOAD_PORTAL_URL}{download_id}", timeout=30 + ) + header_content_disposition = response.headers["content-disposition"] + filename = re.findall("filename=(.+)", header_content_disposition)[0] + self.__firmware_version_available_last_request = ( + datetime.utcnow().timestamp() + ) + # Filename e.g.: wp2reg-V2.88.1-9086 + # Extract 'V2.88.1' from 'wp2reg-V2.88.1-9086' + self.__firmware_version_available = filename.split("-", 1)[1] + except Exception as err: # pylint: disable=broad-except + LOGGER.warning( + "Could not request download portal firmware version.", + exc_info=True, + ) + self.__firmware_version_available = STATE_UNAVAILABLE + + download_id = self._get_firmware_download_id(self.installed_version) + threading.Thread( + target=do_request_available_firmware_version, args=(self, download_id) + ).start() + + def _get_manufacturer_firmware_url_by_model(self, model: str) -> str: + """Return the manufacturer firmware download url.""" + layout_id = 0 + + if model is None: + layout_id = 0 + elif model.startswith(tuple(LUX_MODELS_AlphaInnotec)): + layout_id = 1 + elif model.startswith(tuple(LUX_MODELS_Novelan)): + layout_id = 2 + elif model.startswith(tuple(LUX_MODELS_Other)): + layout_id = 3 + return f"https://www.heatpump24.com/DownloadArea.php?layout={layout_id}"