From 815d1f98be4dcdff191c6d4ba9c660302d1a6f17 Mon Sep 17 00:00:00 2001 From: ucpy7374 Date: Mon, 24 Apr 2023 18:13:46 +0200 Subject: [PATCH 01/74] first code as-is --- .devcontainer.json | 2 +- custom_components/deltadore-tydom/__init__.py | 45 ++ .../deltadore-tydom/config_flow.py | 199 +++++++++ custom_components/deltadore-tydom/const.py | 11 + custom_components/deltadore-tydom/cover.py | 162 +++++++ custom_components/deltadore-tydom/hub.py | 157 +++++++ .../deltadore-tydom/manifest.json | 18 + custom_components/deltadore-tydom/sensor.py | 132 ++++++ .../deltadore-tydom/strings.json | 23 + .../deltadore-tydom/translations/en.json | 23 + .../deltadore-tydom/tydom/MessageHandler.py | 23 + .../deltadore-tydom/tydom/TydomClient.py | 399 ++++++++++++++++++ requirements.txt | 2 +- 13 files changed, 1194 insertions(+), 2 deletions(-) create mode 100644 custom_components/deltadore-tydom/__init__.py create mode 100644 custom_components/deltadore-tydom/config_flow.py create mode 100644 custom_components/deltadore-tydom/const.py create mode 100644 custom_components/deltadore-tydom/cover.py create mode 100644 custom_components/deltadore-tydom/hub.py create mode 100644 custom_components/deltadore-tydom/manifest.json create mode 100644 custom_components/deltadore-tydom/sensor.py create mode 100644 custom_components/deltadore-tydom/strings.json create mode 100644 custom_components/deltadore-tydom/translations/en.json create mode 100644 custom_components/deltadore-tydom/tydom/MessageHandler.py create mode 100644 custom_components/deltadore-tydom/tydom/TydomClient.py diff --git a/.devcontainer.json b/.devcontainer.json index 1c53f02..60d9aed 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -37,6 +37,6 @@ }, "remoteUser": "vscode", "features": { - "rust": "latest" + "ghcr.io/devcontainers/features/rust:latest": {} } } \ No newline at end of file diff --git a/custom_components/deltadore-tydom/__init__.py b/custom_components/deltadore-tydom/__init__.py new file mode 100644 index 0000000..066c336 --- /dev/null +++ b/custom_components/deltadore-tydom/__init__.py @@ -0,0 +1,45 @@ +"""The Detailed Hello World Push integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import hub +from .const import DOMAIN + +# List of platforms to support. There should be a matching .py file for each, +# eg and +PLATFORMS: list[str] = ["cover", "sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Delta Dore Tydom from a config entry.""" + # Store an instance of the "connecting" class that does the work of speaking + # with your actual devices. + pin = None + if "alarmpin" in entry.data: + pin = entry.data["alarmpin"] + + tydomHub = hub.Hub( + hass, entry.data["host"], entry.data["macaddress"], entry.data["password"], pin + ) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = tydomHub + + await tydomHub.setup() + + # This creates each HA object for each platform your device requires. + # It's done by calling the `async_setup_entry` function in each platform module. + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + # This is called when an entry/configured device is to be removed. The class + # needs to unload itself, and remove callbacks. See the classes for further + # details + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py new file mode 100644 index 0000000..eb6933d --- /dev/null +++ b/custom_components/deltadore-tydom/config_flow.py @@ -0,0 +1,199 @@ +"""Config flow for Tydom integration.""" +from __future__ import annotations +import traceback +import logging +from typing import Any + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PIN +from homeassistant.core import HomeAssistant +from homeassistant.components import dhcp + +from .const import DOMAIN # pylint:disable=unused-import +from .hub import Hub + +_LOGGER = logging.getLogger(__name__) + +# This is the schema that used to display the UI to the user. This simple +# schema has a single required host field, but it could include a number of fields +# such as username, password etc. See other components in the HA core code for +# further examples. +# Note the input displayed to the user will be translated. See the +# translations/.json file and strings.json. See here for further information: +# https://developers.home-assistant.io/docs/config_entries_config_flow_handler/#translations +# At the time of writing I found the translations created by the scaffold didn't +# quite work as documented and always gave me the "Lokalise key references" string +# (in square brackets), rather than the actual translated value. I did not attempt to +# figure this out or look further into it. + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_MAC): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_PIN): str, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: + """Validate the user input allows us to connect. + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + # Validate the data can be used to set up a connection. + + # This is a simple example to show an error in the UI for a short hostname + # The exceptions are defined at the end of this file, and are used in the + # `async_step_user` method below. + if CONF_HOST not in data: + raise InvalidHost + + if len(data[CONF_HOST]) < 3: + raise InvalidHost + + if len(data[CONF_MAC]) < 3: + raise InvalidMacAddress + + if len(data[CONF_PASSWORD]) < 3: + raise InvalidPassword + + pin = None + if CONF_PIN in data: + pin = data[CONF_PIN] + + hub = Hub(hass, data[CONF_HOST], data[CONF_MAC], data[CONF_PASSWORD], pin) + # The dummy hub provides a `test_connection` method to ensure it's working + # as expected + result = hub.test_connection() + if not result: + # If there is an error, raise an exception to notify HA that there was a + # problem. The UI will also show there was a problem + raise CannotConnect + + # If your PyPI package is not built with async, pass your methods + # to the executor: + # await hass.async_add_executor_job( + # your_validate_func, data["username"], data["password"] + # ) + + # If you cannot connect: + # throw CannotConnect + # If the authentication is wrong: + # InvalidAuth + + # Return info that you want to store in the config entry. + # "Title" is what is displayed to the user for this hub device + # It is stored internally in HA as part of the device config. + # See `async_step_user` below for how this is used + return {"title": data[CONF_MAC]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Hello World.""" + + VERSION = 1 + + # Pick one of the available connection classes in homeassistant/config_entries.py + # This tells HA if it should be asking for updates, or it'll be notified of updates + # automatically. This example uses PUSH, as the dummy hub will notify HA of + # changes. + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + self._discovered_host = None + self._discovered_mac = None + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + # This goes through the steps to take the user through the setup process. + # Using this it is possible to update the UI and prompt for additional + # information. This example provides a single form (built from `DATA_SCHEMA`), + # and when that has some validated input, it calls `async_create_entry` to + # actually create the HA config entry. Note the "title" value is returned by + # `validate_input` above. + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + await self.async_set_unique_id(user_input[CONF_MAC]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info["title"], data=user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidHost: + # The error string is set here, and should be translated. + # This example does not currently cover translations, see the + # comments on `DATA_SCHEMA` for further details. + # Set the error on the `host` field, not the entire form. + errors[CONF_HOST] = "cannot_connect" + except InvalidMacAddress: + errors[CONF_MAC] = "cannot_connect" + except InvalidPassword: + errors[CONF_PASSWORD] = "cannot_connect" + except Exception: # pylint: disable=broad-except + traceback.print_exc() + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + user_input = user_input or {} + # If there is no user input or there were errors, show the form again, including any errors that were found with the input. + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, + vol.Required(CONF_MAC, default=user_input.get(CONF_MAC, "")): str, + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, + vol.Optional(CONF_PIN, default=user_input.get(CONF_PIN, "")): str, + } + ), + errors=errors, + ) + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo): + """Handle the discovery from dhcp.""" + self._discovered_host = discovery_info.ip + self._discovered_mac = discovery_info.macaddress + return await self._async_handle_discovery() + + async def _async_handle_discovery(self): + self.context[CONF_HOST] = self._discovered_host + self.context[CONF_MAC] = self._discovered_mac + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm(self, user_input=None): + """Confirm discovery.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, + vol.Required(CONF_MAC, default=user_input.get(CONF_MAC, "")): str, + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, + vol.Optional(CONF_PIN, default=user_input.get(CONF_PIN, "")): str, + } + ), + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidHost(exceptions.HomeAssistantError): + """Error to indicate there is an invalid hostname.""" + + +class InvalidMacAddress(exceptions.HomeAssistantError): + """Error to indicate there is an invalid Mac address.""" + + +class InvalidPassword(exceptions.HomeAssistantError): + """Error to indicate there is an invalid Password.""" diff --git a/custom_components/deltadore-tydom/const.py b/custom_components/deltadore-tydom/const.py new file mode 100644 index 0000000..b202cad --- /dev/null +++ b/custom_components/deltadore-tydom/const.py @@ -0,0 +1,11 @@ +"""Constants for deltadore-tydom integration.""" +from logging import Logger, getLogger + +LOGGER: Logger = getLogger(__package__) + +# This is the internal name of the integration, it should also match the directory +# name for the integration. +DOMAIN = "tydom" +NAME = "Integration blueprint" +VERSION = "0.0.1" +ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/" diff --git a/custom_components/deltadore-tydom/cover.py b/custom_components/deltadore-tydom/cover.py new file mode 100644 index 0000000..4ae421e --- /dev/null +++ b/custom_components/deltadore-tydom/cover.py @@ -0,0 +1,162 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +from typing import Any + +# These constants are relevant to the type of entity we are using. +# See below for how they are used. +from homeassistant.components.cover import ( + ATTR_POSITION, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + + +# This function is called as part of the __init__.async_setup_entry (via the +# hass.config_entries.async_forward_entry_setup call) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add cover for passed config_entry in HA.""" + # The hub is loaded from the associated hass.data entry that was created in the + # __init__.async_setup_entry function + hub = hass.data[DOMAIN][config_entry.entry_id] + + # Add all entities to HA + async_add_entities(HelloWorldCover(roller) for roller in hub.rollers) + + +# This entire class could be written to extend a base class to ensure common attributes +# are kept identical/in sync. It's broken apart here between the Cover and Sensors to +# be explicit about what is returned, and the comments outline where the overlap is. +class HelloWorldCover(CoverEntity): + """Representation of a dummy Cover.""" + + # Our dummy class is PUSH, so we tell HA that it should not be polled + should_poll = False + # The supported features of a cover are done using a bitmask. Using the constants + # imported above, we can tell HA the features that are supported by this entity. + # If the supported features were dynamic (ie: different depending on the external + # device it connected to), then this should be function with an @property decorator. + supported_features = SUPPORT_SET_POSITION | SUPPORT_OPEN | SUPPORT_CLOSE + + def __init__(self, roller) -> None: + """Initialize the sensor.""" + # Usual setup is done here. Callbacks are added in async_added_to_hass. + self._roller = roller + + # A unique_id for this entity with in this domain. This means for example if you + # have a sensor on this cover, you must ensure the value returned is unique, + # which is done here by appending "_cover". For more information, see: + # https://developers.home-assistant.io/docs/entity_registry_index/#unique-id-requirements + # Note: This is NOT used to generate the user visible Entity ID used in automations. + self._attr_unique_id = f"{self._roller.roller_id}_cover" + + # This is the name for this *entity*, the "name" attribute from "device_info" + # is used as the device name for device screens in the UI. This name is used on + # entity screens, and used to build the Entity ID that's used is automations etc. + self._attr_name = self._roller.name + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + # Importantly for a push integration, the module that will be getting updates + # needs to notify HA of changes. The dummy device has a registercallback + # method, so to this we add the 'self.async_write_ha_state' method, to be + # called where ever there are changes. + # The call back registration is done once this entity is registered with HA + # (rather than in the __init__) + self._roller.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._roller.remove_callback(self.async_write_ha_state) + + # Information about the devices that is partially visible in the UI. + # The most critical thing here is to give this entity a name so it is displayed + # as a "device" in the HA UI. This name is used on the Devices overview table, + # and the initial screen when the device is added (rather than the entity name + # property below). You can then associate other Entities (eg: a battery + # sensor) with this device, so it shows more like a unified element in the UI. + # For example, an associated battery sensor will be displayed in the right most + # column in the Configuration > Devices view for a device. + # To associate an entity with this device, the device_info must also return an + # identical "identifiers" attribute, but not return a name attribute. + # See the sensors.py file for the corresponding example setup. + # Additional meta data can also be returned here, including sw_version (displayed + # as Firmware), model and manufacturer (displayed as by ) + # shown on the device info screen. The Manufacturer and model also have their + # respective columns on the Devices overview table. Note: Many of these must be + # set when the device is first added, and they are not always automatically + # refreshed by HA from it's internal cache. + # For more information see: + # https://developers.home-assistant.io/docs/device_registry_index/#device-properties + @property + def device_info(self) -> DeviceInfo: + """Information about this entity/device.""" + return { + "identifiers": {(DOMAIN, self._roller.roller_id)}, + # If desired, the name for the device could be different to the entity + "name": self.name, + "sw_version": self._roller.firmware_version, + "model": self._roller.model, + "manufacturer": self._roller.hub.manufacturer, + } + + # This property is important to let HA know if this entity is online or not. + # If an entity is offline (return False), the UI will refelect this. + @property + def available(self) -> bool: + """Return True if roller and hub is available.""" + return self._roller.online and self._roller.hub.online + + # The following properties are how HA knows the current state of the device. + # These must return a value from memory, not make a live query to the device/hub + # etc when called (hence they are properties). For a push based integration, + # HA is notified of changes via the async_write_ha_state call. See the __init__ + # method for hos this is implemented in this example. + # The properties that are expected for a cover are based on the supported_features + # property of the object. In the case of a cover, see the following for more + # details: https://developers.home-assistant.io/docs/core/entity/cover/ + @property + def current_cover_position(self): + """Return the current position of the cover.""" + return self._roller.position + + @property + def is_closed(self) -> bool: + """Return if the cover is closed, same as position 0.""" + return self._roller.position == 0 + + @property + def is_closing(self) -> bool: + """Return if the cover is closing or not.""" + return self._roller.moving < 0 + + @property + def is_opening(self) -> bool: + """Return if the cover is opening or not.""" + return self._roller.moving > 0 + + # These methods allow HA to tell the actual device what to do. In this case, move + # the cover to the desired position, or open and close it all the way. + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self._roller.set_position(100) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + await self._roller.set_position(0) + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Close the cover.""" + await self._roller.set_position(kwargs[ATTR_POSITION]) diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py new file mode 100644 index 0000000..11dcf4c --- /dev/null +++ b/custom_components/deltadore-tydom/hub.py @@ -0,0 +1,157 @@ +"""A demonstration 'hub' that connects several devices.""" +from __future__ import annotations + +# In a real implementation, this would be in an external library that's on PyPI. +# The PyPI package needs to be included in the `requirements` section of manifest.json +# See https://developers.home-assistant.io/docs/creating_integration_manifest +# for more information. +# This dummy hub always returns 3 rollers. +import asyncio +import random +import logging + +from homeassistant.core import HomeAssistant +from .tydom.TydomClient import TydomClient + +logger = logging.getLogger(__name__) + + +class Hub: + """Hub for Delta Dore Tydom.""" + + manufacturer = "Delta Dore" + + def __init__( + self, + hass: HomeAssistant, + host: str, + mac: str, + password: str, + alarmpin: str, + ) -> None: + """Init dummy hub.""" + self._host = host + self._mac = mac + self._pass = password + self._pin = alarmpin + self._hass = hass + self._name = mac + self._id = mac.lower() + + self._tydom_client = TydomClient( + mac=self._mac, + host=self._host, + password=self._pass, + alarm_pin=self._pin, + ) + + self.rollers = [ + Roller(f"{self._id}_1", f"{self._name} 1", self), + Roller(f"{self._id}_2", f"{self._name} 2", self), + Roller(f"{self._id}_3", f"{self._name} 3", self), + ] + self.online = True + + @property + def hub_id(self) -> str: + """ID for dummy hub.""" + return self._id + + async def test_connection(self) -> bool: + """Test connectivity to the Tydom is OK.""" + try: + return await self._tydom_client.connect() is not None + except: + return False + + async def setup(self) -> None: + # Listen to tydom events. + await self._hass.async_add_executor_job(self._tydom_client.listen_tydom()) + + +class Roller: + """Dummy roller (device for HA) for Hello World example.""" + + def __init__(self, rollerid: str, name: str, hub: Hub) -> None: + """Init dummy roller.""" + self._id = rollerid + self.hub = hub + self.name = name + self._callbacks = set() + self._loop = asyncio.get_event_loop() + self._target_position = 100 + self._current_position = 100 + # Reports if the roller is moving up or down. + # >0 is up, <0 is down. This very much just for demonstration. + self.moving = 0 + + # Some static information about this device + self.firmware_version = f"0.0.{random.randint(1, 9)}" + self.model = "Test Device" + + @property + def roller_id(self) -> str: + """Return ID for roller.""" + return self._id + + @property + def position(self): + """Return position for roller.""" + return self._current_position + + async def set_position(self, position: int) -> None: + """ + Set dummy cover to the given position. + State is announced a random number of seconds later. + """ + self._target_position = position + + # Update the moving status, and broadcast the update + self.moving = position - 50 + await self.publish_updates() + + self._loop.create_task(self.delayed_update()) + + async def delayed_update(self) -> None: + """Publish updates, with a random delay to emulate interaction with device.""" + await asyncio.sleep(random.randint(1, 10)) + self.moving = 0 + await self.publish_updates() + + def register_callback(self, callback: Callable[[], None]) -> None: + """Register callback, called when Roller changes state.""" + self._callbacks.add(callback) + + def remove_callback(self, callback: Callable[[], None]) -> None: + """Remove previously registered callback.""" + self._callbacks.discard(callback) + + # In a real implementation, this library would call it's call backs when it was + # notified of any state changeds for the relevant device. + async def publish_updates(self) -> None: + """Schedule call all registered callbacks.""" + self._current_position = self._target_position + for callback in self._callbacks: + callback() + + @property + def online(self) -> float: + """Roller is online.""" + # The dummy roller is offline about 10% of the time. Returns True if online, + # False if offline. + return random.random() > 0.1 + + @property + def battery_level(self) -> int: + """Battery level as a percentage.""" + return random.randint(0, 100) + + @property + def battery_voltage(self) -> float: + """Return a random voltage roughly that of a 12v battery.""" + return round(random.random() * 3 + 10, 2) + + @property + def illuminance(self) -> int: + """Return a sample illuminance in lux.""" + return random.randint(0, 500) diff --git a/custom_components/deltadore-tydom/manifest.json b/custom_components/deltadore-tydom/manifest.json new file mode 100644 index 0000000..78da7a8 --- /dev/null +++ b/custom_components/deltadore-tydom/manifest.json @@ -0,0 +1,18 @@ +{ + "domain": "tydom", + "name": "Delta Dore TYDOM", + "codeowners": ["@CyrilP"], + "config_flow": true, + "dependencies": [], + "documentation": "https://www.home-assistant.io/integrations/tydom", + "iot_class": "local_push", + "requirements": ["websockets>=9.1"], + "dhcp": [ + { + "hostname": "TYDOM-*", + "macaddress": "001A25*" + } + ], + "loggers": ["tydom"], + "version": "0.0.1" +} diff --git a/custom_components/deltadore-tydom/sensor.py b/custom_components/deltadore-tydom/sensor.py new file mode 100644 index 0000000..04ad633 --- /dev/null +++ b/custom_components/deltadore-tydom/sensor.py @@ -0,0 +1,132 @@ +"""Platform for sensor integration.""" +# This file shows the setup for the sensors associated with the cover. +# They are setup in the same way with the call to the async_setup_entry function +# via HA from the module __init__. Each sensor has a device_class, this tells HA how +# to display it in the UI (for know types). The unit_of_measurement property tells HA +# what the unit is, so it can display the correct range. For predefined types (such as +# battery), the unit_of_measurement should match what's expected. +import random + +from homeassistant.const import ( + ATTR_VOLTAGE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ILLUMINANCE, + PERCENTAGE, +) +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN + + +# See cover.py for more details. +# Note how both entities for each roller sensor (battry and illuminance) are added at +# the same time to the same list. This way only a single async_add_devices call is +# required. +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add sensors for passed config_entry in HA.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + new_devices = [] + for roller in hub.rollers: + new_devices.append(BatterySensor(roller)) + new_devices.append(IlluminanceSensor(roller)) + if new_devices: + async_add_entities(new_devices) + + +# This base class shows the common properties and methods for a sensor as used in this +# example. See each sensor for further details about properties and methods that +# have been overridden. +class SensorBase(Entity): + """Base representation of a Hello World Sensor.""" + + should_poll = False + + def __init__(self, roller): + """Initialize the sensor.""" + self._roller = roller + + # To link this entity to the cover device, this property must return an + # identifiers value matching that used in the cover, but no other information such + # as name. If name is returned, this entity will then also become a device in the + # HA UI. + @property + def device_info(self): + """Return information to link this entity with the correct device.""" + return {"identifiers": {(DOMAIN, self._roller.roller_id)}} + + # This property is important to let HA know if this entity is online or not. + # If an entity is offline (return False), the UI will refelect this. + @property + def available(self) -> bool: + """Return True if roller and hub is available.""" + return self._roller.online and self._roller.hub.online + + async def async_added_to_hass(self): + """Run when this Entity has been added to HA.""" + # Sensors should also register callbacks to HA when their state changes + self._roller.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._roller.remove_callback(self.async_write_ha_state) + + +class BatterySensor(SensorBase): + """Representation of a Sensor.""" + + # The class of this device. Note the value should come from the homeassistant.const + # module. More information on the available devices classes can be seen here: + # https://developers.home-assistant.io/docs/core/entity/sensor + device_class = DEVICE_CLASS_BATTERY + + # The unit of measurement for this entity. As it's a DEVICE_CLASS_BATTERY, this + # should be PERCENTAGE. A number of units are supported by HA, for some + # examples, see: + # https://developers.home-assistant.io/docs/core/entity/sensor#available-device-classes + _attr_unit_of_measurement = PERCENTAGE + + def __init__(self, roller): + """Initialize the sensor.""" + super().__init__(roller) + + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._roller.roller_id}_battery" + + # The name of the entity + self._attr_name = f"{self._roller.name} Battery" + + self._state = random.randint(0, 100) + + # The value of this sensor. As this is a DEVICE_CLASS_BATTERY, this value must be + # the battery level as a percentage (between 0 and 100) + @property + def state(self): + """Return the state of the sensor.""" + return self._roller.battery_level + + +# This is another sensor, but more simple compared to the battery above. See the +# comments above for how each field works. +class IlluminanceSensor(SensorBase): + """Representation of a Sensor.""" + + device_class = DEVICE_CLASS_ILLUMINANCE + _attr_unit_of_measurement = "lx" + + def __init__(self, roller): + """Initialize the sensor.""" + super().__init__(roller) + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._roller.roller_id}_illuminance" + + # The name of the entity + self._attr_name = f"{self._roller.name} Illuminance" + + @property + def state(self): + """Return the state of the sensor.""" + return self._roller.illuminance diff --git a/custom_components/deltadore-tydom/strings.json b/custom_components/deltadore-tydom/strings.json new file mode 100644 index 0000000..a5aaa55 --- /dev/null +++ b/custom_components/deltadore-tydom/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "title": "Delta Dore Tydom", + "data": { + "host": "[%key:component::tydom::options::step::user::data::host%]", + "mac": "mac", + "password": "[%key:common::config_flow::data::password%]", + "pin": "[%key:common::config_flow::data::pin%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/custom_components/deltadore-tydom/translations/en.json b/custom_components/deltadore-tydom/translations/en.json new file mode 100644 index 0000000..cb4d21d --- /dev/null +++ b/custom_components/deltadore-tydom/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "title": "Delta Dore Tydom", + "data": { + "host": "Host", + "password": "Password", + "mac": "Mac Address", + "pin": "Alarm PIN" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py new file mode 100644 index 0000000..7eba946 --- /dev/null +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -0,0 +1,23 @@ +import json +import logging +from http.client import HTTPResponse +from http.server import BaseHTTPRequestHandler +from io import BytesIO + +import urllib3 + +logger = logging.getLogger(__name__) + + +class MessageHandler: + def __init__(self, incoming_bytes, tydom_client): + self.incoming_bytes = incoming_bytes + self.tydom_client = tydom_client + self.cmd_prefix = tydom_client.cmd_prefix + + async def incoming_triage(self): + bytes_str = self.incoming_bytes + incoming = None + first = str(bytes_str[:40]) + + logger.debug("Incoming data parsed with success") diff --git a/custom_components/deltadore-tydom/tydom/TydomClient.py b/custom_components/deltadore-tydom/tydom/TydomClient.py new file mode 100644 index 0000000..e53b1d5 --- /dev/null +++ b/custom_components/deltadore-tydom/tydom/TydomClient.py @@ -0,0 +1,399 @@ +import asyncio +import base64 +import http.client +import logging +import os +import ssl +import signal +import sys +import websockets +import socket + +from requests.auth import HTTPDigestAuth +from .MessageHandler import MessageHandler + +logger = logging.getLogger(__name__) + + +class TydomClient: + def __init__(self, mac, password, alarm_pin=None, host="mediation.tydom.com"): + logger.debug("Initializing TydomClient Class") + + self.password = password + self.mac = mac + self.host = host + self.alarm_pin = alarm_pin + self.connection = None + self.remote_mode = True + self.ssl_context = None + self.cmd_prefix = "\x02" + self.reply_timeout = 4 + self.ping_timeout = None + self.refresh_timeout = 42 + self.sleep_time = 2 + self.incoming = None + # Some devices (like Tywatt) need polling + self.poll_device_urls = [] + self.current_poll_index = 0 + + # Set Host, ssl context and prefix for remote or local connection + if self.host == "mediation.tydom.com": + logger.info("Configure remote mode (%s)", self.host) + self.remote_mode = True + self.ssl_context = ssl._create_unverified_context() + self.cmd_prefix = "\x02" + self.ping_timeout = 40 + + else: + logger.info("Configure local mode (%s)", self.host) + self.remote_mode = False + self.ssl_context = ssl._create_unverified_context() + self.cmd_prefix = "" + self.ping_timeout = None + + async def connect(self): + logger.info("Connecting to tydom") + http_headers = { + "Connection": "Upgrade", + "Upgrade": "websocket", + "Host": self.host + ":443", + "Accept": "*/*", + "Sec-WebSocket-Key": self.generate_random_key(), + "Sec-WebSocket-Version": "13", + } + conn = http.client.HTTPSConnection(self.host, 443, context=self.ssl_context) + + # Get first handshake + conn.request( + "GET", + "/mediation/client?mac={}&appli=1".format(self.mac), + None, + http_headers, + ) + res = conn.getresponse() + conn.close() + + logger.debug("Response headers") + logger.debug(res.headers) + + logger.debug("Response code") + logger.debug(res.getcode()) + + # Read response + logger.debug("response") + logger.debug(res.read()) + res.read() + + # Get authentication + websocket_headers = {} + try: + # Local installations are unauthenticated but we don't *know* that for certain + # so we'll EAFP, try to use the header and fallback if we're unable. + nonce = res.headers["WWW-Authenticate"].split(",", 3) + # Build websocket headers + websocket_headers = {"Authorization": self.build_digest_headers(nonce)} + except AttributeError: + pass + + logger.debug("Upgrading http connection to websocket....") + + if self.ssl_context is not None: + websocket_ssl_context = self.ssl_context + else: + websocket_ssl_context = True # Verify certificate + + # outer loop restarted every time the connection fails + logger.debug("Attempting websocket connection with Tydom hub") + """ + Connecting to webSocket server + websockets.client.connect returns a WebSocketClientProtocol, which is used to send and receive messages + """ + try: + self.connection = await websockets.connect( + f"wss://{self.host}:443/mediation/client?mac={self.mac}&appli=1", + extra_headers=websocket_headers, + ssl=websocket_ssl_context, + ping_timeout=None, + ) + logger.info("Connected to tydom") + return self.connection + except Exception as e: + logger.error("Exception when trying to connect with websocket (%s)", e) + sys.exit(1) + + async def disconnect(self): + if self.connection is not None: + logger.info("Disconnecting") + await self.connection.close() + logger.info("Disconnected") + + # Generate 16 bytes random key for Sec-WebSocket-Keyand convert it to + # base64 + @staticmethod + def generate_random_key(): + return base64.b64encode(os.urandom(16)) + + # Build the headers of Digest Authentication + def build_digest_headers(self, nonce): + digest_auth = HTTPDigestAuth(self.mac, self.password) + chal = dict() + chal["nonce"] = nonce[2].split("=", 1)[1].split('"')[1] + chal["realm"] = "ServiceMedia" if self.remote_mode is True else "protected area" + chal["qop"] = "auth" + digest_auth._thread_local.chal = chal + digest_auth._thread_local.last_nonce = nonce + digest_auth._thread_local.nonce_count = 1 + return digest_auth.build_digest_header( + "GET", + "https://{host}:443/mediation/client?mac={mac}&appli=1".format( + host=self.host, mac=self.mac + ), + ) + + async def notify_alive(self, msg="OK"): + pass + + def add_poll_device_url(self, url): + self.poll_device_urls.append(url) + + # Send Generic message + async def send_message(self, method, msg): + str = ( + self.cmd_prefix + + method + + " " + + msg + + " HTTP/1.1\r\nContent-Length: 0\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" + ) + a_bytes = bytes(str, "ascii") + logger.debug( + "Sending message to tydom (%s %s)", + method, + msg if "pwd" not in msg else "***", + ) + + if self.connection is not None: + await self.connection.send(a_bytes) + else: + logger.warning( + "Cannot send message to Tydom because no connection has been established yet" + ) + + # Give order (name + value) to endpoint + async def put_devices_data(self, device_id, endpoint_id, name, value): + # For shutter, value is the percentage of closing + body = '[{"name":"' + name + '","value":"' + value + '"}]' + # endpoint_id is the endpoint = the device (shutter in this case) to + # open. + str_request = ( + self.cmd_prefix + + f"PUT /devices/{device_id}/endpoints/{endpoint_id}/data HTTP/1.1\r\nContent-Length: " + + str(len(body)) + + "\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" + + body + + "\r\n\r\n" + ) + a_bytes = bytes(str_request, "ascii") + logger.debug("Sending message to tydom (%s %s)", "PUT data", body) + await self.connection.send(a_bytes) + return 0 + + async def put_alarm_cdata(self, device_id, alarm_id=None, value=None, zone_id=None): + # Credits to @mgcrea on github ! + # AWAY # "PUT /devices/{}/endpoints/{}/cdata?name=alarmCmd HTTP/1.1\r\ncontent-length: 29\r\ncontent-type: application/json; charset=utf-8\r\ntransac-id: request_124\r\n\r\n\r\n{"value":"ON","pwd":{}}\r\n\r\n" + # HOME "PUT /devices/{}/endpoints/{}/cdata?name=zoneCmd HTTP/1.1\r\ncontent-length: 41\r\ncontent-type: application/json; charset=utf-8\r\ntransac-id: request_46\r\n\r\n\r\n{"value":"ON","pwd":"{}","zones":[1]}\r\n\r\n" + # DISARM "PUT /devices/{}/endpoints/{}/cdata?name=alarmCmd + # HTTP/1.1\r\ncontent-length: 30\r\ncontent-type: application/json; + # charset=utf-8\r\ntransac-id: + # request_7\r\n\r\n\r\n{"value":"OFF","pwd":"{}"}\r\n\r\n" + + # variables: + # id + # Cmd + # value + # pwd + # zones + + if self.alarm_pin is None: + logger.warning("Tydom alarm pin is not set!") + pass + try: + if zone_id is None: + cmd = "alarmCmd" + body = ( + '{"value":"' + str(value) + '","pwd":"' + str(self.alarm_pin) + '"}' + ) + else: + cmd = "zoneCmd" + body = ( + '{"value":"' + + str(value) + + '","pwd":"' + + str(self.alarm_pin) + + '","zones":"[' + + str(zone_id) + + ']"}' + ) + + str_request = ( + self.cmd_prefix + + "PUT /devices/{device}/endpoints/{alarm}/cdata?name={cmd} HTTP/1.1\r\nContent-Length: ".format( + device=str(device_id), alarm=str(alarm_id), cmd=str(cmd) + ) + + str(len(body)) + + "\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" + + body + + "\r\n\r\n" + ) + + a_bytes = bytes(str_request, "ascii") + logger.debug("Sending message to tydom (%s %s)", "PUT cdata", body) + + try: + await self.connection.send(a_bytes) + return 0 + except BaseException: + logger.error("put_alarm_cdata ERROR !", exc_info=True) + logger.error(a_bytes) + except BaseException: + logger.error("put_alarm_cdata ERROR !", exc_info=True) + + # Get some information on Tydom + async def get_info(self): + msg_type = "/info" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + # Refresh (all) + async def post_refresh(self): + msg_type = "/refresh/all" + req = "POST" + await self.send_message(method=req, msg=msg_type) + # Get poll device data + nb_poll_devices = len(self.poll_device_urls) + if self.current_poll_index < nb_poll_devices - 1: + self.current_poll_index = self.current_poll_index + 1 + else: + self.current_poll_index = 0 + if nb_poll_devices > 0: + await self.get_poll_device_data( + self.poll_device_urls[self.current_poll_index] + ) + + # Get the moments (programs) + async def get_moments(self): + msg_type = "/moments/file" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + # Get the scenarios + async def get_scenarii(self): + msg_type = "/scenarios/file" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + # Send a ping (pong should be returned) + async def ping(self): + msg_type = "/ping" + req = "GET" + await self.send_message(method=req, msg=msg_type) + logger.debug("Ping") + + # Get all devices metadata + async def get_devices_meta(self): + msg_type = "/devices/meta" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + # Get all devices data + async def get_devices_data(self): + msg_type = "/devices/data" + req = "GET" + await self.send_message(method=req, msg=msg_type) + # Get poll devices data + for url in self.poll_device_urls: + await self.get_poll_device_data(url) + + # List the device to get the endpoint id + async def get_configs_file(self): + msg_type = "/configs/file" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + # Get metadata configuration to list poll devices (like Tywatt) + async def get_devices_cmeta(self): + msg_type = "/devices/cmeta" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + async def get_data(self): + await self.get_configs_file() + await self.get_devices_cmeta() + await self.get_devices_data() + + # Give order to endpoint + async def get_device_data(self, id): + # 10 here is the endpoint = the device (shutter in this case) to open. + device_id = str(id) + str_request = ( + self.cmd_prefix + + f"GET /devices/{device_id}/endpoints/{device_id}/data HTTP/1.1\r\nContent-Length: 0\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" + ) + a_bytes = bytes(str_request, "ascii") + await self.connection.send(a_bytes) + + async def get_poll_device_data(self, url): + msg_type = url + req = "GET" + await self.send_message(method=req, msg=msg_type) + + async def setup(self): + logger.info("Setup tydom client") + await self.get_info() + await self.post_refresh() + await self.get_data() + + # Listen to tydom events. + async def listen_tydom(self): + try: + await self.connect() + await self.setup() + while True: + try: + incoming_bytes_str = await self.connection.recv() + message_handler = MessageHandler( + incoming_bytes=incoming_bytes_str, + tydom_client=self, + ) + await message_handler.incoming_triage() + except websockets.exceptions.ConnectionClosed: + # try to reconnect + await self.connect() + await self.setup() + except Exception as e: + logger.warning("Unable to handle message: %s", e) + + except socket.gaierror as e: + logger.error("Socket error (%s)", e) + sys.exit(1) + except ConnectionRefusedError as e: + logger.error("Connection refused (%s)", e) + sys.exit(1) + + async def shutdown(self, signal, loop): + logging.info("Received exit signal %s", signal.name) + logging.info("Cancelling running tasks") + + try: + # Close connections + await self.disconnect() + + # Cancel async tasks + tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] + [task.cancel() for task in tasks] + await asyncio.gather(*tasks) + logging.info("All running tasks cancelled") + except Exception as e: + logging.info("Some errors occurred when stopping tasks (%s)", e) + finally: + loop.stop() diff --git a/requirements.txt b/requirements.txt index 8008ccb..67ac646 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ colorlog==6.7.0 -homeassistant==2023.2.0 +homeassistant==2023.4.6 pip>=21.0,<23.2 ruff==0.0.261 From fbea4cc15e827c09457a9a62911b5f199127601b Mon Sep 17 00:00:00 2001 From: ucpy7374 Date: Mon, 24 Apr 2023 18:28:02 +0200 Subject: [PATCH 02/74] first code as-is --- .../deltadore-tydom/config_flow.py | 23 ++++++++----------- .../deltadore-tydom/translations/en.json | 1 + 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index eb6933d..b0c63fa 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Tydom integration.""" from __future__ import annotations import traceback -import logging from typing import Any import voluptuous as vol @@ -12,11 +11,9 @@ from homeassistant.core import HomeAssistant from homeassistant.components import dhcp -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN, LOGGER from .hub import Hub -_LOGGER = logging.getLogger(__name__) - # This is the schema that used to display the UI to the user. This simple # schema has a single required host field, but it could include a number of fields # such as username, password etc. See other components in the HA core code for @@ -106,7 +103,7 @@ def __init__(self): self._discovered_host = None self._discovered_mac = None - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> config_entries.FlowResult: """Handle the initial step.""" # This goes through the steps to take the user through the setup process. # Using this it is possible to update the UI and prompt for additional @@ -114,7 +111,7 @@ async def async_step_user(self, user_input=None): # and when that has some validated input, it calls `async_create_entry` to # actually create the HA config entry. Note the "title" value is returned by # `validate_input` above. - errors = {} + _errors = {} if user_input is not None: try: info = await validate_input(self.hass, user_input) @@ -122,21 +119,21 @@ async def async_step_user(self, user_input=None): self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) except CannotConnect: - errors["base"] = "cannot_connect" + _errors["base"] = "cannot_connect" except InvalidHost: # The error string is set here, and should be translated. # This example does not currently cover translations, see the # comments on `DATA_SCHEMA` for further details. # Set the error on the `host` field, not the entire form. - errors[CONF_HOST] = "cannot_connect" + _errors[CONF_HOST] = "cannot_connect" except InvalidMacAddress: - errors[CONF_MAC] = "cannot_connect" + _errors[CONF_MAC] = "cannot_connect" except InvalidPassword: - errors[CONF_PASSWORD] = "cannot_connect" + _errors[CONF_PASSWORD] = "cannot_connect" except Exception: # pylint: disable=broad-except traceback.print_exc() - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" + LOGGER.exception("Unexpected exception") + _errors["base"] = "unknown" user_input = user_input or {} # If there is no user input or there were errors, show the form again, including any errors that were found with the input. @@ -152,7 +149,7 @@ async def async_step_user(self, user_input=None): vol.Optional(CONF_PIN, default=user_input.get(CONF_PIN, "")): str, } ), - errors=errors, + errors=_errors, ) async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo): diff --git a/custom_components/deltadore-tydom/translations/en.json b/custom_components/deltadore-tydom/translations/en.json index cb4d21d..9777e23 100644 --- a/custom_components/deltadore-tydom/translations/en.json +++ b/custom_components/deltadore-tydom/translations/en.json @@ -11,6 +11,7 @@ "step": { "user": { "title": "Delta Dore Tydom", + "description": "test", "data": { "host": "Host", "password": "Password", From 246b3f9a6a207d347e6c1585baa745e780c45f30 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 25 Apr 2023 13:20:28 +0200 Subject: [PATCH 03/74] updates... --- custom_components/deltadore-tydom/__init__.py | 19 ++- .../deltadore-tydom/config_flow.py | 55 ++++--- custom_components/deltadore-tydom/const.py | 1 + .../deltadore-tydom/manifest.json | 19 ++- .../deltadore-tydom/strings.json | 23 --- .../deltadore-tydom/translations/en.json | 30 ++-- .../deltadore-tydom/tydom/TydomClient.py | 14 +- custom_components/livebox/__init__.py | 79 +++++++++ custom_components/livebox/binary_sensor.py | 93 +++++++++++ custom_components/livebox/bridge.py | 154 ++++++++++++++++++ custom_components/livebox/button.py | 51 ++++++ custom_components/livebox/config_flow.py | 136 ++++++++++++++++ custom_components/livebox/const.py | 78 +++++++++ custom_components/livebox/coordinator.py | 51 ++++++ custom_components/livebox/device_tracker.py | 98 +++++++++++ custom_components/livebox/manifest.json | 17 ++ custom_components/livebox/sensor.py | 61 +++++++ custom_components/livebox/services.yaml | 13 ++ custom_components/livebox/strings.json | 33 ++++ custom_components/livebox/switch.py | 87 ++++++++++ .../livebox/translations/en.json | 34 ++++ .../livebox/translations/fr.json | 34 ++++ .../livebox/translations/nb.json | 34 ++++ hacs.json | 8 +- 24 files changed, 1137 insertions(+), 85 deletions(-) delete mode 100644 custom_components/deltadore-tydom/strings.json create mode 100644 custom_components/livebox/__init__.py create mode 100644 custom_components/livebox/binary_sensor.py create mode 100644 custom_components/livebox/bridge.py create mode 100644 custom_components/livebox/button.py create mode 100644 custom_components/livebox/config_flow.py create mode 100644 custom_components/livebox/const.py create mode 100644 custom_components/livebox/coordinator.py create mode 100644 custom_components/livebox/device_tracker.py create mode 100644 custom_components/livebox/manifest.json create mode 100644 custom_components/livebox/sensor.py create mode 100644 custom_components/livebox/services.yaml create mode 100644 custom_components/livebox/strings.json create mode 100644 custom_components/livebox/switch.py create mode 100644 custom_components/livebox/translations/en.json create mode 100644 custom_components/livebox/translations/fr.json create mode 100644 custom_components/livebox/translations/nb.json diff --git a/custom_components/deltadore-tydom/__init__.py b/custom_components/deltadore-tydom/__init__.py index 066c336..085ccc1 100644 --- a/custom_components/deltadore-tydom/__init__.py +++ b/custom_components/deltadore-tydom/__init__.py @@ -1,6 +1,7 @@ """The Detailed Hello World Push integration.""" from __future__ import annotations +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -17,15 +18,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Store an instance of the "connecting" class that does the work of speaking # with your actual devices. pin = None - if "alarmpin" in entry.data: - pin = entry.data["alarmpin"] - - tydomHub = hub.Hub( - hass, entry.data["host"], entry.data["macaddress"], entry.data["password"], pin + if CONF_PIN in entry.data: + pin = entry.data[CONF_PIN] + + tydom_hub = hub.Hub( + hass, + entry.data[CONF_HOST], + entry.data[CONF_MAC], + entry.data[CONF_PASSWORD], + pin, ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = tydomHub + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = tydom_hub - await tydomHub.setup() + await tydom_hub.setup() # This creates each HA object for each platform your device requires. # It's done by calling the `async_setup_entry` function in each platform module. diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index b0c63fa..601d720 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Tydom integration.""" from __future__ import annotations import traceback +import ipaddress +import re from typing import Any import voluptuous as vol @@ -28,14 +30,24 @@ DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_HOST): str, - vol.Required(CONF_MAC): str, - vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_MAC): cv.string, + vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_PIN): str, } ) +def host_valid(host): + """Return True if hostname or IP address is valid""" + try: + if ipaddress.ip_address(host).version == (4 or 6): + return True + except ValueError: + disallowed = re.compile(r"[^a-zA-Z\d\-]") + return all(x and not disallowed.search(x) for x in host.split(".")) + + async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -51,7 +63,7 @@ async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: if len(data[CONF_HOST]) < 3: raise InvalidHost - if len(data[CONF_MAC]) < 3: + if len(data[CONF_MAC]) != 12: raise InvalidMacAddress if len(data[CONF_PASSWORD]) < 3: @@ -85,11 +97,16 @@ async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: # "Title" is what is displayed to the user for this hub device # It is stored internally in HA as part of the device config. # See `async_step_user` below for how this is used - return {"title": data[CONF_MAC]} + return { + CONF_HOST: data[CONF_HOST], + CONF_MAC: data[CONF_MAC], + CONF_PASSWORD: data[CONF_PASSWORD], + CONF_PIN: pin, + } class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Hello World.""" + """Handle a config flow for Tydom.""" VERSION = 1 @@ -103,6 +120,10 @@ def __init__(self): self._discovered_host = None self._discovered_mac = None + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + async def async_step_user(self, user_input=None) -> config_entries.FlowResult: """Handle the initial step.""" # This goes through the steps to take the user through the setup process. @@ -117,7 +138,6 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: info = await validate_input(self.hass, user_input) await self.async_set_unique_id(user_input[CONF_MAC]) self._abort_if_unique_id_configured() - return self.async_create_entry(title=info["title"], data=user_input) except CannotConnect: _errors["base"] = "cannot_connect" except InvalidHost: @@ -125,31 +145,22 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: # This example does not currently cover translations, see the # comments on `DATA_SCHEMA` for further details. # Set the error on the `host` field, not the entire form. - _errors[CONF_HOST] = "cannot_connect" + _errors[CONF_HOST] = "invalid_host" except InvalidMacAddress: - _errors[CONF_MAC] = "cannot_connect" + _errors[CONF_MAC] = "invalid_macaddress" except InvalidPassword: - _errors[CONF_PASSWORD] = "cannot_connect" + _errors[CONF_PASSWORD] = "invalid_password" except Exception: # pylint: disable=broad-except traceback.print_exc() LOGGER.exception("Unexpected exception") _errors["base"] = "unknown" + else: + return self.async_create_entry(title=info[CONF_MAC], data=user_input) user_input = user_input or {} # If there is no user input or there were errors, show the form again, including any errors that were found with the input. return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, - vol.Required(CONF_MAC, default=user_input.get(CONF_MAC, "")): str, - vol.Required( - CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") - ): str, - vol.Optional(CONF_PIN, default=user_input.get(CONF_PIN, "")): str, - } - ), - errors=_errors, + step_id="user", data_schema=DATA_SCHEMA, errors=_errors ) async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo): diff --git a/custom_components/deltadore-tydom/const.py b/custom_components/deltadore-tydom/const.py index b202cad..2332547 100644 --- a/custom_components/deltadore-tydom/const.py +++ b/custom_components/deltadore-tydom/const.py @@ -9,3 +9,4 @@ NAME = "Integration blueprint" VERSION = "0.0.1" ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/" +CONF_FORECAST = "pouet" diff --git a/custom_components/deltadore-tydom/manifest.json b/custom_components/deltadore-tydom/manifest.json index 78da7a8..805b907 100644 --- a/custom_components/deltadore-tydom/manifest.json +++ b/custom_components/deltadore-tydom/manifest.json @@ -1,18 +1,23 @@ { "domain": "tydom", "name": "Delta Dore TYDOM", - "codeowners": ["@CyrilP"], + "codeowners": [ + "@CyrilP" + ], "config_flow": true, - "dependencies": [], "documentation": "https://www.home-assistant.io/integrations/tydom", "iot_class": "local_push", - "requirements": ["websockets>=9.1"], + "requirements": [ + "websockets>=9.1" + ], "dhcp": [ { - "hostname": "TYDOM-*", - "macaddress": "001A25*" + "hostname": "TYDOM-*", + "macaddress": "001A25*" } ], - "loggers": ["tydom"], + "loggers": [ + "tydom" + ], "version": "0.0.1" -} +} \ No newline at end of file diff --git a/custom_components/deltadore-tydom/strings.json b/custom_components/deltadore-tydom/strings.json deleted file mode 100644 index a5aaa55..0000000 --- a/custom_components/deltadore-tydom/strings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Delta Dore Tydom", - "data": { - "host": "[%key:component::tydom::options::step::user::data::host%]", - "mac": "mac", - "password": "[%key:common::config_flow::data::password%]", - "pin": "[%key:common::config_flow::data::pin%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" - } - } -} \ No newline at end of file diff --git a/custom_components/deltadore-tydom/translations/en.json b/custom_components/deltadore-tydom/translations/en.json index 9777e23..7fa4eb0 100644 --- a/custom_components/deltadore-tydom/translations/en.json +++ b/custom_components/deltadore-tydom/translations/en.json @@ -1,24 +1,26 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, "step": { "user": { - "title": "Delta Dore Tydom", - "description": "test", + "title": "Inverter Connection Configuration", + "description": "If you need help with the configuration go to: https://github.com/alexdelprete/ha-abb-powerone-pvi-sunspec", "data": { - "host": "Host", - "password": "Password", - "mac": "Mac Address", - "pin": "Alarm PIN" + "name": "Custom Name of the inverter (used for sensors' prefix)", + "host": "IP or hostname", + "port": "TCP port", + "password": "pass", + "pin": "pin", + "slave_id": "Modbus Slave address of the inverter", + "base_addr": "Modbus Register Map Base Address", + "scan_interval": "Polling Period (min: 5s max: 600s)" } } + }, + "error": { + "already_configured": "Device is already configured" + }, + "abort": { + "already_configured": "Device is already configured" } } } \ No newline at end of file diff --git a/custom_components/deltadore-tydom/tydom/TydomClient.py b/custom_components/deltadore-tydom/tydom/TydomClient.py index e53b1d5..5008342 100644 --- a/custom_components/deltadore-tydom/tydom/TydomClient.py +++ b/custom_components/deltadore-tydom/tydom/TydomClient.py @@ -354,22 +354,22 @@ async def setup(self): await self.get_data() # Listen to tydom events. - async def listen_tydom(self): + def listen_tydom(self): try: - await self.connect() - await self.setup() + self.connect() + self.setup() while True: try: - incoming_bytes_str = await self.connection.recv() + incoming_bytes_str = self.connection.recv() message_handler = MessageHandler( incoming_bytes=incoming_bytes_str, tydom_client=self, ) - await message_handler.incoming_triage() + message_handler.incoming_triage() except websockets.exceptions.ConnectionClosed: # try to reconnect - await self.connect() - await self.setup() + self.connect() + self.setup() except Exception as e: logger.warning("Unable to handle message: %s", e) diff --git a/custom_components/livebox/__init__.py b/custom_components/livebox/__init__.py new file mode 100644 index 0000000..365eb5f --- /dev/null +++ b/custom_components/livebox/__init__.py @@ -0,0 +1,79 @@ +"""Orange Livebox.""" +import logging + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import device_registry as dr + +from .const import ( + CALLID, + CONF_TRACKING_TIMEOUT, + COORDINATOR, + DOMAIN, + LIVEBOX_API, + LIVEBOX_ID, + PLATFORMS, +) +from .coordinator import LiveboxDataUpdateCoordinator + +CALLMISSED_SCHEMA = vol.Schema({vol.Optional(CALLID): str}) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Livebox as config entry.""" + hass.data.setdefault(DOMAIN, {}) + + coordinator = LiveboxDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + if (infos := coordinator.data.get("infos")) is None: + raise PlatformNotReady + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, infos.get("SerialNumber"))}, + manufacturer=infos.get("Manufacturer"), + name=infos.get("ProductClass"), + model=infos.get("ModelName"), + sw_version=infos.get("SoftwareVersion"), + configuration_url="http://{}:{}".format( + entry.data.get("host"), entry.data.get("port") + ), + ) + + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + hass.data[DOMAIN][entry.entry_id] = { + LIVEBOX_ID: entry.unique_id, + COORDINATOR: coordinator, + LIVEBOX_API: coordinator.bridge.api, + CONF_TRACKING_TIMEOUT: entry.options.get(CONF_TRACKING_TIMEOUT, 0), + } + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + async def async_remove_cmissed(call) -> None: + await coordinator.bridge.async_remove_cmissed(call) + await coordinator.async_refresh() + + hass.services.async_register( + DOMAIN, "remove_call_missed", async_remove_cmissed, schema=CALLMISSED_SCHEMA + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Reload device tracker if change option.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/custom_components/livebox/binary_sensor.py b/custom_components/livebox/binary_sensor.py new file mode 100644 index 0000000..333f170 --- /dev/null +++ b/custom_components/livebox/binary_sensor.py @@ -0,0 +1,93 @@ +"""Livebox binary sensor entities.""" +import logging +from datetime import datetime, timedelta + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorDeviceClass, +) +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import COORDINATOR, DOMAIN, LIVEBOX_ID, MISSED_ICON +from .coordinator import LiveboxDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Defer binary sensor setup to the shared sensor module.""" + datas = hass.data[DOMAIN][config_entry.entry_id] + box_id = datas[LIVEBOX_ID] + coordinator = datas[COORDINATOR] + async_add_entities( + [WanStatus(coordinator, box_id), CallMissed(coordinator, box_id)], True + ) + + +class WanStatus(CoordinatorEntity[LiveboxDataUpdateCoordinator], BinarySensorEntity): + """Wan status sensor.""" + + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True + _attr_name = "WAN Status" + + def __init__(self, coordinator, box_id): + """Initialize the sensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{box_id}_connectivity" + self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + wstatus = self.coordinator.data.get("wan_status", {}).get("data", {}) + return wstatus.get("WanState") == "up" + + @property + def extra_state_attributes(self): + """Return the device state attributes.""" + wstatus = self.coordinator.data.get("wan_status", {}).get("data", {}) + uptime = datetime.today() - timedelta( + seconds=self.coordinator.data["infos"].get("UpTime") + ) + _attributs = { + "link_type": wstatus.get("LinkType"), + "link_state": wstatus.get("LinkState"), + "last_connection_error": wstatus.get("LastConnectionError"), + "wan_ipaddress": wstatus.get("IPAddress"), + "wan_ipv6address": wstatus.get("IPv6Address"), + "uptime": uptime, + } + cwired = self.coordinator.data.get("count_wired_devices") + if cwired > 0: + _attributs.update({"wired clients": cwired}) + cwireless = self.coordinator.data.get("count_wireless_devices") + if cwireless > 0: + _attributs.update({"wireless clients": cwireless}) + + return _attributs + + +class CallMissed(CoordinatorEntity[LiveboxDataUpdateCoordinator], BinarySensorEntity): + """Call missed sensor.""" + + _attr_name = "Call missed" + _attr_icon = MISSED_ICON + _attr_has_entity_name = True + + def __init__(self, coordinator, box_id): + """Initialize the sensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{box_id}_callmissed" + self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return len(self.coordinator.data.get("cmissed").get("call missed")) > 0 + + @property + def extra_state_attributes(self): + """Return attributs.""" + return self.coordinator.data.get("cmissed") diff --git a/custom_components/livebox/bridge.py b/custom_components/livebox/bridge.py new file mode 100644 index 0000000..fb0bcb5 --- /dev/null +++ b/custom_components/livebox/bridge.py @@ -0,0 +1,154 @@ +"""Collect datas information from livebox.""" +import logging +from datetime import datetime + +from aiosysbus import AIOSysbus +from aiosysbus.exceptions import ( + AuthorizationError, + HttpRequestError, + InsufficientPermissionsError, + LiveboxException, + NotOpenError, +) +from homeassistant.util.dt import ( + UTC, + DEFAULT_TIME_ZONE, +) +from .const import CALLID + +_LOGGER = logging.getLogger(__name__) + + +class BridgeData: + """Simplification of API calls.""" + + def __init__(self, hass): + """Init parameters.""" + self.hass = hass + self.api = None + self.count_wired_devices = 0 + self.count_wireless_devices = 0 + + async def async_connect(self, **kwargs): + """Connect at livebox.""" + self.api = AIOSysbus( + username=kwargs.get("username"), + password=kwargs.get("password"), + host=kwargs.get("host"), + port=kwargs.get("port"), + ) + + try: + await self.hass.async_add_executor_job(self.api.connect) + await self.hass.async_add_executor_job(self.api.get_permissions) + except AuthorizationError as error: + _LOGGER.error("Error Authorization (%s)", error) + raise AuthorizationError from error + except NotOpenError as error: + _LOGGER.error("Error Not open (%s)", error) + raise NotOpenError from error + except LiveboxException as error: + _LOGGER.error("Error Unknown (%s)", error) + raise LiveboxException from error + except InsufficientPermissionsError as error: + _LOGGER.error("Error Insufficient Permissions (%s)", error) + raise InsufficientPermissionsError from error + + async def async_make_request(self, call_api, **kwargs): + """Make request for API.""" + try: + return await self.hass.async_add_executor_job(call_api, kwargs) + except HttpRequestError as error: + _LOGGER.error("HTTP Request (%s)", error) + raise LiveboxException from error + except LiveboxException as error: + _LOGGER.error("Error Unknown (%s)", error) + raise LiveboxException from error + + async def async_get_devices(self, lan_tracking=False): + """Get all devices.""" + devices_tracker = {} + parameters = { + "expression": { + "wifi": 'wifi && (edev || hnid) and .PhysAddress!=""', + "eth": 'eth && (edev || hnid) and .PhysAddress!=""', + } + } + devices = await self.async_make_request( + self.api.devices.get_devices, **parameters + ) + devices_status_wireless = devices.get("status", {}).get("wifi", {}) + self.count_wireless_devices = len(devices_status_wireless) + for device in devices_status_wireless: + if device.get("Key"): + devices_tracker.setdefault(device.get("Key"), {}).update(device) + + if lan_tracking: + devices_status_wired = devices.get("status", {}).get("eth", {}) + self.count_wired_devices = len(devices_status_wired) + for device in devices_status_wired: + if device.get("Key"): + devices_tracker.setdefault(device.get("Key"), {}).update(device) + + return devices_tracker + + async def async_get_infos(self): + """Get router infos.""" + infos = await self.async_make_request(self.api.deviceinfo.get_deviceinfo) + return infos.get("status", {}) + + async def async_get_wan_status(self): + """Get status.""" + wan_status = await self.async_make_request(self.api.system.get_wanstatus) + return wan_status + + async def async_get_caller_missed(self): + """Get caller missed.""" + cmisseds = [] + calls = await self.async_make_request( + self.api.call.get_voiceapplication_calllist + ) + + for call in calls.get("status", {}): + if call["callType"] != "succeeded": + utc_dt = datetime.strptime(call["startTime"], "%Y-%m-%dT%H:%M:%SZ") + local_dt = utc_dt.replace(tzinfo=UTC).astimezone(tz=DEFAULT_TIME_ZONE) + cmisseds.append( + { + "phone_number": call["remoteNumber"], + "date": str(local_dt), + "callId": call["callId"], + } + ) + + return {"call missed": cmisseds} + + async def async_get_dsl_status(self): + """Get dsl status.""" + parameters = {"mibs": "dsl", "flag": "", "traverse": "down"} + dsl_status = await self.async_make_request( + self.api.connection.get_data_MIBS, **parameters + ) + return dsl_status.get("status", {}).get("dsl", {}).get("dsl0", {}) + + async def async_get_nmc(self): + """Get dsl status.""" + nmc = await self.async_make_request(self.api.system.get_nmc) + return nmc.get("status", {}) + + async def async_get_wifi(self): + """Get dsl status.""" + wifi = await self.async_make_request(self.api.wifi.get_wifi) + return wifi.get("status", {}).get("Enable") is True + + async def async_get_guest_wifi(self): + """Get Guest Wifi status.""" + guest_wifi = await self.async_make_request(self.api.guestwifi.get_guest_wifi) + return guest_wifi.get("status", {}).get("Enable") is True + + async def async_remove_cmissed(self, call) -> None: + """Remove call missed.""" + await self.async_make_request( + self.api.call.get_voiceapplication_clearlist, + **{CALLID: call.data.get(CALLID)}, + ) diff --git a/custom_components/livebox/button.py b/custom_components/livebox/button.py new file mode 100644 index 0000000..4c8ce2e --- /dev/null +++ b/custom_components/livebox/button.py @@ -0,0 +1,51 @@ +"""Button for Livebox router.""" +import logging + +from homeassistant.components.button import ButtonEntity + +from .const import DOMAIN, LIVEBOX_API, LIVEBOX_ID, RESTART_ICON, RING_ICON + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the sensors.""" + box_id = hass.data[DOMAIN][config_entry.entry_id][LIVEBOX_ID] + api = hass.data[DOMAIN][config_entry.entry_id][LIVEBOX_API] + async_add_entities([RestartButton(box_id, api), RingButton(box_id, api)], True) + + +class RestartButton(ButtonEntity): + """Representation of a livebox sensor.""" + + _attr_name = "Livebox restart" + _attr_icon = RESTART_ICON + _attr_has_entity_name = True + + def __init__(self, box_id, api): + """Initialize the sensor.""" + self._api = api + self._attr_unique_id = f"{box_id}_restart" + self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} + + async def async_press(self) -> None: + """Handle the button press.""" + await self.hass.async_add_executor_job(self._api.system.reboot) + + +class RingButton(ButtonEntity): + """Representation of a livebox sensor.""" + + _attr_name = "Ring your phone" + _attr_icon = RING_ICON + _attr_has_entity_name = True + + def __init__(self, box_id, api): + """Initialize the sensor.""" + self._api = api + self._attr_unique_id = f"{box_id}_ring" + self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} + + async def async_press(self) -> None: + """Handle the button press.""" + await self.hass.async_add_executor_job(self._api.call.set_voiceapplication_ring) diff --git a/custom_components/livebox/config_flow.py b/custom_components/livebox/config_flow.py new file mode 100644 index 0000000..341357a --- /dev/null +++ b/custom_components/livebox/config_flow.py @@ -0,0 +1,136 @@ +"""Config flow to configure Livebox.""" +import logging +from urllib.parse import urlparse + +import voluptuous as vol +from aiosysbus.exceptions import ( + AuthorizationError, + InsufficientPermissionsError, + LiveboxException, + NotOpenError, +) +from homeassistant import config_entries +from homeassistant.components import ssdp +from homeassistant.components.ssdp import ATTR_SSDP_UDN, ATTR_SSDP_USN +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_UNIQUE_ID, + CONF_USERNAME, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv + +from .bridge import BridgeData +from .const import ( + CONF_LAN_TRACKING, + CONF_TRACKING_TIMEOUT, + DEFAULT_HOST, + DEFAULT_LAN_TRACKING, + DEFAULT_PORT, + DEFAULT_TRACKING_TIMEOUT, + DEFAULT_USERNAME, + DOMAIN, +) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) + +_LOGGER = logging.getLogger(__name__) + + +class LiveboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Livebox config flow.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get option flow.""" + return LiveboxOptionsFlowHandler(config_entry) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None and user_input.get(CONF_USERNAME) is not None: + try: + bridge = BridgeData(self.hass) + await bridge.async_connect(**user_input) + infos = await bridge.async_get_infos() + await self.async_set_unique_id(infos["SerialNumber"]) + self._abort_if_unique_id_configured() + except AuthorizationError: + errors["base"] = "login_inccorect" + except InsufficientPermissionsError: + errors["base"] = "insufficient_permission" + except NotOpenError: + errors["base"] = "cannot_connect" + except LiveboxException: + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=infos["ProductClass"], data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_ssdp(self, discovery_info): + """Handle a discovered device.""" + hostname = urlparse(discovery_info.ssdp_location).hostname + friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] + unique_id = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + user_input = { + CONF_HOST: hostname, + CONF_NAME: friendly_name, + CONF_UNIQUE_ID: unique_id, + ATTR_SSDP_USN: discovery_info.ssdp_usn, + ATTR_SSDP_UDN: discovery_info.ssdp_udn, + } + return await self.async_step_user(user_input) + + +class LiveboxOptionsFlowHandler(config_entries.OptionsFlow): + """Handle option.""" + + def __init__(self, config_entry): + """Initialize the options flow.""" + self.config_entry = config_entry + self._lan_tracking = self.config_entry.options.get( + CONF_LAN_TRACKING, DEFAULT_LAN_TRACKING + ) + self._tracking_timeout = self.config_entry.options.get( + CONF_TRACKING_TIMEOUT, DEFAULT_TRACKING_TIMEOUT + ) + + async def async_step_init(self, user_input=None): + """Handle a flow initialized by the user.""" + options_schema = vol.Schema( + { + vol.Required(CONF_LAN_TRACKING, default=self._lan_tracking): bool, + vol.Required( + CONF_TRACKING_TIMEOUT, default=self._tracking_timeout + ): int, + }, + ) + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form(step_id="init", data_schema=options_schema) diff --git a/custom_components/livebox/const.py b/custom_components/livebox/const.py new file mode 100644 index 0000000..f0fa0b0 --- /dev/null +++ b/custom_components/livebox/const.py @@ -0,0 +1,78 @@ +"""Constants for the Livebox component.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Final +from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) + +DOMAIN = "livebox" +COORDINATOR = "coordinator" +UNSUB_LISTENER = "unsubscribe_listener" +LIVEBOX_ID = "id" +LIVEBOX_API = "api" +PLATFORMS = ["sensor", "binary_sensor", "device_tracker", "switch", "button"] + +TEMPLATE_SENSOR = "Orange Livebox" + +DEFAULT_USERNAME = "admin" +DEFAULT_HOST = "192.168.1.1" +DEFAULT_PORT = 80 + +CALLID = "callId" + +CONF_LAN_TRACKING = "lan_tracking" +DEFAULT_LAN_TRACKING = False + +CONF_TRACKING_TIMEOUT = "timeout_tracking" +DEFAULT_TRACKING_TIMEOUT = 300 + +UPLOAD_ICON = "mdi:upload-network" +DOWNLOAD_ICON = "mdi:download-network" +MISSED_ICON = "mdi:phone-alert" +RESTART_ICON = "mdi:restart-alert" +RING_ICON = "mdi:phone-classic" +GUESTWIFI_ICON = "mdi:wifi-lock-open" + + +@dataclass +class FlowSensorEntityDescription(SensorEntityDescription): + """Represents an Flow Sensor.""" + + current_rate: str | None = None + attr: dict | None = None + + +SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( + FlowSensorEntityDescription( + key="down", + name="Orange Livebox Download speed", + icon=DOWNLOAD_ICON, + current_rate="DownstreamCurrRate", + native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + state_class=STATE_CLASS_MEASUREMENT, + attr={ + "downstream_maxrate": "DownstreamMaxRate", + "downstream_lineattenuation": "DownstreamLineAttenuation", + "downstream_noisemargin": "DownstreamNoiseMargin", + "downstream_power": "DownstreamPower", + }, + ), + FlowSensorEntityDescription( + key="up", + name="Orange Livebox Upload speed", + icon=UPLOAD_ICON, + current_rate="UpstreamCurrRate", + native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + state_class=STATE_CLASS_MEASUREMENT, + attr={ + "upstream_maxrate": "UpstreamMaxRate", + "upstream_lineattenuation": "UpstreamLineAttenuation", + "upstream_noisemargin": "UpstreamNoiseMargin", + "upstream_power": "UpstreamPower", + }, + ), +) diff --git a/custom_components/livebox/coordinator.py b/custom_components/livebox/coordinator.py new file mode 100644 index 0000000..3cf0809 --- /dev/null +++ b/custom_components/livebox/coordinator.py @@ -0,0 +1,51 @@ +"""Corddinator for Livebox.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aiosysbus.exceptions import LiveboxException + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .bridge import BridgeData +from .const import DOMAIN, CONF_LAN_TRACKING + +_LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=1) + + +class LiveboxDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to fetch datas.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry, + ) -> None: + """Class to manage fetching data API.""" + self.bridge = BridgeData(hass) + self.config_entry = config_entry + self.api = None + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + + async def _async_update_data(self) -> dict: + """Fetch datas.""" + try: + lan_tracking = self.config_entry.options.get(CONF_LAN_TRACKING, False) + self.api = await self.bridge.async_connect(**self.config_entry.data) + return { + "cmissed": await self.bridge.async_get_caller_missed(), + "devices": await self.bridge.async_get_devices(lan_tracking), + "dsl_status": await self.bridge.async_get_dsl_status(), + "infos": await self.bridge.async_get_infos(), + "nmc": await self.bridge.async_get_nmc(), + "wan_status": await self.bridge.async_get_wan_status(), + "wifi": await self.bridge.async_get_wifi(), + "guest_wifi": await self.bridge.async_get_guest_wifi(), + "count_wired_devices": self.bridge.count_wired_devices, + "count_wireless_devices": self.bridge.count_wireless_devices, + } + except LiveboxException as error: + raise LiveboxException(error) from error diff --git a/custom_components/livebox/device_tracker.py b/custom_components/livebox/device_tracker.py new file mode 100644 index 0000000..ea91544 --- /dev/null +++ b/custom_components/livebox/device_tracker.py @@ -0,0 +1,98 @@ +"""Support for the Livebox platform.""" +import logging +from datetime import datetime, timedelta + +from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import CONF_TRACKING_TIMEOUT, COORDINATOR, DOMAIN, LIVEBOX_ID +from .coordinator import LiveboxDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up device tracker from config entry.""" + datas = hass.data[DOMAIN][config_entry.entry_id] + box_id = datas[LIVEBOX_ID] + coordinator = datas[COORDINATOR] + timeout = datas[CONF_TRACKING_TIMEOUT] + + device_trackers = coordinator.data["devices"] + entities = [ + LiveboxDeviceScannerEntity(key, box_id, coordinator, timeout) + for key, device in device_trackers.items() + if "IPAddress" and "PhysAddress" in device + ] + async_add_entities(entities, True) + + +class LiveboxDeviceScannerEntity( + CoordinatorEntity[LiveboxDataUpdateCoordinator], ScannerEntity +): + """Represent a tracked device.""" + + _attr_has_entity_name = True + + def __init__(self, key, bridge_id, coordinator, timeout): + """Initialize the device tracker.""" + super().__init__(coordinator) + self.box_id = bridge_id + self.key = key + self._device = coordinator.data.get("devices", {}).get(key, {}) + self._timeout_tracking = timeout + self._old_status = datetime.today() + + self._attr_name = self._device.get("Name") + self._attr_unique_id = key + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + status = ( + self.coordinator.data.get("devices", {}) + .get(self.unique_id, {}) + .get("Active") + ) + if status is True: + self._old_status = datetime.today() + timedelta( + seconds=self._timeout_tracking + ) + if status is False and self._old_status > datetime.today(): + _LOGGER.debug("%s will be disconnected at %s", self.name, self._old_status) + return True + + return status + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_ROUTER + + @property + def ip_address(self): + """Return ip address.""" + device = self.coordinator.data["devices"].get(self.unique_id, {}) + return device.get("IPAddress") + + @property + def mac_address(self): + """Return mac address.""" + return self.key + + @property + def device_info(self): + """Return the device info.""" + return { + "name": self.name, + "identifiers": {(DOMAIN, self.unique_id)}, + "via_device": (DOMAIN, self.box_id), + } + + @property + def extra_state_attributes(self): + """Return the device state attributes.""" + return { + "first_seen": self._device.get("FirstSeen"), + } diff --git a/custom_components/livebox/manifest.json b/custom_components/livebox/manifest.json new file mode 100644 index 0000000..ae826b7 --- /dev/null +++ b/custom_components/livebox/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "livebox", + "name": "Orange Livebox", + "codeowners": ["@cyr-ius"], + "config_flow": true, + "dependencies": ["ssdp"], + "documentation": "https://github.com/cyr-ius/hass-livebox-component", + "iot_class": "local_polling", + "issue_tracker": "https://github.com/cyr-ius/hass-livebox-component/issues", + "loggers": ["aiosysbus"], + "requirements": ["aiosysbus==0.2.1"], + "ssdp": [{ + "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:2", + "friendlyName": "Orange Livebox" + }], + "version": "1.8.6" +} diff --git a/custom_components/livebox/sensor.py b/custom_components/livebox/sensor.py new file mode 100644 index 0000000..0b7e3f1 --- /dev/null +++ b/custom_components/livebox/sensor.py @@ -0,0 +1,61 @@ +"""Sensor for Livebox router.""" +import logging + +from homeassistant.components.sensor import SensorEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import COORDINATOR, DOMAIN, LIVEBOX_ID, SENSOR_TYPES +from .coordinator import LiveboxDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the sensors.""" + datas = hass.data[DOMAIN][config_entry.entry_id] + box_id = datas[LIVEBOX_ID] + coordinator = datas[COORDINATOR] + nmc = coordinator.data["nmc"] + entities = [ + FlowSensor( + coordinator, + box_id, + description, + ) + for description in SENSOR_TYPES + ] + if nmc.get("WanMode") is not None and "ETHERNET" not in nmc["WanMode"].upper(): + async_add_entities(entities, True) + + +class FlowSensor(CoordinatorEntity[LiveboxDataUpdateCoordinator], SensorEntity): + """Representation of a livebox sensor.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator, box_id, description): + """Initialize the sensor.""" + super().__init__(coordinator) + self._attributs = description.attr + self._current = description.current_rate + self.entity_description = description + self._attr_unique_id = f"{box_id}_{self._current}" + self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} + + @property + def native_value(self): + """Return the native value of the device.""" + if self.coordinator.data["dsl_status"].get(self._current): + return round( + self.coordinator.data["dsl_status"][self._current] / 1000, + 2, + ) + return None + + @property + def extra_state_attributes(self): + """Return the device state attributes.""" + attributs = {} + for key, value in self._attributs.items(): + attributs[key] = self.coordinator.data["dsl_status"].get(value) + return attributs diff --git a/custom_components/livebox/services.yaml b/custom_components/livebox/services.yaml new file mode 100644 index 0000000..3d8b51a --- /dev/null +++ b/custom_components/livebox/services.yaml @@ -0,0 +1,13 @@ +# Livebox service entries description. + +reboot: + # Description of the service + description: Restart the Livebox. + +remove_call_missed: + description: Remove call missed + fields: + callId: + name: Call Id + description: ID of the call (information in the attribute field of the binary sensor). Value to put in quotation marks. If you omit so that it deletes all calls + example: '"666"' diff --git a/custom_components/livebox/strings.json b/custom_components/livebox/strings.json new file mode 100644 index 0000000..ec1e6ac --- /dev/null +++ b/custom_components/livebox/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "user": { + "title": "[%key:components::livebox::config::step::user::title%]", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]" + } + } + }, + "error": { + "login_inccorect": "[%key:components::livebox::config::error::login_inccorect%]", + "insufficient_permission": "[%key:components::livebox::config::error::insufficient_permission%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + }, + "options": { + "step": { + "init": { + "title": "[%key:components::livebox::options::step::init::title%]", + "description": "[%key:components::livebox::options::step::init::description%]", + "data": { + "lan_tracking": "[%key:components::livebox::options::step::init::data::lan_tracking%]", + "timeout_tracking": "[%key:components::livebox::options::step::init::data::timeout_tracking%]" + } + } + } + } +} diff --git a/custom_components/livebox/switch.py b/custom_components/livebox/switch.py new file mode 100644 index 0000000..8fc8159 --- /dev/null +++ b/custom_components/livebox/switch.py @@ -0,0 +1,87 @@ +"""Sensor for Livebox router.""" +import logging + +from homeassistant.components.switch import SwitchEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import COORDINATOR, DOMAIN, GUESTWIFI_ICON, LIVEBOX_API, LIVEBOX_ID +from .coordinator import LiveboxDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the sensors.""" + datas = hass.data[DOMAIN][config_entry.entry_id] + box_id = datas[LIVEBOX_ID] + api = datas[LIVEBOX_API] + coordinator = datas[COORDINATOR] + async_add_entities([WifiSwitch(coordinator, box_id, api)], True) + async_add_entities([GuestWifiSwitch(coordinator, box_id, api)], True) + + +class WifiSwitch(CoordinatorEntity[LiveboxDataUpdateCoordinator], SwitchEntity): + """Representation of a livebox sensor.""" + + _attr_name = "Wifi switch" + _attr_has_entity_name = True + + def __init__(self, coordinator, box_id, api): + """Initialize the sensor.""" + super().__init__(coordinator) + self._api = api + self._attr_unique_id = f"{box_id}_wifi" + self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} + + @property + def is_on(self): + """Return true if device is on.""" + return self.coordinator.data.get("wifi") + + async def async_turn_on(self): + """Turn the switch on.""" + parameters = {"Enable": "true", "Status": "true"} + await self.hass.async_add_executor_job(self._api.wifi.set_wifi, parameters) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self): + """Turn the switch off.""" + parameters = {"Enable": "false", "Status": "false"} + await self.hass.async_add_executor_job(self._api.wifi.set_wifi, parameters) + await self.coordinator.async_request_refresh() + + +class GuestWifiSwitch(CoordinatorEntity, SwitchEntity): + """Representation of a livebox sensor.""" + + _attr_name = "Guest Wifi switch" + _attr_icon = GUESTWIFI_ICON + _attr_has_entity_name = True + + def __init__(self, coordinator, box_id, api): + """Initialize the sensor.""" + super().__init__(coordinator) + self._api = api + self._attr_unique_id = f"{box_id}_guest_wifi" + self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} + + @property + def is_on(self): + """Return true if device is on.""" + return self.coordinator.data.get("guest_wifi") + + async def async_turn_on(self): + """Turn the switch on.""" + parameters = {"Enable": "true", "Status": "true"} + await self.hass.async_add_executor_job( + self._api.guestwifi.set_guest_wifi, parameters + ) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self): + """Turn the switch off.""" + parameters = {"Enable": "false", "Status": "false"} + await self.hass.async_add_executor_job( + self._api.guestwifi.set_guest_wifi, parameters + ) + await self.coordinator.async_request_refresh() diff --git a/custom_components/livebox/translations/en.json b/custom_components/livebox/translations/en.json new file mode 100644 index 0000000..1e12417 --- /dev/null +++ b/custom_components/livebox/translations/en.json @@ -0,0 +1,34 @@ +{ + "config": { + "title": "Orange Livebox", + "step": { + "user": { + "title": "Check your livebox", + "data": { + "host": "IP Address", + "username": "Username", + "password": "Password", + "port": "Port" + } + } + }, + "error": { + "login_inccorect": "User or password inccorect", + "insufficient_permission": "Insufficient permission.", + "unknown": "Unknown error occurred", + "cannot_connect": "Unable to connect to the livebox" + } + }, + "options": { + "step": { + "init": { + "title": "Device Tracking - Track devices on lan", + "description": "you want tracking yours wired devices in addition to wireless devices", + "data": { + "lan_tracking": "Wired tracking", + "timeout_tracking": "Timeout tracking" + } + } + } + } +} diff --git a/custom_components/livebox/translations/fr.json b/custom_components/livebox/translations/fr.json new file mode 100644 index 0000000..d354c76 --- /dev/null +++ b/custom_components/livebox/translations/fr.json @@ -0,0 +1,34 @@ +{ + "config": { + "title": "Orange Livebox", + "step": { + "user": { + "title": "Vérification de votre livebox.", + "data": { + "host": "Adresse IP", + "username": "Utilisateur", + "password": "Mot de passe", + "port": "Port" + } + } + }, + "error": { + "login_inccorect": "Utilisateur ou mot de passe incorrect.", + "insufficient_permission": "Permission insuffisante.", + "unknown": "Erreur inconnue.", + "cannot_connect": "Impossible de se connecter à la Livebox." + } + }, + "options": { + "step": { + "init": { + "title": "Suivre les équipements filaires", + "description": "Pour suivre les équipements filaires en plus des équipements Wifi", + "data": { + "lan_tracking": "Equipements Filaires", + "timeout_tracking": "Délai avant de considérer un équipement absent" + } + } + } + } +} diff --git a/custom_components/livebox/translations/nb.json b/custom_components/livebox/translations/nb.json new file mode 100644 index 0000000..e7da10b --- /dev/null +++ b/custom_components/livebox/translations/nb.json @@ -0,0 +1,34 @@ +{ + "config": { + "title": "Orange Livebox", + "step": { + "user": { + "title": "Sjekk liveboxen din", + "data": { + "host": "IP Address", + "username": "Brukernavn", + "password": "Passord", + "port": "Port" + } + } + }, + "error": { + "login_inccorect": "Feil bruker eller passord", + "insufficient_permission": "Utilstrekkelig tillatelse.", + "unknown": "Det oppstod en ukjent feil", + "cannot_connect": "Kan ikke koble til liveboxen" + } + }, + "options": { + "step": { + "init": { + "title": "Enhetssporing - Spor enheter på lan", + "description": "du vil spore dine kablede enheter i tillegg til trådløse enheter", + "data": { + "lan_tracking": "Kablet sporing", + "timeout_tracking": "Tid før overvejelse om manglende udstyr" + } + } + } + } +} diff --git a/hacs.json b/hacs.json index 65f7335..de5c1e1 100644 --- a/hacs.json +++ b/hacs.json @@ -1,8 +1,6 @@ { - "name": "Integration blueprint", - "filename": "integration_blueprint.zip", - "hide_default_branch": true, + "name": "Delta Dore Tydom", + "country": "FR", "homeassistant": "2023.3.0", - "render_readme": true, - "zip_release": true + "render_readme": true } \ No newline at end of file From 9a2688c7a5b5c0e00f6cb757690c371039179c08 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 25 Apr 2023 13:21:17 +0200 Subject: [PATCH 04/74] add logger --- config/configuration.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/configuration.yaml b/config/configuration.yaml index 8c0d4e4..b4b0f6f 100644 --- a/config/configuration.yaml +++ b/config/configuration.yaml @@ -6,3 +6,4 @@ logger: default: info logs: custom_components.integration_blueprint: debug + custom_components.deltadore-tydom: debug From 6227c1748bb224831503c0c599e8a8cc50c48913 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 25 Apr 2023 13:46:22 +0200 Subject: [PATCH 05/74] fix messages in config flox --- custom_components/deltadore-tydom/config_flow.py | 15 ++++++++++++++- custom_components/deltadore-tydom/const.py | 3 +-- custom_components/deltadore-tydom/manifest.json | 2 +- .../deltadore-tydom/translations/en.json | 14 +++++++------- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index 601d720..b0b41d7 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -160,7 +160,20 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: user_input = user_input or {} # If there is no user input or there were errors, show the form again, including any errors that were found with the input. return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=_errors + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_HOST, default=user_input.get(CONF_HOST) + ): cv.string, + vol.Required(CONF_MAC, default=user_input.get(CONF_MAC)): cv.string, + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD) + ): cv.string, + vol.Optional(CONF_PIN): str, + } + ), + errors=_errors, ) async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo): diff --git a/custom_components/deltadore-tydom/const.py b/custom_components/deltadore-tydom/const.py index 2332547..c57107a 100644 --- a/custom_components/deltadore-tydom/const.py +++ b/custom_components/deltadore-tydom/const.py @@ -5,8 +5,7 @@ # This is the internal name of the integration, it should also match the directory # name for the integration. -DOMAIN = "tydom" +DOMAIN = "deltadore-tydom" NAME = "Integration blueprint" VERSION = "0.0.1" ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/" -CONF_FORECAST = "pouet" diff --git a/custom_components/deltadore-tydom/manifest.json b/custom_components/deltadore-tydom/manifest.json index 805b907..07e73b6 100644 --- a/custom_components/deltadore-tydom/manifest.json +++ b/custom_components/deltadore-tydom/manifest.json @@ -1,5 +1,5 @@ { - "domain": "tydom", + "domain": "deltadore-tydom", "name": "Delta Dore TYDOM", "codeowners": [ "@CyrilP" diff --git a/custom_components/deltadore-tydom/translations/en.json b/custom_components/deltadore-tydom/translations/en.json index 7fa4eb0..5141df0 100644 --- a/custom_components/deltadore-tydom/translations/en.json +++ b/custom_components/deltadore-tydom/translations/en.json @@ -7,17 +7,17 @@ "data": { "name": "Custom Name of the inverter (used for sensors' prefix)", "host": "IP or hostname", - "port": "TCP port", - "password": "pass", - "pin": "pin", - "slave_id": "Modbus Slave address of the inverter", - "base_addr": "Modbus Register Map Base Address", - "scan_interval": "Polling Period (min: 5s max: 600s)" + "mac": "MAC address", + "password": "Password", + "pin": "Alarm PIN" } } }, "error": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "invalid_host": "Hostname or IP is invalid", + "invalid_macaddress": "MAC address is invalid", + "invalid_password": "Password is invalid" }, "abort": { "already_configured": "Device is already configured" From 4f78fbc5187731b52f6103901d736fa6197571cd Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 25 Apr 2023 23:34:41 +0200 Subject: [PATCH 06/74] async connection test --- .../deltadore-tydom/config_flow.py | 67 +++++---- .../deltadore-tydom/tydom/tydom_client.py | 131 ++++++++++++++++++ 2 files changed, 172 insertions(+), 26 deletions(-) create mode 100644 custom_components/deltadore-tydom/tydom/tydom_client.py diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index b0b41d7..d5c9dec 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol +from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from homeassistant import config_entries, exceptions from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PIN @@ -15,6 +16,12 @@ from .const import DOMAIN, LOGGER from .hub import Hub +from .tydom.tydom_client import ( + TydomClient, + TydomClientApiClientCommunicationError, + TydomClientApiClientAuthenticationError, + TydomClientApiClientError, +) # This is the schema that used to display the UI to the user. This simple # schema has a single required host field, but it could include a number of fields @@ -73,30 +80,6 @@ async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: if CONF_PIN in data: pin = data[CONF_PIN] - hub = Hub(hass, data[CONF_HOST], data[CONF_MAC], data[CONF_PASSWORD], pin) - # The dummy hub provides a `test_connection` method to ensure it's working - # as expected - result = hub.test_connection() - if not result: - # If there is an error, raise an exception to notify HA that there was a - # problem. The UI will also show there was a problem - raise CannotConnect - - # If your PyPI package is not built with async, pass your methods - # to the executor: - # await hass.async_add_executor_job( - # your_validate_func, data["username"], data["password"] - # ) - - # If you cannot connect: - # throw CannotConnect - # If the authentication is wrong: - # InvalidAuth - - # Return info that you want to store in the config entry. - # "Title" is what is displayed to the user for this hub device - # It is stored internally in HA as part of the device config. - # See `async_step_user` below for how this is used return { CONF_HOST: data[CONF_HOST], CONF_MAC: data[CONF_MAC], @@ -135,7 +118,14 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: _errors = {} if user_input is not None: try: - info = await validate_input(self.hass, user_input) + await validate_input(self.hass, user_input) + # Ensure it's working as expected + await self._test_credentials( + mac=user_input[CONF_MAC], + password=user_input[CONF_PASSWORD], + pin=None, + host=user_input[CONF_HOST], + ) await self.async_set_unique_id(user_input[CONF_MAC]) self._abort_if_unique_id_configured() except CannotConnect: @@ -150,12 +140,24 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: _errors[CONF_MAC] = "invalid_macaddress" except InvalidPassword: _errors[CONF_PASSWORD] = "invalid_password" + except TydomClientApiClientCommunicationError: + traceback.print_exc() + _errors["base"] = "communication_error" + except TydomClientApiClientAuthenticationError: + traceback.print_exc() + _errors["base"] = "authentication_error" + except TydomClientApiClientError: + traceback.print_exc() + _errors["base"] = "unknown" + except Exception: # pylint: disable=broad-except traceback.print_exc() LOGGER.exception("Unexpected exception") _errors["base"] = "unknown" else: - return self.async_create_entry(title=info[CONF_MAC], data=user_input) + return self.async_create_entry( + title=user_input[CONF_MAC], data=user_input + ) user_input = user_input or {} # If there is no user input or there were errors, show the form again, including any errors that were found with the input. @@ -203,6 +205,19 @@ async def async_step_discovery_confirm(self, user_input=None): ), ) + async def _test_credentials( + self, mac: str, password: str, pin: str, host: str + ) -> None: + """Validate credentials.""" + client = TydomClient( + session=async_create_clientsession(self.hass, False), + mac=mac, + password=password, + alarm_pin=pin, + host=host, + ) + await client.async_connect() + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py new file mode 100644 index 0000000..95ee2e1 --- /dev/null +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -0,0 +1,131 @@ +"""Tydom API Client.""" +import os +import logging +import asyncio +import socket +import base64 +import async_timeout +import aiohttp + +from requests.auth import HTTPDigestAuth + +logger = logging.getLogger(__name__) + + +class TydomClientApiClientError(Exception): + """Exception to indicate a general API error.""" + + +class TydomClientApiClientCommunicationError(TydomClientApiClientError): + """Exception to indicate a communication error.""" + + +class TydomClientApiClientAuthenticationError(TydomClientApiClientError): + """Exception to indicate an authentication error.""" + + +class TydomClient: + """Tydom API Client.""" + + def __init__( + self, + session: aiohttp.ClientSession, + mac: str, + password: str, + alarm_pin: str = None, + host: str = "mediation.tydom.com", + ) -> None: + logger.debug("Initializing TydomClient Class") + + self._session = session + self._password = password + self._mac = mac + self._host = host + self._alarm_pin = alarm_pin + self._remote_mode = self._host == "mediation.tydom.com" + + if self._remote_mode: + logger.info("Configure remote mode (%s)", self._host) + self._cmd_prefix = "\x02" + self._ping_timeout = 40 + else: + logger.info("Configure local mode (%s)", self._host) + self._cmd_prefix = "" + self._ping_timeout = None + + async def async_connect(self) -> any: + """Connect to the Tydom API.""" + http_headers = { + "Connection": "Upgrade", + "Upgrade": "websocket", + "Host": self._host + ":443", + "Accept": "*/*", + "Sec-WebSocket-Key": self.generate_random_key(), + "Sec-WebSocket-Version": "13", + } + + return await self._api_wrapper( + method="get", + url=f"https://{self._host}/mediation/client?mac={self._mac}&appli=1", + headers=http_headers, + ) + + async def _api_wrapper( + self, + method: str, + url: str, + data: dict | None = None, + headers: dict | None = None, + ) -> any: + """Get information from the API.""" + try: + async with async_timeout.timeout(10): + response = await self._session.request( + method=method, + url=url, + headers=headers, + json=data, + ) + if response.status in (401, 403): + raise TydomClientApiClientAuthenticationError( + "Invalid credentials", + ) + response.raise_for_status() + return await response.json() + + except asyncio.TimeoutError as exception: + raise TydomClientApiClientCommunicationError( + "Timeout error fetching information", + ) from exception + except (aiohttp.ClientError, socket.gaierror) as exception: + raise TydomClientApiClientCommunicationError( + "Error fetching information", + ) from exception + except Exception as exception: # pylint: disable=broad-except + raise TydomClientApiClientError( + "Something really wrong happened!" + ) from exception + + def build_digest_headers(self, nonce): + """Build the headers of Digest Authentication.""" + digest_auth = HTTPDigestAuth(self._mac, self._password) + chal = {} + chal["nonce"] = nonce[2].split("=", 1)[1].split('"')[1] + chal["realm"] = ( + "ServiceMedia" if self._remote_mode is True else "protected area" + ) + chal["qop"] = "auth" + digest_auth._thread_local.chal = chal + digest_auth._thread_local.last_nonce = nonce + digest_auth._thread_local.nonce_count = 1 + return digest_auth.build_digest_header( + "GET", + "https://{host}:443/mediation/client?mac={mac}&appli=1".format( + host=self._host, mac=self._mac + ), + ) + + @staticmethod + def generate_random_key(): + """Generate 16 bytes random key for Sec-WebSocket-Keyand convert it to base64.""" + return str(base64.b64encode(os.urandom(16))) From d1e3c53ce64899d6cb3204e9e974949805d9ed0b Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Wed, 26 Apr 2023 13:54:27 +0200 Subject: [PATCH 07/74] async connection test --- .../deltadore-tydom/tydom/tydom_client.py | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index 95ee2e1..ba19485 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -6,6 +6,7 @@ import base64 import async_timeout import aiohttp +import re from requests.auth import HTTPDigestAuth @@ -86,6 +87,52 @@ async def _api_wrapper( headers=headers, json=data, ) + logger.info("response status : %s", response.status) + logger.info("response content : %s", await response.text()) + logger.info("response headers : %s", response.headers) + + m = re.match( + '.*nonce="([a-zA-Z0-9+=]+)".*', + response.headers.get("WWW-Authenticate"), + ) + if m: + logger.info("nonce : %s", m.group(1)) + else: + raise TydomClientApiClientError("Could't find auth nonce") + + headers["Authorization"] = self.build_digest_headers(m.group(1)) + + logger.info("new request headers : %s", headers) + # {'Authorization': ' + # Digest username="############", + # realm="protected area", + # nonce="e0317d0d0d4d2afe6c54ec928dfd7614", + # uri="/mediation/client?mac=############&appli=1", + # response="98a019dca77980a230eafed5e0acdf18", + # qop="auth", + # nc=00000001, + # cnonce="77e7fea3ef6ecc1d"'} + + # 'Authorization': ' + # Digest username="############", + # realm="protected area", + # nonce="31a7d8554d11c367dda81159b485c408", + # uri="/mediation/client?mac=############&appli=1", + # response="45fd17aecff639f29532091162ad64a1", + # qop="auth", + # nc=00000002, + # cnonce="e92ddde26cac2ad5" + + response = await self._session.ws_connect( + method=method, + url=url, + headers=headers, + ) + + logger.info("response status : %s", response.status) + logger.info("response content : %s", await response.text()) + logger.info("response headers : %s", response.headers) + if response.status in (401, 403): raise TydomClientApiClientAuthenticationError( "Invalid credentials", @@ -110,7 +157,7 @@ def build_digest_headers(self, nonce): """Build the headers of Digest Authentication.""" digest_auth = HTTPDigestAuth(self._mac, self._password) chal = {} - chal["nonce"] = nonce[2].split("=", 1)[1].split('"')[1] + chal["nonce"] = nonce chal["realm"] = ( "ServiceMedia" if self._remote_mode is True else "protected area" ) @@ -118,12 +165,13 @@ def build_digest_headers(self, nonce): digest_auth._thread_local.chal = chal digest_auth._thread_local.last_nonce = nonce digest_auth._thread_local.nonce_count = 1 - return digest_auth.build_digest_header( + digest = digest_auth.build_digest_header( "GET", "https://{host}:443/mediation/client?mac={mac}&appli=1".format( host=self._host, mac=self._mac ), ) + return digest @staticmethod def generate_random_key(): From 427942d09d4d6e051b86226304c3217aeb7c4ed0 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Wed, 26 Apr 2023 23:32:42 +0200 Subject: [PATCH 08/74] add some messages parsing --- custom_components/deltadore-tydom/__init__.py | 12 +- .../deltadore-tydom/config_flow.py | 32 +- custom_components/deltadore-tydom/hub.py | 36 +- .../deltadore-tydom/tydom/MessageHandler.py | 986 +++++++++++++++++- .../deltadore-tydom/tydom/TydomClient.py | 399 ------- .../deltadore-tydom/tydom/tydom_client.py | 333 ++++-- 6 files changed, 1302 insertions(+), 496 deletions(-) delete mode 100644 custom_components/deltadore-tydom/tydom/TydomClient.py diff --git a/custom_components/deltadore-tydom/__init__.py b/custom_components/deltadore-tydom/__init__.py index 085ccc1..413d8fa 100644 --- a/custom_components/deltadore-tydom/__init__.py +++ b/custom_components/deltadore-tydom/__init__.py @@ -4,6 +4,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from . import hub from .const import DOMAIN @@ -30,7 +31,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = tydom_hub - await tydom_hub.setup() + try: + connection = await tydom_hub.connect() + entry.async_create_background_task( + target=tydom_hub.setup(connection), hass=hass, name="Tydom" + ) + # entry.async_create_background_task( + # target=tydom_hub.ping(connection), hass=hass, name="Tydom ping" + # ) + except Exception as err: + raise ConfigEntryNotReady from err # This creates each HA object for each platform your device requires. # It's done by calling the `async_setup_entry` function in each platform module. diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index d5c9dec..e4c8627 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol + from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from homeassistant import config_entries, exceptions @@ -15,9 +16,8 @@ from homeassistant.components import dhcp from .const import DOMAIN, LOGGER -from .hub import Hub +from . import hub from .tydom.tydom_client import ( - TydomClient, TydomClientApiClientCommunicationError, TydomClientApiClientAuthenticationError, TydomClientApiClientError, @@ -70,6 +70,9 @@ async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: if len(data[CONF_HOST]) < 3: raise InvalidHost + if not host_valid(data[CONF_HOST]): + raise InvalidHost + if len(data[CONF_MAC]) != 12: raise InvalidMacAddress @@ -120,12 +123,14 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: try: await validate_input(self.hass, user_input) # Ensure it's working as expected - await self._test_credentials( - mac=user_input[CONF_MAC], - password=user_input[CONF_PASSWORD], - pin=None, - host=user_input[CONF_HOST], + tydom_hub = hub.Hub( + self.hass, + user_input[CONF_HOST], + user_input[CONF_MAC], + user_input[CONF_PASSWORD], + None, ) + await tydom_hub.test_credentials() await self.async_set_unique_id(user_input[CONF_MAC]) self._abort_if_unique_id_configured() except CannotConnect: @@ -205,19 +210,6 @@ async def async_step_discovery_confirm(self, user_input=None): ), ) - async def _test_credentials( - self, mac: str, password: str, pin: str, host: str - ) -> None: - """Validate credentials.""" - client = TydomClient( - session=async_create_clientsession(self.hass, False), - mac=mac, - password=password, - alarm_pin=pin, - host=host, - ) - await client.async_connect() - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 11dcf4c..76f5990 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -9,9 +9,12 @@ import asyncio import random import logging +import time +from aiohttp import ClientWebSocketResponse from homeassistant.core import HomeAssistant -from .tydom.TydomClient import TydomClient +from homeassistant.helpers.aiohttp_client import async_create_clientsession +from .tydom.tydom_client import TydomClient logger = logging.getLogger(__name__) @@ -39,6 +42,7 @@ def __init__( self._id = mac.lower() self._tydom_client = TydomClient( + session=async_create_clientsession(self._hass, False), mac=self._mac, host=self._host, password=self._pass, @@ -57,16 +61,26 @@ def hub_id(self) -> str: """ID for dummy hub.""" return self._id - async def test_connection(self) -> bool: - """Test connectivity to the Tydom is OK.""" - try: - return await self._tydom_client.connect() is not None - except: - return False - - async def setup(self) -> None: - # Listen to tydom events. - await self._hass.async_add_executor_job(self._tydom_client.listen_tydom()) + async def connect(self) -> ClientWebSocketResponse: + """Connect to Tydom""" + return await self._tydom_client.async_connect() + + async def test_credentials(self) -> None: + """Validate credentials.""" + connection = await self.connect() + await connection.close() + + async def setup(self, connection: ClientWebSocketResponse) -> None: + """Listen to tydom events.""" + logger.info("Listen to tydom events") + await self._tydom_client.listen_tydom(connection) + + async def ping(self, connection: ClientWebSocketResponse) -> None: + """Periodically send pings""" + logger.info("Sending ping") + while True: + await self._tydom_client.ping() + await asyncio.sleep(10) class Roller: diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 7eba946..d936fb8 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -8,16 +8,996 @@ logger = logging.getLogger(__name__) +# Dicts +deviceAlarmKeywords = [ + "alarmMode", + "alarmState", + "alarmSOS", + "zone1State", + "zone2State", + "zone3State", + "zone4State", + "zone5State", + "zone6State", + "zone7State", + "zone8State", + "gsmLevel", + "inactiveProduct", + "zone1State", + "liveCheckRunning", + "networkDefect", + "unitAutoProtect", + "unitBatteryDefect", + "unackedEvent", + "alarmTechnical", + "systAutoProtect", + "systBatteryDefect", + "systSupervisionDefect", + "systOpenIssue", + "systTechnicalDefect", + "videoLinkDefect", + "outTemperature", + "kernelUpToDate", + "irv1State", + "irv2State", + "irv3State", + "irv4State", + "simDefect", + "remoteSurveyDefect", + "systSectorDefect", +] +deviceAlarmDetailsKeywords = [ + "alarmSOS", + "zone1State", + "zone2State", + "zone3State", + "zone4State", + "zone5State", + "zone6State", + "zone7State", + "zone8State", + "gsmLevel", + "inactiveProduct", + "zone1State", + "liveCheckRunning", + "networkDefect", + "unitAutoProtect", + "unitBatteryDefect", + "unackedEvent", + "alarmTechnical", + "systAutoProtect", + "systBatteryDefect", + "systSupervisionDefect", + "systOpenIssue", + "systTechnicalDefect", + "videoLinkDefect", + "outTemperature", +] + +deviceLightKeywords = [ + "level", + "onFavPos", + "thermicDefect", + "battDefect", + "loadDefect", + "cmdDefect", + "onPresenceDetected", + "onDusk", +] +deviceLightDetailsKeywords = [ + "onFavPos", + "thermicDefect", + "battDefect", + "loadDefect", + "cmdDefect", + "onPresenceDetected", + "onDusk", +] + +deviceDoorKeywords = ["openState", "intrusionDetect"] +deviceDoorDetailsKeywords = [ + "onFavPos", + "thermicDefect", + "obstacleDefect", + "intrusion", + "battDefect", +] + +deviceCoverKeywords = [ + "position", + "slope", + "onFavPos", + "thermicDefect", + "obstacleDefect", + "intrusion", + "battDefect", +] +deviceCoverDetailsKeywords = [ + "onFavPos", + "thermicDefect", + "obstacleDefect", + "intrusion", + "battDefect", + "position", + "slope", +] + +deviceBoilerKeywords = [ + "thermicLevel", + "delayThermicLevel", + "temperature", + "authorization", + "hvacMode", + "timeDelay", + "tempoOn", + "antifrostOn", + "openingDetected", + "presenceDetected", + "absence", + "loadSheddingOn", + "setpoint", + "delaySetpoint", + "anticipCoeff", + "outTemperature", +] + +deviceSwitchKeywords = ["thermicDefect"] +deviceSwitchDetailsKeywords = ["thermicDefect"] + +deviceMotionKeywords = ["motionDetect"] +deviceMotionDetailsKeywords = ["motionDetect"] + +device_conso_classes = { + "energyInstantTotElec": "current", + "energyInstantTotElec_Min": "current", + "energyInstantTotElec_Max": "current", + "energyScaleTotElec_Min": "current", + "energyScaleTotElec_Max": "current", + "energyInstantTotElecP": "power", + "energyInstantTotElec_P_Min": "power", + "energyInstantTotElec_P_Max": "power", + "energyScaleTotElec_P_Min": "power", + "energyScaleTotElec_P_Max": "power", + "energyInstantTi1P": "power", + "energyInstantTi1P_Min": "power", + "energyInstantTi1P_Max": "power", + "energyScaleTi1P_Min": "power", + "energyScaleTi1P_Max": "power", + "energyInstantTi1I": "current", + "energyInstantTi1I_Min": "current", + "energyInstantTi1I_Max": "current", + "energyScaleTi1I_Min": "current", + "energyScaleTi1I_Max": "current", + "energyTotIndexWatt": "energy", + "energyIndexHeatWatt": "energy", + "energyIndexECSWatt": "energy", + "energyIndexHeatGas": "energy", + "outTemperature": "temperature", +} + +device_conso_unit_of_measurement = { + "energyInstantTotElec": "A", + "energyInstantTotElec_Min": "A", + "energyInstantTotElec_Max": "A", + "energyScaleTotElec_Min": "A", + "energyScaleTotElec_Max": "A", + "energyInstantTotElecP": "W", + "energyInstantTotElec_P_Min": "W", + "energyInstantTotElec_P_Max": "W", + "energyScaleTotElec_P_Min": "W", + "energyScaleTotElec_P_Max": "W", + "energyInstantTi1P": "W", + "energyInstantTi1P_Min": "W", + "energyInstantTi1P_Max": "W", + "energyScaleTi1P_Min": "W", + "energyScaleTi1P_Max": "W", + "energyInstantTi1I": "A", + "energyInstantTi1I_Min": "A", + "energyInstantTi1I_Max": "A", + "energyScaleTi1I_Min": "A", + "energyScaleTi1I_Max": "A", + "energyTotIndexWatt": "Wh", + "energyIndexHeatWatt": "Wh", + "energyIndexECSWatt": "Wh", + "energyIndexHeatGas": "Wh", + "outTemperature": "C", +} +device_conso_keywords = device_conso_classes.keys() + +deviceSmokeKeywords = ["techSmokeDefect"] + +# Device dict for parsing +device_name = dict() +device_endpoint = dict() +device_type = dict() + class MessageHandler: - def __init__(self, incoming_bytes, tydom_client): + """Handle incomming Tydom messages""" + + def __init__(self, incoming_bytes, tydom_client, cmd_prefix): self.incoming_bytes = incoming_bytes self.tydom_client = tydom_client - self.cmd_prefix = tydom_client.cmd_prefix + self.cmd_prefix = cmd_prefix async def incoming_triage(self): + """Identify message type and dispatch the result""" + bytes_str = self.incoming_bytes incoming = None first = str(bytes_str[:40]) + try: + if "Uri-Origin: /refresh/all" in first in first: + pass + elif ("PUT /devices/data" in first) or ("/devices/cdata" in first): + logger.debug("PUT /devices/data message detected !") + try: + try: + incoming = self.parse_put_response(bytes_str) + except BaseException: + # Tywatt response starts at 7 + incoming = self.parse_put_response(bytes_str, 7) + await self.parse_response(incoming) + except BaseException: + logger.error( + "Error when parsing devices/data tydom message (%s)", bytes_str + ) + elif "scn" in first: + try: + # FIXME + # incoming = get(bytes_str) + incoming = first + await self.parse_response(incoming) + logger.debug("Scenarii message processed") + except BaseException: + logger.error( + "Error when parsing Scenarii tydom message (%s)", bytes_str + ) + elif "POST" in first: + try: + incoming = self.parse_put_response(bytes_str) + await self.parse_response(incoming) + logger.debug("POST message processed") + except BaseException: + logger.error( + "Error when parsing POST tydom message (%s)", bytes_str + ) + elif "HTTP/1.1" in first: + response = self.response_from_bytes(bytes_str[len(self.cmd_prefix) :]) + incoming = response.data.decode("utf-8") + try: + await self.parse_response(incoming) + except BaseException: + logger.error( + "Error when parsing HTTP/1.1 tydom message (%s)", bytes_str + ) + else: + logger.warning("Unknown tydom message type received (%s)", bytes_str) + + except Exception as e: + logger.error("Technical error when parsing tydom message (%s)", bytes_str) + logger.debug("Incoming payload (%s)", incoming) + + # Basic response parsing. Typically GET responses + instanciate covers and + # alarm class for updating data + async def parse_response(self, incoming): + data = incoming + msg_type = None + first = str(data[:40]) + + if data != "": + if "id_catalog" in data: + msg_type = "msg_config" + elif "cmetadata" in data: + msg_type = "msg_cmetadata" + elif "cdata" in data: + msg_type = "msg_cdata" + elif "id" in first: + msg_type = "msg_data" + elif "doctype" in first: + msg_type = "msg_html" + elif "productName" in first: + msg_type = "msg_info" + + if msg_type is None: + logger.warning("Unknown message type received %s", data) + else: + logger.debug("Message received detected as (%s)", msg_type) + try: + if msg_type == "msg_config": + parsed = json.loads(data) + await self.parse_config_data(parsed=parsed) + + elif msg_type == "msg_cmetadata": + parsed = json.loads(data) + await self.parse_cmeta_data(parsed=parsed) + + elif msg_type == "msg_data": + parsed = json.loads(data) + await self.parse_devices_data(parsed=parsed) + + elif msg_type == "msg_cdata": + parsed = json.loads(data) + await self.parse_devices_cdata(parsed=parsed) + + elif msg_type == "msg_html": + logger.debug("HTML Response ?") + + elif msg_type == "msg_info": + pass + except Exception as e: + logger.error("Error on parsing tydom response (%s)", e) + logger.debug("Incoming data parsed with success") + + @staticmethod + async def parse_config_data(parsed): + for i in parsed["endpoints"]: + device_unique_id = str(i["id_endpoint"]) + "_" + str(i["id_device"]) + + if ( + i["last_usage"] == "shutter" + or i["last_usage"] == "klineShutter" + or i["last_usage"] == "light" + or i["last_usage"] == "window" + or i["last_usage"] == "windowFrench" + or i["last_usage"] == "windowSliding" + or i["last_usage"] == "belmDoor" + or i["last_usage"] == "klineDoor" + or i["last_usage"] == "klineWindowFrench" + or i["last_usage"] == "klineWindowSliding" + or i["last_usage"] == "garage_door" + or i["last_usage"] == "gate" + ): + device_name[device_unique_id] = i["name"] + device_type[device_unique_id] = i["last_usage"] + device_endpoint[device_unique_id] = i["id_endpoint"] + + if i["last_usage"] == "boiler" or i["last_usage"] == "conso": + device_name[device_unique_id] = i["name"] + device_type[device_unique_id] = i["last_usage"] + device_endpoint[device_unique_id] = i["id_endpoint"] + + if i["last_usage"] == "alarm": + device_name[device_unique_id] = "Tyxal Alarm" + device_type[device_unique_id] = "alarm" + device_endpoint[device_unique_id] = i["id_endpoint"] + + if i["last_usage"] == "electric": + device_name[device_unique_id] = i["name"] + device_type[device_unique_id] = "boiler" + device_endpoint[device_unique_id] = i["id_endpoint"] + + if i["last_usage"] == "sensorDFR": + device_name[device_unique_id] = i["name"] + device_type[device_unique_id] = "smoke" + device_endpoint[device_unique_id] = i["id_endpoint"] + + if i["last_usage"] == "": + device_name[device_unique_id] = i["name"] + device_type[device_unique_id] = "unknown" + device_endpoint[device_unique_id] = i["id_endpoint"] + + logger.debug("Configuration updated") + + async def parse_cmeta_data(self, parsed): + for i in parsed: + for endpoint in i["endpoints"]: + if len(endpoint["cmetadata"]) > 0: + for elem in endpoint["cmetadata"]: + device_id = i["id"] + endpoint_id = endpoint["id"] + unique_id = str(endpoint_id) + "_" + str(device_id) + + if elem["name"] == "energyIndex": + device_name[unique_id] = "Tywatt" + device_type[unique_id] = "conso" + for params in elem["parameters"]: + if params["name"] == "dest": + for dest in params["enum_values"]: + url = ( + "/devices/" + + str(i["id"]) + + "/endpoints/" + + str(endpoint["id"]) + + "/cdata?name=" + + elem["name"] + + "&dest=" + + dest + + "&reset=false" + ) + self.tydom_client.add_poll_device_url(url) + logger.debug("Add poll device : %s", url) + elif elem["name"] == "energyInstant": + device_name[unique_id] = "Tywatt" + device_type[unique_id] = "conso" + for params in elem["parameters"]: + if params["name"] == "unit": + for unit in params["enum_values"]: + url = ( + "/devices/" + + str(i["id"]) + + "/endpoints/" + + str(endpoint["id"]) + + "/cdata?name=" + + elem["name"] + + "&unit=" + + unit + + "&reset=false" + ) + self.tydom_client.add_poll_device_url(url) + logger.debug("Add poll device : " + url) + elif elem["name"] == "energyDistrib": + device_name[unique_id] = "Tywatt" + device_type[unique_id] = "conso" + for params in elem["parameters"]: + if params["name"] == "src": + for src in params["enum_values"]: + url = ( + "/devices/" + + str(i["id"]) + + "/endpoints/" + + str(endpoint["id"]) + + "/cdata?name=" + + elem["name"] + + "&period=YEAR&periodOffset=0&src=" + + src + ) + self.tydom_client.add_poll_device_url(url) + logger.debug("Add poll device : " + url) + + logger.debug("Metadata configuration updated") + + async def parse_devices_data(self, parsed): + for i in parsed: + for endpoint in i["endpoints"]: + if endpoint["error"] == 0 and len(endpoint["data"]) > 0: + try: + attr_alarm = {} + attr_cover = {} + attr_door = {} + attr_ukn = {} + attr_window = {} + attr_light = {} + attr_gate = {} + attr_boiler = {} + attr_smoke = {} + device_id = i["id"] + endpoint_id = endpoint["id"] + unique_id = str(endpoint_id) + "_" + str(device_id) + name_of_id = self.get_name_from_id(unique_id) + type_of_id = self.get_type_from_id(unique_id) + + logger.info( + "Device update (id=%s, endpoint=%s, name=%s, type=%s)", + device_id, + endpoint_id, + name_of_id, + type_of_id, + ) + + for elem in endpoint["data"]: + element_name = elem["name"] + element_value = elem["value"] + element_validity = elem["validity"] + print_id = name_of_id if len(name_of_id) != 0 else device_id + + if type_of_id == "light": + if ( + element_name in deviceLightKeywords + and element_validity == "upToDate" + ): + attr_light["device_id"] = device_id + attr_light["endpoint_id"] = endpoint_id + attr_light["id"] = ( + str(device_id) + "_" + str(endpoint_id) + ) + attr_light["light_name"] = print_id + attr_light["name"] = print_id + attr_light["device_type"] = "light" + attr_light[element_name] = element_value + + if type_of_id == "shutter" or type_of_id == "klineShutter": + if ( + element_name in deviceCoverKeywords + and element_validity == "upToDate" + ): + attr_cover["device_id"] = device_id + attr_cover["endpoint_id"] = endpoint_id + attr_cover["id"] = ( + str(device_id) + "_" + str(endpoint_id) + ) + attr_cover["cover_name"] = print_id + attr_cover["name"] = print_id + attr_cover["device_type"] = "cover" + + if element_name == "slope": + attr_cover["tilt"] = element_value + else: + attr_cover[element_name] = element_value + + if type_of_id == "belmDoor" or type_of_id == "klineDoor": + if ( + element_name in deviceDoorKeywords + and element_validity == "upToDate" + ): + attr_door["device_id"] = device_id + attr_door["endpoint_id"] = endpoint_id + attr_door["id"] = ( + str(device_id) + "_" + str(endpoint_id) + ) + attr_door["door_name"] = print_id + attr_door["name"] = print_id + attr_door["device_type"] = "sensor" + attr_door["element_name"] = element_name + attr_door[element_name] = element_value + + if ( + type_of_id == "windowFrench" + or type_of_id == "window" + or type_of_id == "windowSliding" + or type_of_id == "klineWindowFrench" + or type_of_id == "klineWindowSliding" + ): + if ( + element_name in deviceDoorKeywords + and element_validity == "upToDate" + ): + attr_window["device_id"] = device_id + attr_window["endpoint_id"] = endpoint_id + attr_window["id"] = ( + str(device_id) + "_" + str(endpoint_id) + ) + attr_window["door_name"] = print_id + attr_window["name"] = print_id + attr_window["device_type"] = "sensor" + attr_window["element_name"] = element_name + attr_window[element_name] = element_value + + if type_of_id == "boiler": + if ( + element_name in deviceBoilerKeywords + and element_validity == "upToDate" + ): + attr_boiler["device_id"] = device_id + attr_boiler["endpoint_id"] = endpoint_id + attr_boiler["id"] = ( + str(device_id) + "_" + str(endpoint_id) + ) + # attr_boiler['boiler_name'] = print_id + attr_boiler["name"] = print_id + attr_boiler["device_type"] = "climate" + attr_boiler[element_name] = element_value + + if type_of_id == "alarm": + if ( + element_name in deviceAlarmKeywords + and element_validity == "upToDate" + ): + attr_alarm["device_id"] = device_id + attr_alarm["endpoint_id"] = endpoint_id + attr_alarm["id"] = ( + str(device_id) + "_" + str(endpoint_id) + ) + attr_alarm["alarm_name"] = "Tyxal Alarm" + attr_alarm["name"] = "Tyxal Alarm" + attr_alarm["device_type"] = "alarm_control_panel" + attr_alarm[element_name] = element_value + + if type_of_id == "garage_door" or type_of_id == "gate": + if ( + element_name in deviceSwitchKeywords + and element_validity == "upToDate" + ): + attr_gate["device_id"] = device_id + attr_gate["endpoint_id"] = endpoint_id + attr_gate["id"] = ( + str(device_id) + "_" + str(endpoint_id) + ) + attr_gate["switch_name"] = print_id + attr_gate["name"] = print_id + attr_gate["device_type"] = "switch" + attr_gate[element_name] = element_value + + if type_of_id == "conso": + if ( + element_name in device_conso_keywords + and element_validity == "upToDate" + ): + attr_conso = { + "device_id": device_id, + "endpoint_id": endpoint_id, + "id": str(device_id) + "_" + str(endpoint_id), + "name": print_id, + "device_type": "sensor", + element_name: element_value, + } + + if element_name in device_conso_classes: + attr_conso[ + "device_class" + ] = device_conso_classes[element_name] + + if element_name in device_conso_unit_of_measurement: + attr_conso[ + "unit_of_measurement" + ] = device_conso_unit_of_measurement[ + element_name + ] + + # new_conso = Sensor( + # elem_name=element_name, + # tydom_attributes_payload=attr_conso, + # mqtt=self.mqtt_client, + # ) + # await new_conso.update() + + if type_of_id == "smoke": + if ( + element_name in deviceSmokeKeywords + and element_validity == "upToDate" + ): + attr_smoke["device_id"] = device_id + attr_smoke["device_class"] = "smoke" + attr_smoke["endpoint_id"] = endpoint_id + attr_smoke["id"] = ( + str(device_id) + "_" + str(endpoint_id) + ) + attr_smoke["name"] = print_id + attr_smoke["device_type"] = "sensor" + attr_smoke["element_name"] = element_name + attr_smoke[element_name] = element_value + + if type_of_id == "unknown": + if ( + element_name in deviceMotionKeywords + and element_validity == "upToDate" + ): + attr_ukn["device_id"] = device_id + attr_ukn["endpoint_id"] = endpoint_id + attr_ukn["id"] = ( + str(device_id) + "_" + str(endpoint_id) + ) + attr_ukn["name"] = print_id + attr_ukn["device_type"] = "sensor" + attr_ukn["element_name"] = element_name + attr_ukn[element_name] = element_value + elif ( + element_name in deviceDoorKeywords + and element_validity == "upToDate" + ): + attr_ukn["device_id"] = device_id + attr_ukn["endpoint_id"] = endpoint_id + attr_ukn["id"] = ( + str(device_id) + "_" + str(endpoint_id) + ) + attr_ukn["name"] = print_id + attr_ukn["device_type"] = "sensor" + attr_ukn["element_name"] = element_name + attr_ukn[element_name] = element_value + + except Exception as e: + logger.error("msg_data error in parsing !") + logger.error(e) + + if ( + "device_type" in attr_cover + and attr_cover["device_type"] == "cover" + ): + # new_cover = Cover( + # tydom_attributes=attr_cover, mqtt=self.mqtt_client + # ) + # await new_cover.update() + pass + elif ( + "device_type" in attr_door + and attr_door["device_type"] == "sensor" + ): + # new_door = Sensor( + # elem_name=attr_door["element_name"], + # tydom_attributes_payload=attr_door, + # mqtt=self.mqtt_client, + # ) + # await new_door.update() + pass + elif ( + "device_type" in attr_window + and attr_window["device_type"] == "sensor" + ): + # new_window = Sensor( + # elem_name=attr_window["element_name"], + # tydom_attributes_payload=attr_window, + # mqtt=self.mqtt_client, + # ) + # await new_window.update() + pass + elif ( + "device_type" in attr_light + and attr_light["device_type"] == "light" + ): + # new_light = Light( + # tydom_attributes=attr_light, mqtt=self.mqtt_client + # ) + # await new_light.update() + pass + elif ( + "device_type" in attr_boiler + and attr_boiler["device_type"] == "climate" + ): + # new_boiler = Boiler( + # tydom_attributes=attr_boiler, + # tydom_client=self.tydom_client, + # mqtt=self.mqtt_client, + # ) + # await new_boiler.update() + pass + elif ( + "device_type" in attr_gate + and attr_gate["device_type"] == "switch" + ): + # new_gate = Switch( + # tydom_attributes=attr_gate, mqtt=self.mqtt_client + # ) + # await new_gate.update() + pass + elif ( + "device_type" in attr_smoke + and attr_smoke["device_type"] == "sensor" + ): + # new_smoke = Sensor( + # elem_name=attr_smoke["element_name"], + # tydom_attributes_payload=attr_smoke, + # mqtt=self.mqtt_client, + # ) + # await new_smoke.update() + pass + elif ( + "device_type" in attr_ukn + and attr_ukn["device_type"] == "sensor" + ): + # new_ukn = Sensor( + # elem_name=attr_ukn["element_name"], + # tydom_attributes_payload=attr_ukn, + # mqtt=self.mqtt_client, + # ) + # await new_ukn.update() + pass + + # Get last known state (for alarm) # NEW METHOD + elif ( + "device_type" in attr_alarm + and attr_alarm["device_type"] == "alarm_control_panel" + ): + state = None + sos_state = False + try: + if ( + "alarmState" in attr_alarm + and attr_alarm["alarmState"] == "ON" + ) or ( + "alarmState" in attr_alarm and attr_alarm["alarmState"] + ) == "QUIET": + state = "triggered" + + elif ( + "alarmState" in attr_alarm + and attr_alarm["alarmState"] == "DELAYED" + ): + state = "pending" + + if ( + "alarmSOS" in attr_alarm + and attr_alarm["alarmSOS"] == "true" + ): + state = "triggered" + sos_state = True + + elif ( + "alarmMode" in attr_alarm + and attr_alarm["alarmMode"] == "ON" + ): + state = "armed_away" + elif ( + "alarmMode" in attr_alarm + and attr_alarm["alarmMode"] == "ZONE" + ): + state = "armed_home" + elif ( + "alarmMode" in attr_alarm + and attr_alarm["alarmMode"] == "OFF" + ): + state = "disarmed" + elif ( + "alarmMode" in attr_alarm + and attr_alarm["alarmMode"] == "MAINTENANCE" + ): + state = "disarmed" + + if sos_state: + logger.warning("SOS !") + + if not (state is None): + # alarm = Alarm( + # current_state=state, + # alarm_pin=self.tydom_client.alarm_pin, + # tydom_attributes=attr_alarm, + # mqtt=self.mqtt_client, + # ) + # await alarm.update() + pass + + except Exception as e: + logger.error("Error in alarm parsing !") + logger.error(e) + pass + else: + pass + + async def parse_devices_cdata(self, parsed): + for i in parsed: + for endpoint in i["endpoints"]: + if endpoint["error"] == 0 and len(endpoint["cdata"]) > 0: + try: + device_id = i["id"] + endpoint_id = endpoint["id"] + unique_id = str(endpoint_id) + "_" + str(device_id) + name_of_id = self.get_name_from_id(unique_id) + type_of_id = self.get_type_from_id(unique_id) + logger.info( + "Device configured (id=%s, endpoint=%s, name=%s, type=%s)", + device_id, + endpoint_id, + name_of_id, + type_of_id, + ) + + for elem in endpoint["cdata"]: + if type_of_id == "conso": + if elem["name"] == "energyIndex": + device_class_of_id = "energy" + state_class_of_id = "total_increasing" + unit_of_measurement_of_id = "Wh" + element_name = elem["parameters"]["dest"] + element_index = "counter" + + attr_conso = { + "device_id": device_id, + "endpoint_id": endpoint_id, + "id": unique_id, + "name": name_of_id, + "device_type": "sensor", + "device_class": device_class_of_id, + "state_class": state_class_of_id, + "unit_of_measurement": unit_of_measurement_of_id, + element_name: elem["values"][element_index], + } + + # new_conso = Sensor( + # elem_name=element_name, + # tydom_attributes_payload=attr_conso, + # mqtt=self.mqtt_client, + # ) + # await new_conso.update() + + elif elem["name"] == "energyInstant": + device_class_of_id = "current" + state_class_of_id = "measurement" + unit_of_measurement_of_id = "VA" + element_name = elem["parameters"]["unit"] + element_index = "measure" + + attr_conso = { + "device_id": device_id, + "endpoint_id": endpoint_id, + "id": unique_id, + "name": name_of_id, + "device_type": "sensor", + "device_class": device_class_of_id, + "state_class": state_class_of_id, + "unit_of_measurement": unit_of_measurement_of_id, + element_name: elem["values"][element_index], + } + + # new_conso = Sensor( + # elem_name=element_name, + # tydom_attributes_payload=attr_conso, + # mqtt=self.mqtt_client, + # ) + # await new_conso.update() + + elif elem["name"] == "energyDistrib": + for elName in elem["values"]: + if elName != "date": + element_name = elName + element_index = elName + attr_conso = { + "device_id": device_id, + "endpoint_id": endpoint_id, + "id": unique_id, + "name": name_of_id, + "device_type": "sensor", + "device_class": "energy", + "state_class": "total_increasing", + "unit_of_measurement": "Wh", + element_name: elem["values"][ + element_index + ], + } + + # new_conso = Sensor( + # elem_name=element_name, + # tydom_attributes_payload=attr_conso, + # mqtt=self.mqtt_client, + # ) + # await new_conso.update() + + except Exception as e: + logger.error("Error when parsing msg_cdata (%s)", e) + + # PUT response DIRTY parsing + def parse_put_response(self, bytes_str, start=6): + # TODO : Find a cooler way to parse nicely the PUT HTTP response + resp = bytes_str[len(self.cmd_prefix) :].decode("utf-8") + fields = resp.split("\r\n") + fields = fields[start:] # ignore the PUT / HTTP/1.1 + end_parsing = False + i = 0 + output = str() + while not end_parsing: + field = fields[i] + if len(field) == 0 or field == "0": + end_parsing = True + else: + output += field + i = i + 2 + parsed = json.loads(output) + return json.dumps(parsed) + + # FUNCTIONS + + @staticmethod + def response_from_bytes(data): + sock = BytesIOSocket(data) + response = HTTPResponse(sock) + response.begin() + return urllib3.HTTPResponse.from_httplib(response) + + @staticmethod + def put_response_from_bytes(data): + request = HTTPRequest(data) + return request + + def get_type_from_id(self, id): + device_type_detected = "" + if id in device_type.keys(): + device_type_detected = device_type[id] + else: + logger.warning("Unknown device type (%s)", id) + return device_type_detected + + # Get pretty name for a device id + def get_name_from_id(self, id): + name = "" + if id in device_name.keys(): + name = device_name[id] + else: + logger.warning("Unknown device name (%s)", id) + return name + + +class BytesIOSocket: + def __init__(self, content): + self.handle = BytesIO(content) + + def makefile(self, mode): + return self.handle + + +class HTTPRequest(BaseHTTPRequestHandler): + def __init__(self, request_text): + self.raw_requestline = request_text + self.error_code = self.error_message = None + self.parse_request() - logger.debug("Incoming data parsed with success") + def send_error(self, code, message): + self.error_code = code + self.error_message = message diff --git a/custom_components/deltadore-tydom/tydom/TydomClient.py b/custom_components/deltadore-tydom/tydom/TydomClient.py deleted file mode 100644 index 5008342..0000000 --- a/custom_components/deltadore-tydom/tydom/TydomClient.py +++ /dev/null @@ -1,399 +0,0 @@ -import asyncio -import base64 -import http.client -import logging -import os -import ssl -import signal -import sys -import websockets -import socket - -from requests.auth import HTTPDigestAuth -from .MessageHandler import MessageHandler - -logger = logging.getLogger(__name__) - - -class TydomClient: - def __init__(self, mac, password, alarm_pin=None, host="mediation.tydom.com"): - logger.debug("Initializing TydomClient Class") - - self.password = password - self.mac = mac - self.host = host - self.alarm_pin = alarm_pin - self.connection = None - self.remote_mode = True - self.ssl_context = None - self.cmd_prefix = "\x02" - self.reply_timeout = 4 - self.ping_timeout = None - self.refresh_timeout = 42 - self.sleep_time = 2 - self.incoming = None - # Some devices (like Tywatt) need polling - self.poll_device_urls = [] - self.current_poll_index = 0 - - # Set Host, ssl context and prefix for remote or local connection - if self.host == "mediation.tydom.com": - logger.info("Configure remote mode (%s)", self.host) - self.remote_mode = True - self.ssl_context = ssl._create_unverified_context() - self.cmd_prefix = "\x02" - self.ping_timeout = 40 - - else: - logger.info("Configure local mode (%s)", self.host) - self.remote_mode = False - self.ssl_context = ssl._create_unverified_context() - self.cmd_prefix = "" - self.ping_timeout = None - - async def connect(self): - logger.info("Connecting to tydom") - http_headers = { - "Connection": "Upgrade", - "Upgrade": "websocket", - "Host": self.host + ":443", - "Accept": "*/*", - "Sec-WebSocket-Key": self.generate_random_key(), - "Sec-WebSocket-Version": "13", - } - conn = http.client.HTTPSConnection(self.host, 443, context=self.ssl_context) - - # Get first handshake - conn.request( - "GET", - "/mediation/client?mac={}&appli=1".format(self.mac), - None, - http_headers, - ) - res = conn.getresponse() - conn.close() - - logger.debug("Response headers") - logger.debug(res.headers) - - logger.debug("Response code") - logger.debug(res.getcode()) - - # Read response - logger.debug("response") - logger.debug(res.read()) - res.read() - - # Get authentication - websocket_headers = {} - try: - # Local installations are unauthenticated but we don't *know* that for certain - # so we'll EAFP, try to use the header and fallback if we're unable. - nonce = res.headers["WWW-Authenticate"].split(",", 3) - # Build websocket headers - websocket_headers = {"Authorization": self.build_digest_headers(nonce)} - except AttributeError: - pass - - logger.debug("Upgrading http connection to websocket....") - - if self.ssl_context is not None: - websocket_ssl_context = self.ssl_context - else: - websocket_ssl_context = True # Verify certificate - - # outer loop restarted every time the connection fails - logger.debug("Attempting websocket connection with Tydom hub") - """ - Connecting to webSocket server - websockets.client.connect returns a WebSocketClientProtocol, which is used to send and receive messages - """ - try: - self.connection = await websockets.connect( - f"wss://{self.host}:443/mediation/client?mac={self.mac}&appli=1", - extra_headers=websocket_headers, - ssl=websocket_ssl_context, - ping_timeout=None, - ) - logger.info("Connected to tydom") - return self.connection - except Exception as e: - logger.error("Exception when trying to connect with websocket (%s)", e) - sys.exit(1) - - async def disconnect(self): - if self.connection is not None: - logger.info("Disconnecting") - await self.connection.close() - logger.info("Disconnected") - - # Generate 16 bytes random key for Sec-WebSocket-Keyand convert it to - # base64 - @staticmethod - def generate_random_key(): - return base64.b64encode(os.urandom(16)) - - # Build the headers of Digest Authentication - def build_digest_headers(self, nonce): - digest_auth = HTTPDigestAuth(self.mac, self.password) - chal = dict() - chal["nonce"] = nonce[2].split("=", 1)[1].split('"')[1] - chal["realm"] = "ServiceMedia" if self.remote_mode is True else "protected area" - chal["qop"] = "auth" - digest_auth._thread_local.chal = chal - digest_auth._thread_local.last_nonce = nonce - digest_auth._thread_local.nonce_count = 1 - return digest_auth.build_digest_header( - "GET", - "https://{host}:443/mediation/client?mac={mac}&appli=1".format( - host=self.host, mac=self.mac - ), - ) - - async def notify_alive(self, msg="OK"): - pass - - def add_poll_device_url(self, url): - self.poll_device_urls.append(url) - - # Send Generic message - async def send_message(self, method, msg): - str = ( - self.cmd_prefix - + method - + " " - + msg - + " HTTP/1.1\r\nContent-Length: 0\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" - ) - a_bytes = bytes(str, "ascii") - logger.debug( - "Sending message to tydom (%s %s)", - method, - msg if "pwd" not in msg else "***", - ) - - if self.connection is not None: - await self.connection.send(a_bytes) - else: - logger.warning( - "Cannot send message to Tydom because no connection has been established yet" - ) - - # Give order (name + value) to endpoint - async def put_devices_data(self, device_id, endpoint_id, name, value): - # For shutter, value is the percentage of closing - body = '[{"name":"' + name + '","value":"' + value + '"}]' - # endpoint_id is the endpoint = the device (shutter in this case) to - # open. - str_request = ( - self.cmd_prefix - + f"PUT /devices/{device_id}/endpoints/{endpoint_id}/data HTTP/1.1\r\nContent-Length: " - + str(len(body)) - + "\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" - + body - + "\r\n\r\n" - ) - a_bytes = bytes(str_request, "ascii") - logger.debug("Sending message to tydom (%s %s)", "PUT data", body) - await self.connection.send(a_bytes) - return 0 - - async def put_alarm_cdata(self, device_id, alarm_id=None, value=None, zone_id=None): - # Credits to @mgcrea on github ! - # AWAY # "PUT /devices/{}/endpoints/{}/cdata?name=alarmCmd HTTP/1.1\r\ncontent-length: 29\r\ncontent-type: application/json; charset=utf-8\r\ntransac-id: request_124\r\n\r\n\r\n{"value":"ON","pwd":{}}\r\n\r\n" - # HOME "PUT /devices/{}/endpoints/{}/cdata?name=zoneCmd HTTP/1.1\r\ncontent-length: 41\r\ncontent-type: application/json; charset=utf-8\r\ntransac-id: request_46\r\n\r\n\r\n{"value":"ON","pwd":"{}","zones":[1]}\r\n\r\n" - # DISARM "PUT /devices/{}/endpoints/{}/cdata?name=alarmCmd - # HTTP/1.1\r\ncontent-length: 30\r\ncontent-type: application/json; - # charset=utf-8\r\ntransac-id: - # request_7\r\n\r\n\r\n{"value":"OFF","pwd":"{}"}\r\n\r\n" - - # variables: - # id - # Cmd - # value - # pwd - # zones - - if self.alarm_pin is None: - logger.warning("Tydom alarm pin is not set!") - pass - try: - if zone_id is None: - cmd = "alarmCmd" - body = ( - '{"value":"' + str(value) + '","pwd":"' + str(self.alarm_pin) + '"}' - ) - else: - cmd = "zoneCmd" - body = ( - '{"value":"' - + str(value) - + '","pwd":"' - + str(self.alarm_pin) - + '","zones":"[' - + str(zone_id) - + ']"}' - ) - - str_request = ( - self.cmd_prefix - + "PUT /devices/{device}/endpoints/{alarm}/cdata?name={cmd} HTTP/1.1\r\nContent-Length: ".format( - device=str(device_id), alarm=str(alarm_id), cmd=str(cmd) - ) - + str(len(body)) - + "\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" - + body - + "\r\n\r\n" - ) - - a_bytes = bytes(str_request, "ascii") - logger.debug("Sending message to tydom (%s %s)", "PUT cdata", body) - - try: - await self.connection.send(a_bytes) - return 0 - except BaseException: - logger.error("put_alarm_cdata ERROR !", exc_info=True) - logger.error(a_bytes) - except BaseException: - logger.error("put_alarm_cdata ERROR !", exc_info=True) - - # Get some information on Tydom - async def get_info(self): - msg_type = "/info" - req = "GET" - await self.send_message(method=req, msg=msg_type) - - # Refresh (all) - async def post_refresh(self): - msg_type = "/refresh/all" - req = "POST" - await self.send_message(method=req, msg=msg_type) - # Get poll device data - nb_poll_devices = len(self.poll_device_urls) - if self.current_poll_index < nb_poll_devices - 1: - self.current_poll_index = self.current_poll_index + 1 - else: - self.current_poll_index = 0 - if nb_poll_devices > 0: - await self.get_poll_device_data( - self.poll_device_urls[self.current_poll_index] - ) - - # Get the moments (programs) - async def get_moments(self): - msg_type = "/moments/file" - req = "GET" - await self.send_message(method=req, msg=msg_type) - - # Get the scenarios - async def get_scenarii(self): - msg_type = "/scenarios/file" - req = "GET" - await self.send_message(method=req, msg=msg_type) - - # Send a ping (pong should be returned) - async def ping(self): - msg_type = "/ping" - req = "GET" - await self.send_message(method=req, msg=msg_type) - logger.debug("Ping") - - # Get all devices metadata - async def get_devices_meta(self): - msg_type = "/devices/meta" - req = "GET" - await self.send_message(method=req, msg=msg_type) - - # Get all devices data - async def get_devices_data(self): - msg_type = "/devices/data" - req = "GET" - await self.send_message(method=req, msg=msg_type) - # Get poll devices data - for url in self.poll_device_urls: - await self.get_poll_device_data(url) - - # List the device to get the endpoint id - async def get_configs_file(self): - msg_type = "/configs/file" - req = "GET" - await self.send_message(method=req, msg=msg_type) - - # Get metadata configuration to list poll devices (like Tywatt) - async def get_devices_cmeta(self): - msg_type = "/devices/cmeta" - req = "GET" - await self.send_message(method=req, msg=msg_type) - - async def get_data(self): - await self.get_configs_file() - await self.get_devices_cmeta() - await self.get_devices_data() - - # Give order to endpoint - async def get_device_data(self, id): - # 10 here is the endpoint = the device (shutter in this case) to open. - device_id = str(id) - str_request = ( - self.cmd_prefix - + f"GET /devices/{device_id}/endpoints/{device_id}/data HTTP/1.1\r\nContent-Length: 0\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" - ) - a_bytes = bytes(str_request, "ascii") - await self.connection.send(a_bytes) - - async def get_poll_device_data(self, url): - msg_type = url - req = "GET" - await self.send_message(method=req, msg=msg_type) - - async def setup(self): - logger.info("Setup tydom client") - await self.get_info() - await self.post_refresh() - await self.get_data() - - # Listen to tydom events. - def listen_tydom(self): - try: - self.connect() - self.setup() - while True: - try: - incoming_bytes_str = self.connection.recv() - message_handler = MessageHandler( - incoming_bytes=incoming_bytes_str, - tydom_client=self, - ) - message_handler.incoming_triage() - except websockets.exceptions.ConnectionClosed: - # try to reconnect - self.connect() - self.setup() - except Exception as e: - logger.warning("Unable to handle message: %s", e) - - except socket.gaierror as e: - logger.error("Socket error (%s)", e) - sys.exit(1) - except ConnectionRefusedError as e: - logger.error("Connection refused (%s)", e) - sys.exit(1) - - async def shutdown(self, signal, loop): - logging.info("Received exit signal %s", signal.name) - logging.info("Cancelling running tasks") - - try: - # Close connections - await self.disconnect() - - # Cancel async tasks - tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] - [task.cancel() for task in tasks] - await asyncio.gather(*tasks) - logging.info("All running tasks cancelled") - except Exception as e: - logging.info("Some errors occurred when stopping tasks (%s)", e) - finally: - loop.stop() diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index ba19485..9e72ea3 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -4,9 +4,12 @@ import asyncio import socket import base64 +import re import async_timeout import aiohttp -import re + +from aiohttp import ClientWebSocketResponse +from .MessageHandler import MessageHandler from requests.auth import HTTPDigestAuth @@ -44,6 +47,11 @@ def __init__( self._host = host self._alarm_pin = alarm_pin self._remote_mode = self._host == "mediation.tydom.com" + self._connection = None + + # Some devices (like Tywatt) need polling + self.poll_device_urls = [] + self.current_poll_index = 0 if self._remote_mode: logger.info("Configure remote mode (%s)", self._host) @@ -54,7 +62,7 @@ def __init__( self._cmd_prefix = "" self._ping_timeout = None - async def async_connect(self) -> any: + async def async_connect(self) -> ClientWebSocketResponse: """Connect to the Tydom API.""" http_headers = { "Connection": "Upgrade", @@ -65,80 +73,45 @@ async def async_connect(self) -> any: "Sec-WebSocket-Version": "13", } - return await self._api_wrapper( - method="get", - url=f"https://{self._host}/mediation/client?mac={self._mac}&appli=1", - headers=http_headers, - ) - - async def _api_wrapper( - self, - method: str, - url: str, - data: dict | None = None, - headers: dict | None = None, - ) -> any: - """Get information from the API.""" try: async with async_timeout.timeout(10): response = await self._session.request( - method=method, - url=url, - headers=headers, - json=data, + method="GET", + url=f"https://{self._host}:443/mediation/client?mac={self._mac}&appli=1", + headers=http_headers, + json=None, ) logger.info("response status : %s", response.status) logger.info("response content : %s", await response.text()) logger.info("response headers : %s", response.headers) - m = re.match( + re_matcher = re.match( '.*nonce="([a-zA-Z0-9+=]+)".*', response.headers.get("WWW-Authenticate"), ) - if m: - logger.info("nonce : %s", m.group(1)) + if re_matcher: + logger.info("nonce : %s", re_matcher.group(1)) else: raise TydomClientApiClientError("Could't find auth nonce") - headers["Authorization"] = self.build_digest_headers(m.group(1)) - - logger.info("new request headers : %s", headers) - # {'Authorization': ' - # Digest username="############", - # realm="protected area", - # nonce="e0317d0d0d4d2afe6c54ec928dfd7614", - # uri="/mediation/client?mac=############&appli=1", - # response="98a019dca77980a230eafed5e0acdf18", - # qop="auth", - # nc=00000001, - # cnonce="77e7fea3ef6ecc1d"'} - - # 'Authorization': ' - # Digest username="############", - # realm="protected area", - # nonce="31a7d8554d11c367dda81159b485c408", - # uri="/mediation/client?mac=############&appli=1", - # response="45fd17aecff639f29532091162ad64a1", - # qop="auth", - # nc=00000002, - # cnonce="e92ddde26cac2ad5" - - response = await self._session.ws_connect( - method=method, - url=url, - headers=headers, + http_headers = {} + http_headers["Authorization"] = self.build_digest_headers( + re_matcher.group(1) ) - logger.info("response status : %s", response.status) - logger.info("response content : %s", await response.text()) - logger.info("response headers : %s", response.headers) + logger.info("new request headers : %s", http_headers) - if response.status in (401, 403): - raise TydomClientApiClientAuthenticationError( - "Invalid credentials", - ) - response.raise_for_status() - return await response.json() + connection = await self._session.ws_connect( + method="GET", + url=f"wss://{self._host}:443/mediation/client?mac={self._mac}&appli=1", + headers=http_headers, + autoclose=False, + autoping=True, + timeout=100, + heartbeat=10, + ) + + return connection except asyncio.TimeoutError as exception: raise TydomClientApiClientCommunicationError( @@ -153,6 +126,39 @@ async def _api_wrapper( "Something really wrong happened!" ) from exception + async def listen_tydom(self, connection: ClientWebSocketResponse): + """Listen for Tydom messages""" + logger.info("Listen for Tydom messages") + self._connection = connection + await self.get_info() + await self.post_refresh() + await self.get_configs_file() + await self.get_devices_cmeta() + await self.get_devices_data() + + while True: + try: + if self._connection.closed: + self._connection = await self.async_connect() + incoming_bytes_str = await self._connection.receive_bytes() + # logger.info(incoming_bytes_str.type) + logger.info(incoming_bytes_str) + + message_handler = MessageHandler( + incoming_bytes=incoming_bytes_str, + tydom_client=self, + cmd_prefix=self._cmd_prefix, + ) + await message_handler.incoming_triage() + # message_handler = MessageHandler( + # incoming_bytes=incoming_bytes_str, + # tydom_client=tydom_client, + # mqtt_client=mqtt_client, + # ) + # await message_handler.incoming_triage() + except Exception as e: + logger.warning("Unable to handle message: %s", e) + def build_digest_headers(self, nonce): """Build the headers of Digest Authentication.""" digest_auth = HTTPDigestAuth(self._mac, self._password) @@ -167,13 +173,216 @@ def build_digest_headers(self, nonce): digest_auth._thread_local.nonce_count = 1 digest = digest_auth.build_digest_header( "GET", - "https://{host}:443/mediation/client?mac={mac}&appli=1".format( - host=self._host, mac=self._mac - ), + f"https://{self._host}:443/mediation/client?mac={self._mac}&appli=1", ) return digest + async def send_message(self, method, msg): + """Send Generic message to Tydom""" + message = ( + self._cmd_prefix + + method + + " " + + msg + + " HTTP/1.1\r\nContent-Length: 0\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" + ) + a_bytes = bytes(message, "ascii") + logger.debug( + "Sending message to tydom (%s %s)", + method, + msg if "pwd" not in msg else "***", + ) + + if self._connection is not None: + await self._connection.send_bytes(a_bytes) + else: + logger.warning( + "Cannot send message to Tydom because no connection has been established yet" + ) + + # ######################## + # Utils methods + # ######################## + @staticmethod def generate_random_key(): """Generate 16 bytes random key for Sec-WebSocket-Keyand convert it to base64.""" return str(base64.b64encode(os.urandom(16))) + + # ######################## + # Tydom messages + # ######################## + async def get_info(self): + """Ask some information from Tydom""" + msg_type = "/info" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + # Refresh (all) + async def post_refresh(self): + msg_type = "/refresh/all" + req = "POST" + await self.send_message(method=req, msg=msg_type) + # Get poll device data + nb_poll_devices = len(self.poll_device_urls) + if self.current_poll_index < nb_poll_devices - 1: + self.current_poll_index = self.current_poll_index + 1 + else: + self.current_poll_index = 0 + if nb_poll_devices > 0: + await self.get_poll_device_data( + self.poll_device_urls[self.current_poll_index] + ) + + # Send a ping (pong should be returned) + async def ping(self): + msg_type = "/ping" + req = "GET" + await self.send_message(method=req, msg=msg_type) + logger.debug("Ping") + + async def get_devices_meta(self): + """Get all devices metadata""" + msg_type = "/devices/meta" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + async def get_devices_data(self): + """Get all devices data""" + msg_type = "/devices/data" + req = "GET" + await self.send_message(method=req, msg=msg_type) + # Get poll devices data + for url in self.poll_device_urls: + await self.get_poll_device_data(url) + + async def get_configs_file(self): + """List the device to get the endpoint id""" + msg_type = "/configs/file" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + # Get metadata configuration to list poll devices (like Tywatt) + async def get_devices_cmeta(self): + msg_type = "/devices/cmeta" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + async def get_data(self): + await self.get_configs_file() + await self.get_devices_cmeta() + await self.get_devices_data() + + # Give order to endpoint + async def get_device_data(self, id): + # 10 here is the endpoint = the device (shutter in this case) to open. + device_id = str(id) + str_request = ( + self._cmd_prefix + + f"GET /devices/{device_id}/endpoints/{device_id}/data HTTP/1.1\r\nContent-Length: 0\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" + ) + a_bytes = bytes(str_request, "ascii") + await self._connection.send(a_bytes) + + async def get_poll_device_data(self, url): + msg_type = url + req = "GET" + await self.send_message(method=req, msg=msg_type) + + # Get the moments (programs) + async def get_moments(self): + msg_type = "/moments/file" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + # Get the scenarios + async def get_scenarii(self): + msg_type = "/scenarios/file" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + # Give order (name + value) to endpoint + async def put_devices_data(self, device_id, endpoint_id, name, value): + # For shutter, value is the percentage of closing + body = '[{"name":"' + name + '","value":"' + value + '"}]' + # endpoint_id is the endpoint = the device (shutter in this case) to + # open. + str_request = ( + self._cmd_prefix + + f"PUT /devices/{device_id}/endpoints/{endpoint_id}/data HTTP/1.1\r\nContent-Length: " + + str(len(body)) + + "\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" + + body + + "\r\n\r\n" + ) + a_bytes = bytes(str_request, "ascii") + logger.debug("Sending message to tydom (%s %s)", "PUT data", body) + await self._connection.send(a_bytes) + return 0 + + async def put_alarm_cdata(self, device_id, alarm_id=None, value=None, zone_id=None): + # Credits to @mgcrea on github ! + # AWAY # "PUT /devices/{}/endpoints/{}/cdata?name=alarmCmd HTTP/1.1\r\ncontent-length: 29\r\ncontent-type: application/json; charset=utf-8\r\ntransac-id: request_124\r\n\r\n\r\n{"value":"ON","pwd":{}}\r\n\r\n" + # HOME "PUT /devices/{}/endpoints/{}/cdata?name=zoneCmd HTTP/1.1\r\ncontent-length: 41\r\ncontent-type: application/json; charset=utf-8\r\ntransac-id: request_46\r\n\r\n\r\n{"value":"ON","pwd":"{}","zones":[1]}\r\n\r\n" + # DISARM "PUT /devices/{}/endpoints/{}/cdata?name=alarmCmd + # HTTP/1.1\r\ncontent-length: 30\r\ncontent-type: application/json; + # charset=utf-8\r\ntransac-id: + # request_7\r\n\r\n\r\n{"value":"OFF","pwd":"{}"}\r\n\r\n" + + # variables: + # id + # Cmd + # value + # pwd + # zones + + if self._alarm_pin is None: + logger.warning("Tydom alarm pin is not set!") + + try: + if zone_id is None: + cmd = "alarmCmd" + body = ( + '{"value":"' + + str(value) + + '","pwd":"' + + str(self._alarm_pin) + + '"}' + ) + else: + cmd = "zoneCmd" + body = ( + '{"value":"' + + str(value) + + '","pwd":"' + + str(self._alarm_pin) + + '","zones":"[' + + str(zone_id) + + ']"}' + ) + + str_request = ( + self._cmd_prefix + + "PUT /devices/{device}/endpoints/{alarm}/cdata?name={cmd} HTTP/1.1\r\nContent-Length: ".format( + device=str(device_id), alarm=str(alarm_id), cmd=str(cmd) + ) + + str(len(body)) + + "\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" + + body + + "\r\n\r\n" + ) + + a_bytes = bytes(str_request, "ascii") + logger.debug("Sending message to tydom (%s %s)", "PUT cdata", body) + + try: + await self._connection.send(a_bytes) + return 0 + except BaseException: + logger.error("put_alarm_cdata ERROR !", exc_info=True) + logger.error(a_bytes) + except BaseException: + logger.error("put_alarm_cdata ERROR !", exc_info=True) + + def add_poll_device_url(self, url): + self.poll_device_urls.append(url) From 3392062a5a5c1106b3afbdaca88508616863fbf4 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Thu, 27 Apr 2023 22:59:42 +0200 Subject: [PATCH 09/74] get tydom credentials from Delta Dore --- .../deltadore-tydom/config_flow.py | 42 ++++++++-- custom_components/deltadore-tydom/hub.py | 11 ++- .../deltadore-tydom/translations/en.json | 2 + .../deltadore-tydom/tydom/tydom_client.py | 84 ++++++++++++++++++- 4 files changed, 129 insertions(+), 10 deletions(-) diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index e4c8627..ee0efc8 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -11,7 +11,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from homeassistant import config_entries, exceptions -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_EMAIL, CONF_PASSWORD, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.components import dhcp @@ -39,13 +39,14 @@ { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_MAC): cv.string, + vol.Required(CONF_EMAIL): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_PIN): str, } ) -def host_valid(host): +def host_valid(host) -> bool: """Return True if hostname or IP address is valid""" try: if ipaddress.ip_address(host).version == (4 or 6): @@ -55,6 +56,14 @@ def host_valid(host): return all(x and not disallowed.search(x) for x in host.split(".")) +regex = re.compile(r"([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+") + + +def email_valid(email) -> bool: + """Return True if email is valid""" + return re.fullmatch(regex, email) + + async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -64,11 +73,6 @@ async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: # This is a simple example to show an error in the UI for a short hostname # The exceptions are defined at the end of this file, and are used in the # `async_step_user` method below. - if CONF_HOST not in data: - raise InvalidHost - - if len(data[CONF_HOST]) < 3: - raise InvalidHost if not host_valid(data[CONF_HOST]): raise InvalidHost @@ -76,9 +80,20 @@ async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: if len(data[CONF_MAC]) != 12: raise InvalidMacAddress + if not email_valid(data[CONF_EMAIL]): + raise InvalidEmail + if len(data[CONF_PASSWORD]) < 3: raise InvalidPassword + password = await hub.Hub.get_tydom_credentials( + async_create_clientsession(hass, False), + data[CONF_EMAIL], + data[CONF_PASSWORD], + data[CONF_MAC], + ) + data[CONF_PASSWORD] = password + pin = None if CONF_PIN in data: pin = data[CONF_PIN] @@ -123,6 +138,7 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: try: await validate_input(self.hass, user_input) # Ensure it's working as expected + tydom_hub = hub.Hub( self.hass, user_input[CONF_HOST], @@ -131,6 +147,7 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: None, ) await tydom_hub.test_credentials() + await self.async_set_unique_id(user_input[CONF_MAC]) self._abort_if_unique_id_configured() except CannotConnect: @@ -143,6 +160,8 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: _errors[CONF_HOST] = "invalid_host" except InvalidMacAddress: _errors[CONF_MAC] = "invalid_macaddress" + except InvalidEmail: + _errors[CONF_MAC] = "invalid_email" except InvalidPassword: _errors[CONF_PASSWORD] = "invalid_password" except TydomClientApiClientCommunicationError: @@ -161,7 +180,7 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: _errors["base"] = "unknown" else: return self.async_create_entry( - title=user_input[CONF_MAC], data=user_input + title="Tydom-" + user_input[CONF_MAC], data=user_input ) user_input = user_input or {} @@ -174,6 +193,9 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: CONF_HOST, default=user_input.get(CONF_HOST) ): cv.string, vol.Required(CONF_MAC, default=user_input.get(CONF_MAC)): cv.string, + vol.Required( + CONF_EMAIL, default=user_input.get(CONF_EMAIL) + ): cv.string, vol.Required( CONF_PASSWORD, default=user_input.get(CONF_PASSWORD) ): cv.string, @@ -223,5 +245,9 @@ class InvalidMacAddress(exceptions.HomeAssistantError): """Error to indicate there is an invalid Mac address.""" +class InvalidEmail(exceptions.HomeAssistantError): + """Error to indicate there is an invalid Email.""" + + class InvalidPassword(exceptions.HomeAssistantError): """Error to indicate there is an invalid Password.""" diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 76f5990..44aa6ca 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -10,7 +10,7 @@ import random import logging import time -from aiohttp import ClientWebSocketResponse +from aiohttp import ClientWebSocketResponse, ClientSession from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -65,6 +65,15 @@ async def connect(self) -> ClientWebSocketResponse: """Connect to Tydom""" return await self._tydom_client.async_connect() + @staticmethod + async def get_tydom_credentials( + session: ClientSession, email: str, password: str, macaddress: str + ): + """Get Tydom credentials""" + return await TydomClient.async_get_credentials( + session, email, password, macaddress + ) + async def test_credentials(self) -> None: """Validate credentials.""" connection = await self.connect() diff --git a/custom_components/deltadore-tydom/translations/en.json b/custom_components/deltadore-tydom/translations/en.json index 5141df0..df18c9f 100644 --- a/custom_components/deltadore-tydom/translations/en.json +++ b/custom_components/deltadore-tydom/translations/en.json @@ -8,6 +8,7 @@ "name": "Custom Name of the inverter (used for sensors' prefix)", "host": "IP or hostname", "mac": "MAC address", + "email": "Email", "password": "Password", "pin": "Alarm PIN" } @@ -17,6 +18,7 @@ "already_configured": "Device is already configured", "invalid_host": "Hostname or IP is invalid", "invalid_macaddress": "MAC address is invalid", + "invalid_email": "Email is invalid", "invalid_password": "Password is invalid" }, "abort": { diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index 9e72ea3..b6f0a60 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -8,7 +8,8 @@ import async_timeout import aiohttp -from aiohttp import ClientWebSocketResponse +from urllib3 import encode_multipart_formdata +from aiohttp import ClientWebSocketResponse, ClientSession from .MessageHandler import MessageHandler from requests.auth import HTTPDigestAuth @@ -62,6 +63,87 @@ def __init__( self._cmd_prefix = "" self._ping_timeout = None + @staticmethod + async def async_get_credentials( + session: ClientSession, email: str, password: str, macaddress: str + ): + """get tydom credentials from Delta Dore""" + try: + async with async_timeout.timeout(10): + response = await session.request( + method="GET", + url="https://deltadoreadb2ciot.b2clogin.com/deltadoreadb2ciot.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_AccountProviderROPC_SignIn", + ) + logger.info("response status : %s", response.status) + logger.info("response content : %s", await response.text()) + logger.info("response headers : %s", response.headers) + + json_response = await response.json() + signin_url = json_response["token_endpoint"] + logger.info("signin_url : %s", signin_url) + + body, ct_header = encode_multipart_formdata( + { + "username": f"{email}", + "password": f"{password}", + "grant_type": "password", + "client_id": "8782839f-3264-472a-ab87-4d4e23524da4", + "scope": "openid profile offline_access https://deltadoreadb2ciot.onmicrosoft.com/iotapi/video_config https://deltadoreadb2ciot.onmicrosoft.com/iotapi/video_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_gateway_credentials https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_camera_credentials https://deltadoreadb2ciot.onmicrosoft.com/iotapi/comptage_europe_collect_reader https://deltadoreadb2ciot.onmicrosoft.com/iotapi/comptage_europe_site_config_contributor https://deltadoreadb2ciot.onmicrosoft.com/iotapi/pilotage_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/consent_mgt_contributor https://deltadoreadb2ciot.onmicrosoft.com/iotapi/b2caccountprovider_manage_account https://deltadoreadb2ciot.onmicrosoft.com/iotapi/b2caccountprovider_allow_view_account https://deltadoreadb2ciot.onmicrosoft.com/iotapi/tydom_backend_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/websocket_remote_access https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_device https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_view https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_space https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_connector https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_endpoint https://deltadoreadb2ciot.onmicrosoft.com/iotapi/rule_management_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/collect_read_datas", + } + ) + logger.info("body : %s", body) + logger.info("header : %s", ct_header) + + response = await session.post( + url=signin_url, + headers={"Content-Type": ct_header}, + data=body, + ) + + logger.info("response status : %s", response.status) + logger.info("response content : %s", await response.text()) + logger.info("response headers : %s", response.headers) + + json_response = await response.json() + access_token = json_response["access_token"] + + logger.info("access_token : %s", access_token) + + response = await session.request( + method="GET", + url=f"https://prod.iotdeltadore.com/sitesmanagement/api/v1/sites?gateway_mac={macaddress}", + headers={"Authorization": f"Bearer {access_token}"}, + ) + + logger.info("response status : %s", response.status) + logger.info("response content : %s", await response.text()) + logger.info("response headers : %s", response.headers) + + json_response = await response.json() + + if ( + "sites" in json_response + and len(json_response["sites"]) == 1 + and "gateway" in json_response["sites"][0] + and "password" in json_response["sites"][0]["gateway"] + ): + password = json_response["sites"][0]["gateway"]["password"] + return password + else: + raise TydomClientApiClientError("Tydom credentials not found") + except asyncio.TimeoutError as exception: + raise TydomClientApiClientCommunicationError( + "Timeout error fetching information", + ) from exception + except (aiohttp.ClientError, socket.gaierror) as exception: + raise TydomClientApiClientCommunicationError( + "Error fetching information", + ) from exception + except Exception as exception: # pylint: disable=broad-except + raise TydomClientApiClientError( + "Something really wrong happened!" + ) from exception + async def async_connect(self) -> ClientWebSocketResponse: """Connect to the Tydom API.""" http_headers = { From 0da66be6afa5f3a6465bf5bf8d51692a91003c3a Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Thu, 27 Apr 2023 23:25:55 +0200 Subject: [PATCH 10/74] update readme --- README_EXAMPLE.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/README_EXAMPLE.md b/README_EXAMPLE.md index 566d4dd..bfabff8 100644 --- a/README_EXAMPLE.md +++ b/README_EXAMPLE.md @@ -1,4 +1,4 @@ -# Integration Blueprint +# Delta Dore Tydom [![GitHub Release][releases-shield]][releases] [![GitHub Activity][commits-shield]][commits] @@ -11,7 +11,11 @@ [![Discord][discord-shield]][discord] [![Community Forum][forum-shield]][forum] -_Integration to integrate with [integration_blueprint][integration_blueprint]._ +This a *custom component* for [Home Assistant](https://www.home-assistant.io/). +The `Delta Dore Tydom` integration allows you to observe and control [Delta Dore Tydom smart home gateway](https://www.deltadore.fr/). + +![GitHub release](https://img.shields.io/github/release/CyrilP/hass-deltadore-tydom-component) +[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) **This integration will set up the following platforms.** @@ -23,13 +27,20 @@ Platform | Description ## Installation +The preferred way to install the Delta Dore Tydom integration is by addig it using HACS. +Add your device via the Integration menu + +[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=deltadore-tydom) + +Manual method : + 1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). 1. If you do not have a `custom_components` directory (folder) there, you need to create it. -1. In the `custom_components` directory (folder) create a new folder called `integration_blueprint`. -1. Download _all_ the files from the `custom_components/integration_blueprint/` directory (folder) in this repository. +1. In the `custom_components` directory (folder) create a new folder called `deltadore-tydom`. +1. Download _all_ the files from the `custom_components/deltadore-tydom/` directory (folder) in this repository. 1. Place the files you downloaded in the new directory (folder) you created. 1. Restart Home Assistant -1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Integration blueprint" +1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Delta Dore Tydom" ## Configuration is done in the UI From 38d7090426fd2f46053164b5fe58f24dc3e87ebe Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Thu, 27 Apr 2023 23:44:49 +0200 Subject: [PATCH 11/74] update readme --- README_EXAMPLE.md | 20 ++++++++----------- .../deltadore-tydom/config_flow.py | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/README_EXAMPLE.md b/README_EXAMPLE.md index bfabff8..88a7f3a 100644 --- a/README_EXAMPLE.md +++ b/README_EXAMPLE.md @@ -8,7 +8,6 @@ ![Project Maintenance][maintenance-shield] [![BuyMeCoffee][buymecoffeebadge]][buymecoffee] -[![Discord][discord-shield]][discord] [![Community Forum][forum-shield]][forum] This a *custom component* for [Home Assistant](https://www.home-assistant.io/). @@ -28,6 +27,7 @@ Platform | Description ## Installation The preferred way to install the Delta Dore Tydom integration is by addig it using HACS. + Add your device via the Integration menu [![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=deltadore-tydom) @@ -52,19 +52,15 @@ If you want to contribute to this please read the [Contribution guidelines](CONT *** -[integration_blueprint]: https://github.com/ludeeus/integration_blueprint -[buymecoffee]: https://www.buymeacoffee.com/ludeeus +[integration_blueprint]: https://github.com/CyrilP/hass-deltadore-tydom-component +[buymecoffee]: https://www.buymeacoffee.com/cyrilp [buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge -[commits-shield]: https://img.shields.io/github/commit-activity/y/ludeeus/integration_blueprint.svg?style=for-the-badge -[commits]: https://github.com/ludeeus/integration_blueprint/commits/main +[commits-shield]: https://img.shields.io/github/commit-activity/y/CyrilP/hass-deltadore-tydom-component.svg?style=for-the-badge +[commits]: https://github.com/CyrilP/hass-deltadore-tydom-component/commits/main [hacs]: https://github.com/hacs/integration [hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge -[discord]: https://discord.gg/Qa5fW2R -[discord-shield]: https://img.shields.io/discord/330944238910963714.svg?style=for-the-badge [exampleimg]: example.png -[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge [forum]: https://community.home-assistant.io/ -[license-shield]: https://img.shields.io/github/license/ludeeus/integration_blueprint.svg?style=for-the-badge -[maintenance-shield]: https://img.shields.io/badge/maintainer-Joakim%20Sørensen%20%40ludeeus-blue.svg?style=for-the-badge -[releases-shield]: https://img.shields.io/github/release/ludeeus/integration_blueprint.svg?style=for-the-badge -[releases]: https://github.com/ludeeus/integration_blueprint/releases +[license-shield]: https://img.shields.io/github/license/CyrilP/hass-deltadore-tydom-component.svg?style=for-the-badge +[releases-shield]: https://img.shields.io/github/release/CyrilP/hass-deltadore-tydom-component.svg?style=for-the-badge +[releases]: https://github.com/CyrilP/hass-deltadore-tydom-component/releases diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index ee0efc8..7a9bfaa 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -161,7 +161,7 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: except InvalidMacAddress: _errors[CONF_MAC] = "invalid_macaddress" except InvalidEmail: - _errors[CONF_MAC] = "invalid_email" + _errors[CONF_EMAIL] = "invalid_email" except InvalidPassword: _errors[CONF_PASSWORD] = "invalid_password" except TydomClientApiClientCommunicationError: From de22b07ab8754cdda0cf9870dd3b84a3286f2642 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Thu, 27 Apr 2023 23:46:31 +0200 Subject: [PATCH 12/74] update readme --- README_EXAMPLE.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README_EXAMPLE.md b/README_EXAMPLE.md index 88a7f3a..91ca6e4 100644 --- a/README_EXAMPLE.md +++ b/README_EXAMPLE.md @@ -1,11 +1,8 @@ # Delta Dore Tydom [![GitHub Release][releases-shield]][releases] -[![GitHub Activity][commits-shield]][commits] [![License][license-shield]](LICENSE) -[![hacs][hacsbadge]][hacs] -![Project Maintenance][maintenance-shield] [![BuyMeCoffee][buymecoffeebadge]][buymecoffee] [![Community Forum][forum-shield]][forum] @@ -55,10 +52,6 @@ If you want to contribute to this please read the [Contribution guidelines](CONT [integration_blueprint]: https://github.com/CyrilP/hass-deltadore-tydom-component [buymecoffee]: https://www.buymeacoffee.com/cyrilp [buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge -[commits-shield]: https://img.shields.io/github/commit-activity/y/CyrilP/hass-deltadore-tydom-component.svg?style=for-the-badge -[commits]: https://github.com/CyrilP/hass-deltadore-tydom-component/commits/main -[hacs]: https://github.com/hacs/integration -[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge [exampleimg]: example.png [forum]: https://community.home-assistant.io/ [license-shield]: https://img.shields.io/github/license/CyrilP/hass-deltadore-tydom-component.svg?style=for-the-badge From 2b2bd5a22615688327481dfb3812da026a5754cb Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Thu, 27 Apr 2023 23:46:56 +0200 Subject: [PATCH 13/74] update readme --- README_EXAMPLE.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README_EXAMPLE.md b/README_EXAMPLE.md index 91ca6e4..fa23ce9 100644 --- a/README_EXAMPLE.md +++ b/README_EXAMPLE.md @@ -5,8 +5,6 @@ [![BuyMeCoffee][buymecoffeebadge]][buymecoffee] -[![Community Forum][forum-shield]][forum] - This a *custom component* for [Home Assistant](https://www.home-assistant.io/). The `Delta Dore Tydom` integration allows you to observe and control [Delta Dore Tydom smart home gateway](https://www.deltadore.fr/). From 7b1cb1be874b0caa3f832602cde01248280a5711 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Thu, 27 Apr 2023 23:47:19 +0200 Subject: [PATCH 14/74] update readme --- README_EXAMPLE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README_EXAMPLE.md b/README_EXAMPLE.md index fa23ce9..052d095 100644 --- a/README_EXAMPLE.md +++ b/README_EXAMPLE.md @@ -6,6 +6,7 @@ [![BuyMeCoffee][buymecoffeebadge]][buymecoffee] This a *custom component* for [Home Assistant](https://www.home-assistant.io/). + The `Delta Dore Tydom` integration allows you to observe and control [Delta Dore Tydom smart home gateway](https://www.deltadore.fr/). ![GitHub release](https://img.shields.io/github/release/CyrilP/hass-deltadore-tydom-component) From e4e0a0139385a00c62b6a0a1d3804d7dece8f11e Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Thu, 27 Apr 2023 23:50:02 +0200 Subject: [PATCH 15/74] update readme --- README_EXAMPLE.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README_EXAMPLE.md b/README_EXAMPLE.md index 052d095..c4838ae 100644 --- a/README_EXAMPLE.md +++ b/README_EXAMPLE.md @@ -1,6 +1,5 @@ # Delta Dore Tydom -[![GitHub Release][releases-shield]][releases] [![License][license-shield]](LICENSE) [![BuyMeCoffee][buymecoffeebadge]][buymecoffee] @@ -54,5 +53,3 @@ If you want to contribute to this please read the [Contribution guidelines](CONT [exampleimg]: example.png [forum]: https://community.home-assistant.io/ [license-shield]: https://img.shields.io/github/license/CyrilP/hass-deltadore-tydom-component.svg?style=for-the-badge -[releases-shield]: https://img.shields.io/github/release/CyrilP/hass-deltadore-tydom-component.svg?style=for-the-badge -[releases]: https://github.com/CyrilP/hass-deltadore-tydom-component/releases From 5e54508c410af706e616bdf6afaa74e9ae4f89cf Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Thu, 27 Apr 2023 23:52:53 +0200 Subject: [PATCH 16/74] update config --- config/configuration.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/configuration.yaml b/config/configuration.yaml index b4b0f6f..9e4088c 100644 --- a/config/configuration.yaml +++ b/config/configuration.yaml @@ -3,7 +3,7 @@ default_config: # https://www.home-assistant.io/integrations/logger/ logger: - default: info + default: warn logs: - custom_components.integration_blueprint: debug custom_components.deltadore-tydom: debug + From 3884e9b47549f5427bcbda67c3e4780a1e3ecd08 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Thu, 27 Apr 2023 23:56:47 +0200 Subject: [PATCH 17/74] remove useless code --- custom_components/deltadore-tydom/hub.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 44aa6ca..7e0e698 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -84,13 +84,6 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: logger.info("Listen to tydom events") await self._tydom_client.listen_tydom(connection) - async def ping(self, connection: ClientWebSocketResponse) -> None: - """Periodically send pings""" - logger.info("Sending ping") - while True: - await self._tydom_client.ping() - await asyncio.sleep(10) - class Roller: """Dummy roller (device for HA) for Hello World example.""" From a86b0269fb0053433fed89ea41ffbb77d8c895a1 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Fri, 28 Apr 2023 08:35:10 +0200 Subject: [PATCH 18/74] update docs/translations --- README_EXAMPLE.md | 11 +++++++++++ .../deltadore-tydom/translations/en.json | 5 ++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README_EXAMPLE.md b/README_EXAMPLE.md index c4838ae..9a14959 100644 --- a/README_EXAMPLE.md +++ b/README_EXAMPLE.md @@ -8,6 +8,8 @@ This a *custom component* for [Home Assistant](https://www.home-assistant.io/). The `Delta Dore Tydom` integration allows you to observe and control [Delta Dore Tydom smart home gateway](https://www.deltadore.fr/). +This integration can work in local mode or cloud mode depending on how the integration is configured (see Configuration part) + ![GitHub release](https://img.shields.io/github/release/CyrilP/hass-deltadore-tydom-component) [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) @@ -40,6 +42,15 @@ Manual method : ## Configuration is done in the UI +The hostname/ip can be : +* The hostname/ip of your Tydom (local mode only). An access to the cloud is done to retrieve the Tydom credentials +* mediation.tydom.com. Using this configuration makes the integration work through the cloud + +The Mac address is the Mac of you Tydom + +Email/Password are you Dela Dore credentials + +The alarm PIN is optional and used to set your alarm mode ## Contributions are welcome! diff --git a/custom_components/deltadore-tydom/translations/en.json b/custom_components/deltadore-tydom/translations/en.json index df18c9f..3557e0c 100644 --- a/custom_components/deltadore-tydom/translations/en.json +++ b/custom_components/deltadore-tydom/translations/en.json @@ -2,10 +2,9 @@ "config": { "step": { "user": { - "title": "Inverter Connection Configuration", - "description": "If you need help with the configuration go to: https://github.com/alexdelprete/ha-abb-powerone-pvi-sunspec", + "title": "Delta Dore Tydom Configuration", + "description": "If you need help with the configuration go to: https://github.com/CyrilP/hass-deltadore-tydom-component", "data": { - "name": "Custom Name of the inverter (used for sensors' prefix)", "host": "IP or hostname", "mac": "MAC address", "email": "Email", From 621420d1d2cd81e210e63568915ae230f6e5c48b Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Fri, 28 Apr 2023 08:50:38 +0200 Subject: [PATCH 19/74] cleanup --- .../deltadore-tydom/tydom/tydom_client.py | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index b6f0a60..47bdf0d 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -74,9 +74,13 @@ async def async_get_credentials( method="GET", url="https://deltadoreadb2ciot.b2clogin.com/deltadoreadb2ciot.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_AccountProviderROPC_SignIn", ) - logger.info("response status : %s", response.status) - logger.info("response content : %s", await response.text()) - logger.info("response headers : %s", response.headers) + + logger.debug( + "response status : %s\nheaders : %s\ncontent : %s", + response.status, + response.headers, + await response.text(), + ) json_response = await response.json() signin_url = json_response["token_endpoint"] @@ -91,8 +95,6 @@ async def async_get_credentials( "scope": "openid profile offline_access https://deltadoreadb2ciot.onmicrosoft.com/iotapi/video_config https://deltadoreadb2ciot.onmicrosoft.com/iotapi/video_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_gateway_credentials https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_camera_credentials https://deltadoreadb2ciot.onmicrosoft.com/iotapi/comptage_europe_collect_reader https://deltadoreadb2ciot.onmicrosoft.com/iotapi/comptage_europe_site_config_contributor https://deltadoreadb2ciot.onmicrosoft.com/iotapi/pilotage_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/consent_mgt_contributor https://deltadoreadb2ciot.onmicrosoft.com/iotapi/b2caccountprovider_manage_account https://deltadoreadb2ciot.onmicrosoft.com/iotapi/b2caccountprovider_allow_view_account https://deltadoreadb2ciot.onmicrosoft.com/iotapi/tydom_backend_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/websocket_remote_access https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_device https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_view https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_space https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_connector https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_endpoint https://deltadoreadb2ciot.onmicrosoft.com/iotapi/rule_management_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/collect_read_datas", } ) - logger.info("body : %s", body) - logger.info("header : %s", ct_header) response = await session.post( url=signin_url, @@ -100,24 +102,28 @@ async def async_get_credentials( data=body, ) - logger.info("response status : %s", response.status) - logger.info("response content : %s", await response.text()) - logger.info("response headers : %s", response.headers) + logger.debug( + "response status : %s\nheaders : %s\ncontent : %s", + response.status, + response.headers, + await response.text(), + ) json_response = await response.json() access_token = json_response["access_token"] - logger.info("access_token : %s", access_token) - response = await session.request( method="GET", url=f"https://prod.iotdeltadore.com/sitesmanagement/api/v1/sites?gateway_mac={macaddress}", headers={"Authorization": f"Bearer {access_token}"}, ) - logger.info("response status : %s", response.status) - logger.info("response content : %s", await response.text()) - logger.info("response headers : %s", response.headers) + logger.debug( + "response status : %s\nheaders : %s\ncontent : %s", + response.status, + response.headers, + await response.text(), + ) json_response = await response.json() @@ -163,9 +169,12 @@ async def async_connect(self) -> ClientWebSocketResponse: headers=http_headers, json=None, ) - logger.info("response status : %s", response.status) - logger.info("response content : %s", await response.text()) - logger.info("response headers : %s", response.headers) + logger.debug( + "response status : %s\nheaders : %s\ncontent : %s", + response.status, + response.headers, + await response.text(), + ) re_matcher = re.match( '.*nonce="([a-zA-Z0-9+=]+)".*', @@ -181,8 +190,6 @@ async def async_connect(self) -> ClientWebSocketResponse: re_matcher.group(1) ) - logger.info("new request headers : %s", http_headers) - connection = await self._session.ws_connect( method="GET", url=f"wss://{self._host}:443/mediation/client?mac={self._mac}&appli=1", @@ -232,12 +239,7 @@ async def listen_tydom(self, connection: ClientWebSocketResponse): cmd_prefix=self._cmd_prefix, ) await message_handler.incoming_triage() - # message_handler = MessageHandler( - # incoming_bytes=incoming_bytes_str, - # tydom_client=tydom_client, - # mqtt_client=mqtt_client, - # ) - # await message_handler.incoming_triage() + except Exception as e: logger.warning("Unable to handle message: %s", e) From befbde822af07bc6f19e080801a1b2b4ecc018c6 Mon Sep 17 00:00:00 2001 From: ucpy7374 Date: Fri, 28 Apr 2023 14:34:33 +0200 Subject: [PATCH 20/74] add logs + ping --- custom_components/deltadore-tydom/__init__.py | 6 +- custom_components/deltadore-tydom/hub.py | 19 ++++++- .../deltadore-tydom/tydom/MessageHandler.py | 6 +- .../deltadore-tydom/tydom/tydom_client.py | 56 +++++++++++-------- 4 files changed, 55 insertions(+), 32 deletions(-) diff --git a/custom_components/deltadore-tydom/__init__.py b/custom_components/deltadore-tydom/__init__.py index 413d8fa..5260bc7 100644 --- a/custom_components/deltadore-tydom/__init__.py +++ b/custom_components/deltadore-tydom/__init__.py @@ -36,9 +36,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_create_background_task( target=tydom_hub.setup(connection), hass=hass, name="Tydom" ) - # entry.async_create_background_task( - # target=tydom_hub.ping(connection), hass=hass, name="Tydom ping" - # ) + entry.async_create_background_task( + target=tydom_hub.ping(connection), hass=hass, name="Tydom ping" + ) except Exception as err: raise ConfigEntryNotReady from err diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 7e0e698..e98d6f8 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -39,7 +39,7 @@ def __init__( self._pin = alarmpin self._hass = hass self._name = mac - self._id = mac.lower() + self._id = "Tydom-" + mac self._tydom_client = TydomClient( session=async_create_clientsession(self._hass, False), @@ -84,6 +84,13 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: logger.info("Listen to tydom events") await self._tydom_client.listen_tydom(connection) + async def ping(self, connection: ClientWebSocketResponse) -> None: + """Periodically send pings""" + logger.info("Sending ping") + while True: + await self._tydom_client.ping() + await asyncio.sleep(10) + class Roller: """Dummy roller (device for HA) for Hello World example.""" @@ -113,6 +120,7 @@ def roller_id(self) -> str: @property def position(self): """Return position for roller.""" + logger.error("get roller position") return self._current_position async def set_position(self, position: int) -> None: @@ -120,6 +128,7 @@ async def set_position(self, position: int) -> None: Set dummy cover to the given position. State is announced a random number of seconds later. """ + logger.error("set roller position") self._target_position = position # Update the moving status, and broadcast the update @@ -130,22 +139,26 @@ async def set_position(self, position: int) -> None: async def delayed_update(self) -> None: """Publish updates, with a random delay to emulate interaction with device.""" + logger.error("delayed_update") await asyncio.sleep(random.randint(1, 10)) self.moving = 0 await self.publish_updates() def register_callback(self, callback: Callable[[], None]) -> None: """Register callback, called when Roller changes state.""" + logger.error("register_callback %s", callback) self._callbacks.add(callback) def remove_callback(self, callback: Callable[[], None]) -> None: """Remove previously registered callback.""" + logger.error("remove_callback") self._callbacks.discard(callback) # In a real implementation, this library would call it's call backs when it was # notified of any state changeds for the relevant device. async def publish_updates(self) -> None: """Schedule call all registered callbacks.""" + logger.error("publish_updates") self._current_position = self._target_position for callback in self._callbacks: callback() @@ -153,6 +166,7 @@ async def publish_updates(self) -> None: @property def online(self) -> float: """Roller is online.""" + logger.error("online") # The dummy roller is offline about 10% of the time. Returns True if online, # False if offline. return random.random() > 0.1 @@ -160,14 +174,17 @@ def online(self) -> float: @property def battery_level(self) -> int: """Battery level as a percentage.""" + logger.error("battery_level") return random.randint(0, 100) @property def battery_voltage(self) -> float: + logger.error("battery_voltage") """Return a random voltage roughly that of a 12v battery.""" return round(random.random() * 3 + 10, 2) @property def illuminance(self) -> int: + logger.error("illuminance") """Return a sample illuminance in lux.""" return random.randint(0, 500) diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index d936fb8..3087fa2 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -215,15 +215,13 @@ class MessageHandler: """Handle incomming Tydom messages""" - def __init__(self, incoming_bytes, tydom_client, cmd_prefix): - self.incoming_bytes = incoming_bytes + def __init__(self, tydom_client, cmd_prefix): self.tydom_client = tydom_client self.cmd_prefix = cmd_prefix - async def incoming_triage(self): + async def incoming_triage(self, bytes_str): """Identify message type and dispatch the result""" - bytes_str = self.incoming_bytes incoming = None first = str(bytes_str[:40]) try: diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index 47bdf0d..37d70dd 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -8,6 +8,7 @@ import async_timeout import aiohttp +from typing import cast from urllib3 import encode_multipart_formdata from aiohttp import ClientWebSocketResponse, ClientSession from .MessageHandler import MessageHandler @@ -76,13 +77,14 @@ async def async_get_credentials( ) logger.debug( - "response status : %s\nheaders : %s\ncontent : %s", + "response status for openid-config: %s\nheaders : %s\ncontent : %s", response.status, response.headers, await response.text(), ) json_response = await response.json() + response.close() signin_url = json_response["token_endpoint"] logger.info("signin_url : %s", signin_url) @@ -103,13 +105,14 @@ async def async_get_credentials( ) logger.debug( - "response status : %s\nheaders : %s\ncontent : %s", + "response status for signin : %s\nheaders : %s\ncontent : %s", response.status, response.headers, await response.text(), ) json_response = await response.json() + response.close() access_token = json_response["access_token"] response = await session.request( @@ -119,14 +122,16 @@ async def async_get_credentials( ) logger.debug( - "response status : %s\nheaders : %s\ncontent : %s", + "response status for https://prod.iotdeltadore.com/sitesmanagement/api/v1/sites?gateway_mac= : %s\nheaders : %s\ncontent : %s", response.status, response.headers, await response.text(), ) json_response = await response.json() + response.close(); + session.close() if ( "sites" in json_response and len(json_response["sites"]) == 1 @@ -180,11 +185,13 @@ async def async_connect(self) -> ClientWebSocketResponse: '.*nonce="([a-zA-Z0-9+=]+)".*', response.headers.get("WWW-Authenticate"), ) + response.close() if re_matcher: logger.info("nonce : %s", re_matcher.group(1)) else: raise TydomClientApiClientError("Could't find auth nonce") + http_headers = {} http_headers["Authorization"] = self.build_digest_headers( re_matcher.group(1) @@ -196,7 +203,7 @@ async def async_connect(self) -> ClientWebSocketResponse: headers=http_headers, autoclose=False, autoping=True, - timeout=100, + timeout=-1, heartbeat=10, ) @@ -225,20 +232,19 @@ async def listen_tydom(self, connection: ClientWebSocketResponse): await self.get_devices_cmeta() await self.get_devices_data() + message_handler = MessageHandler(tydom_client=self, cmd_prefix=self._cmd_prefix) + while True: try: if self._connection.closed: + await self._connection.close() self._connection = await self.async_connect() - incoming_bytes_str = await self._connection.receive_bytes() - # logger.info(incoming_bytes_str.type) - logger.info(incoming_bytes_str) - - message_handler = MessageHandler( - incoming_bytes=incoming_bytes_str, - tydom_client=self, - cmd_prefix=self._cmd_prefix, - ) - await message_handler.incoming_triage() + + msg = await self._connection.receive() + logger.info("Incomming message - type : %s - message : %s", msg.type, msg.data) + incoming_bytes_str = cast(bytes, msg.data) + + await message_handler.incoming_triage(incoming_bytes_str) except Exception as e: logger.warning("Unable to handle message: %s", e) @@ -302,8 +308,8 @@ async def get_info(self): req = "GET" await self.send_message(method=req, msg=msg_type) - # Refresh (all) async def post_refresh(self): + """Refresh (all)""" msg_type = "/refresh/all" req = "POST" await self.send_message(method=req, msg=msg_type) @@ -318,8 +324,8 @@ async def post_refresh(self): self.poll_device_urls[self.current_poll_index] ) - # Send a ping (pong should be returned) async def ping(self): + """Send a ping (pong should be returned)""" msg_type = "/ping" req = "GET" await self.send_message(method=req, msg=msg_type) @@ -346,19 +352,20 @@ async def get_configs_file(self): req = "GET" await self.send_message(method=req, msg=msg_type) - # Get metadata configuration to list poll devices (like Tywatt) async def get_devices_cmeta(self): + """Get metadata configuration to list poll devices (like Tywatt)""" msg_type = "/devices/cmeta" req = "GET" await self.send_message(method=req, msg=msg_type) async def get_data(self): + """Get all config/metadata/data""" await self.get_configs_file() await self.get_devices_cmeta() await self.get_devices_data() - # Give order to endpoint async def get_device_data(self, id): + """Give order to endpoint""" # 10 here is the endpoint = the device (shutter in this case) to open. device_id = str(id) str_request = ( @@ -369,24 +376,28 @@ async def get_device_data(self, id): await self._connection.send(a_bytes) async def get_poll_device_data(self, url): + logger.error("poll device data %s", url) msg_type = url req = "GET" await self.send_message(method=req, msg=msg_type) - # Get the moments (programs) + def add_poll_device_url(self, url): + self.poll_device_urls.append(url) + async def get_moments(self): + """Get the moments (programs)""" msg_type = "/moments/file" req = "GET" await self.send_message(method=req, msg=msg_type) - # Get the scenarios async def get_scenarii(self): + """Get the scenarios""" msg_type = "/scenarios/file" req = "GET" await self.send_message(method=req, msg=msg_type) - # Give order (name + value) to endpoint async def put_devices_data(self, device_id, endpoint_id, name, value): + """Give order (name + value) to endpoint""" # For shutter, value is the percentage of closing body = '[{"name":"' + name + '","value":"' + value + '"}]' # endpoint_id is the endpoint = the device (shutter in this case) to @@ -467,6 +478,3 @@ async def put_alarm_cdata(self, device_id, alarm_id=None, value=None, zone_id=No logger.error(a_bytes) except BaseException: logger.error("put_alarm_cdata ERROR !", exc_info=True) - - def add_poll_device_url(self, url): - self.poll_device_urls.append(url) From 79a21edfc7c3ddc78d2008186b3e91ec0e3d43c0 Mon Sep 17 00:00:00 2001 From: ucpy7374 Date: Fri, 5 May 2023 16:23:29 +0200 Subject: [PATCH 21/74] updates --- custom_components/deltadore-tydom/__init__.py | 10 +- custom_components/deltadore-tydom/cover.py | 8 +- .../deltadore-tydom/ha_entities.py | 158 ++++++++++++++++++ custom_components/deltadore-tydom/hub.py | 78 ++++++++- .../deltadore-tydom/tydom/MessageHandler.py | 83 +++++++-- .../deltadore-tydom/tydom/tydom_client.py | 51 ++++-- .../deltadore-tydom/tydom/tydom_devices.py | 109 ++++++++++++ custom_components/deltadore-tydom/update.py | 78 +++++++++ 8 files changed, 533 insertions(+), 42 deletions(-) create mode 100644 custom_components/deltadore-tydom/ha_entities.py create mode 100644 custom_components/deltadore-tydom/tydom/tydom_devices.py create mode 100644 custom_components/deltadore-tydom/update.py diff --git a/custom_components/deltadore-tydom/__init__.py b/custom_components/deltadore-tydom/__init__.py index 5260bc7..95a2e79 100644 --- a/custom_components/deltadore-tydom/__init__.py +++ b/custom_components/deltadore-tydom/__init__.py @@ -1,7 +1,7 @@ """The Detailed Hello World Push integration.""" from __future__ import annotations -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PIN, Platform from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -11,7 +11,7 @@ # List of platforms to support. There should be a matching .py file for each, # eg and -PLATFORMS: list[str] = ["cover", "sensor"] +PLATFORMS: list[str] = [Platform.COVER, Platform.SENSOR, Platform.UPDATE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -36,9 +36,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_create_background_task( target=tydom_hub.setup(connection), hass=hass, name="Tydom" ) - entry.async_create_background_task( - target=tydom_hub.ping(connection), hass=hass, name="Tydom ping" - ) + #entry.async_create_background_task( + # target=tydom_hub.ping(connection), hass=hass, name="Tydom ping" + #) except Exception as err: raise ConfigEntryNotReady from err diff --git a/custom_components/deltadore-tydom/cover.py b/custom_components/deltadore-tydom/cover.py index 4ae421e..47588b8 100644 --- a/custom_components/deltadore-tydom/cover.py +++ b/custom_components/deltadore-tydom/cover.py @@ -10,13 +10,15 @@ SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + SUPPORT_STOP, CoverEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity import DeviceInfo -from .const import DOMAIN +from .const import DOMAIN, LOGGER # This function is called as part of the __init__.async_setup_entry (via the @@ -27,9 +29,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add cover for passed config_entry in HA.""" + LOGGER.error("***** async_setup_entry *****") # The hub is loaded from the associated hass.data entry that was created in the # __init__.async_setup_entry function hub = hass.data[DOMAIN][config_entry.entry_id] + hub.add_cover_callback = async_add_entities # Add all entities to HA async_add_entities(HelloWorldCover(roller) for roller in hub.rollers) @@ -47,7 +51,7 @@ class HelloWorldCover(CoverEntity): # imported above, we can tell HA the features that are supported by this entity. # If the supported features were dynamic (ie: different depending on the external # device it connected to), then this should be function with an @property decorator. - supported_features = SUPPORT_SET_POSITION | SUPPORT_OPEN | SUPPORT_CLOSE + supported_features = SUPPORT_SET_POSITION | SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP def __init__(self, roller) -> None: """Initialize the sensor.""" diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py new file mode 100644 index 0000000..e7894e3 --- /dev/null +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -0,0 +1,158 @@ +"""Home assistant entites""" +from typing import Any + +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.components.cover import ( + ATTR_POSITION, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, + CoverEntity, + CoverDeviceClass +) +from .tydom.tydom_devices import TydomShutter +from .const import DOMAIN, LOGGER + +# This entire class could be written to extend a base class to ensure common attributes +# are kept identical/in sync. It's broken apart here between the Cover and Sensors to +# be explicit about what is returned, and the comments outline where the overlap is. +class HACover(CoverEntity): + """Representation of a dummy Cover.""" + + # Our dummy class is PUSH, so we tell HA that it should not be polled + should_poll = False + # The supported features of a cover are done using a bitmask. Using the constants + # imported above, we can tell HA the features that are supported by this entity. + # If the supported features were dynamic (ie: different depending on the external + # device it connected to), then this should be function with an @property decorator. + supported_features = SUPPORT_SET_POSITION | SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + device_class= CoverDeviceClass.SHUTTER + + def __init__(self, shutter: TydomShutter) -> None: + """Initialize the sensor.""" + # Usual setup is done here. Callbacks are added in async_added_to_hass. + self._shutter = shutter + + # A unique_id for this entity with in this domain. This means for example if you + # have a sensor on this cover, you must ensure the value returned is unique, + # which is done here by appending "_cover". For more information, see: + # https://developers.home-assistant.io/docs/entity_registry_index/#unique-id-requirements + # Note: This is NOT used to generate the user visible Entity ID used in automations. + self._attr_unique_id = self._shutter.uid + + # This is the name for this *entity*, the "name" attribute from "device_info" + # is used as the device name for device screens in the UI. This name is used on + # entity screens, and used to build the Entity ID that's used is automations etc. + self._attr_name = self._shutter.name + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + # Importantly for a push integration, the module that will be getting updates + # needs to notify HA of changes. The dummy device has a registercallback + # method, so to this we add the 'self.async_write_ha_state' method, to be + # called where ever there are changes. + # The call back registration is done once this entity is registered with HA + # (rather than in the __init__) + self._shutter.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._shutter.remove_callback(self.async_write_ha_state) + + # Information about the devices that is partially visible in the UI. + # The most critical thing here is to give this entity a name so it is displayed + # as a "device" in the HA UI. This name is used on the Devices overview table, + # and the initial screen when the device is added (rather than the entity name + # property below). You can then associate other Entities (eg: a battery + # sensor) with this device, so it shows more like a unified element in the UI. + # For example, an associated battery sensor will be displayed in the right most + # column in the Configuration > Devices view for a device. + # To associate an entity with this device, the device_info must also return an + # identical "identifiers" attribute, but not return a name attribute. + # See the sensors.py file for the corresponding example setup. + # Additional meta data can also be returned here, including sw_version (displayed + # as Firmware), model and manufacturer (displayed as by ) + # shown on the device info screen. The Manufacturer and model also have their + # respective columns on the Devices overview table. Note: Many of these must be + # set when the device is first added, and they are not always automatically + # refreshed by HA from it's internal cache. + # For more information see: + # https://developers.home-assistant.io/docs/device_registry_index/#device-properties + @property + def device_info(self) -> DeviceInfo: + """Information about this entity/device.""" + return { + "identifiers": {(DOMAIN, self._shutter.uid)}, + # If desired, the name for the device could be different to the entity + "name": self.name, + #"sw_version": self._shutter.firmware_version, + #"model": self._shutter.model, + #"manufacturer": self._shutter.hub.manufacturer, + } + + # This property is important to let HA know if this entity is online or not. + # If an entity is offline (return False), the UI will refelect this. + @property + def available(self) -> bool: + """Return True if roller and hub is available.""" + # return self._shutter.online and self._shutter.hub.online + # FIXME + return True + + # The following properties are how HA knows the current state of the device. + # These must return a value from memory, not make a live query to the device/hub + # etc when called (hence they are properties). For a push based integration, + # HA is notified of changes via the async_write_ha_state call. See the __init__ + # method for hos this is implemented in this example. + # The properties that are expected for a cover are based on the supported_features + # property of the object. In the case of a cover, see the following for more + # details: https://developers.home-assistant.io/docs/core/entity/cover/ + @property + def current_cover_position(self): + """Return the current position of the cover.""" + return self._shutter.position + + @property + def is_closed(self) -> bool: + """Return if the cover is closed, same as position 0.""" + return self._shutter.position == 0 + + #@property + #def is_closing(self) -> bool: + # """Return if the cover is closing or not.""" + # return self._shutter.moving < 0 + + #@property + #def is_opening(self) -> bool: + # """Return if the cover is opening or not.""" + # return self._shutter.moving > 0 + + + + #self.on_fav_pos = None + #self.up_defect = None + #self.down_defect = None + #self.obstacle_defect = None + #self.intrusion = None + #self.batt_defect = None + + @property + def is_thermic_defect(self) -> bool: + """Return the thermic_defect status""" + return self._shutter.thermic_defect + + # These methods allow HA to tell the actual device what to do. In this case, move + # the cover to the desired position, or open and close it all the way. + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self._shutter.set_position(100) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + await self._shutter.set_position(0) + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Close the cover.""" + await self._shutter.set_position(kwargs[ATTR_POSITION]) diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index e98d6f8..ac98278 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -10,11 +10,14 @@ import random import logging import time +from typing import Callable from aiohttp import ClientWebSocketResponse, ClientSession from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession from .tydom.tydom_client import TydomClient +from .tydom.tydom_devices import TydomBaseEntity +from .ha_entities import HACover logger = logging.getLogger(__name__) @@ -24,6 +27,10 @@ class Hub: manufacturer = "Delta Dore" + def handle_event(self, event): + """Event callback""" + pass + def __init__( self, hass: HomeAssistant, @@ -32,7 +39,7 @@ def __init__( password: str, alarmpin: str, ) -> None: - """Init dummy hub.""" + """Init hub.""" self._host = host self._mac = mac self._pass = password @@ -40,13 +47,17 @@ def __init__( self._hass = hass self._name = mac self._id = "Tydom-" + mac + self.device_info = TydomBaseEntity(None, None, None, None, None, None, None, None, None, None, None, False) + self.devices = {} + self.add_cover_callback = None self._tydom_client = TydomClient( - session=async_create_clientsession(self._hass, False), + hass=self._hass, mac=self._mac, host=self._host, password=self._pass, alarm_pin=self._pin, + event_callback=self.handle_event ) self.rollers = [ @@ -83,14 +94,69 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: """Listen to tydom events.""" logger.info("Listen to tydom events") await self._tydom_client.listen_tydom(connection) - - async def ping(self, connection: ClientWebSocketResponse) -> None: + while True: + devices = await self._tydom_client.consume_messages() + if devices is not None: + for device in devices: + logger.info("*** device %s", device) + if isinstance(device, TydomBaseEntity): + await self.update_tydom_entity(device) + else: + logger.error("*** publish_updates for device : %s", device) + if device.uid not in self.devices: + self.devices[device.uid] = device + await self.create_ha_device(device) + else: + await self.update_ha_device(self.devices[device.uid], device) + + async def update_tydom_entity(self, updated_entity: TydomBaseEntity) -> None: + """Update Tydom Base entity values and push to HA""" + logger.error("update Tydom ") + self.device_info.product_name = updated_entity.product_name + self.device_info.main_version_sw = updated_entity.main_version_sw + self.device_info.main_version_hw = updated_entity.main_version_hw + self.device_info.main_id = updated_entity.main_id + self.device_info.main_reference = updated_entity.main_reference + self.device_info.key_version_sw = updated_entity.key_version_sw + self.device_info.key_version_hw = updated_entity.key_version_hw + self.device_info.key_version_stack = updated_entity.key_version_stack + self.device_info.key_reference = updated_entity.key_reference + self.device_info.boot_reference = updated_entity.boot_reference + self.device_info.boot_version = updated_entity.boot_version + self.device_info.update_available = updated_entity.update_available + await self.device_info.publish_updates() + + async def create_ha_device(self, device): + """Create a new HA device""" + logger.debug("Create device %s", device.uid) + ha_device = HACover(device) + if self.add_cover_callback is not None: + self.add_cover_callback([ha_device]) + + + async def update_ha_device(self, ha_device, device): + """Update HA device values""" + logger.debug("Update device %s", device.uid) + ha_device.thermic_defect = device.thermic_defect + ha_device.position = device.position + ha_device.on_fav_pos = device.on_fav_pos + ha_device.up_defect = device.up_defect + ha_device.down_defect = device.down_defect + ha_device.obstacle_defect = device.obstacle_defect + ha_device.intrusion = device.intrusion + ha_device.batt_defect = device.batt_defect + await ha_device.publish_updates() + + async def ping(self) -> None: """Periodically send pings""" logger.info("Sending ping") while True: await self._tydom_client.ping() await asyncio.sleep(10) + async def async_trigger_firmware_update(self) -> None: + """Trigger firmware update""" + class Roller: """Dummy roller (device for HA) for Hello World example.""" @@ -179,12 +245,12 @@ def battery_level(self) -> int: @property def battery_voltage(self) -> float: - logger.error("battery_voltage") """Return a random voltage roughly that of a 12v battery.""" + logger.error("battery_voltage") return round(random.random() * 3 + 10, 2) @property def illuminance(self) -> int: - logger.error("illuminance") """Return a sample illuminance in lux.""" + logger.error("illuminance") return random.randint(0, 500) diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 3087fa2..912731b 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -3,9 +3,11 @@ from http.client import HTTPResponse from http.server import BaseHTTPRequestHandler from io import BytesIO - +import traceback import urllib3 +from .tydom_devices import TydomBaseEntity, TydomDevice, TydomShutter + logger = logging.getLogger(__name__) # Dicts @@ -211,7 +213,6 @@ device_endpoint = dict() device_type = dict() - class MessageHandler: """Handle incomming Tydom messages""" @@ -235,46 +236,55 @@ async def incoming_triage(self, bytes_str): except BaseException: # Tywatt response starts at 7 incoming = self.parse_put_response(bytes_str, 7) - await self.parse_response(incoming) + return await self.parse_response(incoming) except BaseException: logger.error( "Error when parsing devices/data tydom message (%s)", bytes_str ) + return None elif "scn" in first: try: # FIXME # incoming = get(bytes_str) incoming = first - await self.parse_response(incoming) + scenarii = await self.parse_response(incoming) logger.debug("Scenarii message processed") + return scenarii except BaseException: logger.error( "Error when parsing Scenarii tydom message (%s)", bytes_str ) + return None elif "POST" in first: try: incoming = self.parse_put_response(bytes_str) - await self.parse_response(incoming) + post = await self.parse_response(incoming) logger.debug("POST message processed") + return post except BaseException: logger.error( "Error when parsing POST tydom message (%s)", bytes_str ) + return None elif "HTTP/1.1" in first: response = self.response_from_bytes(bytes_str[len(self.cmd_prefix) :]) incoming = response.data.decode("utf-8") try: - await self.parse_response(incoming) + return await self.parse_response(incoming) except BaseException: logger.error( "Error when parsing HTTP/1.1 tydom message (%s)", bytes_str ) + return None else: logger.warning("Unknown tydom message type received (%s)", bytes_str) + return None except Exception as e: logger.error("Technical error when parsing tydom message (%s)", bytes_str) logger.debug("Incoming payload (%s)", incoming) + logger.debug("exception : %s", e) + return None # Basic response parsing. Typically GET responses + instanciate covers and # alarm class for updating data @@ -304,34 +314,68 @@ async def parse_response(self, incoming): try: if msg_type == "msg_config": parsed = json.loads(data) - await self.parse_config_data(parsed=parsed) + return await MessageHandler.parse_config_data(parsed=parsed) elif msg_type == "msg_cmetadata": parsed = json.loads(data) - await self.parse_cmeta_data(parsed=parsed) + return await self.parse_cmeta_data(parsed=parsed) elif msg_type == "msg_data": parsed = json.loads(data) - await self.parse_devices_data(parsed=parsed) + return await self.parse_devices_data(parsed=parsed) elif msg_type == "msg_cdata": parsed = json.loads(data) - await self.parse_devices_cdata(parsed=parsed) + return await self.parse_devices_cdata(parsed=parsed) elif msg_type == "msg_html": logger.debug("HTML Response ?") elif msg_type == "msg_info": - pass + parsed = json.loads(data) + return await self.parse_msg_info(parsed) + except Exception as e: logger.error("Error on parsing tydom response (%s)", e) + traceback.print_exception(e) logger.debug("Incoming data parsed with success") + async def parse_msg_info(self, parsed): + logger.debug("parse_msg_info : %s", parsed) + product_name = parsed["productName"] + main_version_sw = parsed["mainVersionSW"] + main_version_hw = parsed["mainVersionHW"] + main_id = parsed["mainId"] + main_reference = parsed["mainReference"] + key_version_sw = parsed["keyVersionSW"] + key_version_hw = parsed["keyVersionHW"] + key_version_stack = parsed["keyVersionStack"] + key_reference = parsed["keyReference"] + boot_reference = parsed["bootReference"] + boot_version = parsed["bootVersion"] + update_available = parsed["updateAvailable"] + return [TydomBaseEntity(product_name, main_version_sw, main_version_hw, main_id, main_reference, key_version_sw, key_version_hw, key_version_stack, key_reference, boot_reference, boot_version, update_available)] + + @staticmethod + async def get_device(last_usage, uid, name, endpoint = None, data = None) -> TydomDevice: + """Get device class from its last usage""" + match last_usage: + case "shutter" | "klineShutter": + return TydomShutter(uid, name, last_usage, endpoint, data) + case _: + return + @staticmethod async def parse_config_data(parsed): + logger.debug("parse_config_data : %s", parsed) + devices = [] for i in parsed["endpoints"]: device_unique_id = str(i["id_endpoint"]) + "_" + str(i["id_device"]) + device = await MessageHandler.get_device(i["last_usage"], device_unique_id, i["name"], i["id_endpoint"], None) + if device is not None: + devices.append(device) + if ( i["last_usage"] == "shutter" or i["last_usage"] == "klineShutter" @@ -376,8 +420,10 @@ async def parse_config_data(parsed): device_endpoint[device_unique_id] = i["id_endpoint"] logger.debug("Configuration updated") + return devices async def parse_cmeta_data(self, parsed): + logger.debug("parse_cmeta_data : %s", parsed) for i in parsed: for endpoint in i["endpoints"]: if len(endpoint["cmetadata"]) > 0: @@ -446,6 +492,9 @@ async def parse_cmeta_data(self, parsed): logger.debug("Metadata configuration updated") async def parse_devices_data(self, parsed): + logger.debug("parse_devices_data : %s", parsed) + devices = [] + for i in parsed: for endpoint in i["endpoints"]: if endpoint["error"] == 0 and len(endpoint["data"]) > 0: @@ -473,10 +522,16 @@ async def parse_devices_data(self, parsed): type_of_id, ) + data = {} + for elem in endpoint["data"]: element_name = elem["name"] element_value = elem["value"] element_validity = elem["validity"] + + if element_validity == "upToDate": + data[element_name] = element_value + print_id = name_of_id if len(name_of_id) != 0 else device_id if type_of_id == "light": @@ -495,6 +550,7 @@ async def parse_devices_data(self, parsed): attr_light[element_name] = element_value if type_of_id == "shutter" or type_of_id == "klineShutter": + if ( element_name in deviceCoverKeywords and element_validity == "upToDate" @@ -677,6 +733,10 @@ async def parse_devices_data(self, parsed): logger.error("msg_data error in parsing !") logger.error(e) + device = await MessageHandler.get_device(type_of_id, unique_id, None, endpoint_id, data) + if device is not None: + devices.append(device) + if ( "device_type" in attr_cover and attr_cover["device_type"] == "cover" @@ -831,6 +891,7 @@ async def parse_devices_data(self, parsed): pass async def parse_devices_cdata(self, parsed): + logger.debug("parse_devices_data : %s", parsed) for i in parsed: for endpoint in i["endpoints"]: if endpoint["error"] == 0 and len(endpoint["cdata"]) > 0: diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index 37d70dd..eb66aeb 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -7,12 +7,17 @@ import re import async_timeout import aiohttp +import traceback from typing import cast from urllib3 import encode_multipart_formdata from aiohttp import ClientWebSocketResponse, ClientSession +from homeassistant.helpers.aiohttp_client import async_create_clientsession + from .MessageHandler import MessageHandler + + from requests.auth import HTTPDigestAuth logger = logging.getLogger(__name__) @@ -35,21 +40,23 @@ class TydomClient: def __init__( self, - session: aiohttp.ClientSession, + hass, mac: str, password: str, alarm_pin: str = None, host: str = "mediation.tydom.com", + event_callback=None ) -> None: logger.debug("Initializing TydomClient Class") - self._session = session + self._hass = hass self._password = password self._mac = mac self._host = host self._alarm_pin = alarm_pin self._remote_mode = self._host == "mediation.tydom.com" self._connection = None + self.event_callback = event_callback # Some devices (like Tywatt) need polling self.poll_device_urls = [] @@ -64,6 +71,8 @@ def __init__( self._cmd_prefix = "" self._ping_timeout = None + self._message_handler = MessageHandler(tydom_client=self, cmd_prefix=self._cmd_prefix) + @staticmethod async def async_get_credentials( session: ClientSession, email: str, password: str, macaddress: str @@ -151,6 +160,7 @@ async def async_get_credentials( "Error fetching information", ) from exception except Exception as exception: # pylint: disable=broad-except + traceback.print_exception raise TydomClientApiClientError( "Something really wrong happened!" ) from exception @@ -166,6 +176,8 @@ async def async_connect(self) -> ClientWebSocketResponse: "Sec-WebSocket-Version": "13", } + self._session = async_create_clientsession(self._hass, False) + try: async with async_timeout.timeout(10): response = await self._session.request( @@ -173,6 +185,7 @@ async def async_connect(self) -> ClientWebSocketResponse: url=f"https://{self._host}:443/mediation/client?mac={self._mac}&appli=1", headers=http_headers, json=None, + #proxy="http://proxy.rd.francetelecom.fr:8080" ) logger.debug( "response status : %s\nheaders : %s\ncontent : %s", @@ -186,6 +199,7 @@ async def async_connect(self) -> ClientWebSocketResponse: response.headers.get("WWW-Authenticate"), ) response.close() + if re_matcher: logger.info("nonce : %s", re_matcher.group(1)) else: @@ -201,10 +215,9 @@ async def async_connect(self) -> ClientWebSocketResponse: method="GET", url=f"wss://{self._host}:443/mediation/client?mac={self._mac}&appli=1", headers=http_headers, - autoclose=False, autoping=True, - timeout=-1, - heartbeat=10, + heartbeat=2, + #proxy="http://proxy.rd.francetelecom.fr:8080" ) return connection @@ -218,6 +231,7 @@ async def async_connect(self) -> ClientWebSocketResponse: "Error fetching information", ) from exception except Exception as exception: # pylint: disable=broad-except + traceback.print_exception(exception) raise TydomClientApiClientError( "Something really wrong happened!" ) from exception @@ -232,22 +246,23 @@ async def listen_tydom(self, connection: ClientWebSocketResponse): await self.get_devices_cmeta() await self.get_devices_data() - message_handler = MessageHandler(tydom_client=self, cmd_prefix=self._cmd_prefix) - - while True: - try: - if self._connection.closed: - await self._connection.close() - self._connection = await self.async_connect() + async def consume_messages(self): + """Read and parse incomming messages""" + try: + if self._connection.closed: + await self._connection.close() + await asyncio.sleep(10) + self._connection = await self.async_connect() - msg = await self._connection.receive() - logger.info("Incomming message - type : %s - message : %s", msg.type, msg.data) - incoming_bytes_str = cast(bytes, msg.data) + msg = await self._connection.receive() + logger.info("Incomming message - type : %s - message : %s", msg.type, msg.data) + incoming_bytes_str = cast(bytes, msg.data) - await message_handler.incoming_triage(incoming_bytes_str) + return await self._message_handler.incoming_triage(incoming_bytes_str) - except Exception as e: - logger.warning("Unable to handle message: %s", e) + except Exception as e: + logger.warning("Unable to handle message: %s", e) + return None def build_digest_headers(self, nonce): """Build the headers of Digest Authentication.""" diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py new file mode 100644 index 0000000..dd73d8a --- /dev/null +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -0,0 +1,109 @@ +"""Support for Tydom classes""" +from typing import Callable +import logging + +logger = logging.getLogger(__name__) + +class TydomBaseEntity: + """Tydom entity base class.""" + def __init__(self, product_name, main_version_sw, main_version_hw, main_id, main_reference, + key_version_sw, key_version_hw, key_version_stack, key_reference, boot_reference, boot_version, update_available): + self.product_name = product_name + self.main_version_sw = main_version_sw + self.main_version_hw = main_version_hw + self.main_id = main_id + self.main_reference = main_reference + self.key_version_sw = key_version_sw + self.key_version_hw = key_version_hw + self.key_version_stack = key_version_stack + self.key_reference = key_reference + self.boot_reference = boot_reference + self.boot_version = boot_version + self.update_available = update_available + self._callbacks = set() + + def register_callback(self, callback: Callable[[], None]) -> None: + """Register callback, called when Roller changes state.""" + logger.error("register_callback %s", callback) + self._callbacks.add(callback) + + def remove_callback(self, callback: Callable[[], None]) -> None: + """Remove previously registered callback.""" + logger.error("remove_callback") + self._callbacks.discard(callback) + + # In a real implementation, this library would call it's call backs when it was + # notified of any state changeds for the relevant device. + async def publish_updates(self) -> None: + """Schedule call all registered callbacks.""" + logger.error("publish_updates") + for callback in self._callbacks: + callback() + + +class TydomDevice(): + """represents a generic device""" + + def __init__(self, uid, name, device_type, endpoint): + self.uid = uid + self.name = name + self.type = device_type + self.endpoint = endpoint + self._callbacks = set() + + def register_callback(self, callback: Callable[[], None]) -> None: + """Register callback, called when Roller changes state.""" + logger.error("register_callback %s", callback) + self._callbacks.add(callback) + + def remove_callback(self, callback: Callable[[], None]) -> None: + """Remove previously registered callback.""" + logger.error("remove_callback") + self._callbacks.discard(callback) + + @property + def device_id(self) -> str: + """Return ID for device.""" + return self.uid + +class TydomShutter(TydomDevice): + """Represents a shutter""" + def __init__(self, uid, name, device_type, endpoint, data): + logger.info("TydomShutter : data %s", data) + self.thermic_defect = None + logger.info("TydomShutter : pos") + #self.position = None + logger.info("TydomShutter : on_fav_pos") + self.on_fav_pos = None + self.up_defect = None + self.down_defect = None + self.obstacle_defect = None + self.intrusion = None + self.batt_defect = None + + if data is not None: + logger.info("TydomShutter : data not none %s", data) + if "thermicDefect" in data: + self.thermic_defect = data["thermicDefect"] + if "position" in data: + logger.error("positio : %s", data["position"]) + self.position = data["position"] + if "onFavPos" in data: + self.on_fav_pos = data["onFavPos"] + if "upDefect" in data: + self.up_defect = data["upDefect"] + if "downDefect" in data: + self.down_defect = data["downDefect"] + if "obstacleDefect" in data: + self.obstacle_defect = data["obstacleDefect"] + if "intrusion" in data: + self.intrusion = data["intrusion"] + if "battDefect" in data: + self.batt_defect = data["battDefect"] + super().__init__(uid, name, device_type, endpoint) + + @property + def position(self): + """Return position for shutter.""" + # logger.error("get shutter position") + return self.position diff --git a/custom_components/deltadore-tydom/update.py b/custom_components/deltadore-tydom/update.py new file mode 100644 index 0000000..01d45c2 --- /dev/null +++ b/custom_components/deltadore-tydom/update.py @@ -0,0 +1,78 @@ +"""Platform updateintegration.""" + +from typing import Any + +from homeassistant.components.update import UpdateEntity, UpdateEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, LOGGER +from .hub import Hub + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tydom update entities.""" + LOGGER.debug("Setting up Tydom update entities") + hub: Hub = hass.data[DOMAIN][entry.entry_id] + + entities = [TydomUpdateEntity(hub, entry.title)] + + async_add_entities(entities) + +class TydomUpdateEntity(UpdateEntity): + """Mixin for update entity specific attributes.""" + + _attr_supported_features = UpdateEntityFeature.INSTALL + _attr_title = "Tydom" + + def __init__( + self, + hub: Hub, + device_friendly_name: str, + ) -> None: + """Init Tydom connectivity class.""" + self._attr_name = f"{device_friendly_name} Tydom" + # self._attr_unique_id = f"{hub.hub_id()}-update" + self._hub = hub + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + # Importantly for a push integration, the module that will be getting updates + # needs to notify HA of changes. The dummy device has a registercallback + # method, so to this we add the 'self.async_write_ha_state' method, to be + # called where ever there are changes. + # The call back registration is done once this entity is registered with HA + # (rather than in the __init__) + self._hub.device_info.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._hub.device_info.remove_callback(self.async_write_ha_state) + + @property + def installed_version(self) -> str | None: + """Version currently in use.""" + if self._hub.device_info is None: + return None + # return self._hub.current_firmware + return self._hub.device_info.main_version_sw + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + if self._hub.device_info is not None: + if self._hub.device_info.update_available: + return self._hub.device_info.main_version_sw + return self._hub.device_info.main_version_sw + # FIXME : return correct version on update + return None + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + await self._hub.async_trigger_firmware_update() \ No newline at end of file From 7a8c03a8dfa713a329f9e8f1b1329ca216bdbb55 Mon Sep 17 00:00:00 2001 From: ucpy7374 Date: Fri, 5 May 2023 17:34:02 +0200 Subject: [PATCH 22/74] fix update --- .../deltadore-tydom/ha_entities.py | 76 ++++++++++++++++++- custom_components/deltadore-tydom/hub.py | 32 +++++--- custom_components/deltadore-tydom/sensor.py | 1 + .../deltadore-tydom/tydom/MessageHandler.py | 2 + .../deltadore-tydom/tydom/tydom_client.py | 4 +- .../deltadore-tydom/tydom/tydom_devices.py | 15 ++-- 6 files changed, 108 insertions(+), 22 deletions(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index e7894e3..134c141 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -1,7 +1,11 @@ """Home assistant entites""" from typing import Any -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.helpers.entity import Entity, DeviceInfo from homeassistant.components.cover import ( ATTR_POSITION, SUPPORT_CLOSE, @@ -11,7 +15,9 @@ CoverEntity, CoverDeviceClass ) + from .tydom.tydom_devices import TydomShutter + from .const import DOMAIN, LOGGER # This entire class could be written to extend a base class to ensure common attributes @@ -39,7 +45,7 @@ def __init__(self, shutter: TydomShutter) -> None: # which is done here by appending "_cover". For more information, see: # https://developers.home-assistant.io/docs/entity_registry_index/#unique-id-requirements # Note: This is NOT used to generate the user visible Entity ID used in automations. - self._attr_unique_id = self._shutter.uid + self._attr_unique_id = f"{self._shutter.uid}_cover" # This is the name for this *entity*, the "name" attribute from "device_info" # is used as the device name for device screens in the UI. This name is used on @@ -156,3 +162,69 @@ async def async_close_cover(self, **kwargs: Any) -> None: async def async_set_cover_position(self, **kwargs: Any) -> None: """Close the cover.""" await self._shutter.set_position(kwargs[ATTR_POSITION]) + + +class CoverBinarySensorBase(BinarySensorEntity): + """Base representation of a Sensor.""" + + should_poll = False + + def __init__(self, shutter: TydomShutter): + """Initialize the sensor.""" + self._shutter = shutter + + # To link this entity to the cover device, this property must return an + # identifiers value matching that used in the cover, but no other information such + # as name. If name is returned, this entity will then also become a device in the + # HA UI. + @property + def device_info(self): + """Return information to link this entity with the correct device.""" + return {"identifiers": {(DOMAIN, self._shutter.uid)}} + + # This property is important to let HA know if this entity is online or not. + # If an entity is offline (return False), the UI will refelect this. + @property + def available(self) -> bool: + """Return True if roller and hub is available.""" + #return self._roller.online and self._roller.hub.online + # FIXME + return True + + async def async_added_to_hass(self): + """Run when this Entity has been added to HA.""" + # Sensors should also register callbacks to HA when their state changes + self._shutter.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._shutter.remove_callback(self.async_write_ha_state) + +class BatterySensor(CoverBinarySensorBase): + """Representation of a Sensor.""" + + # The class of this device. Note the value should come from the homeassistant.const + # module. More information on the available devices classes can be seen here: + # https://developers.home-assistant.io/docs/core/entity/sensor + device_class = BinarySensorDeviceClass.PROBLEM + + def __init__(self, shutter): + """Initialize the sensor.""" + super().__init__(shutter) + + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._shutter.uid}_battery" + + # The name of the entity + self._attr_name = f"{self._shutter.name} Battery" + + self._state = False + + # The value of this sensor. As this is a DEVICE_CLASS_BATTERY, this value must be + # the battery level as a percentage (between 0 and 100) + @property + def is_on(self): + """Return the state of the sensor.""" + return self._shutter.batt_defect \ No newline at end of file diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index ac98278..3e09669 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -17,7 +17,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .tydom.tydom_client import TydomClient from .tydom.tydom_devices import TydomBaseEntity -from .ha_entities import HACover +from .ha_entities import HACover, BatterySensor logger = logging.getLogger(__name__) @@ -49,7 +49,9 @@ def __init__( self._id = "Tydom-" + mac self.device_info = TydomBaseEntity(None, None, None, None, None, None, None, None, None, None, None, False) self.devices = {} + self.ha_devices = {} self.add_cover_callback = None + self.add_sensor_callback = None self._tydom_client = TydomClient( hass=self._hass, @@ -128,24 +130,30 @@ async def update_tydom_entity(self, updated_entity: TydomBaseEntity) -> None: async def create_ha_device(self, device): """Create a new HA device""" - logger.debug("Create device %s", device.uid) + logger.warn("Create device %s", device.uid) + logger.warn("### defect = %s", device.batt_defect) ha_device = HACover(device) + self.ha_devices[device.uid] = ha_device if self.add_cover_callback is not None: self.add_cover_callback([ha_device]) + batt_sensor = BatterySensor(device) + if self.add_sensor_callback is not None: + self.add_sensor_callback([batt_sensor]) - async def update_ha_device(self, ha_device, device): + async def update_ha_device(self, stored_device, device): """Update HA device values""" logger.debug("Update device %s", device.uid) - ha_device.thermic_defect = device.thermic_defect - ha_device.position = device.position - ha_device.on_fav_pos = device.on_fav_pos - ha_device.up_defect = device.up_defect - ha_device.down_defect = device.down_defect - ha_device.obstacle_defect = device.obstacle_defect - ha_device.intrusion = device.intrusion - ha_device.batt_defect = device.batt_defect - await ha_device.publish_updates() + stored_device.thermic_defect = device.thermic_defect + stored_device.position = device.position + stored_device.on_fav_pos = device.on_fav_pos + stored_device.up_defect = device.up_defect + stored_device.down_defect = device.down_defect + stored_device.obstacle_defect = device.obstacle_defect + stored_device.intrusion = device.intrusion + logger.warn("### defect = %s", device.batt_defect) + stored_device.batt_defect = device.batt_defect + await stored_device.publish_updates() async def ping(self) -> None: """Periodically send pings""" diff --git a/custom_components/deltadore-tydom/sensor.py b/custom_components/deltadore-tydom/sensor.py index 04ad633..5b922ef 100644 --- a/custom_components/deltadore-tydom/sensor.py +++ b/custom_components/deltadore-tydom/sensor.py @@ -25,6 +25,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Add sensors for passed config_entry in HA.""" hub = hass.data[DOMAIN][config_entry.entry_id] + hub.add_sensor_callback = async_add_entities new_devices = [] for roller in hub.rollers: diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 912731b..f44c4cc 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -420,6 +420,7 @@ async def parse_config_data(parsed): device_endpoint[device_unique_id] = i["id_endpoint"] logger.debug("Configuration updated") + logger.debug("devices : %s", devices) return devices async def parse_cmeta_data(self, parsed): @@ -889,6 +890,7 @@ async def parse_devices_data(self, parsed): pass else: pass + return devices async def parse_devices_cdata(self, parsed): logger.debug("parse_devices_data : %s", parsed) diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index eb66aeb..cbe5969 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -185,7 +185,7 @@ async def async_connect(self) -> ClientWebSocketResponse: url=f"https://{self._host}:443/mediation/client?mac={self._mac}&appli=1", headers=http_headers, json=None, - #proxy="http://proxy.rd.francetelecom.fr:8080" + proxy="http://proxy.rd.francetelecom.fr:8080" ) logger.debug( "response status : %s\nheaders : %s\ncontent : %s", @@ -217,7 +217,7 @@ async def async_connect(self) -> ClientWebSocketResponse: headers=http_headers, autoping=True, heartbeat=2, - #proxy="http://proxy.rd.francetelecom.fr:8080" + proxy="http://proxy.rd.francetelecom.fr:8080" ) return connection diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index dd73d8a..047953b 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -66,13 +66,21 @@ def device_id(self) -> str: """Return ID for device.""" return self.uid + # In a real implementation, this library would call it's call backs when it was + # notified of any state changeds for the relevant device. + async def publish_updates(self) -> None: + """Schedule call all registered callbacks.""" + logger.error("publish_updates") + for callback in self._callbacks: + callback() + class TydomShutter(TydomDevice): """Represents a shutter""" def __init__(self, uid, name, device_type, endpoint, data): logger.info("TydomShutter : data %s", data) self.thermic_defect = None logger.info("TydomShutter : pos") - #self.position = None + self.position = None logger.info("TydomShutter : on_fav_pos") self.on_fav_pos = None self.up_defect = None @@ -102,8 +110,3 @@ def __init__(self, uid, name, device_type, endpoint, data): self.batt_defect = data["battDefect"] super().__init__(uid, name, device_type, endpoint) - @property - def position(self): - """Return position for shutter.""" - # logger.error("get shutter position") - return self.position From b40ef8220b42b77b2cc946b6e1fc0384599c6f45 Mon Sep 17 00:00:00 2001 From: ucpy7374 Date: Tue, 9 May 2023 18:46:52 +0200 Subject: [PATCH 23/74] conso sensor --- .../deltadore-tydom/ha_entities.py | 669 +++++++++++++++++- custom_components/deltadore-tydom/hub.py | 53 +- .../deltadore-tydom/tydom/MessageHandler.py | 4 +- .../deltadore-tydom/tydom/tydom_devices.py | 162 ++++- 4 files changed, 848 insertions(+), 40 deletions(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 134c141..48747f2 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -5,7 +5,7 @@ BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.helpers.entity import Entity, DeviceInfo +from homeassistant.helpers.entity import Entity, DeviceInfo, Entity from homeassistant.components.cover import ( ATTR_POSITION, SUPPORT_CLOSE, @@ -15,16 +15,559 @@ CoverEntity, CoverDeviceClass ) +from homeassistant.components.sensor import SensorDeviceClass -from .tydom.tydom_devices import TydomShutter + +from .tydom.tydom_devices import TydomShutter, TydomEnergy from .const import DOMAIN, LOGGER + +class HAEnergy(Entity): + """Representation of an energy sensor""" + + should_poll = False + device_class = None + supported_features = None + + def __init__(self, energy: TydomEnergy) -> None: + self._energy = energy + self._attr_unique_id = f"{self._energy.uid}_energy" + self._attr_name = self._energy.name + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + # Importantly for a push integration, the module that will be getting updates + # needs to notify HA of changes. The dummy device has a registercallback + # method, so to this we add the 'self.async_write_ha_state' method, to be + # called where ever there are changes. + # The call back registration is done once this entity is registered with HA + # (rather than in the __init__) + self._energy.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._energy.remove_callback(self.async_write_ha_state) + + # To link this entity to the cover device, this property must return an + # identifiers value matching that used in the cover, but no other information such + # as name. If name is returned, this entity will then also become a device in the + # HA UI. + @property + def device_info(self): + """Return information to link this entity with the correct device.""" + return {"identifiers": {(DOMAIN, self._energy.uid)}, "name": self._energy.name} + + def get_sensors(self): + """Get available sensors for this entity""" + sensors = [] + if self._energy.energyInstantTotElec is not None: + sensors.append(EnergyInstantTotElecSensor(self._energy)) + if self._energy.energyInstantTotElec_Min is not None: + sensors.append(EnergyInstantTotElec_MinSensor(self._energy)) + if self._energy.energyInstantTotElec_Max is not None: + sensors.append(EnergyInstantTotElec_MaxSensor(self._energy)) + if self._energy.energyScaleTotElec_Min is not None: + sensors.append(EnergyScaleTotElec_MinSensor(self._energy)) + if self._energy.energyScaleTotElec_Max is not None: + sensors.append(EnergyScaleTotElec_MaxSensor(self._energy)) + if self._energy.energyInstantTotElecP is not None: + sensors.append(EnergyInstantTotElecPSensor(self._energy)) + if self._energy.energyInstantTotElec_P_Min is not None: + sensors.append(EnergyInstantTotElec_P_MinSensor(self._energy)) + if self._energy.energyInstantTotElec_P_Max is not None: + sensors.append(EnergyInstantTotElec_P_MaxSensor(self._energy)) + if self._energy.energyScaleTotElec_P_Min is not None: + sensors.append(EnergyScaleTotElec_P_MinSensor(self._energy)) + if self._energy.energyScaleTotElec_P_Max is not None: + sensors.append(EnergyScaleTotElec_P_MaxSensor(self._energy)) + if self._energy.energyInstantTi1P is not None: + sensors.append(EnergyInstantTi1PSensor(self._energy)) + if self._energy.energyInstantTi1P_Min is not None: + sensors.append(EnergyInstantTi1P_MinSensor(self._energy)) + if self._energy.energyInstantTi1P_Max is not None: + sensors.append(EnergyInstantTi1P_MaxSensor(self._energy)) + if self._energy.energyScaleTi1P_Min is not None: + sensors.append(EnergyScaleTi1P_MinSensor(self._energy)) + if self._energy.energyScaleTi1P_Max is not None: + sensors.append(EnergyScaleTi1P_MaxSensor(self._energy)) + if self._energy.energyInstantTi1I is not None: + sensors.append(EnergyInstantTi1ISensor(self._energy)) + if self._energy.energyInstantTi1I_Min is not None: + sensors.append(EnergyInstantTi1I_MinSensor(self._energy)) + if self._energy.energyInstantTi1I_Max is not None: + sensors.append(EnergyInstantTi1I_MaxSensor(self._energy)) + if self._energy.energyTotIndexWatt is not None: + sensors.append(EnergyTotIndexWattSensor(self._energy)) + if self._energy.energyIndexHeatWatt is not None: + sensors.append(EnergyIndexHeatWattSensor(self._energy)) + if self._energy.energyIndexECSWatt is not None: + sensors.append(EnergyIndexECSWattSensor(self._energy)) + if self._energy.energyIndexHeatGas is not None: + sensors.append(EnergyIndexHeatGasSensor(self._energy)) + if self._energy.outTemperature is not None: + sensors.append(OutTemperatureSensor(self._energy)) + return sensors + +class EnergySensorBase(Entity): + """Base representation of a Sensor.""" + + should_poll = False + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + self._device = device + + # To link this entity to the cover device, this property must return an + # identifiers value matching that used in the cover, but no other information such + # as name. If name is returned, this entity will then also become a device in the + # HA UI. + @property + def device_info(self): + """Return information to link this entity with the correct device.""" + return {"identifiers": {(DOMAIN, self._device.uid)}} + + # This property is important to let HA know if this entity is online or not. + # If an entity is offline (return False), the UI will refelect this. + @property + def available(self) -> bool: + """Return True if roller and hub is available.""" + # FIXME + #return self._device.online and self._device.hub.online + return True + + async def async_added_to_hass(self): + """Run when this Entity has been added to HA.""" + # Sensors should also register callbacks to HA when their state changes + self._device.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._device.remove_callback(self.async_write_ha_state) + +class EnergyInstantTotElecSensor(EnergySensorBase): + """energyInstantTotElec sensor""" + + device_class = SensorDeviceClass.CURRENT + #_attr_unit_of_measurement = "lx" + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._device.uid}_energyInstantTotElec" + + # The name of the entity + self._attr_name = f"{self._device.name} energyInstantTotElec" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyInstantTotElec + +class EnergyInstantTotElec_MinSensor(EnergySensorBase): + """energyInstantTotElec_Min sensor""" + + device_class = SensorDeviceClass.CURRENT + #_attr_unit_of_measurement = "lx" + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._device.uid}_energyInstantTotElec_Min" + + # The name of the entity + self._attr_name = f"{self._device.name} energyInstantTotElec_Min" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyInstantTotElec_Min + +class EnergyInstantTotElec_MaxSensor(EnergySensorBase): + """energyInstantTotElec_Max sensor""" + + device_class = SensorDeviceClass.CURRENT + #_attr_unit_of_measurement = "lx" + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._device.uid}_energyInstantTotElec_Max" + + # The name of the entity + self._attr_name = f"{self._device.name} energyInstantTotElec_Max" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyInstantTotElec_Max + + +class EnergyScaleTotElec_MinSensor(EnergySensorBase): + """energyScaleTotElec_Min sensor""" + + device_class = SensorDeviceClass.CURRENT + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyScaleTotElec_Min" + self._attr_name = f"{self._device.name} energyScaleTotElec_Min" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyScaleTotElec_Min + +class EnergyScaleTotElec_MaxSensor(EnergySensorBase): + """energyScaleTotElec_Min sensor""" + + device_class = SensorDeviceClass.CURRENT + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyScaleTotElec_Max" + self._attr_name = f"{self._device.name} energyScaleTotElec_Max" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyScaleTotElec_Max + +class EnergyInstantTotElecPSensor(EnergySensorBase): + """energyInstantTotElecP sensor""" + + device_class = SensorDeviceClass.POWER + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyInstantTotElecP" + self._attr_name = f"{self._device.name} energyInstantTotElecP" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyInstantTotElecP + +class EnergyInstantTotElec_P_MinSensor(EnergySensorBase): + """energyInstantTotElec_P_Min sensor""" + + device_class = SensorDeviceClass.POWER + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyInstantTotElec_P_Min" + self._attr_name = f"{self._device.name} energyInstantTotElec_P_Min" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyInstantTotElec_P_Min + +class EnergyInstantTotElec_P_MaxSensor(EnergySensorBase): + """energyInstantTotElec_P_Max sensor""" + + device_class = SensorDeviceClass.POWER + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyInstantTotElec_P_Max" + self._attr_name = f"{self._device.name} energyInstantTotElec_P_Max" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyInstantTotElec_P_Max + +class EnergyScaleTotElec_P_MinSensor(EnergySensorBase): + """energyScaleTotElec_P_Min sensor""" + + device_class = SensorDeviceClass.POWER + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyScaleTotElec_P_Min" + self._attr_name = f"{self._device.name} energyScaleTotElec_P_Min" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyScaleTotElec_P_Min + +class EnergyScaleTotElec_P_MaxSensor(EnergySensorBase): + """energyScaleTotElec_P_Max sensor""" + + device_class = SensorDeviceClass.POWER + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyScaleTotElec_P_Max" + self._attr_name = f"{self._device.name} energyScaleTotElec_P_Max" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyScaleTotElec_P_Max + +class EnergyInstantTi1PSensor(EnergySensorBase): + """energyInstantTi1P sensor""" + + device_class = SensorDeviceClass.POWER + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyInstantTi1P" + self._attr_name = f"{self._device.name} energyInstantTi1P" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyInstantTi1P + +class EnergyInstantTi1P_MinSensor(EnergySensorBase): + """energyInstantTi1P_Min sensor""" + + device_class = SensorDeviceClass.POWER + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyInstantTi1P_Min" + self._attr_name = f"{self._device.name} energyInstantTi1P_Min" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyInstantTi1P_Min + +class EnergyInstantTi1P_MaxSensor(EnergySensorBase): + """energyInstantTi1P_Max sensor""" + + device_class = SensorDeviceClass.POWER + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyInstantTi1P_Max" + self._attr_name = f"{self._device.name} energyInstantTi1P_Max" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyInstantTi1P_Max + +class EnergyScaleTi1P_MinSensor(EnergySensorBase): + """energyInstantenergyScaleTi1P_MinTi1P sensor""" + + device_class = SensorDeviceClass.POWER + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyScaleTi1P_Min" + self._attr_name = f"{self._device.name} energyScaleTi1P_Min" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyScaleTi1P_Min + +class EnergyScaleTi1P_MaxSensor(EnergySensorBase): + """energyScaleTi1P_Max sensor""" + + device_class = SensorDeviceClass.POWER + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyScaleTi1P_Max" + self._attr_name = f"{self._device.name} energyScaleTi1P_Max" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyScaleTi1P_Max + +class EnergyInstantTi1ISensor(EnergySensorBase): + """energyInstantTi1I sensor""" + + device_class = SensorDeviceClass.CURRENT + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyInstantTi1I" + self._attr_name = f"{self._device.name} energyInstantTi1I" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyInstantTi1I + +class EnergyInstantTi1I_MinSensor(EnergySensorBase): + """energyInstantTi1I_Min sensor""" + + device_class = SensorDeviceClass.CURRENT + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyInstantTi1I_Min" + self._attr_name = f"{self._device.name} energyInstantTi1I_Min" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyInstantTi1I_Min + +class EnergyInstantTi1I_MaxSensor(EnergySensorBase): + """energyInstantTi1I_Max sensor""" + + device_class = SensorDeviceClass.CURRENT + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyInstantTi1I_Max" + self._attr_name = f"{self._device.name} energyInstantTi1I_Max" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyInstantTi1I_Max + +class EnergyScaleTi1I_MinSensor(EnergySensorBase): + """energyScaleTi1I_Min sensor""" + + device_class = SensorDeviceClass.CURRENT + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyScaleTi1I_Min" + self._attr_name = f"{self._device.name} energyScaleTi1I_Min" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyScaleTi1I_Min + +class EnergyScaleTi1I_MaxSensor(EnergySensorBase): + """energyScaleTi1I_Max sensor""" + + device_class = SensorDeviceClass.CURRENT + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyScaleTi1I_Max" + self._attr_name = f"{self._device.name} energyScaleTi1I_Max" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyScaleTi1I_Max + +class EnergyTotIndexWattSensor(EnergySensorBase): + """energyTotIndexWatt sensor""" + + device_class = SensorDeviceClass.ENERGY + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyTotIndexWatt" + self._attr_name = f"{self._device.name} energyTotIndexWatt" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyTotIndexWatt + +class EnergyIndexHeatWattSensor(EnergySensorBase): + """energyIndexHeatWatt sensor""" + + device_class = SensorDeviceClass.ENERGY + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyIndexHeatWatt" + self._attr_name = f"{self._device.name} energyIndexHeatWatt" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyIndexHeatWatt + +class EnergyIndexECSWattSensor(EnergySensorBase): + """energyIndexECSWatt sensor""" + + device_class = SensorDeviceClass.ENERGY + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyIndexECSWatt" + self._attr_name = f"{self._device.name} energyIndexECSWatt" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyIndexECSWatt + +class EnergyIndexHeatGasSensor(EnergySensorBase): + """energyIndexHeatGas sensor""" + + device_class = SensorDeviceClass.ENERGY + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_energyIndexHeatGas" + self._attr_name = f"{self._device.name} energyIndexHeatGas" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyIndexHeatGas + +class OutTemperatureSensor(EnergySensorBase): + """outTemperature sensor""" + + device_class = SensorDeviceClass.POWER + + def __init__(self, device: TydomEnergy): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.uid}_outTemperature" + self._attr_name = f"{self._device.name} outTemperature" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.outTemperature + # This entire class could be written to extend a base class to ensure common attributes # are kept identical/in sync. It's broken apart here between the Cover and Sensors to # be explicit about what is returned, and the comments outline where the overlap is. class HACover(CoverEntity): - """Representation of a dummy Cover.""" + """Representation of a Cover.""" # Our dummy class is PUSH, so we tell HA that it should not be polled should_poll = False @@ -201,8 +744,8 @@ async def async_will_remove_from_hass(self): # The opposite of async_added_to_hass. Remove any registered call backs here. self._shutter.remove_callback(self.async_write_ha_state) -class BatterySensor(CoverBinarySensorBase): - """Representation of a Sensor.""" +class BatteryDefectSensor(CoverBinarySensorBase): + """Representation of a Battery Defect Sensor.""" # The class of this device. Note the value should come from the homeassistant.const # module. More information on the available devices classes can be seen here: @@ -218,7 +761,7 @@ def __init__(self, shutter): self._attr_unique_id = f"{self._shutter.uid}_battery" # The name of the entity - self._attr_name = f"{self._shutter.name} Battery" + self._attr_name = f"{self._shutter.name} Battery defect" self._state = False @@ -227,4 +770,116 @@ def __init__(self, shutter): @property def is_on(self): """Return the state of the sensor.""" - return self._shutter.batt_defect \ No newline at end of file + return self._shutter.batt_defect + +class ThermicDefectSensor(CoverBinarySensorBase): + """Representation of a Thermic Defect Sensor.""" + # The class of this device. Note the value should come from the homeassistant.const + # module. More information on the available devices classes can be seen here: + # https://developers.home-assistant.io/docs/core/entity/sensor + device_class = BinarySensorDeviceClass.PROBLEM + + def __init__(self, shutter): + """Initialize the sensor.""" + super().__init__(shutter) + + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._shutter.uid}_thermic" + + # The name of the entity + self._attr_name = f"{self._shutter.name} Thermic defect" + + self._state = False + + # The value of this sensor. As this is a DEVICE_CLASS_BATTERY, this value must be + # the battery level as a percentage (between 0 and 100) + @property + def is_on(self): + """Return the state of the sensor.""" + return self._shutter.thermic_defect + +class OnFavPosSensor(CoverBinarySensorBase): + """Representation of a fav position Sensor.""" + device_class = None + + def __init__(self, shutter): + """Initialize the sensor.""" + super().__init__(shutter) + + self._attr_unique_id = f"{self._shutter.uid}_on_fav_pos" + self._attr_name = f"{self._shutter.name} On favorite position" + self._state = False + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._shutter.on_fav_pos + +class UpDefectSensor(CoverBinarySensorBase): + """Representation of a Up Defect Sensor.""" + device_class = BinarySensorDeviceClass.PROBLEM + + def __init__(self, shutter): + """Initialize the sensor.""" + super().__init__(shutter) + + self._attr_unique_id = f"{self._shutter.uid}_up_defect" + self._attr_name = f"{self._shutter.name} Up defect" + self._state = False + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._shutter.up_defect + +class DownDefectSensor(CoverBinarySensorBase): + """Representation of a Down Defect Sensor.""" + device_class = BinarySensorDeviceClass.PROBLEM + + def __init__(self, shutter): + """Initialize the sensor.""" + super().__init__(shutter) + + self._attr_unique_id = f"{self._shutter.uid}_down_defect" + self._attr_name = f"{self._shutter.name} Down defect" + self._state = False + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._shutter.down_defect + +class ObstacleDefectSensor(CoverBinarySensorBase): + """Representation of a Obstacle Defect Sensor.""" + device_class = BinarySensorDeviceClass.PROBLEM + + def __init__(self, shutter): + """Initialize the sensor.""" + super().__init__(shutter) + + self._attr_unique_id = f"{self._shutter.uid}_obstacle_defect" + self._attr_name = f"{self._shutter.name} Obstacle defect" + self._state = False + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._shutter.obstacle_defect + +class IntrusionDefectSensor(CoverBinarySensorBase): + """Representation of a Obstacle Defect Sensor.""" + device_class = BinarySensorDeviceClass.PROBLEM + + def __init__(self, shutter): + """Initialize the sensor.""" + super().__init__(shutter) + + self._attr_unique_id = f"{self._shutter.uid}_intrusion_defect" + self._attr_name = f"{self._shutter.name} Intrusion defect" + self._state = False + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._shutter.intrusion diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 3e09669..0eeb9b6 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -17,7 +17,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .tydom.tydom_client import TydomClient from .tydom.tydom_devices import TydomBaseEntity -from .ha_entities import HACover, BatterySensor +from .ha_entities import * logger = logging.getLogger(__name__) @@ -130,30 +130,39 @@ async def update_tydom_entity(self, updated_entity: TydomBaseEntity) -> None: async def create_ha_device(self, device): """Create a new HA device""" - logger.warn("Create device %s", device.uid) - logger.warn("### defect = %s", device.batt_defect) - ha_device = HACover(device) - self.ha_devices[device.uid] = ha_device - if self.add_cover_callback is not None: - self.add_cover_callback([ha_device]) - batt_sensor = BatterySensor(device) - if self.add_sensor_callback is not None: - self.add_sensor_callback([batt_sensor]) - + logger.warn("device type %s", device.type) + match device.type: + case "shutter": + logger.warn("Create cover %s", device.uid) + ha_device = HACover(device) + self.ha_devices[device.uid] = ha_device + if self.add_cover_callback is not None: + self.add_cover_callback([ha_device]) + batt_sensor = BatteryDefectSensor(device) + thermic_sensor = ThermicDefectSensor(device) + on_fav_pos = OnFavPosSensor(device) + up_defect= UpDefectSensor(device) + down_defect = DownDefectSensor(device) + obstacle_defect = ObstacleDefectSensor(device) + intrusion_defect = IntrusionDefectSensor(device) + if self.add_sensor_callback is not None: + self.add_sensor_callback([batt_sensor, thermic_sensor, on_fav_pos, up_defect, down_defect, obstacle_defect, intrusion_defect]) + case "conso": + logger.warn("Create conso %s", device.uid) + ha_device = HAEnergy(device) + if self.add_sensor_callback is not None: + self.add_sensor_callback([ha_device]) + + if self.add_sensor_callback is not None: + self.add_sensor_callback(ha_device.get_sensors()) + + case _: + return async def update_ha_device(self, stored_device, device): """Update HA device values""" - logger.debug("Update device %s", device.uid) - stored_device.thermic_defect = device.thermic_defect - stored_device.position = device.position - stored_device.on_fav_pos = device.on_fav_pos - stored_device.up_defect = device.up_defect - stored_device.down_defect = device.down_defect - stored_device.obstacle_defect = device.obstacle_defect - stored_device.intrusion = device.intrusion - logger.warn("### defect = %s", device.batt_defect) - stored_device.batt_defect = device.batt_defect - await stored_device.publish_updates() + await stored_device.update_device(device) + async def ping(self) -> None: """Periodically send pings""" diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index f44c4cc..73dfd3c 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -6,7 +6,7 @@ import traceback import urllib3 -from .tydom_devices import TydomBaseEntity, TydomDevice, TydomShutter +from .tydom_devices import * logger = logging.getLogger(__name__) @@ -362,6 +362,8 @@ async def get_device(last_usage, uid, name, endpoint = None, data = None) -> Tyd match last_usage: case "shutter" | "klineShutter": return TydomShutter(uid, name, last_usage, endpoint, data) + case "conso": + return TydomEnergy(uid, name, last_usage, endpoint, data) case _: return diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index 047953b..5f8010e 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -24,19 +24,16 @@ def __init__(self, product_name, main_version_sw, main_version_hw, main_id, main def register_callback(self, callback: Callable[[], None]) -> None: """Register callback, called when Roller changes state.""" - logger.error("register_callback %s", callback) self._callbacks.add(callback) def remove_callback(self, callback: Callable[[], None]) -> None: """Remove previously registered callback.""" - logger.error("remove_callback") self._callbacks.discard(callback) # In a real implementation, this library would call it's call backs when it was # notified of any state changeds for the relevant device. async def publish_updates(self) -> None: """Schedule call all registered callbacks.""" - logger.error("publish_updates") for callback in self._callbacks: callback() @@ -53,12 +50,10 @@ def __init__(self, uid, name, device_type, endpoint): def register_callback(self, callback: Callable[[], None]) -> None: """Register callback, called when Roller changes state.""" - logger.error("register_callback %s", callback) self._callbacks.add(callback) def remove_callback(self, callback: Callable[[], None]) -> None: """Remove previously registered callback.""" - logger.error("remove_callback") self._callbacks.discard(callback) @property @@ -70,18 +65,14 @@ def device_id(self) -> str: # notified of any state changeds for the relevant device. async def publish_updates(self) -> None: """Schedule call all registered callbacks.""" - logger.error("publish_updates") for callback in self._callbacks: callback() class TydomShutter(TydomDevice): """Represents a shutter""" def __init__(self, uid, name, device_type, endpoint, data): - logger.info("TydomShutter : data %s", data) self.thermic_defect = None - logger.info("TydomShutter : pos") self.position = None - logger.info("TydomShutter : on_fav_pos") self.on_fav_pos = None self.up_defect = None self.down_defect = None @@ -90,7 +81,6 @@ def __init__(self, uid, name, device_type, endpoint, data): self.batt_defect = None if data is not None: - logger.info("TydomShutter : data not none %s", data) if "thermicDefect" in data: self.thermic_defect = data["thermicDefect"] if "position" in data: @@ -110,3 +100,155 @@ def __init__(self, uid, name, device_type, endpoint, data): self.batt_defect = data["battDefect"] super().__init__(uid, name, device_type, endpoint) + async def update_device(self, device): + """Update the device values from another device""" + logger.debug("Update device %s", device.uid) + self.thermic_defect = device.thermic_defect + self.position = device.position + self.on_fav_pos = device.on_fav_pos + self.up_defect = device.up_defect + self.down_defect = device.down_defect + self.obstacle_defect = device.obstacle_defect + self.intrusion = device.intrusion + self.batt_defect = device.batt_defect + await self.publish_updates() + +class TydomEnergy(TydomDevice): + """Represents an energy sensor (for example TYWATT)""" + + def __init__(self, uid, name, device_type, endpoint, data): + logger.info("TydomEnergy : data %s", data) + self.energyInstantTotElec = None + self.energyInstantTotElec_Min = None + self.energyInstantTotElec_Max = None + self.energyScaleTotElec_Min = None + self.energyScaleTotElec_Max = None + self.energyInstantTotElecP = None + self.energyInstantTotElec_P_Min = None + self.energyInstantTotElec_P_Max = None + self.energyScaleTotElec_P_Min = None + self.energyScaleTotElec_P_Max = None + self.energyInstantTi1P = None + self.energyInstantTi1P_Min = None + self.energyInstantTi1P_Max = None + self.energyScaleTi1P_Min = None + self.energyScaleTi1P_Max = None + self.energyInstantTi1I = None + self.energyInstantTi1I_Min = None + self.energyInstantTi1I_Max = None + self.energyScaleTi1I_Min = None + self.energyScaleTi1I_Max = None + self.energyTotIndexWatt = None + self.energyIndexHeatWatt = None + self.energyIndexECSWatt = None + self.energyIndexHeatGas = None + self.outTemperature = None + + if data is not None: + if "energyInstantTotElec" in data: + self.energyInstantTotElec = data["energyInstantTotElec"] + if "energyInstantTotElec_Min" in data: + self.energyInstantTotElec_Min = data["energyInstantTotElec_Min"] + if "energyInstantTotElec_Max" in data: + self.energyInstantTotElec_Max = data["energyInstantTotElec_Max"] + if "energyScaleTotElec_Min" in data: + self.energyScaleTotElec_Min = data["energyScaleTotElec_Min"] + if "energyScaleTotElec_Max" in data: + self.energyScaleTotElec_Max = data["energyScaleTotElec_Max"] + if "energyInstantTotElecP" in data: + self.energyInstantTotElecP = data["energyInstantTotElecP"] + if "energyInstantTotElec_P_Min" in data: + self.energyInstantTotElec_P_Min = data["energyInstantTotElec_P_Min"] + if "energyInstantTotElec_P_Max" in data: + self.energyInstantTotElec_P_Max = data["energyInstantTotElec_P_Max"] + if "energyScaleTotElec_P_Min" in data: + self.energyScaleTotElec_P_Min = data["energyScaleTotElec_P_Min"] + if "energyScaleTotElec_P_Max" in data: + self.energyScaleTotElec_P_Max = data["energyScaleTotElec_P_Max"] + if "energyInstantTi1P" in data: + self.energyInstantTi1P = data["energyInstantTi1P"] + if "energyInstantTi1P_Min" in data: + self.energyInstantTi1P_Min = data["energyInstantTi1P_Min"] + if "energyInstantTi1P_Max" in data: + self.energyInstantTi1P_Max = data["energyInstantTi1P_Max"] + if "energyScaleTi1P_Min" in data: + self.energyScaleTi1P_Min = data["energyScaleTi1P_Min"] + if "energyScaleTi1P_Max" in data: + self.energyScaleTi1P_Max = data["energyScaleTi1P_Max"] + if "energyInstantTi1I" in data: + self.energyInstantTi1I = data["energyInstantTi1I"] + if "energyInstantTi1I_Min" in data: + self.energyInstantTi1I_Min = data["energyInstantTi1I_Min"] + if "energyInstantTi1I_Max" in data: + self.energyInstantTi1I_Max = data["energyInstantTi1I_Max"] + if "energyScaleTi1I_Min" in data: + self.energyScaleTi1I_Min = data["energyScaleTi1I_Min"] + if "energyScaleTi1I_Max" in data: + self.energyScaleTi1I_Max = data["energyScaleTi1I_Max"] + if "energyTotIndexWatt" in data: + self.energyTotIndexWatt = data["energyTotIndexWatt"] + if "energyIndexHeatWatt" in data: + self.energyIndexHeatWatt = data["energyIndexHeatWatt"] + if "energyIndexECSWatt" in data: + self.energyIndexECSWatt = data["energyIndexECSWatt"] + if "energyIndexHeatGas" in data: + self.energyIndexHeatGas = data["energyIndexHeatGas"] + if "outTemperature" in data: + self.outTemperature = data["outTemperature"] + super().__init__(uid, name, device_type, endpoint) + + + async def update_device(self, device): + """Update the device values from another device""" + logger.debug("Update device %s", device.uid) + if device.energyInstantTotElec is not None: + self.energyInstantTotElec = device.energyInstantTotElec + if device.energyInstantTotElec_Min is not None: + self.energyInstantTotElec_Min = device.energyInstantTotElec_Min + if device.energyInstantTotElec_Max is not None: + self.energyInstantTotElec_Max = device.energyInstantTotElec_Max + if device.energyScaleTotElec_Min is not None: + self.energyScaleTotElec_Min = device.energyScaleTotElec_Min + if device.energyScaleTotElec_Max is not None: + self.energyScaleTotElec_Max = device.energyScaleTotElec_Max + if device.energyInstantTotElecP is not None: + self.energyInstantTotElecP = device.energyInstantTotElecP + if device.energyInstantTotElec_P_Min is not None: + self.energyInstantTotElec_P_Min = device.energyInstantTotElec_P_Min + if device.energyInstantTotElec_P_Max is not None: + self.energyInstantTotElec_P_Max = device.energyInstantTotElec_P_Max + if device.energyScaleTotElec_P_Min is not None: + self.energyScaleTotElec_P_Min = device.energyScaleTotElec_P_Min + if device.energyScaleTotElec_P_Max is not None: + self.energyScaleTotElec_P_Max = device.energyScaleTotElec_P_Max + if device.energyInstantTi1P is not None: + self.energyInstantTi1P = device.energyInstantTi1P + if device.energyInstantTi1P_Min is not None: + self.energyInstantTi1P_Min = device.energyInstantTi1P_Min + if device.energyInstantTi1P_Max is not None: + self.energyInstantTi1P_Max = device.energyInstantTi1P_Max + if device.energyScaleTi1P_Min is not None: + self.energyScaleTi1P_Min = device.energyScaleTi1P_Min + if device.energyScaleTi1P_Max is not None: + self.energyScaleTi1P_Max = device.energyScaleTi1P_Max + if device.energyInstantTi1I is not None: + self.energyInstantTi1I = device.energyInstantTi1I + if device.energyInstantTi1I_Min is not None: + self.energyInstantTi1I_Min = device.energyInstantTi1I_Min + if device.energyInstantTi1I_Max is not None: + self.energyInstantTi1I_Max = device.energyInstantTi1I_Max + if device.energyScaleTi1I_Min is not None: + self.energyScaleTi1I_Min = device.energyScaleTi1I_Min + if device.energyScaleTi1I_Max is not None: + self.energyScaleTi1I_Max = device.energyScaleTi1I_Max + if device.energyTotIndexWatt is not None: + self.energyTotIndexWatt = device.energyTotIndexWatt + if device.energyIndexHeatWatt is not None: + self.energyIndexHeatWatt = device.energyIndexHeatWatt + if device.energyIndexECSWatt is not None: + self.energyIndexECSWatt = device.energyIndexECSWatt + if device.energyIndexHeatGas is not None: + self.energyIndexHeatGas = device.energyIndexHeatGas + if device.outTemperature is not None: + self.outTemperature = device.outTemperature + await self.publish_updates() \ No newline at end of file From feaa33b391aaff9eadcc7e39eebd59b51ec23b30 Mon Sep 17 00:00:00 2001 From: ucpy7374 Date: Wed, 10 May 2023 18:35:09 +0200 Subject: [PATCH 24/74] updates --- custom_components/deltadore-tydom/__init__.py | 3 +- .../deltadore-tydom/ha_entities.py | 326 +++++++++++++----- custom_components/deltadore-tydom/hub.py | 54 ++- .../deltadore-tydom/tydom/MessageHandler.py | 23 +- .../deltadore-tydom/tydom/tydom_client.py | 29 +- .../deltadore-tydom/tydom/tydom_devices.py | 45 +++ 6 files changed, 337 insertions(+), 143 deletions(-) diff --git a/custom_components/deltadore-tydom/__init__.py b/custom_components/deltadore-tydom/__init__.py index 95a2e79..a603688 100644 --- a/custom_components/deltadore-tydom/__init__.py +++ b/custom_components/deltadore-tydom/__init__.py @@ -42,9 +42,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except Exception as err: raise ConfigEntryNotReady from err + # This creates each HA object for each platform your device requires. # It's done by calling the `async_setup_entry` function in each platform module. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 48747f2..c3cfe62 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -5,6 +5,7 @@ BinarySensorDeviceClass, BinarySensorEntity, ) + from homeassistant.helpers.entity import Entity, DeviceInfo, Entity from homeassistant.components.cover import ( ATTR_POSITION, @@ -18,7 +19,7 @@ from homeassistant.components.sensor import SensorDeviceClass -from .tydom.tydom_devices import TydomShutter, TydomEnergy +from .tydom.tydom_devices import * from .const import DOMAIN, LOGGER @@ -30,10 +31,12 @@ class HAEnergy(Entity): device_class = None supported_features = None + def __init__(self, energy: TydomEnergy) -> None: self._energy = energy self._attr_unique_id = f"{self._energy.uid}_energy" self._attr_name = self._energy.name + self.registered_sensors = [] async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" @@ -62,56 +65,79 @@ def device_info(self): def get_sensors(self): """Get available sensors for this entity""" sensors = [] - if self._energy.energyInstantTotElec is not None: + if self._energy.energyInstantTotElec is not None and "energyInstantTotElec" not in self.registered_sensors: sensors.append(EnergyInstantTotElecSensor(self._energy)) - if self._energy.energyInstantTotElec_Min is not None: + self.registered_sensors.append("energyInstantTotElec") + if self._energy.energyInstantTotElec_Min is not None and "energyInstantTotElec_Min" not in self.registered_sensors: sensors.append(EnergyInstantTotElec_MinSensor(self._energy)) - if self._energy.energyInstantTotElec_Max is not None: + self.registered_sensors.append("energyInstantTotElec_Min") + if self._energy.energyInstantTotElec_Max is not None and "energyInstantTotElec_Max" not in self.registered_sensors: sensors.append(EnergyInstantTotElec_MaxSensor(self._energy)) - if self._energy.energyScaleTotElec_Min is not None: + self.registered_sensors.append("energyInstantTotElec_Max") + if self._energy.energyScaleTotElec_Min is not None and "energyScaleTotElec_Min" not in self.registered_sensors: sensors.append(EnergyScaleTotElec_MinSensor(self._energy)) - if self._energy.energyScaleTotElec_Max is not None: + self.registered_sensors.append("energyScaleTotElec_Min") + if self._energy.energyScaleTotElec_Max is not None and "energyScaleTotElec_Max" not in self.registered_sensors: sensors.append(EnergyScaleTotElec_MaxSensor(self._energy)) - if self._energy.energyInstantTotElecP is not None: + self.registered_sensors.append("energyScaleTotElec_Max") + if self._energy.energyInstantTotElecP is not None and "energyInstantTotElecP" not in self.registered_sensors: sensors.append(EnergyInstantTotElecPSensor(self._energy)) - if self._energy.energyInstantTotElec_P_Min is not None: + self.registered_sensors.append("energyInstantTotElecP") + if self._energy.energyInstantTotElec_P_Min is not None and "energyInstantTotElec_P_Min" not in self.registered_sensors: sensors.append(EnergyInstantTotElec_P_MinSensor(self._energy)) - if self._energy.energyInstantTotElec_P_Max is not None: + self.registered_sensors.append("energyInstantTotElec_P_Min") + if self._energy.energyInstantTotElec_P_Max is not None and "energyInstantTotElec_P_Max" not in self.registered_sensors: sensors.append(EnergyInstantTotElec_P_MaxSensor(self._energy)) - if self._energy.energyScaleTotElec_P_Min is not None: + self.registered_sensors.append("energyInstantTotElec_P_Max") + if self._energy.energyScaleTotElec_P_Min is not None and "energyScaleTotElec_P_Min" not in self.registered_sensors: sensors.append(EnergyScaleTotElec_P_MinSensor(self._energy)) - if self._energy.energyScaleTotElec_P_Max is not None: + self.registered_sensors.append("energyScaleTotElec_P_Min") + if self._energy.energyScaleTotElec_P_Max is not None and "energyScaleTotElec_P_Max" not in self.registered_sensors: sensors.append(EnergyScaleTotElec_P_MaxSensor(self._energy)) - if self._energy.energyInstantTi1P is not None: + self.registered_sensors.append("energyScaleTotElec_P_Max") + if self._energy.energyInstantTi1P is not None and "energyInstantTi1P" not in self.registered_sensors: sensors.append(EnergyInstantTi1PSensor(self._energy)) - if self._energy.energyInstantTi1P_Min is not None: + self.registered_sensors.append("energyInstantTi1P") + if self._energy.energyInstantTi1P_Min is not None and "energyInstantTi1P_Min" not in self.registered_sensors: sensors.append(EnergyInstantTi1P_MinSensor(self._energy)) - if self._energy.energyInstantTi1P_Max is not None: + self.registered_sensors.append("energyInstantTi1P_Min") + if self._energy.energyInstantTi1P_Max is not None and "energyInstantTi1P_Max" not in self.registered_sensors: sensors.append(EnergyInstantTi1P_MaxSensor(self._energy)) - if self._energy.energyScaleTi1P_Min is not None: + self.registered_sensors.append("energyInstantTi1P_Max") + if self._energy.energyScaleTi1P_Min is not None and "energyScaleTi1P_Min" not in self.registered_sensors: sensors.append(EnergyScaleTi1P_MinSensor(self._energy)) - if self._energy.energyScaleTi1P_Max is not None: + self.registered_sensors.append("energyScaleTi1P_Min") + if self._energy.energyScaleTi1P_Max is not None and "energyScaleTi1P_Max" not in self.registered_sensors: sensors.append(EnergyScaleTi1P_MaxSensor(self._energy)) - if self._energy.energyInstantTi1I is not None: + self.registered_sensors.append("energyScaleTi1P_Max") + if self._energy.energyInstantTi1I is not None and "energyInstantTi1I" not in self.registered_sensors: sensors.append(EnergyInstantTi1ISensor(self._energy)) - if self._energy.energyInstantTi1I_Min is not None: + self.registered_sensors.append("energyInstantTi1I") + if self._energy.energyInstantTi1I_Min is not None and "energyInstantTi1I_Min" not in self.registered_sensors: sensors.append(EnergyInstantTi1I_MinSensor(self._energy)) - if self._energy.energyInstantTi1I_Max is not None: + self.registered_sensors.append("energyInstantTi1I_Min") + if self._energy.energyInstantTi1I_Max is not None and "energyInstantTi1I_Max" not in self.registered_sensors: sensors.append(EnergyInstantTi1I_MaxSensor(self._energy)) - if self._energy.energyTotIndexWatt is not None: + self.registered_sensors.append("energyInstantTi1I_Max") + if self._energy.energyTotIndexWatt is not None and "energyTotIndexWatt" not in self.registered_sensors: sensors.append(EnergyTotIndexWattSensor(self._energy)) - if self._energy.energyIndexHeatWatt is not None: + self.registered_sensors.append("energyTotIndexWatt") + if self._energy.energyIndexHeatWatt is not None and "energyIndexHeatWatt" not in self.registered_sensors: sensors.append(EnergyIndexHeatWattSensor(self._energy)) - if self._energy.energyIndexECSWatt is not None: + self.registered_sensors.append("energyIndexHeatWatt") + if self._energy.energyIndexECSWatt is not None and "energyIndexECSWatt" not in self.registered_sensors: sensors.append(EnergyIndexECSWattSensor(self._energy)) - if self._energy.energyIndexHeatGas is not None: + self.registered_sensors.append("energyIndexECSWatt") + if self._energy.energyIndexHeatGas is not None and "energyIndexHeatGas" not in self.registered_sensors: sensors.append(EnergyIndexHeatGasSensor(self._energy)) - if self._energy.outTemperature is not None: + self.registered_sensors.append("energyIndexHeatGas") + if self._energy.outTemperature is not None and "outTemperature" not in self.registered_sensors: sensors.append(OutTemperatureSensor(self._energy)) + self.registered_sensors.append("outTemperature") return sensors -class EnergySensorBase(Entity): - """Base representation of a Sensor.""" +class SensorBase(Entity): + """Representation of a Tydom.""" should_poll = False @@ -147,11 +173,10 @@ async def async_will_remove_from_hass(self): # The opposite of async_added_to_hass. Remove any registered call backs here. self._device.remove_callback(self.async_write_ha_state) -class EnergyInstantTotElecSensor(EnergySensorBase): +class EnergyInstantTotElecSensor(SensorBase): """energyInstantTotElec sensor""" device_class = SensorDeviceClass.CURRENT - #_attr_unit_of_measurement = "lx" def __init__(self, device: TydomEnergy): """Initialize the sensor.""" @@ -168,11 +193,10 @@ def state(self): """Return the state of the sensor.""" return self._device.energyInstantTotElec -class EnergyInstantTotElec_MinSensor(EnergySensorBase): +class EnergyInstantTotElec_MinSensor(SensorBase): """energyInstantTotElec_Min sensor""" device_class = SensorDeviceClass.CURRENT - #_attr_unit_of_measurement = "lx" def __init__(self, device: TydomEnergy): """Initialize the sensor.""" @@ -189,11 +213,10 @@ def state(self): """Return the state of the sensor.""" return self._device.energyInstantTotElec_Min -class EnergyInstantTotElec_MaxSensor(EnergySensorBase): +class EnergyInstantTotElec_MaxSensor(SensorBase): """energyInstantTotElec_Max sensor""" device_class = SensorDeviceClass.CURRENT - #_attr_unit_of_measurement = "lx" def __init__(self, device: TydomEnergy): """Initialize the sensor.""" @@ -211,7 +234,7 @@ def state(self): return self._device.energyInstantTotElec_Max -class EnergyScaleTotElec_MinSensor(EnergySensorBase): +class EnergyScaleTotElec_MinSensor(SensorBase): """energyScaleTotElec_Min sensor""" device_class = SensorDeviceClass.CURRENT @@ -227,7 +250,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyScaleTotElec_Min -class EnergyScaleTotElec_MaxSensor(EnergySensorBase): +class EnergyScaleTotElec_MaxSensor(SensorBase): """energyScaleTotElec_Min sensor""" device_class = SensorDeviceClass.CURRENT @@ -243,7 +266,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyScaleTotElec_Max -class EnergyInstantTotElecPSensor(EnergySensorBase): +class EnergyInstantTotElecPSensor(SensorBase): """energyInstantTotElecP sensor""" device_class = SensorDeviceClass.POWER @@ -259,7 +282,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyInstantTotElecP -class EnergyInstantTotElec_P_MinSensor(EnergySensorBase): +class EnergyInstantTotElec_P_MinSensor(SensorBase): """energyInstantTotElec_P_Min sensor""" device_class = SensorDeviceClass.POWER @@ -275,7 +298,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyInstantTotElec_P_Min -class EnergyInstantTotElec_P_MaxSensor(EnergySensorBase): +class EnergyInstantTotElec_P_MaxSensor(SensorBase): """energyInstantTotElec_P_Max sensor""" device_class = SensorDeviceClass.POWER @@ -291,7 +314,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyInstantTotElec_P_Max -class EnergyScaleTotElec_P_MinSensor(EnergySensorBase): +class EnergyScaleTotElec_P_MinSensor(SensorBase): """energyScaleTotElec_P_Min sensor""" device_class = SensorDeviceClass.POWER @@ -307,7 +330,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyScaleTotElec_P_Min -class EnergyScaleTotElec_P_MaxSensor(EnergySensorBase): +class EnergyScaleTotElec_P_MaxSensor(SensorBase): """energyScaleTotElec_P_Max sensor""" device_class = SensorDeviceClass.POWER @@ -323,7 +346,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyScaleTotElec_P_Max -class EnergyInstantTi1PSensor(EnergySensorBase): +class EnergyInstantTi1PSensor(SensorBase): """energyInstantTi1P sensor""" device_class = SensorDeviceClass.POWER @@ -339,7 +362,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyInstantTi1P -class EnergyInstantTi1P_MinSensor(EnergySensorBase): +class EnergyInstantTi1P_MinSensor(SensorBase): """energyInstantTi1P_Min sensor""" device_class = SensorDeviceClass.POWER @@ -355,7 +378,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyInstantTi1P_Min -class EnergyInstantTi1P_MaxSensor(EnergySensorBase): +class EnergyInstantTi1P_MaxSensor(SensorBase): """energyInstantTi1P_Max sensor""" device_class = SensorDeviceClass.POWER @@ -371,7 +394,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyInstantTi1P_Max -class EnergyScaleTi1P_MinSensor(EnergySensorBase): +class EnergyScaleTi1P_MinSensor(SensorBase): """energyInstantenergyScaleTi1P_MinTi1P sensor""" device_class = SensorDeviceClass.POWER @@ -387,7 +410,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyScaleTi1P_Min -class EnergyScaleTi1P_MaxSensor(EnergySensorBase): +class EnergyScaleTi1P_MaxSensor(SensorBase): """energyScaleTi1P_Max sensor""" device_class = SensorDeviceClass.POWER @@ -403,7 +426,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyScaleTi1P_Max -class EnergyInstantTi1ISensor(EnergySensorBase): +class EnergyInstantTi1ISensor(SensorBase): """energyInstantTi1I sensor""" device_class = SensorDeviceClass.CURRENT @@ -419,7 +442,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyInstantTi1I -class EnergyInstantTi1I_MinSensor(EnergySensorBase): +class EnergyInstantTi1I_MinSensor(SensorBase): """energyInstantTi1I_Min sensor""" device_class = SensorDeviceClass.CURRENT @@ -435,7 +458,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyInstantTi1I_Min -class EnergyInstantTi1I_MaxSensor(EnergySensorBase): +class EnergyInstantTi1I_MaxSensor(SensorBase): """energyInstantTi1I_Max sensor""" device_class = SensorDeviceClass.CURRENT @@ -451,7 +474,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyInstantTi1I_Max -class EnergyScaleTi1I_MinSensor(EnergySensorBase): +class EnergyScaleTi1I_MinSensor(SensorBase): """energyScaleTi1I_Min sensor""" device_class = SensorDeviceClass.CURRENT @@ -467,7 +490,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyScaleTi1I_Min -class EnergyScaleTi1I_MaxSensor(EnergySensorBase): +class EnergyScaleTi1I_MaxSensor(SensorBase): """energyScaleTi1I_Max sensor""" device_class = SensorDeviceClass.CURRENT @@ -483,7 +506,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyScaleTi1I_Max -class EnergyTotIndexWattSensor(EnergySensorBase): +class EnergyTotIndexWattSensor(SensorBase): """energyTotIndexWatt sensor""" device_class = SensorDeviceClass.ENERGY @@ -499,7 +522,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyTotIndexWatt -class EnergyIndexHeatWattSensor(EnergySensorBase): +class EnergyIndexHeatWattSensor(SensorBase): """energyIndexHeatWatt sensor""" device_class = SensorDeviceClass.ENERGY @@ -515,7 +538,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyIndexHeatWatt -class EnergyIndexECSWattSensor(EnergySensorBase): +class EnergyIndexECSWattSensor(SensorBase): """energyIndexECSWatt sensor""" device_class = SensorDeviceClass.ENERGY @@ -531,7 +554,7 @@ def state(self): """Return the state of the sensor.""" return self._device.energyIndexECSWatt -class EnergyIndexHeatGasSensor(EnergySensorBase): +class EnergyIndexHeatGasSensor(SensorBase): """energyIndexHeatGas sensor""" device_class = SensorDeviceClass.ENERGY @@ -547,10 +570,10 @@ def state(self): """Return the state of the sensor.""" return self._device.energyIndexHeatGas -class OutTemperatureSensor(EnergySensorBase): +class OutTemperatureSensor(SensorBase): """outTemperature sensor""" - device_class = SensorDeviceClass.POWER + device_class = SensorDeviceClass.TEMPERATURE def __init__(self, device: TydomEnergy): """Initialize the sensor.""" @@ -706,15 +729,43 @@ async def async_set_cover_position(self, **kwargs: Any) -> None: """Close the cover.""" await self._shutter.set_position(kwargs[ATTR_POSITION]) + def get_sensors(self) -> list: + """Get available sensors for this entity""" + sensors = [] + device = self._shutter + if self._shutter.batt_defect is not None: + batt_sensor = BatteryDefectSensor(device) + sensors.append(batt_sensor) + if self._shutter.thermic_defect is not None: + thermic_sensor = ThermicDefectSensor(device) + sensors.append(thermic_sensor) + if self._shutter.on_fav_pos is not None: + on_fav_pos = OnFavPosSensor(device) + sensors.append(on_fav_pos) + if self._shutter.up_defect is not None: + up_defect= UpDefectSensor(device) + sensors.append(up_defect) + if self._shutter.down_defect is not None: + down_defect = DownDefectSensor(device) + sensors.append(down_defect) + if self._shutter.obstacle_defect is not None: + obstacle_defect = ObstacleDefectSensor(device) + sensors.append(obstacle_defect) + if self._shutter.intrusion is not None: + intrusion_defect = IntrusionDefectSensor(device) + sensors.append(intrusion_defect) + return sensors + + -class CoverBinarySensorBase(BinarySensorEntity): +class BinarySensorBase(BinarySensorEntity): """Base representation of a Sensor.""" should_poll = False - def __init__(self, shutter: TydomShutter): + def __init__(self, device: TydomDevice): """Initialize the sensor.""" - self._shutter = shutter + self._device = device # To link this entity to the cover device, this property must return an # identifiers value matching that used in the cover, but no other information such @@ -723,7 +774,7 @@ def __init__(self, shutter: TydomShutter): @property def device_info(self): """Return information to link this entity with the correct device.""" - return {"identifiers": {(DOMAIN, self._shutter.uid)}} + return {"identifiers": {(DOMAIN, self._device.uid)}} # This property is important to let HA know if this entity is online or not. # If an entity is offline (return False), the UI will refelect this. @@ -737,14 +788,14 @@ def available(self) -> bool: async def async_added_to_hass(self): """Run when this Entity has been added to HA.""" # Sensors should also register callbacks to HA when their state changes - self._shutter.register_callback(self.async_write_ha_state) + self._device.register_callback(self.async_write_ha_state) async def async_will_remove_from_hass(self): """Entity being removed from hass.""" # The opposite of async_added_to_hass. Remove any registered call backs here. - self._shutter.remove_callback(self.async_write_ha_state) + self._device.remove_callback(self.async_write_ha_state) -class BatteryDefectSensor(CoverBinarySensorBase): +class BatteryDefectSensor(BinarySensorBase): """Representation of a Battery Defect Sensor.""" # The class of this device. Note the value should come from the homeassistant.const @@ -752,16 +803,16 @@ class BatteryDefectSensor(CoverBinarySensorBase): # https://developers.home-assistant.io/docs/core/entity/sensor device_class = BinarySensorDeviceClass.PROBLEM - def __init__(self, shutter): + def __init__(self, device): """Initialize the sensor.""" - super().__init__(shutter) + super().__init__(device) # As per the sensor, this must be a unique value within this domain. This is done # by using the device ID, and appending "_battery" - self._attr_unique_id = f"{self._shutter.uid}_battery" + self._attr_unique_id = f"{self._device.uid}_battery" # The name of the entity - self._attr_name = f"{self._shutter.name} Battery defect" + self._attr_name = f"{self._device.name} Battery defect" self._state = False @@ -770,9 +821,9 @@ def __init__(self, shutter): @property def is_on(self): """Return the state of the sensor.""" - return self._shutter.batt_defect + return self._device.batt_defect -class ThermicDefectSensor(CoverBinarySensorBase): +class ThermicDefectSensor(BinarySensorBase): """Representation of a Thermic Defect Sensor.""" # The class of this device. Note the value should come from the homeassistant.const # module. More information on the available devices classes can be seen here: @@ -785,10 +836,10 @@ def __init__(self, shutter): # As per the sensor, this must be a unique value within this domain. This is done # by using the device ID, and appending "_battery" - self._attr_unique_id = f"{self._shutter.uid}_thermic" + self._attr_unique_id = f"{self._device.uid}_thermic" # The name of the entity - self._attr_name = f"{self._shutter.name} Thermic defect" + self._attr_name = f"{self._device.name} Thermic defect" self._state = False @@ -797,9 +848,9 @@ def __init__(self, shutter): @property def is_on(self): """Return the state of the sensor.""" - return self._shutter.thermic_defect + return self._device.thermic_defect -class OnFavPosSensor(CoverBinarySensorBase): +class OnFavPosSensor(BinarySensorBase): """Representation of a fav position Sensor.""" device_class = None @@ -807,16 +858,16 @@ def __init__(self, shutter): """Initialize the sensor.""" super().__init__(shutter) - self._attr_unique_id = f"{self._shutter.uid}_on_fav_pos" - self._attr_name = f"{self._shutter.name} On favorite position" + self._attr_unique_id = f"{self._device.uid}_on_fav_pos" + self._attr_name = f"{self._device.name} On favorite position" self._state = False @property def is_on(self): """Return the state of the sensor.""" - return self._shutter.on_fav_pos + return self._device.on_fav_pos -class UpDefectSensor(CoverBinarySensorBase): +class UpDefectSensor(BinarySensorBase): """Representation of a Up Defect Sensor.""" device_class = BinarySensorDeviceClass.PROBLEM @@ -824,16 +875,16 @@ def __init__(self, shutter): """Initialize the sensor.""" super().__init__(shutter) - self._attr_unique_id = f"{self._shutter.uid}_up_defect" - self._attr_name = f"{self._shutter.name} Up defect" + self._attr_unique_id = f"{self._device.uid}_up_defect" + self._attr_name = f"{self._device.name} Up defect" self._state = False @property def is_on(self): """Return the state of the sensor.""" - return self._shutter.up_defect + return self._device.up_defect -class DownDefectSensor(CoverBinarySensorBase): +class DownDefectSensor(BinarySensorBase): """Representation of a Down Defect Sensor.""" device_class = BinarySensorDeviceClass.PROBLEM @@ -841,16 +892,16 @@ def __init__(self, shutter): """Initialize the sensor.""" super().__init__(shutter) - self._attr_unique_id = f"{self._shutter.uid}_down_defect" - self._attr_name = f"{self._shutter.name} Down defect" + self._attr_unique_id = f"{self._device.uid}_down_defect" + self._attr_name = f"{self._device.name} Down defect" self._state = False @property def is_on(self): """Return the state of the sensor.""" - return self._shutter.down_defect + return self._device.down_defect -class ObstacleDefectSensor(CoverBinarySensorBase): +class ObstacleDefectSensor(BinarySensorBase): """Representation of a Obstacle Defect Sensor.""" device_class = BinarySensorDeviceClass.PROBLEM @@ -858,16 +909,16 @@ def __init__(self, shutter): """Initialize the sensor.""" super().__init__(shutter) - self._attr_unique_id = f"{self._shutter.uid}_obstacle_defect" - self._attr_name = f"{self._shutter.name} Obstacle defect" + self._attr_unique_id = f"{self._device.uid}_obstacle_defect" + self._attr_name = f"{self._device.name} Obstacle defect" self._state = False @property def is_on(self): """Return the state of the sensor.""" - return self._shutter.obstacle_defect + return self._device.obstacle_defect -class IntrusionDefectSensor(CoverBinarySensorBase): +class IntrusionDefectSensor(BinarySensorBase): """Representation of a Obstacle Defect Sensor.""" device_class = BinarySensorDeviceClass.PROBLEM @@ -875,11 +926,104 @@ def __init__(self, shutter): """Initialize the sensor.""" super().__init__(shutter) - self._attr_unique_id = f"{self._shutter.uid}_intrusion_defect" - self._attr_name = f"{self._shutter.name} Intrusion defect" + self._attr_unique_id = f"{self._device.uid}_intrusion_defect" + self._attr_name = f"{self._device.name} Intrusion defect" + self._state = False + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._device.intrusion + +class ConfigSensor(SensorBase): + """config sensor""" + + device_class = None + + def __init__(self, device: TydomDevice): + """Initialize the sensor.""" + super().__init__(device) + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._device.uid}_config" + + # The name of the entity + self._attr_name = f"{self._device.name} config" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.config + +class SupervisionModeSensor(SensorBase): + """supervisionMode sensor""" + + device_class = None + + def __init__(self, device: TydomDevice): + """Initialize the sensor.""" + super().__init__(device) + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._device.uid}_supervisionMode" + + # The name of the entity + self._attr_name = f"{self._device.name} supervisionMode" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.supervisionMode + +class HASmoke(BinarySensorEntity): + """Representation of an smoke sensor""" + should_poll = False + device_class = None + supported_features = None + + device_class = BinarySensorDeviceClass.PROBLEM + + def __init__(self, smoke: TydomSmoke) -> None: + self._smoke = smoke + self._attr_unique_id = f"{self._smoke.uid}_smoke_defect" + self._attr_name = self._smoke.name self._state = False @property def is_on(self): """Return the state of the sensor.""" - return self._shutter.intrusion + return self._smoke.techSmokeDefect + + @property + def device_info(self): + """Return information to link this entity with the correct device.""" + return { + "identifiers": {(DOMAIN, self._smoke.uid)}, + "name": self._smoke.name} + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + # Importantly for a push integration, the module that will be getting updates + # needs to notify HA of changes. The dummy device has a registercallback + # method, so to this we add the 'self.async_write_ha_state' method, to be + # called where ever there are changes. + # The call back registration is done once this entity is registered with HA + # (rather than in the __init__) + self._smoke.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._smoke.remove_callback(self.async_write_ha_state) + + def get_sensors(self): + """Get available sensors for this entity""" + sensors = [] + if self._smoke.config is not None: + sensors.append(ConfigSensor(self._smoke)) + if self._smoke.batt_defect is not None: + sensors.append(BatteryDefectSensor(self._smoke)) + if self._smoke.supervisionMode is not None: + sensors.append(SupervisionModeSensor(self._smoke)) + + return sensors diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 0eeb9b6..4632b0e 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -63,9 +63,9 @@ def __init__( ) self.rollers = [ - Roller(f"{self._id}_1", f"{self._name} 1", self), - Roller(f"{self._id}_2", f"{self._name} 2", self), - Roller(f"{self._id}_3", f"{self._name} 3", self), + # Roller(f"{self._id}_1", f"{self._name} 1", self), + # Roller(f"{self._id}_2", f"{self._name} 2", self), + # Roller(f"{self._id}_3", f"{self._name} 3", self), ] self.online = True @@ -76,7 +76,9 @@ def hub_id(self) -> str: async def connect(self) -> ClientWebSocketResponse: """Connect to Tydom""" - return await self._tydom_client.async_connect() + connection = await self._tydom_client.async_connect() + await self._tydom_client.listen_tydom(connection) + return connection @staticmethod async def get_tydom_credentials( @@ -95,14 +97,13 @@ async def test_credentials(self) -> None: async def setup(self, connection: ClientWebSocketResponse) -> None: """Listen to tydom events.""" logger.info("Listen to tydom events") - await self._tydom_client.listen_tydom(connection) while True: devices = await self._tydom_client.consume_messages() if devices is not None: for device in devices: logger.info("*** device %s", device) if isinstance(device, TydomBaseEntity): - await self.update_tydom_entity(device) + await self.device_info.update_device(device) else: logger.error("*** publish_updates for device : %s", device) if device.uid not in self.devices: @@ -111,23 +112,6 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: else: await self.update_ha_device(self.devices[device.uid], device) - async def update_tydom_entity(self, updated_entity: TydomBaseEntity) -> None: - """Update Tydom Base entity values and push to HA""" - logger.error("update Tydom ") - self.device_info.product_name = updated_entity.product_name - self.device_info.main_version_sw = updated_entity.main_version_sw - self.device_info.main_version_hw = updated_entity.main_version_hw - self.device_info.main_id = updated_entity.main_id - self.device_info.main_reference = updated_entity.main_reference - self.device_info.key_version_sw = updated_entity.key_version_sw - self.device_info.key_version_hw = updated_entity.key_version_hw - self.device_info.key_version_stack = updated_entity.key_version_stack - self.device_info.key_reference = updated_entity.key_reference - self.device_info.boot_reference = updated_entity.boot_reference - self.device_info.boot_version = updated_entity.boot_version - self.device_info.update_available = updated_entity.update_available - await self.device_info.publish_updates() - async def create_ha_device(self, device): """Create a new HA device""" logger.warn("device type %s", device.type) @@ -138,30 +122,38 @@ async def create_ha_device(self, device): self.ha_devices[device.uid] = ha_device if self.add_cover_callback is not None: self.add_cover_callback([ha_device]) - batt_sensor = BatteryDefectSensor(device) - thermic_sensor = ThermicDefectSensor(device) - on_fav_pos = OnFavPosSensor(device) - up_defect= UpDefectSensor(device) - down_defect = DownDefectSensor(device) - obstacle_defect = ObstacleDefectSensor(device) - intrusion_defect = IntrusionDefectSensor(device) if self.add_sensor_callback is not None: - self.add_sensor_callback([batt_sensor, thermic_sensor, on_fav_pos, up_defect, down_defect, obstacle_defect, intrusion_defect]) + self.add_sensor_callback(ha_device.get_sensors()) case "conso": logger.warn("Create conso %s", device.uid) ha_device = HAEnergy(device) + self.ha_devices[device.uid] = ha_device if self.add_sensor_callback is not None: self.add_sensor_callback([ha_device]) if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) + case "smoke": + logger.warn("Create smoke %s", device.uid) + ha_device = HASmoke(device) + self.ha_devices[device.uid] = ha_device + if self.add_sensor_callback is not None: + self.add_sensor_callback([ha_device]) + + if self.add_sensor_callback is not None: + self.add_sensor_callback(ha_device.get_sensors()) case _: return async def update_ha_device(self, stored_device, device): """Update HA device values""" await stored_device.update_device(device) + ha_device = self.ha_devices[device.uid] + new_sensors = ha_device.get_sensors() + if len(new_sensors) > 0 and self.add_sensor_callback is not None: + # add new sensors + self.add_sensor_callback(new_sensors) async def ping(self) -> None: diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 73dfd3c..8f34fca 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -359,11 +359,26 @@ async def parse_msg_info(self, parsed): @staticmethod async def get_device(last_usage, uid, name, endpoint = None, data = None) -> TydomDevice: """Get device class from its last usage""" + + #FIXME voir: class CoverDeviceClass(StrEnum): + # Refer to the cover dev docs for device class descriptions + #AWNING = "awning" + #BLIND = "blind" + #CURTAIN = "curtain" + #DAMPER = "damper" + #DOOR = "door" + #GARAGE = "garage" + #GATE = "gate" + #SHADE = "shade" + #SHUTTER = "shutter" + #WINDOW = "window" match last_usage: case "shutter" | "klineShutter": return TydomShutter(uid, name, last_usage, endpoint, data) case "conso": return TydomEnergy(uid, name, last_usage, endpoint, data) + case "smoke": + return TydomSmoke(uid, name, last_usage, endpoint, data) case _: return @@ -374,9 +389,9 @@ async def parse_config_data(parsed): for i in parsed["endpoints"]: device_unique_id = str(i["id_endpoint"]) + "_" + str(i["id_device"]) - device = await MessageHandler.get_device(i["last_usage"], device_unique_id, i["name"], i["id_endpoint"], None) - if device is not None: - devices.append(device) + #device = await MessageHandler.get_device(i["last_usage"], device_unique_id, i["name"], i["id_endpoint"], None) + #if device is not None: + # devices.append(device) if ( i["last_usage"] == "shutter" @@ -736,7 +751,7 @@ async def parse_devices_data(self, parsed): logger.error("msg_data error in parsing !") logger.error(e) - device = await MessageHandler.get_device(type_of_id, unique_id, None, endpoint_id, data) + device = await MessageHandler.get_device(type_of_id, unique_id, name_of_id, endpoint_id, data) if device is not None: devices.append(device) diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index cbe5969..632a747 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -16,8 +16,6 @@ from .MessageHandler import MessageHandler - - from requests.auth import HTTPDigestAuth logger = logging.getLogger(__name__) @@ -138,19 +136,18 @@ async def async_get_credentials( ) json_response = await response.json() - response.close(); + response.close() - session.close() + await session.close() if ( "sites" in json_response - and len(json_response["sites"]) == 1 - and "gateway" in json_response["sites"][0] - and "password" in json_response["sites"][0]["gateway"] + and len(json_response["sites"]) > 0 ): - password = json_response["sites"][0]["gateway"]["password"] - return password - else: - raise TydomClientApiClientError("Tydom credentials not found") + for site in json_response["sites"]: + if "gateway" in site and site["gateway"]["mac"] == macaddress: + password = json_response["sites"][0]["gateway"]["password"] + return password + raise TydomClientApiClientAuthenticationError("Tydom credentials not found") except asyncio.TimeoutError as exception: raise TydomClientApiClientCommunicationError( "Timeout error fetching information", @@ -176,16 +173,16 @@ async def async_connect(self) -> ClientWebSocketResponse: "Sec-WebSocket-Version": "13", } - self._session = async_create_clientsession(self._hass, False) + session = async_create_clientsession(self._hass, False) try: async with async_timeout.timeout(10): - response = await self._session.request( + response = await session.request( method="GET", url=f"https://{self._host}:443/mediation/client?mac={self._mac}&appli=1", headers=http_headers, json=None, - proxy="http://proxy.rd.francetelecom.fr:8080" + #proxy="http://proxy.rd.francetelecom.fr:8080" ) logger.debug( "response status : %s\nheaders : %s\ncontent : %s", @@ -211,13 +208,13 @@ async def async_connect(self) -> ClientWebSocketResponse: re_matcher.group(1) ) - connection = await self._session.ws_connect( + connection = await session.ws_connect( method="GET", url=f"wss://{self._host}:443/mediation/client?mac={self._mac}&appli=1", headers=http_headers, autoping=True, heartbeat=2, - proxy="http://proxy.rd.francetelecom.fr:8080" + #proxy="http://proxy.rd.francetelecom.fr:8080" ) return connection diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index 5f8010e..1bf526f 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -30,6 +30,23 @@ def remove_callback(self, callback: Callable[[], None]) -> None: """Remove previously registered callback.""" self._callbacks.discard(callback) + async def update_device(self, updated_entity): + """Update the device values from another device""" + logger.error("update Tydom ") + self.product_name = updated_entity.product_name + self.main_version_sw = updated_entity.main_version_sw + self.main_version_hw = updated_entity.main_version_hw + self.main_id = updated_entity.main_id + self.main_reference = updated_entity.main_reference + self.key_version_sw = updated_entity.key_version_sw + self.key_version_hw = updated_entity.key_version_hw + self.key_version_stack = updated_entity.key_version_stack + self.key_reference = updated_entity.key_reference + self.boot_reference = updated_entity.boot_reference + self.boot_version = updated_entity.boot_version + self.update_available = updated_entity.update_available + await self.publish_updates() + # In a real implementation, this library would call it's call backs when it was # notified of any state changeds for the relevant device. async def publish_updates(self) -> None: @@ -251,4 +268,32 @@ async def update_device(self, device): self.energyIndexHeatGas = device.energyIndexHeatGas if device.outTemperature is not None: self.outTemperature = device.outTemperature + await self.publish_updates() + +class TydomSmoke(TydomDevice): + """Represents an smoke detector sensor""" + + def __init__(self, uid, name, device_type, endpoint, data): + logger.info("TydomSmoke : data %s", data) + if "config" in data: + self.config = data["config"] + if "battDefect" in data: + self.batt_defect = data["battDefect"] + if "supervisionMode" in data: + self.supervisionMode = data["supervisionMode"] + if "techSmokeDefect" in data: + self.techSmokeDefect = data["techSmokeDefect"] + super().__init__(uid, name, device_type, endpoint) + + async def update_device(self, device): + """Update the device values from another device""" + logger.debug("Update device %s", device.uid) + if device.config is not None: + self.config = device.config + if device.batt_defect is not None: + self.batt_defect = device.batt_defect + if device.supervisionMode is not None: + self.supervisionMode = device.supervisionMode + if device.techSmokeDefect is not None: + self.techSmokeDefect = device.techSmokeDefect await self.publish_updates() \ No newline at end of file From c6180f51a3756a439a031b2bd0145dc605444613 Mon Sep 17 00:00:00 2001 From: ucpy7374 Date: Mon, 15 May 2023 16:46:19 +0200 Subject: [PATCH 25/74] add climate, cleanup --- custom_components/deltadore-tydom/__init__.py | 2 +- custom_components/deltadore-tydom/climate.py | 37 + .../deltadore-tydom/ha_entities.py | 1040 +++++------------ custom_components/deltadore-tydom/hub.py | 37 +- .../deltadore-tydom/tydom/MessageHandler.py | 3 + .../deltadore-tydom/tydom/tydom_client.py | 10 +- .../deltadore-tydom/tydom/tydom_devices.py | 242 +--- custom_components/deltadore-tydom/update.py | 2 +- 8 files changed, 414 insertions(+), 959 deletions(-) create mode 100644 custom_components/deltadore-tydom/climate.py diff --git a/custom_components/deltadore-tydom/__init__.py b/custom_components/deltadore-tydom/__init__.py index a603688..8281ec6 100644 --- a/custom_components/deltadore-tydom/__init__.py +++ b/custom_components/deltadore-tydom/__init__.py @@ -11,7 +11,7 @@ # List of platforms to support. There should be a matching .py file for each, # eg and -PLATFORMS: list[str] = [Platform.COVER, Platform.SENSOR, Platform.UPDATE] +PLATFORMS: list[str] = [Platform.CLIMATE, Platform.COVER, Platform.SENSOR, Platform.UPDATE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/custom_components/deltadore-tydom/climate.py b/custom_components/deltadore-tydom/climate.py new file mode 100644 index 0000000..b517562 --- /dev/null +++ b/custom_components/deltadore-tydom/climate.py @@ -0,0 +1,37 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +from typing import Any + +# These constants are relevant to the type of entity we are using. +# See below for how they are used. +from homeassistant.components.cover import ( + ATTR_POSITION, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, + CoverEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity import DeviceInfo + +from .const import DOMAIN, LOGGER + + +# This function is called as part of the __init__.async_setup_entry (via the +# hass.config_entries.async_forward_entry_setup call) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add cover for passed config_entry in HA.""" + LOGGER.error("***** async_setup_entry *****") + # The hub is loaded from the associated hass.data entry that was created in the + # __init__.async_setup_entry function + hub = hass.data[DOMAIN][config_entry.entry_id] + hub.add_climate_callback = async_add_entities + diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index c3cfe62..eb04fd2 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -6,6 +6,21 @@ BinarySensorEntity, ) +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + FAN_OFF, + FAN_ON, + PRESET_ECO, + PRESET_NONE, + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature + from homeassistant.helpers.entity import Entity, DeviceInfo, Entity from homeassistant.components.cover import ( ATTR_POSITION, @@ -24,124 +39,27 @@ from .const import DOMAIN, LOGGER -class HAEnergy(Entity): - """Representation of an energy sensor""" - - should_poll = False - device_class = None - supported_features = None - - - def __init__(self, energy: TydomEnergy) -> None: - self._energy = energy - self._attr_unique_id = f"{self._energy.uid}_energy" - self._attr_name = self._energy.name - self.registered_sensors = [] - - async def async_added_to_hass(self) -> None: - """Run when this Entity has been added to HA.""" - # Importantly for a push integration, the module that will be getting updates - # needs to notify HA of changes. The dummy device has a registercallback - # method, so to this we add the 'self.async_write_ha_state' method, to be - # called where ever there are changes. - # The call back registration is done once this entity is registered with HA - # (rather than in the __init__) - self._energy.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self) -> None: - """Entity being removed from hass.""" - # The opposite of async_added_to_hass. Remove any registered call backs here. - self._energy.remove_callback(self.async_write_ha_state) +class GenericSensor(SensorBase): + """Representation of a generic sensor """ + def __init__(self, device: TydomEnergy, device_class: SensorDeviceClass, name: str, attribute: str): + """Initialize the sensor.""" + super().__init__(device) + self._attr_unique_id = f"{self._device.device_id}_{name}" + self._attr_name = f"{self._device.device_name} {name}" + self._attribute = attribute + self._attr_device_class = device_class - # To link this entity to the cover device, this property must return an - # identifiers value matching that used in the cover, but no other information such - # as name. If name is returned, this entity will then also become a device in the - # HA UI. @property - def device_info(self): - """Return information to link this entity with the correct device.""" - return {"identifiers": {(DOMAIN, self._energy.uid)}, "name": self._energy.name} - - def get_sensors(self): - """Get available sensors for this entity""" - sensors = [] - if self._energy.energyInstantTotElec is not None and "energyInstantTotElec" not in self.registered_sensors: - sensors.append(EnergyInstantTotElecSensor(self._energy)) - self.registered_sensors.append("energyInstantTotElec") - if self._energy.energyInstantTotElec_Min is not None and "energyInstantTotElec_Min" not in self.registered_sensors: - sensors.append(EnergyInstantTotElec_MinSensor(self._energy)) - self.registered_sensors.append("energyInstantTotElec_Min") - if self._energy.energyInstantTotElec_Max is not None and "energyInstantTotElec_Max" not in self.registered_sensors: - sensors.append(EnergyInstantTotElec_MaxSensor(self._energy)) - self.registered_sensors.append("energyInstantTotElec_Max") - if self._energy.energyScaleTotElec_Min is not None and "energyScaleTotElec_Min" not in self.registered_sensors: - sensors.append(EnergyScaleTotElec_MinSensor(self._energy)) - self.registered_sensors.append("energyScaleTotElec_Min") - if self._energy.energyScaleTotElec_Max is not None and "energyScaleTotElec_Max" not in self.registered_sensors: - sensors.append(EnergyScaleTotElec_MaxSensor(self._energy)) - self.registered_sensors.append("energyScaleTotElec_Max") - if self._energy.energyInstantTotElecP is not None and "energyInstantTotElecP" not in self.registered_sensors: - sensors.append(EnergyInstantTotElecPSensor(self._energy)) - self.registered_sensors.append("energyInstantTotElecP") - if self._energy.energyInstantTotElec_P_Min is not None and "energyInstantTotElec_P_Min" not in self.registered_sensors: - sensors.append(EnergyInstantTotElec_P_MinSensor(self._energy)) - self.registered_sensors.append("energyInstantTotElec_P_Min") - if self._energy.energyInstantTotElec_P_Max is not None and "energyInstantTotElec_P_Max" not in self.registered_sensors: - sensors.append(EnergyInstantTotElec_P_MaxSensor(self._energy)) - self.registered_sensors.append("energyInstantTotElec_P_Max") - if self._energy.energyScaleTotElec_P_Min is not None and "energyScaleTotElec_P_Min" not in self.registered_sensors: - sensors.append(EnergyScaleTotElec_P_MinSensor(self._energy)) - self.registered_sensors.append("energyScaleTotElec_P_Min") - if self._energy.energyScaleTotElec_P_Max is not None and "energyScaleTotElec_P_Max" not in self.registered_sensors: - sensors.append(EnergyScaleTotElec_P_MaxSensor(self._energy)) - self.registered_sensors.append("energyScaleTotElec_P_Max") - if self._energy.energyInstantTi1P is not None and "energyInstantTi1P" not in self.registered_sensors: - sensors.append(EnergyInstantTi1PSensor(self._energy)) - self.registered_sensors.append("energyInstantTi1P") - if self._energy.energyInstantTi1P_Min is not None and "energyInstantTi1P_Min" not in self.registered_sensors: - sensors.append(EnergyInstantTi1P_MinSensor(self._energy)) - self.registered_sensors.append("energyInstantTi1P_Min") - if self._energy.energyInstantTi1P_Max is not None and "energyInstantTi1P_Max" not in self.registered_sensors: - sensors.append(EnergyInstantTi1P_MaxSensor(self._energy)) - self.registered_sensors.append("energyInstantTi1P_Max") - if self._energy.energyScaleTi1P_Min is not None and "energyScaleTi1P_Min" not in self.registered_sensors: - sensors.append(EnergyScaleTi1P_MinSensor(self._energy)) - self.registered_sensors.append("energyScaleTi1P_Min") - if self._energy.energyScaleTi1P_Max is not None and "energyScaleTi1P_Max" not in self.registered_sensors: - sensors.append(EnergyScaleTi1P_MaxSensor(self._energy)) - self.registered_sensors.append("energyScaleTi1P_Max") - if self._energy.energyInstantTi1I is not None and "energyInstantTi1I" not in self.registered_sensors: - sensors.append(EnergyInstantTi1ISensor(self._energy)) - self.registered_sensors.append("energyInstantTi1I") - if self._energy.energyInstantTi1I_Min is not None and "energyInstantTi1I_Min" not in self.registered_sensors: - sensors.append(EnergyInstantTi1I_MinSensor(self._energy)) - self.registered_sensors.append("energyInstantTi1I_Min") - if self._energy.energyInstantTi1I_Max is not None and "energyInstantTi1I_Max" not in self.registered_sensors: - sensors.append(EnergyInstantTi1I_MaxSensor(self._energy)) - self.registered_sensors.append("energyInstantTi1I_Max") - if self._energy.energyTotIndexWatt is not None and "energyTotIndexWatt" not in self.registered_sensors: - sensors.append(EnergyTotIndexWattSensor(self._energy)) - self.registered_sensors.append("energyTotIndexWatt") - if self._energy.energyIndexHeatWatt is not None and "energyIndexHeatWatt" not in self.registered_sensors: - sensors.append(EnergyIndexHeatWattSensor(self._energy)) - self.registered_sensors.append("energyIndexHeatWatt") - if self._energy.energyIndexECSWatt is not None and "energyIndexECSWatt" not in self.registered_sensors: - sensors.append(EnergyIndexECSWattSensor(self._energy)) - self.registered_sensors.append("energyIndexECSWatt") - if self._energy.energyIndexHeatGas is not None and "energyIndexHeatGas" not in self.registered_sensors: - sensors.append(EnergyIndexHeatGasSensor(self._energy)) - self.registered_sensors.append("energyIndexHeatGas") - if self._energy.outTemperature is not None and "outTemperature" not in self.registered_sensors: - sensors.append(OutTemperatureSensor(self._energy)) - self.registered_sensors.append("outTemperature") - return sensors + def state(self): + """Return the state of the sensor.""" + return getattr(self._device, self._attribute) -class SensorBase(Entity): - """Representation of a Tydom.""" +class BinarySensorBase(BinarySensorEntity): + """Base representation of a Sensor.""" should_poll = False - def __init__(self, device: TydomEnergy): + def __init__(self, device: TydomDevice): """Initialize the sensor.""" self._device = device @@ -152,15 +70,15 @@ def __init__(self, device: TydomEnergy): @property def device_info(self): """Return information to link this entity with the correct device.""" - return {"identifiers": {(DOMAIN, self._device.uid)}} + return {"identifiers": {(DOMAIN, self._device.device_id)}} # This property is important to let HA know if this entity is online or not. # If an entity is offline (return False), the UI will refelect this. @property def available(self) -> bool: """Return True if roller and hub is available.""" + #return self._roller.online and self._roller.hub.online # FIXME - #return self._device.online and self._device.hub.online return True async def async_added_to_hass(self): @@ -173,418 +91,139 @@ async def async_will_remove_from_hass(self): # The opposite of async_added_to_hass. Remove any registered call backs here. self._device.remove_callback(self.async_write_ha_state) -class EnergyInstantTotElecSensor(SensorBase): - """energyInstantTotElec sensor""" - - device_class = SensorDeviceClass.CURRENT - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - # As per the sensor, this must be a unique value within this domain. This is done - # by using the device ID, and appending "_battery" - self._attr_unique_id = f"{self._device.uid}_energyInstantTotElec" - - # The name of the entity - self._attr_name = f"{self._device.name} energyInstantTotElec" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyInstantTotElec - -class EnergyInstantTotElec_MinSensor(SensorBase): - """energyInstantTotElec_Min sensor""" - - device_class = SensorDeviceClass.CURRENT - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - # As per the sensor, this must be a unique value within this domain. This is done - # by using the device ID, and appending "_battery" - self._attr_unique_id = f"{self._device.uid}_energyInstantTotElec_Min" - - # The name of the entity - self._attr_name = f"{self._device.name} energyInstantTotElec_Min" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyInstantTotElec_Min - -class EnergyInstantTotElec_MaxSensor(SensorBase): - """energyInstantTotElec_Max sensor""" - - device_class = SensorDeviceClass.CURRENT - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - # As per the sensor, this must be a unique value within this domain. This is done - # by using the device ID, and appending "_battery" - self._attr_unique_id = f"{self._device.uid}_energyInstantTotElec_Max" - - # The name of the entity - self._attr_name = f"{self._device.name} energyInstantTotElec_Max" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyInstantTotElec_Max - - -class EnergyScaleTotElec_MinSensor(SensorBase): - """energyScaleTotElec_Min sensor""" - - device_class = SensorDeviceClass.CURRENT - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyScaleTotElec_Min" - self._attr_name = f"{self._device.name} energyScaleTotElec_Min" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyScaleTotElec_Min - -class EnergyScaleTotElec_MaxSensor(SensorBase): - """energyScaleTotElec_Min sensor""" - - device_class = SensorDeviceClass.CURRENT - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyScaleTotElec_Max" - self._attr_name = f"{self._device.name} energyScaleTotElec_Max" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyScaleTotElec_Max - -class EnergyInstantTotElecPSensor(SensorBase): - """energyInstantTotElecP sensor""" - - device_class = SensorDeviceClass.POWER - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyInstantTotElecP" - self._attr_name = f"{self._device.name} energyInstantTotElecP" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyInstantTotElecP - -class EnergyInstantTotElec_P_MinSensor(SensorBase): - """energyInstantTotElec_P_Min sensor""" - - device_class = SensorDeviceClass.POWER - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyInstantTotElec_P_Min" - self._attr_name = f"{self._device.name} energyInstantTotElec_P_Min" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyInstantTotElec_P_Min - -class EnergyInstantTotElec_P_MaxSensor(SensorBase): - """energyInstantTotElec_P_Max sensor""" - - device_class = SensorDeviceClass.POWER - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyInstantTotElec_P_Max" - self._attr_name = f"{self._device.name} energyInstantTotElec_P_Max" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyInstantTotElec_P_Max - -class EnergyScaleTotElec_P_MinSensor(SensorBase): - """energyScaleTotElec_P_Min sensor""" - - device_class = SensorDeviceClass.POWER - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyScaleTotElec_P_Min" - self._attr_name = f"{self._device.name} energyScaleTotElec_P_Min" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyScaleTotElec_P_Min - -class EnergyScaleTotElec_P_MaxSensor(SensorBase): - """energyScaleTotElec_P_Max sensor""" - - device_class = SensorDeviceClass.POWER - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyScaleTotElec_P_Max" - self._attr_name = f"{self._device.name} energyScaleTotElec_P_Max" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyScaleTotElec_P_Max - -class EnergyInstantTi1PSensor(SensorBase): - """energyInstantTi1P sensor""" - - device_class = SensorDeviceClass.POWER - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyInstantTi1P" - self._attr_name = f"{self._device.name} energyInstantTi1P" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyInstantTi1P - -class EnergyInstantTi1P_MinSensor(SensorBase): - """energyInstantTi1P_Min sensor""" - - device_class = SensorDeviceClass.POWER - - def __init__(self, device: TydomEnergy): +class GenericBinarySensor(BinarySensorBase): + """Generic representation of a Binary Sensor.""" + def __init__(self, device: TydomDevice, device_class: BinarySensorDeviceClass, name: str, attribute: str): """Initialize the sensor.""" super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyInstantTi1P_Min" - self._attr_name = f"{self._device.name} energyInstantTi1P_Min" + self._attr_unique_id = f"{self._device.device_id}_{name}" + self._attr_name = f"{self._device.device_name} {name}" + self._attribute = attribute + self._attr_device_class = device_class + # The value of this sensor. @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyInstantTi1P_Min - -class EnergyInstantTi1P_MaxSensor(SensorBase): - """energyInstantTi1P_Max sensor""" - - device_class = SensorDeviceClass.POWER - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyInstantTi1P_Max" - self._attr_name = f"{self._device.name} energyInstantTi1P_Max" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyInstantTi1P_Max - -class EnergyScaleTi1P_MinSensor(SensorBase): - """energyInstantenergyScaleTi1P_MinTi1P sensor""" - - device_class = SensorDeviceClass.POWER - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyScaleTi1P_Min" - self._attr_name = f"{self._device.name} energyScaleTi1P_Min" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyScaleTi1P_Min - -class EnergyScaleTi1P_MaxSensor(SensorBase): - """energyScaleTi1P_Max sensor""" - - device_class = SensorDeviceClass.POWER - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyScaleTi1P_Max" - self._attr_name = f"{self._device.name} energyScaleTi1P_Max" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyScaleTi1P_Max - -class EnergyInstantTi1ISensor(SensorBase): - """energyInstantTi1I sensor""" - - device_class = SensorDeviceClass.CURRENT - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyInstantTi1I" - self._attr_name = f"{self._device.name} energyInstantTi1I" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyInstantTi1I - -class EnergyInstantTi1I_MinSensor(SensorBase): - """energyInstantTi1I_Min sensor""" - - device_class = SensorDeviceClass.CURRENT - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyInstantTi1I_Min" - self._attr_name = f"{self._device.name} energyInstantTi1I_Min" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyInstantTi1I_Min - -class EnergyInstantTi1I_MaxSensor(SensorBase): - """energyInstantTi1I_Max sensor""" - - device_class = SensorDeviceClass.CURRENT - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyInstantTi1I_Max" - self._attr_name = f"{self._device.name} energyInstantTi1I_Max" - - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyInstantTi1I_Max - -class EnergyScaleTi1I_MinSensor(SensorBase): - """energyScaleTi1I_Min sensor""" - - device_class = SensorDeviceClass.CURRENT - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyScaleTi1I_Min" - self._attr_name = f"{self._device.name} energyScaleTi1I_Min" - - @property - def state(self): + def is_on(self): """Return the state of the sensor.""" - return self._device.energyScaleTi1I_Min + return getattr(self._device, self._attribute) -class EnergyScaleTi1I_MaxSensor(SensorBase): - """energyScaleTi1I_Max sensor""" +class HATydom(Entity): + """Representation of a Tydom.""" - device_class = SensorDeviceClass.CURRENT + should_poll = False def __init__(self, device: TydomEnergy): """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyScaleTi1I_Max" - self._attr_name = f"{self._device.name} energyScaleTi1I_Max" + self._device = device + # To link this entity to the cover device, this property must return an + # identifiers value matching that used in the cover, but no other information such + # as name. If name is returned, this entity will then also become a device in the + # HA UI. @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyScaleTi1I_Max - -class EnergyTotIndexWattSensor(SensorBase): - """energyTotIndexWatt sensor""" - - device_class = SensorDeviceClass.ENERGY - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyTotIndexWatt" - self._attr_name = f"{self._device.name} energyTotIndexWatt" + def device_info(self): + """Return information to link this entity with the correct device.""" + return {"identifiers": {(DOMAIN, self._device.device_id)}} + # This property is important to let HA know if this entity is online or not. + # If an entity is offline (return False), the UI will refelect this. @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyTotIndexWatt - -class EnergyIndexHeatWattSensor(SensorBase): - """energyIndexHeatWatt sensor""" - - device_class = SensorDeviceClass.ENERGY - - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyIndexHeatWatt" - self._attr_name = f"{self._device.name} energyIndexHeatWatt" + def available(self) -> bool: + """Return True if roller and hub is available.""" + # FIXME + #return self._device.online and self._device.hub.online + return True - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyIndexHeatWatt + async def async_added_to_hass(self): + """Run when this Entity has been added to HA.""" + # Sensors should also register callbacks to HA when their state changes + self._device.register_callback(self.async_write_ha_state) -class EnergyIndexECSWattSensor(SensorBase): - """energyIndexECSWatt sensor""" + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._device.remove_callback(self.async_write_ha_state) - device_class = SensorDeviceClass.ENERGY +class HAEnergy(Entity): + """Representation of an energy sensor""" - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyIndexECSWatt" - self._attr_name = f"{self._device.name} energyIndexECSWatt" + should_poll = False + device_class = None + supported_features = None - @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyIndexECSWatt + sensor_classes = { + "energyInstantTotElec": SensorDeviceClass.CURRENT, + "energyInstantTotElec_Min": SensorDeviceClass.CURRENT, + "energyInstantTotElec_Max": SensorDeviceClass.CURRENT, + "energyScaleTotElec_Min": SensorDeviceClass.CURRENT, + "energyScaleTotElec_Max": SensorDeviceClass.CURRENT, + "energyInstantTotElecP": SensorDeviceClass.POWER, + "energyInstantTotElec_P_Min": SensorDeviceClass.POWER, + "energyInstantTotElec_P_Max": SensorDeviceClass.POWER, + "energyScaleTotElec_P_Min": SensorDeviceClass.POWER, + "energyScaleTotElec_P_Max": SensorDeviceClass.POWER, + "energyInstantTi1P": SensorDeviceClass.POWER, + "energyInstantTi1P_Min": SensorDeviceClass.POWER, + "energyInstantTi1P_Max": SensorDeviceClass.POWER, + "energyScaleTi1P_Min": SensorDeviceClass.POWER, + "energyScaleTi1P_Max": SensorDeviceClass.POWER, + "energyInstantTi1I": SensorDeviceClass.CURRENT, + "energyInstantTi1I_Min": SensorDeviceClass.CURRENT, + "energyInstantTi1I_Max": SensorDeviceClass.CURRENT, + "energyTotIndexWatt": SensorDeviceClass.ENERGY, + "energyIndexHeatWatt": SensorDeviceClass.ENERGY, + "energyIndexECSWatt": SensorDeviceClass.ENERGY, + "energyIndexHeatGas": SensorDeviceClass.ENERGY, + "outTemperature": SensorDeviceClass.TEMPERATURE, + } -class EnergyIndexHeatGasSensor(SensorBase): - """energyIndexHeatGas sensor""" + def __init__(self, energy: TydomEnergy) -> None: + self._energy = energy + self._attr_unique_id = f"{self._energy.device_id}_energy" + self._attr_name = self._energy.device_name + self._registered_sensors = [] - device_class = SensorDeviceClass.ENERGY + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + # Importantly for a push integration, the module that will be getting updates + # needs to notify HA of changes. The dummy device has a registercallback + # method, so to this we add the 'self.async_write_ha_state' method, to be + # called where ever there are changes. + # The call back registration is done once this entity is registered with HA + # (rather than in the __init__) + self._energy.register_callback(self.async_write_ha_state) - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_energyIndexHeatGas" - self._attr_name = f"{self._device.name} energyIndexHeatGas" + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._energy.remove_callback(self.async_write_ha_state) + # To link this entity to the cover device, this property must return an + # identifiers value matching that used in the cover, but no other information such + # as name. If name is returned, this entity will then also become a device in the + # HA UI. @property - def state(self): - """Return the state of the sensor.""" - return self._device.energyIndexHeatGas + def device_info(self): + """Return information to link this entity with the correct device.""" + return {"identifiers": {(DOMAIN, self._energy.device_id)}, "name": self._energy.device_name} -class OutTemperatureSensor(SensorBase): - """outTemperature sensor""" + def get_sensors(self): + """Get available sensors for this entity""" + sensors = [] - device_class = SensorDeviceClass.TEMPERATURE + for attribute, value in self._energy.__dict__.items(): + if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + sensor_class = None + if attribute in self.sensor_classes: + sensor_class = self.sensor_classes[attribute] + if isinstance(value, bool): + sensors.append(GenericBinarySensor(self._energy, sensor_class, attribute, attribute)) + else : + sensors.append(GenericSensor(self._energy, sensor_class, attribute, attribute)) + self._registered_sensors.append(attribute) - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - super().__init__(device) - self._attr_unique_id = f"{self._device.uid}_outTemperature" - self._attr_name = f"{self._device.name} outTemperature" + return sensors - @property - def state(self): - """Return the state of the sensor.""" - return self._device.outTemperature # This entire class could be written to extend a base class to ensure common attributes # are kept identical/in sync. It's broken apart here between the Cover and Sensors to @@ -599,7 +238,16 @@ class HACover(CoverEntity): # If the supported features were dynamic (ie: different depending on the external # device it connected to), then this should be function with an @property decorator. supported_features = SUPPORT_SET_POSITION | SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP - device_class= CoverDeviceClass.SHUTTER + device_class = CoverDeviceClass.SHUTTER + + sensor_classes = { + "batt_defect": BinarySensorDeviceClass.PROBLEM, + "thermic_defect": BinarySensorDeviceClass.PROBLEM, + "up_defect": BinarySensorDeviceClass.PROBLEM, + "down_defect": BinarySensorDeviceClass.PROBLEM, + "obstacle_defect": BinarySensorDeviceClass.PROBLEM, + "intrusion": BinarySensorDeviceClass.PROBLEM, + } def __init__(self, shutter: TydomShutter) -> None: """Initialize the sensor.""" @@ -611,12 +259,13 @@ def __init__(self, shutter: TydomShutter) -> None: # which is done here by appending "_cover". For more information, see: # https://developers.home-assistant.io/docs/entity_registry_index/#unique-id-requirements # Note: This is NOT used to generate the user visible Entity ID used in automations. - self._attr_unique_id = f"{self._shutter.uid}_cover" + self._attr_unique_id = f"{self._shutter.device_id}_cover" # This is the name for this *entity*, the "name" attribute from "device_info" # is used as the device name for device screens in the UI. This name is used on # entity screens, and used to build the Entity ID that's used is automations etc. - self._attr_name = self._shutter.name + self._attr_name = self._shutter.device_name + self._registered_sensors = [] async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" @@ -656,7 +305,7 @@ async def async_will_remove_from_hass(self) -> None: def device_info(self) -> DeviceInfo: """Information about this entity/device.""" return { - "identifiers": {(DOMAIN, self._shutter.uid)}, + "identifiers": {(DOMAIN, self._shutter.device_id)}, # If desired, the name for the device could be different to the entity "name": self.name, #"sw_version": self._shutter.firmware_version, @@ -732,274 +381,176 @@ async def async_set_cover_position(self, **kwargs: Any) -> None: def get_sensors(self) -> list: """Get available sensors for this entity""" sensors = [] - device = self._shutter - if self._shutter.batt_defect is not None: - batt_sensor = BatteryDefectSensor(device) - sensors.append(batt_sensor) - if self._shutter.thermic_defect is not None: - thermic_sensor = ThermicDefectSensor(device) - sensors.append(thermic_sensor) - if self._shutter.on_fav_pos is not None: - on_fav_pos = OnFavPosSensor(device) - sensors.append(on_fav_pos) - if self._shutter.up_defect is not None: - up_defect= UpDefectSensor(device) - sensors.append(up_defect) - if self._shutter.down_defect is not None: - down_defect = DownDefectSensor(device) - sensors.append(down_defect) - if self._shutter.obstacle_defect is not None: - obstacle_defect = ObstacleDefectSensor(device) - sensors.append(obstacle_defect) - if self._shutter.intrusion is not None: - intrusion_defect = IntrusionDefectSensor(device) - sensors.append(intrusion_defect) - return sensors - + for attribute, value in self._shutter.__dict__.items(): + if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + sensor_class = None + if attribute in self.sensor_classes: + sensor_class = self.sensor_classes[attribute] + if isinstance(value, bool): + sensors.append(GenericBinarySensor(self._shutter, sensor_class, attribute, attribute)) + else: + sensors.append(GenericSensor(self._shutter, sensor_class, attribute, attribute)) + self._registered_sensors.append(attribute) -class BinarySensorBase(BinarySensorEntity): - """Base representation of a Sensor.""" - - should_poll = False - - def __init__(self, device: TydomDevice): - """Initialize the sensor.""" - self._device = device - - # To link this entity to the cover device, this property must return an - # identifiers value matching that used in the cover, but no other information such - # as name. If name is returned, this entity will then also become a device in the - # HA UI. - @property - def device_info(self): - """Return information to link this entity with the correct device.""" - return {"identifiers": {(DOMAIN, self._device.uid)}} - - # This property is important to let HA know if this entity is online or not. - # If an entity is offline (return False), the UI will refelect this. - @property - def available(self) -> bool: - """Return True if roller and hub is available.""" - #return self._roller.online and self._roller.hub.online - # FIXME - return True + return sensors - async def async_added_to_hass(self): - """Run when this Entity has been added to HA.""" - # Sensors should also register callbacks to HA when their state changes - self._device.register_callback(self.async_write_ha_state) - async def async_will_remove_from_hass(self): - """Entity being removed from hass.""" - # The opposite of async_added_to_hass. Remove any registered call backs here. - self._device.remove_callback(self.async_write_ha_state) -class BatteryDefectSensor(BinarySensorBase): - """Representation of a Battery Defect Sensor.""" +class HASmoke(BinarySensorEntity): + """Representation of an smoke sensor""" + should_poll = False + device_class = None + supported_features = None - # The class of this device. Note the value should come from the homeassistant.const - # module. More information on the available devices classes can be seen here: - # https://developers.home-assistant.io/docs/core/entity/sensor device_class = BinarySensorDeviceClass.PROBLEM - def __init__(self, device): - """Initialize the sensor.""" - super().__init__(device) - - # As per the sensor, this must be a unique value within this domain. This is done - # by using the device ID, and appending "_battery" - self._attr_unique_id = f"{self._device.uid}_battery" - - # The name of the entity - self._attr_name = f"{self._device.name} Battery defect" + sensor_classes = { + "batt_defect" : BinarySensorDeviceClass.PROBLEM + } + def __init__(self, smoke: TydomSmoke) -> None: + self._device = smoke + self._attr_unique_id = f"{self._device.device_id}_smoke_defect" + self._attr_name = self._device.device_name self._state = False + self._registered_sensors = [] - # The value of this sensor. As this is a DEVICE_CLASS_BATTERY, this value must be - # the battery level as a percentage (between 0 and 100) @property def is_on(self): """Return the state of the sensor.""" - return self._device.batt_defect - -class ThermicDefectSensor(BinarySensorBase): - """Representation of a Thermic Defect Sensor.""" - # The class of this device. Note the value should come from the homeassistant.const - # module. More information on the available devices classes can be seen here: - # https://developers.home-assistant.io/docs/core/entity/sensor - device_class = BinarySensorDeviceClass.PROBLEM - - def __init__(self, shutter): - """Initialize the sensor.""" - super().__init__(shutter) - - # As per the sensor, this must be a unique value within this domain. This is done - # by using the device ID, and appending "_battery" - self._attr_unique_id = f"{self._device.uid}_thermic" - - # The name of the entity - self._attr_name = f"{self._device.name} Thermic defect" - - self._state = False + return self._device.techSmokeDefect - # The value of this sensor. As this is a DEVICE_CLASS_BATTERY, this value must be - # the battery level as a percentage (between 0 and 100) @property - def is_on(self): - """Return the state of the sensor.""" - return self._device.thermic_defect - -class OnFavPosSensor(BinarySensorBase): - """Representation of a fav position Sensor.""" - device_class = None - - def __init__(self, shutter): - """Initialize the sensor.""" - super().__init__(shutter) - - self._attr_unique_id = f"{self._device.uid}_on_fav_pos" - self._attr_name = f"{self._device.name} On favorite position" - self._state = False + def device_info(self): + """Return information to link this entity with the correct device.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.device_name} - @property - def is_on(self): - """Return the state of the sensor.""" - return self._device.on_fav_pos + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + # Importantly for a push integration, the module that will be getting updates + # needs to notify HA of changes. The dummy device has a registercallback + # method, so to this we add the 'self.async_write_ha_state' method, to be + # called where ever there are changes. + # The call back registration is done once this entity is registered with HA + # (rather than in the __init__) + self._device.register_callback(self.async_write_ha_state) -class UpDefectSensor(BinarySensorBase): - """Representation of a Up Defect Sensor.""" - device_class = BinarySensorDeviceClass.PROBLEM + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._device.remove_callback(self.async_write_ha_state) - def __init__(self, shutter): - """Initialize the sensor.""" - super().__init__(shutter) + def get_sensors(self): + """Get available sensors for this entity""" + sensors = [] - self._attr_unique_id = f"{self._device.uid}_up_defect" - self._attr_name = f"{self._device.name} Up defect" - self._state = False + for attribute, value in self._device.__dict__.items(): + if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + sensor_class = None + if attribute in self.sensor_classes: + sensor_class = self.sensor_classes[attribute] + if isinstance(value, bool): + sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + else: + sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + self._registered_sensors.append(attribute) - @property - def is_on(self): - """Return the state of the sensor.""" - return self._device.up_defect + return sensors -class DownDefectSensor(BinarySensorBase): - """Representation of a Down Defect Sensor.""" - device_class = BinarySensorDeviceClass.PROBLEM +class HaClimate(ClimateEntity): + """A climate entity.""" - def __init__(self, shutter): - """Initialize the sensor.""" - super().__init__(shutter) + _attr_should_poll = False + should_poll = False - self._attr_unique_id = f"{self._device.uid}_down_defect" - self._attr_name = f"{self._device.name} Down defect" - self._state = False + sensor_classes = { + "TempSensorDefect": BinarySensorDeviceClass.PROBLEM, + "TempSensorOpenCirc": BinarySensorDeviceClass.PROBLEM, + "TempSensorShortCut": BinarySensorDeviceClass.PROBLEM, + "ProductionDefect": BinarySensorDeviceClass.PROBLEM, + "BatteryCmdDefect": BinarySensorDeviceClass.PROBLEM, + } + DICT_HA_TO_DD = { + HVACMode.AUTO: "todo", + HVACMode.COOL: "todo", + HVACMode.HEAT: "todo", + HVACMode.OFF: "todo", + } + DICT_DD_TO_HA = { + "todo": HVACMode.AUTO, + "todo": HVACMode.COOL, + "todo": HVACMode.HEAT, + "todo": HVACMode.OFF, + } + + def __init__(self, device: TydomBoiler) -> None: + super().__init__() + self._device = device + self._attr_unique_id = f"{self._device.device_id}_climate" + self._attr_name = self._device.device_name + self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] # , HVACMode.AUTO, HVACMode.COOL, + self._registered_sensors = [] + + @property + def supported_features(self) -> ClimateEntityFeature: + """Return the list of supported features.""" + features = ClimateEntityFeature(0) + + features = features | ClimateEntityFeature.TARGET_TEMPERATURE + #set_req = self.gateway.const.SetReq + #if set_req.V_HVAC_SPEED in self._values: + # features = features | ClimateEntityFeature.FAN_MODE + #if ( + # set_req.V_HVAC_SETPOINT_COOL in self._values + # and set_req.V_HVAC_SETPOINT_HEAT in self._values + #): + # features = features | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + #else: + # features = features | ClimateEntityFeature.TARGET_TEMPERATURE + return features @property - def is_on(self): - """Return the state of the sensor.""" - return self._device.down_defect - -class ObstacleDefectSensor(BinarySensorBase): - """Representation of a Obstacle Defect Sensor.""" - device_class = BinarySensorDeviceClass.PROBLEM - - def __init__(self, shutter): - """Initialize the sensor.""" - super().__init__(shutter) - - self._attr_unique_id = f"{self._device.uid}_obstacle_defect" - self._attr_name = f"{self._device.name} Obstacle defect" - self._state = False + def device_info(self) -> DeviceInfo: + """Information about this entity/device.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.device_name, + } @property - def is_on(self): - """Return the state of the sensor.""" - return self._device.obstacle_defect - -class IntrusionDefectSensor(BinarySensorBase): - """Representation of a Obstacle Defect Sensor.""" - device_class = BinarySensorDeviceClass.PROBLEM - - def __init__(self, shutter): - """Initialize the sensor.""" - super().__init__(shutter) - - self._attr_unique_id = f"{self._device.uid}_intrusion_defect" - self._attr_name = f"{self._device.name} Intrusion defect" - self._state = False + def temperature_unit(self) -> str: + """Return the unit of temperature measurement for the system.""" + return UnitOfTemperature.CELSIUS @property - def is_on(self): - """Return the state of the sensor.""" - return self._device.intrusion - -class ConfigSensor(SensorBase): - """config sensor""" - - device_class = None - - def __init__(self, device: TydomDevice): - """Initialize the sensor.""" - super().__init__(device) - # As per the sensor, this must be a unique value within this domain. This is done - # by using the device ID, and appending "_battery" - self._attr_unique_id = f"{self._device.uid}_config" - - # The name of the entity - self._attr_name = f"{self._device.name} config" + def hvac_mode(self) -> HVACMode: + """Return the current operation (e.g. heat, cool, idle).""" + # FIXME + #return self._device.hvacMode + return HVACMode.HEAT @property - def state(self): - """Return the state of the sensor.""" - return self._device.config - -class SupervisionModeSensor(SensorBase): - """supervisionMode sensor""" - - device_class = None - - def __init__(self, device: TydomDevice): - """Initialize the sensor.""" - super().__init__(device) - # As per the sensor, this must be a unique value within this domain. This is done - # by using the device ID, and appending "_battery" - self._attr_unique_id = f"{self._device.uid}_supervisionMode" - - # The name of the entity - self._attr_name = f"{self._device.name} supervisionMode" + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self._device.temperature @property - def state(self): - """Return the state of the sensor.""" - return self._device.supervisionMode - -class HASmoke(BinarySensorEntity): - """Representation of an smoke sensor""" - should_poll = False - device_class = None - supported_features = None + def target_temperature(self) -> float | None: + """Return the temperature currently set to be reached.""" + if self._device.authorization == "HEATING": + return self._device.setpoint + return None - device_class = BinarySensorDeviceClass.PROBLEM - - def __init__(self, smoke: TydomSmoke) -> None: - self._smoke = smoke - self._attr_unique_id = f"{self._smoke.uid}_smoke_defect" - self._attr_name = self._smoke.name - self._state = False + async def async_set_hvac_mode(self, hvac_mode): + """Set new target hvac mode.""" + logger.warn("SET HVAC MODE") - @property - def is_on(self): - """Return the state of the sensor.""" - return self._smoke.techSmokeDefect - - @property - def device_info(self): - """Return information to link this entity with the correct device.""" - return { - "identifiers": {(DOMAIN, self._smoke.uid)}, - "name": self._smoke.name} + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + logger.warn("SET TEMPERATURE") async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" @@ -1009,21 +560,26 @@ async def async_added_to_hass(self) -> None: # called where ever there are changes. # The call back registration is done once this entity is registered with HA # (rather than in the __init__) - self._smoke.register_callback(self.async_write_ha_state) + self._device.register_callback(self.async_write_ha_state) async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" # The opposite of async_added_to_hass. Remove any registered call backs here. - self._smoke.remove_callback(self.async_write_ha_state) + self._device.remove_callback(self.async_write_ha_state) def get_sensors(self): """Get available sensors for this entity""" sensors = [] - if self._smoke.config is not None: - sensors.append(ConfigSensor(self._smoke)) - if self._smoke.batt_defect is not None: - sensors.append(BatteryDefectSensor(self._smoke)) - if self._smoke.supervisionMode is not None: - sensors.append(SupervisionModeSensor(self._smoke)) + + for attribute, value in self._device.__dict__.items(): + if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + sensor_class = None + if attribute in self.sensor_classes: + sensor_class = self.sensor_classes[attribute] + if isinstance(value, bool): + sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + else: + sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + self._registered_sensors.append(attribute) return sensors diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 4632b0e..3b56a86 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -52,6 +52,7 @@ def __init__( self.ha_devices = {} self.add_cover_callback = None self.add_sensor_callback = None + self.add_climate_callback = None self._tydom_client = TydomClient( hass=self._hass, @@ -102,32 +103,32 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: if devices is not None: for device in devices: logger.info("*** device %s", device) - if isinstance(device, TydomBaseEntity): + if isinstance(device, HATydom): await self.device_info.update_device(device) else: logger.error("*** publish_updates for device : %s", device) - if device.uid not in self.devices: - self.devices[device.uid] = device + if device.device_id not in self.devices: + self.devices[device.device_id] = device await self.create_ha_device(device) else: - await self.update_ha_device(self.devices[device.uid], device) + await self.update_ha_device(self.devices[device.device_id], device) async def create_ha_device(self, device): """Create a new HA device""" - logger.warn("device type %s", device.type) - match device.type: + logger.warn("device type %s", device.device_type) + match device.device_type: case "shutter": - logger.warn("Create cover %s", device.uid) + logger.warn("Create cover %s", device.device_id) ha_device = HACover(device) - self.ha_devices[device.uid] = ha_device + self.ha_devices[device.device_id] = ha_device if self.add_cover_callback is not None: self.add_cover_callback([ha_device]) if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case "conso": - logger.warn("Create conso %s", device.uid) + logger.warn("Create conso %s", device.device_id) ha_device = HAEnergy(device) - self.ha_devices[device.uid] = ha_device + self.ha_devices[device.device_id] = ha_device if self.add_sensor_callback is not None: self.add_sensor_callback([ha_device]) @@ -135,21 +136,31 @@ async def create_ha_device(self, device): self.add_sensor_callback(ha_device.get_sensors()) case "smoke": - logger.warn("Create smoke %s", device.uid) + logger.warn("Create smoke %s", device.device_id) ha_device = HASmoke(device) - self.ha_devices[device.uid] = ha_device + self.ha_devices[device.device_id] = ha_device if self.add_sensor_callback is not None: self.add_sensor_callback([ha_device]) + if self.add_sensor_callback is not None: + self.add_sensor_callback(ha_device.get_sensors()) + case "boiler": + logger.warn("Create boiler %s", device.device_id) + ha_device = HaClimate(device) + self.ha_devices[device.device_id] = ha_device + if self.add_climate_callback is not None: + self.add_climate_callback([ha_device]) + if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case _: + logger.error("unsupported device type %s for device %s", device.device_type, device.device_id) return async def update_ha_device(self, stored_device, device): """Update HA device values""" await stored_device.update_device(device) - ha_device = self.ha_devices[device.uid] + ha_device = self.ha_devices[device.device_id] new_sensors = ha_device.get_sensors() if len(new_sensors) > 0 and self.add_sensor_callback is not None: # add new sensors diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 8f34fca..810eac6 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -379,7 +379,10 @@ async def get_device(last_usage, uid, name, endpoint = None, data = None) -> Tyd return TydomEnergy(uid, name, last_usage, endpoint, data) case "smoke": return TydomSmoke(uid, name, last_usage, endpoint, data) + case "boiler": + return TydomBoiler(uid, name, last_usage, endpoint, data) case _: + logger.warn("Unknown usage : %s", last_usage) return @staticmethod diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index 632a747..3c856a7 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -32,6 +32,8 @@ class TydomClientApiClientCommunicationError(TydomClientApiClientError): class TydomClientApiClientAuthenticationError(TydomClientApiClientError): """Exception to indicate an authentication error.""" +#proxy = None +proxy = "http://proxy.rd.francetelecom.fr:8080/" class TydomClient: """Tydom API Client.""" @@ -55,7 +57,6 @@ def __init__( self._remote_mode = self._host == "mediation.tydom.com" self._connection = None self.event_callback = event_callback - # Some devices (like Tywatt) need polling self.poll_device_urls = [] self.current_poll_index = 0 @@ -81,6 +82,7 @@ async def async_get_credentials( response = await session.request( method="GET", url="https://deltadoreadb2ciot.b2clogin.com/deltadoreadb2ciot.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_AccountProviderROPC_SignIn", + proxy=proxy ) logger.debug( @@ -109,6 +111,7 @@ async def async_get_credentials( url=signin_url, headers={"Content-Type": ct_header}, data=body, + proxy=proxy ) logger.debug( @@ -126,6 +129,7 @@ async def async_get_credentials( method="GET", url=f"https://prod.iotdeltadore.com/sitesmanagement/api/v1/sites?gateway_mac={macaddress}", headers={"Authorization": f"Bearer {access_token}"}, + proxy=proxy ) logger.debug( @@ -182,7 +186,7 @@ async def async_connect(self) -> ClientWebSocketResponse: url=f"https://{self._host}:443/mediation/client?mac={self._mac}&appli=1", headers=http_headers, json=None, - #proxy="http://proxy.rd.francetelecom.fr:8080" + proxy=proxy ) logger.debug( "response status : %s\nheaders : %s\ncontent : %s", @@ -214,7 +218,7 @@ async def async_connect(self) -> ClientWebSocketResponse: headers=http_headers, autoping=True, heartbeat=2, - #proxy="http://proxy.rd.francetelecom.fr:8080" + proxy=proxy ) return connection diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index 1bf526f..1f9ab8e 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -58,12 +58,15 @@ async def publish_updates(self) -> None: class TydomDevice(): """represents a generic device""" - def __init__(self, uid, name, device_type, endpoint): - self.uid = uid - self.name = name - self.type = device_type - self.endpoint = endpoint + def __init__(self, uid, name, device_type, endpoint, data): + self._uid = uid + self._name = name + self._type = device_type + self._endpoint = endpoint self._callbacks = set() + for key in data: + setattr(self, key, data[key]) + def register_callback(self, callback: Callable[[], None]) -> None: """Register callback, called when Roller changes state.""" @@ -76,7 +79,30 @@ def remove_callback(self, callback: Callable[[], None]) -> None: @property def device_id(self) -> str: """Return ID for device.""" - return self.uid + return self._uid + + @property + def device_name(self) -> str: + """Return name for device""" + return self._name + + @property + def device_type(self) -> str: + """Return type for device""" + return self._type + + @property + def device_endpoint(self) -> str: + """Return endpoint for device""" + return self._endpoint + + async def update_device(self, device): + """Update the device values from another device""" + logger.debug("Update device %s", device.device_id) + for attribute, value in device.__dict__.items(): + if attribute[:1] != '_' and value is not None: + setattr(self, attribute, value) + await self.publish_updates() # In a real implementation, this library would call it's call backs when it was # notified of any state changeds for the relevant device. @@ -88,212 +114,30 @@ async def publish_updates(self) -> None: class TydomShutter(TydomDevice): """Represents a shutter""" def __init__(self, uid, name, device_type, endpoint, data): - self.thermic_defect = None - self.position = None - self.on_fav_pos = None - self.up_defect = None - self.down_defect = None - self.obstacle_defect = None - self.intrusion = None - self.batt_defect = None - if data is not None: - if "thermicDefect" in data: - self.thermic_defect = data["thermicDefect"] - if "position" in data: - logger.error("positio : %s", data["position"]) - self.position = data["position"] - if "onFavPos" in data: - self.on_fav_pos = data["onFavPos"] - if "upDefect" in data: - self.up_defect = data["upDefect"] - if "downDefect" in data: - self.down_defect = data["downDefect"] - if "obstacleDefect" in data: - self.obstacle_defect = data["obstacleDefect"] - if "intrusion" in data: - self.intrusion = data["intrusion"] - if "battDefect" in data: - self.batt_defect = data["battDefect"] - super().__init__(uid, name, device_type, endpoint) - - async def update_device(self, device): - """Update the device values from another device""" - logger.debug("Update device %s", device.uid) - self.thermic_defect = device.thermic_defect - self.position = device.position - self.on_fav_pos = device.on_fav_pos - self.up_defect = device.up_defect - self.down_defect = device.down_defect - self.obstacle_defect = device.obstacle_defect - self.intrusion = device.intrusion - self.batt_defect = device.batt_defect - await self.publish_updates() + super().__init__(uid, name, device_type, endpoint, data) class TydomEnergy(TydomDevice): """Represents an energy sensor (for example TYWATT)""" def __init__(self, uid, name, device_type, endpoint, data): logger.info("TydomEnergy : data %s", data) - self.energyInstantTotElec = None - self.energyInstantTotElec_Min = None - self.energyInstantTotElec_Max = None - self.energyScaleTotElec_Min = None - self.energyScaleTotElec_Max = None - self.energyInstantTotElecP = None - self.energyInstantTotElec_P_Min = None - self.energyInstantTotElec_P_Max = None - self.energyScaleTotElec_P_Min = None - self.energyScaleTotElec_P_Max = None - self.energyInstantTi1P = None - self.energyInstantTi1P_Min = None - self.energyInstantTi1P_Max = None - self.energyScaleTi1P_Min = None - self.energyScaleTi1P_Max = None - self.energyInstantTi1I = None - self.energyInstantTi1I_Min = None - self.energyInstantTi1I_Max = None - self.energyScaleTi1I_Min = None - self.energyScaleTi1I_Max = None - self.energyTotIndexWatt = None - self.energyIndexHeatWatt = None - self.energyIndexECSWatt = None - self.energyIndexHeatGas = None - self.outTemperature = None - if data is not None: - if "energyInstantTotElec" in data: - self.energyInstantTotElec = data["energyInstantTotElec"] - if "energyInstantTotElec_Min" in data: - self.energyInstantTotElec_Min = data["energyInstantTotElec_Min"] - if "energyInstantTotElec_Max" in data: - self.energyInstantTotElec_Max = data["energyInstantTotElec_Max"] - if "energyScaleTotElec_Min" in data: - self.energyScaleTotElec_Min = data["energyScaleTotElec_Min"] - if "energyScaleTotElec_Max" in data: - self.energyScaleTotElec_Max = data["energyScaleTotElec_Max"] - if "energyInstantTotElecP" in data: - self.energyInstantTotElecP = data["energyInstantTotElecP"] - if "energyInstantTotElec_P_Min" in data: - self.energyInstantTotElec_P_Min = data["energyInstantTotElec_P_Min"] - if "energyInstantTotElec_P_Max" in data: - self.energyInstantTotElec_P_Max = data["energyInstantTotElec_P_Max"] - if "energyScaleTotElec_P_Min" in data: - self.energyScaleTotElec_P_Min = data["energyScaleTotElec_P_Min"] - if "energyScaleTotElec_P_Max" in data: - self.energyScaleTotElec_P_Max = data["energyScaleTotElec_P_Max"] - if "energyInstantTi1P" in data: - self.energyInstantTi1P = data["energyInstantTi1P"] - if "energyInstantTi1P_Min" in data: - self.energyInstantTi1P_Min = data["energyInstantTi1P_Min"] - if "energyInstantTi1P_Max" in data: - self.energyInstantTi1P_Max = data["energyInstantTi1P_Max"] - if "energyScaleTi1P_Min" in data: - self.energyScaleTi1P_Min = data["energyScaleTi1P_Min"] - if "energyScaleTi1P_Max" in data: - self.energyScaleTi1P_Max = data["energyScaleTi1P_Max"] - if "energyInstantTi1I" in data: - self.energyInstantTi1I = data["energyInstantTi1I"] - if "energyInstantTi1I_Min" in data: - self.energyInstantTi1I_Min = data["energyInstantTi1I_Min"] - if "energyInstantTi1I_Max" in data: - self.energyInstantTi1I_Max = data["energyInstantTi1I_Max"] - if "energyScaleTi1I_Min" in data: - self.energyScaleTi1I_Min = data["energyScaleTi1I_Min"] - if "energyScaleTi1I_Max" in data: - self.energyScaleTi1I_Max = data["energyScaleTi1I_Max"] - if "energyTotIndexWatt" in data: - self.energyTotIndexWatt = data["energyTotIndexWatt"] - if "energyIndexHeatWatt" in data: - self.energyIndexHeatWatt = data["energyIndexHeatWatt"] - if "energyIndexECSWatt" in data: - self.energyIndexECSWatt = data["energyIndexECSWatt"] - if "energyIndexHeatGas" in data: - self.energyIndexHeatGas = data["energyIndexHeatGas"] - if "outTemperature" in data: - self.outTemperature = data["outTemperature"] - super().__init__(uid, name, device_type, endpoint) + super().__init__(uid, name, device_type, endpoint, data) - async def update_device(self, device): - """Update the device values from another device""" - logger.debug("Update device %s", device.uid) - if device.energyInstantTotElec is not None: - self.energyInstantTotElec = device.energyInstantTotElec - if device.energyInstantTotElec_Min is not None: - self.energyInstantTotElec_Min = device.energyInstantTotElec_Min - if device.energyInstantTotElec_Max is not None: - self.energyInstantTotElec_Max = device.energyInstantTotElec_Max - if device.energyScaleTotElec_Min is not None: - self.energyScaleTotElec_Min = device.energyScaleTotElec_Min - if device.energyScaleTotElec_Max is not None: - self.energyScaleTotElec_Max = device.energyScaleTotElec_Max - if device.energyInstantTotElecP is not None: - self.energyInstantTotElecP = device.energyInstantTotElecP - if device.energyInstantTotElec_P_Min is not None: - self.energyInstantTotElec_P_Min = device.energyInstantTotElec_P_Min - if device.energyInstantTotElec_P_Max is not None: - self.energyInstantTotElec_P_Max = device.energyInstantTotElec_P_Max - if device.energyScaleTotElec_P_Min is not None: - self.energyScaleTotElec_P_Min = device.energyScaleTotElec_P_Min - if device.energyScaleTotElec_P_Max is not None: - self.energyScaleTotElec_P_Max = device.energyScaleTotElec_P_Max - if device.energyInstantTi1P is not None: - self.energyInstantTi1P = device.energyInstantTi1P - if device.energyInstantTi1P_Min is not None: - self.energyInstantTi1P_Min = device.energyInstantTi1P_Min - if device.energyInstantTi1P_Max is not None: - self.energyInstantTi1P_Max = device.energyInstantTi1P_Max - if device.energyScaleTi1P_Min is not None: - self.energyScaleTi1P_Min = device.energyScaleTi1P_Min - if device.energyScaleTi1P_Max is not None: - self.energyScaleTi1P_Max = device.energyScaleTi1P_Max - if device.energyInstantTi1I is not None: - self.energyInstantTi1I = device.energyInstantTi1I - if device.energyInstantTi1I_Min is not None: - self.energyInstantTi1I_Min = device.energyInstantTi1I_Min - if device.energyInstantTi1I_Max is not None: - self.energyInstantTi1I_Max = device.energyInstantTi1I_Max - if device.energyScaleTi1I_Min is not None: - self.energyScaleTi1I_Min = device.energyScaleTi1I_Min - if device.energyScaleTi1I_Max is not None: - self.energyScaleTi1I_Max = device.energyScaleTi1I_Max - if device.energyTotIndexWatt is not None: - self.energyTotIndexWatt = device.energyTotIndexWatt - if device.energyIndexHeatWatt is not None: - self.energyIndexHeatWatt = device.energyIndexHeatWatt - if device.energyIndexECSWatt is not None: - self.energyIndexECSWatt = device.energyIndexECSWatt - if device.energyIndexHeatGas is not None: - self.energyIndexHeatGas = device.energyIndexHeatGas - if device.outTemperature is not None: - self.outTemperature = device.outTemperature - await self.publish_updates() - class TydomSmoke(TydomDevice): """Represents an smoke detector sensor""" def __init__(self, uid, name, device_type, endpoint, data): logger.info("TydomSmoke : data %s", data) - if "config" in data: - self.config = data["config"] - if "battDefect" in data: - self.batt_defect = data["battDefect"] - if "supervisionMode" in data: - self.supervisionMode = data["supervisionMode"] - if "techSmokeDefect" in data: - self.techSmokeDefect = data["techSmokeDefect"] - super().__init__(uid, name, device_type, endpoint) + super().__init__(uid, name, device_type, endpoint, data) - async def update_device(self, device): - """Update the device values from another device""" - logger.debug("Update device %s", device.uid) - if device.config is not None: - self.config = device.config - if device.batt_defect is not None: - self.batt_defect = device.batt_defect - if device.supervisionMode is not None: - self.supervisionMode = device.supervisionMode - if device.techSmokeDefect is not None: - self.techSmokeDefect = device.techSmokeDefect - await self.publish_updates() \ No newline at end of file +class TydomBoiler(TydomDevice): + """represents a boiler""" + + def __init__(self, uid, name, device_type, endpoint, data): + logger.info("TydomBoiler : data %s", data) + # {'authorization': 'HEATING', 'setpoint': 19.0, 'thermicLevel': None, 'hvacMode': 'NORMAL', 'timeDelay': 0, 'temperature': 21.35, 'tempoOn': False, 'antifrostOn': False, 'loadSheddingOn': False, 'openingDetected': False, 'presenceDetected': False, 'absence': False, 'productionDefect': False, 'batteryCmdDefect': False, 'tempSensorDefect': False, 'tempSensorShortCut': False, 'tempSensorOpenCirc': False, 'boostOn': False, 'anticipCoeff': 30} + + super().__init__(uid, name, device_type, endpoint, data) \ No newline at end of file diff --git a/custom_components/deltadore-tydom/update.py b/custom_components/deltadore-tydom/update.py index 01d45c2..079a1b2 100644 --- a/custom_components/deltadore-tydom/update.py +++ b/custom_components/deltadore-tydom/update.py @@ -35,7 +35,7 @@ def __init__( ) -> None: """Init Tydom connectivity class.""" self._attr_name = f"{device_friendly_name} Tydom" - # self._attr_unique_id = f"{hub.hub_id()}-update" + self._attr_unique_id = f"{hub.hub_id()}-update" self._hub = hub async def async_added_to_hass(self) -> None: From a43a3fff75a6ef69cfb31a5f64af6b3f1a6404b7 Mon Sep 17 00:00:00 2001 From: ucpy7374 Date: Mon, 15 May 2023 17:17:06 +0200 Subject: [PATCH 26/74] fix --- .../deltadore-tydom/ha_entities.py | 37 +++++++++++++++++-- custom_components/deltadore-tydom/hub.py | 2 +- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index eb04fd2..8e12049 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -39,11 +39,14 @@ from .const import DOMAIN, LOGGER -class GenericSensor(SensorBase): +class GenericSensor(Entity): """Representation of a generic sensor """ - def __init__(self, device: TydomEnergy, device_class: SensorDeviceClass, name: str, attribute: str): + + should_poll = False + + def __init__(self, device: TydomDevice, device_class: SensorDeviceClass, name: str, attribute: str): """Initialize the sensor.""" - super().__init__(device) + self._device = device self._attr_unique_id = f"{self._device.device_id}_{name}" self._attr_name = f"{self._device.device_name} {name}" self._attribute = attribute @@ -54,6 +57,34 @@ def state(self): """Return the state of the sensor.""" return getattr(self._device, self._attribute) + # To link this entity to the cover device, this property must return an + # identifiers value matching that used in the cover, but no other information such + # as name. If name is returned, this entity will then also become a device in the + # HA UI. + @property + def device_info(self): + """Return information to link this entity with the correct device.""" + return {"identifiers": {(DOMAIN, self._device.device_id)}} + + # This property is important to let HA know if this entity is online or not. + # If an entity is offline (return False), the UI will refelect this. + @property + def available(self) -> bool: + """Return True if roller and hub is available.""" + # FIXME + #return self._device.online and self._device.hub.online + return True + + async def async_added_to_hass(self): + """Run when this Entity has been added to HA.""" + # Sensors should also register callbacks to HA when their state changes + self._device.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._device.remove_callback(self.async_write_ha_state) + class BinarySensorBase(BinarySensorEntity): """Base representation of a Sensor.""" diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 3b56a86..4745a6c 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -103,7 +103,7 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: if devices is not None: for device in devices: logger.info("*** device %s", device) - if isinstance(device, HATydom): + if isinstance(device, TydomBaseEntity): await self.device_info.update_device(device) else: logger.error("*** publish_updates for device : %s", device) From 79ff66edf68a3e237ae873e67a385ce93ac9d223 Mon Sep 17 00:00:00 2001 From: ucpy7374 Date: Tue, 16 May 2023 15:02:44 +0200 Subject: [PATCH 27/74] more functionnalities --- custom_components/deltadore-tydom/__init__.py | 2 +- .../deltadore-tydom/ha_entities.py | 267 +++++++++++++++++- custom_components/deltadore-tydom/hub.py | 60 +++- custom_components/deltadore-tydom/lock.py | 26 ++ .../deltadore-tydom/tydom/MessageHandler.py | 12 + .../deltadore-tydom/tydom/const.py | 6 + .../deltadore-tydom/tydom/tydom_client.py | 15 +- .../deltadore-tydom/tydom/tydom_devices.py | 38 ++- custom_components/deltadore-tydom/update.py | 2 +- 9 files changed, 411 insertions(+), 17 deletions(-) create mode 100644 custom_components/deltadore-tydom/lock.py create mode 100644 custom_components/deltadore-tydom/tydom/const.py diff --git a/custom_components/deltadore-tydom/__init__.py b/custom_components/deltadore-tydom/__init__.py index 8281ec6..9e76ae9 100644 --- a/custom_components/deltadore-tydom/__init__.py +++ b/custom_components/deltadore-tydom/__init__.py @@ -11,7 +11,7 @@ # List of platforms to support. There should be a matching .py file for each, # eg and -PLATFORMS: list[str] = [Platform.CLIMATE, Platform.COVER, Platform.SENSOR, Platform.UPDATE] +PLATFORMS: list[str] = [Platform.CLIMATE, Platform.COVER, Platform.SENSOR, Platform.LOCK, Platform.LIGHT, Platform.UPDATE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 8e12049..7b2c9c9 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -32,7 +32,8 @@ CoverDeviceClass ) from homeassistant.components.sensor import SensorDeviceClass - +from homeassistant.components.light import LightEntity +from homeassistant.components.lock import LockEntity from .tydom.tydom_devices import * @@ -614,3 +615,267 @@ def get_sensors(self): self._registered_sensors.append(attribute) return sensors + +class HaWindow(CoverEntity): + """Representation of a Cover.""" + + should_poll = False + supported_features = None + device_class = CoverDeviceClass.WINDOW + + sensor_classes = { + "battDefect": BinarySensorDeviceClass.PROBLEM, + "intrusionDetect": BinarySensorDeviceClass.PROBLEM, + + } + + def __init__(self, device: TydomWindow) -> None: + """Initialize the sensor.""" + self._device = device + self._attr_unique_id = f"{self._device.device_id}_cover" + self._attr_name = self._device.device_name + self._registered_sensors = [] + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + + self._device.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + self._device.remove_callback(self.async_write_ha_state) + + + @property + def device_info(self) -> DeviceInfo: + """Information about this entity/device.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self.name, + } + + @property + def is_closed(self) -> bool: + """Return if the window is closed""" + return self._device.openState == "LOCKED" + + def get_sensors(self): + """Get available sensors for this entity""" + sensors = [] + + for attribute, value in self._device.__dict__.items(): + if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + sensor_class = None + if attribute in self.sensor_classes: + sensor_class = self.sensor_classes[attribute] + if isinstance(value, bool): + sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + else: + sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + self._registered_sensors.append(attribute) + + return sensors + +class HaDoor(LockEntity, CoverEntity): + """Representation of a Cover.""" + + should_poll = False + supported_features = None + device_class = CoverDeviceClass.DOOR + sensor_classes = { + "battDefect": BinarySensorDeviceClass.PROBLEM, + "calibrationDefect": BinarySensorDeviceClass.PROBLEM, + "intrusionDetect": BinarySensorDeviceClass.PROBLEM, + } + + def __init__(self, device: TydomDoor) -> None: + """Initialize the sensor.""" + self._device = device + self._attr_unique_id = f"{self._device.device_id}_cover" + self._attr_name = self._device.device_name + self._registered_sensors = [] + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + + self._device.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + self._device.remove_callback(self.async_write_ha_state) + + @property + def device_info(self) -> DeviceInfo: + """Information about this entity/device.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self.name, + } + + @property + def is_closed(self) -> bool: + """Return if the door is closed""" + return self._device.openState == "LOCKED" + + def get_sensors(self): + """Get available sensors for this entity""" + sensors = [] + + for attribute, value in self._device.__dict__.items(): + if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + sensor_class = None + if attribute in self.sensor_classes: + sensor_class = self.sensor_classes[attribute] + if isinstance(value, bool): + sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + else: + sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + self._registered_sensors.append(attribute) + + return sensors + +class HaGate(CoverEntity): + """Representation of a Cover.""" + + should_poll = False + supported_features = None + device_class = CoverDeviceClass.GATE + sensor_classes = {} + + def __init__(self, device: TydomGate) -> None: + """Initialize the sensor.""" + self._device = device + self._attr_unique_id = f"{self._device.device_id}_cover" + self._attr_name = self._device.device_name + self._registered_sensors = [] + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + + self._device.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + self._device.remove_callback(self.async_write_ha_state) + + @property + def device_info(self) -> DeviceInfo: + """Information about this entity/device.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self.name, + } + + def get_sensors(self): + """Get available sensors for this entity""" + sensors = [] + + for attribute, value in self._device.__dict__.items(): + if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + sensor_class = None + if attribute in self.sensor_classes: + sensor_class = self.sensor_classes[attribute] + if isinstance(value, bool): + sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + else: + sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + self._registered_sensors.append(attribute) + + return sensors + +class HaGarage(CoverEntity): + """Representation of a Cover.""" + + should_poll = False + supported_features = None + device_class = CoverDeviceClass.GARAGE + sensor_classes = {} + + def __init__(self, device: TydomGarage) -> None: + """Initialize the sensor.""" + self._device = device + self._attr_unique_id = f"{self._device.device_id}_cover" + self._attr_name = self._device.device_name + self._registered_sensors = [] + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + + self._device.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + self._device.remove_callback(self.async_write_ha_state) + + @property + def device_info(self) -> DeviceInfo: + """Information about this entity/device.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self.name, + } + + def get_sensors(self): + """Get available sensors for this entity""" + sensors = [] + + for attribute, value in self._device.__dict__.items(): + if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + sensor_class = None + if attribute in self.sensor_classes: + sensor_class = self.sensor_classes[attribute] + if isinstance(value, bool): + sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + else: + sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + self._registered_sensors.append(attribute) + + return sensors + +class HaLight(LightEntity): + """Representation of a Light.""" + + should_poll = False + supported_features = None + sensor_classes = {} + + def __init__(self, device: TydomLight) -> None: + """Initialize the sensor.""" + self._device = device + self._attr_unique_id = f"{self._device.device_id}_cover" + self._attr_name = self._device.device_name + self._registered_sensors = [] + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + + self._device.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + self._device.remove_callback(self.async_write_ha_state) + + @property + def device_info(self) -> DeviceInfo: + """Information about this entity/device.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self.name, + } + + def get_sensors(self): + """Get available sensors for this entity""" + sensors = [] + + for attribute, value in self._device.__dict__.items(): + if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + sensor_class = None + if attribute in self.sensor_classes: + sensor_class = self.sensor_classes[attribute] + if isinstance(value, bool): + sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + else: + sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + self._registered_sensors.append(attribute) + + return sensors diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 4745a6c..4b84ce2 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -53,6 +53,8 @@ def __init__( self.add_cover_callback = None self.add_sensor_callback = None self.add_climate_callback = None + self.add_light_callback = None + self.add_lock_callback = None self._tydom_client = TydomClient( hass=self._hass, @@ -111,13 +113,14 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: self.devices[device.device_id] = device await self.create_ha_device(device) else: + logger.warn("update device %s : %s", device.device_id, self.devices[device.device_id]) await self.update_ha_device(self.devices[device.device_id], device) async def create_ha_device(self, device): """Create a new HA device""" logger.warn("device type %s", device.device_type) - match device.device_type: - case "shutter": + match device: + case TydomShutter(): logger.warn("Create cover %s", device.device_id) ha_device = HACover(device) self.ha_devices[device.device_id] = ha_device @@ -125,7 +128,7 @@ async def create_ha_device(self, device): self.add_cover_callback([ha_device]) if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) - case "conso": + case TydomEnergy(): logger.warn("Create conso %s", device.device_id) ha_device = HAEnergy(device) self.ha_devices[device.device_id] = ha_device @@ -135,7 +138,7 @@ async def create_ha_device(self, device): if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) - case "smoke": + case TydomSmoke(): logger.warn("Create smoke %s", device.device_id) ha_device = HASmoke(device) self.ha_devices[device.device_id] = ha_device @@ -144,17 +147,62 @@ async def create_ha_device(self, device): if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) - case "boiler": + case TydomBoiler(): logger.warn("Create boiler %s", device.device_id) ha_device = HaClimate(device) self.ha_devices[device.device_id] = ha_device if self.add_climate_callback is not None: self.add_climate_callback([ha_device]) + if self.add_sensor_callback is not None: + self.add_sensor_callback(ha_device.get_sensors()) + case TydomWindow(): + logger.warn("Create window %s", device.device_id) + ha_device = HaWindow(device) + self.ha_devices[device.device_id] = ha_device + if self.add_cover_callback is not None: + self.add_cover_callback([ha_device]) + + if self.add_sensor_callback is not None: + self.add_sensor_callback(ha_device.get_sensors()) + case TydomDoor(): + logger.warn("Create door %s", device.device_id) + ha_device = HaDoor(device) + self.ha_devices[device.device_id] = ha_device + if self.add_cover_callback is not None: + self.add_cover_callback([ha_device]) + + if self.add_sensor_callback is not None: + self.add_sensor_callback(ha_device.get_sensors()) + case TydomGate(): + logger.warn("Create gate %s", device.device_id) + ha_device = HaGate(device) + self.ha_devices[device.device_id] = ha_device + if self.add_cover_callback is not None: + self.add_cover_callback([ha_device]) + + if self.add_sensor_callback is not None: + self.add_sensor_callback(ha_device.get_sensors()) + case TydomGarage(): + logger.warn("Create garage %s", device.device_id) + ha_device = HaGarage(device) + self.ha_devices[device.device_id] = ha_device + if self.add_cover_callback is not None: + self.add_cover_callback([ha_device]) + + if self.add_sensor_callback is not None: + self.add_sensor_callback(ha_device.get_sensors()) + case TydomLight(): + logger.warn("Create light %s", device.device_id) + ha_device = HaLight(device) + self.ha_devices[device.device_id] = ha_device + if self.add_light_callback is not None: + self.add_light_callback([ha_device]) + if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case _: - logger.error("unsupported device type %s for device %s", device.device_type, device.device_id) + logger.error("unsupported device type (%s) %s for device %s", type(device), device.device_type, device.device_id) return async def update_ha_device(self, stored_device, device): diff --git a/custom_components/deltadore-tydom/lock.py b/custom_components/deltadore-tydom/lock.py new file mode 100644 index 0000000..f330270 --- /dev/null +++ b/custom_components/deltadore-tydom/lock.py @@ -0,0 +1,26 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, LOGGER + + +# This function is called as part of the __init__.async_setup_entry (via the +# hass.config_entries.async_forward_entry_setup call) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add cover for passed config_entry in HA.""" + LOGGER.error("***** async_setup_entry *****") + # The hub is loaded from the associated hass.data entry that was created in the + # __init__.async_setup_entry function + hub = hass.data[DOMAIN][config_entry.entry_id] + hub.add_lock_callback = async_add_entities + diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 810eac6..c0267f7 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -375,12 +375,24 @@ async def get_device(last_usage, uid, name, endpoint = None, data = None) -> Tyd match last_usage: case "shutter" | "klineShutter": return TydomShutter(uid, name, last_usage, endpoint, data) + case "window" | "windowFrench" | "windowSliding" | "klineWindowFrench" | "klineWindowSliding": + return TydomWindow(uid, name, last_usage, endpoint, data) + case "belmDoor" | "klineDoor": + return TydomDoor(uid, name, last_usage, endpoint, data) + case "garage_door": + return TydomGarage(uid, name, last_usage, endpoint, data) + case "gate": + return TydomGate(uid, name, last_usage, endpoint, data) + case "light": + return TydomLight(uid, name, last_usage, endpoint, data) case "conso": return TydomEnergy(uid, name, last_usage, endpoint, data) case "smoke": return TydomSmoke(uid, name, last_usage, endpoint, data) case "boiler": return TydomBoiler(uid, name, last_usage, endpoint, data) + case "alarm": + return TydoAlarm(uid, name, last_usage, endpoint, data) case _: logger.warn("Unknown usage : %s", last_usage) return diff --git a/custom_components/deltadore-tydom/tydom/const.py b/custom_components/deltadore-tydom/tydom/const.py new file mode 100644 index 0000000..5646014 --- /dev/null +++ b/custom_components/deltadore-tydom/tydom/const.py @@ -0,0 +1,6 @@ +MEDIATION_URL = "mediation.tydom.com" +DELTADORE_AUTH_URL = "https://deltadoreadb2ciot.b2clogin.com/deltadoreadb2ciot.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_AccountProviderROPC_SignIn" +DELTADORE_AUTH_GRANT_TYPE = "password" +DELTADORE_AUTH_CLIENTID = "8782839f-3264-472a-ab87-4d4e23524da4" +DELTADORE_AUTH_SCOPE = "openid profile offline_access https://deltadoreadb2ciot.onmicrosoft.com/iotapi/video_config https://deltadoreadb2ciot.onmicrosoft.com/iotapi/video_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_gateway_credentials https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_camera_credentials https://deltadoreadb2ciot.onmicrosoft.com/iotapi/comptage_europe_collect_reader https://deltadoreadb2ciot.onmicrosoft.com/iotapi/comptage_europe_site_config_contributor https://deltadoreadb2ciot.onmicrosoft.com/iotapi/pilotage_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/consent_mgt_contributor https://deltadoreadb2ciot.onmicrosoft.com/iotapi/b2caccountprovider_manage_account https://deltadoreadb2ciot.onmicrosoft.com/iotapi/b2caccountprovider_allow_view_account https://deltadoreadb2ciot.onmicrosoft.com/iotapi/tydom_backend_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/websocket_remote_access https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_device https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_view https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_space https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_connector https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_endpoint https://deltadoreadb2ciot.onmicrosoft.com/iotapi/rule_management_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/collect_read_datas" +DELTADORE_API_SITES = "https://prod.iotdeltadore.com/sitesmanagement/api/v1/sites?gateway_mac=" \ No newline at end of file diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index 3c856a7..59dd056 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -14,6 +14,7 @@ from aiohttp import ClientWebSocketResponse, ClientSession from homeassistant.helpers.aiohttp_client import async_create_clientsession +from .const import * from .MessageHandler import MessageHandler from requests.auth import HTTPDigestAuth @@ -44,7 +45,7 @@ def __init__( mac: str, password: str, alarm_pin: str = None, - host: str = "mediation.tydom.com", + host: str = MEDIATION_URL, event_callback=None ) -> None: logger.debug("Initializing TydomClient Class") @@ -54,7 +55,7 @@ def __init__( self._mac = mac self._host = host self._alarm_pin = alarm_pin - self._remote_mode = self._host == "mediation.tydom.com" + self._remote_mode = self._host == MEDIATION_URL self._connection = None self.event_callback = event_callback # Some devices (like Tywatt) need polling @@ -81,7 +82,7 @@ async def async_get_credentials( async with async_timeout.timeout(10): response = await session.request( method="GET", - url="https://deltadoreadb2ciot.b2clogin.com/deltadoreadb2ciot.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_AccountProviderROPC_SignIn", + url=DELTADORE_AUTH_URL, proxy=proxy ) @@ -101,9 +102,9 @@ async def async_get_credentials( { "username": f"{email}", "password": f"{password}", - "grant_type": "password", - "client_id": "8782839f-3264-472a-ab87-4d4e23524da4", - "scope": "openid profile offline_access https://deltadoreadb2ciot.onmicrosoft.com/iotapi/video_config https://deltadoreadb2ciot.onmicrosoft.com/iotapi/video_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_gateway_credentials https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_camera_credentials https://deltadoreadb2ciot.onmicrosoft.com/iotapi/comptage_europe_collect_reader https://deltadoreadb2ciot.onmicrosoft.com/iotapi/comptage_europe_site_config_contributor https://deltadoreadb2ciot.onmicrosoft.com/iotapi/pilotage_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/consent_mgt_contributor https://deltadoreadb2ciot.onmicrosoft.com/iotapi/b2caccountprovider_manage_account https://deltadoreadb2ciot.onmicrosoft.com/iotapi/b2caccountprovider_allow_view_account https://deltadoreadb2ciot.onmicrosoft.com/iotapi/tydom_backend_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/websocket_remote_access https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_device https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_view https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_space https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_connector https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_endpoint https://deltadoreadb2ciot.onmicrosoft.com/iotapi/rule_management_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/collect_read_datas", + "grant_type": DELTADORE_AUTH_GRANT_TYPE, + "client_id": DELTADORE_AUTH_CLIENTID, + "scope": DELTADORE_AUTH_SCOPE, } ) @@ -127,7 +128,7 @@ async def async_get_credentials( response = await session.request( method="GET", - url=f"https://prod.iotdeltadore.com/sitesmanagement/api/v1/sites?gateway_mac={macaddress}", + url=DELTADORE_API_SITES + macaddress, headers={"Authorization": f"Bearer {access_token}"}, proxy=proxy ) diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index 1f9ab8e..991e216 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -140,4 +140,40 @@ def __init__(self, uid, name, device_type, endpoint, data): logger.info("TydomBoiler : data %s", data) # {'authorization': 'HEATING', 'setpoint': 19.0, 'thermicLevel': None, 'hvacMode': 'NORMAL', 'timeDelay': 0, 'temperature': 21.35, 'tempoOn': False, 'antifrostOn': False, 'loadSheddingOn': False, 'openingDetected': False, 'presenceDetected': False, 'absence': False, 'productionDefect': False, 'batteryCmdDefect': False, 'tempSensorDefect': False, 'tempSensorShortCut': False, 'tempSensorOpenCirc': False, 'boostOn': False, 'anticipCoeff': 30} - super().__init__(uid, name, device_type, endpoint, data) \ No newline at end of file + super().__init__(uid, name, device_type, endpoint, data) + +class TydomWindow(TydomDevice): + """represents a window""" + def __init__(self, uid, name, device_type, endpoint, data): + logger.info("TydomWindow : data %s", data) + super().__init__(uid, name, device_type, endpoint, data) + +class TydomDoor(TydomDevice): + """represents a door""" + def __init__(self, uid, name, device_type, endpoint, data): + logger.info("TydomDoor : data %s", data) + super().__init__(uid, name, device_type, endpoint, data) + +class TydomGate(TydomDevice): + """represents a door""" + def __init__(self, uid, name, device_type, endpoint, data): + logger.info("TydomGate : data %s", data) + super().__init__(uid, name, device_type, endpoint, data) + +class TydomGarage(TydomDevice): + """represents a door""" + def __init__(self, uid, name, device_type, endpoint, data): + logger.info("TydomGarage : data %s", data) + super().__init__(uid, name, device_type, endpoint, data) + +class TydomLight(TydomDevice): + """represents a door""" + def __init__(self, uid, name, device_type, endpoint, data): + logger.info("TydomLight : data %s", data) + super().__init__(uid, name, device_type, endpoint, data) + +class TydoAlarm(TydomDevice): + """represents an alarm""" + def __init__(self, uid, name, device_type, endpoint, data): + logger.info("TydomAlarm : data %s", data) + super().__init__(uid, name, device_type, endpoint, data) diff --git a/custom_components/deltadore-tydom/update.py b/custom_components/deltadore-tydom/update.py index 079a1b2..daee7af 100644 --- a/custom_components/deltadore-tydom/update.py +++ b/custom_components/deltadore-tydom/update.py @@ -35,7 +35,7 @@ def __init__( ) -> None: """Init Tydom connectivity class.""" self._attr_name = f"{device_friendly_name} Tydom" - self._attr_unique_id = f"{hub.hub_id()}-update" + self._attr_unique_id = f"{hub.hub_id}-update" self._hub = hub async def async_added_to_hass(self) -> None: From 14ec8ea719d7e94b5483880f8500458b571f0768 Mon Sep 17 00:00:00 2001 From: ucpy7374 Date: Wed, 7 Jun 2023 17:35:07 +0200 Subject: [PATCH 28/74] update --- custom_components/deltadore-tydom/hub.py | 1 + custom_components/deltadore-tydom/update.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 4b84ce2..4e7dcdd 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -224,6 +224,7 @@ async def ping(self) -> None: async def async_trigger_firmware_update(self) -> None: """Trigger firmware update""" + logger.info("Installing update...") class Roller: diff --git a/custom_components/deltadore-tydom/update.py b/custom_components/deltadore-tydom/update.py index daee7af..fc3fdd9 100644 --- a/custom_components/deltadore-tydom/update.py +++ b/custom_components/deltadore-tydom/update.py @@ -6,6 +6,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from datetime import date from .const import DOMAIN, LOGGER from .hub import Hub @@ -66,7 +67,8 @@ def latest_version(self) -> str | None: """Latest version available for install.""" if self._hub.device_info is not None: if self._hub.device_info.update_available: - return self._hub.device_info.main_version_sw + # return version based on today's date for update version + return date.today().strftime("%y.%m.%d") return self._hub.device_info.main_version_sw # FIXME : return correct version on update return None From fe8a54e34662b20de196533a23af052f90b40029 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:34:17 +0000 Subject: [PATCH 29/74] update --- .../deltadore-tydom/ha_entities.py | 274 ++++++++++++------ custom_components/deltadore-tydom/hub.py | 35 ++- custom_components/deltadore-tydom/light.py | 25 ++ .../deltadore-tydom/tydom/MessageHandler.py | 113 ++++++-- .../deltadore-tydom/tydom/README.md | 34 +++ .../deltadore-tydom/tydom/tydom_client.py | 46 +-- .../deltadore-tydom/tydom/tydom_devices.py | 110 ++++--- 7 files changed, 452 insertions(+), 185 deletions(-) create mode 100644 custom_components/deltadore-tydom/light.py create mode 100644 custom_components/deltadore-tydom/tydom/README.md diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 7b2c9c9..0586cae 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -29,7 +29,7 @@ SUPPORT_SET_POSITION, SUPPORT_STOP, CoverEntity, - CoverDeviceClass + CoverDeviceClass, ) from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.light import LightEntity @@ -41,11 +41,17 @@ class GenericSensor(Entity): - """Representation of a generic sensor """ + """Representation of a generic sensor""" should_poll = False - def __init__(self, device: TydomDevice, device_class: SensorDeviceClass, name: str, attribute: str): + def __init__( + self, + device: TydomDevice, + device_class: SensorDeviceClass, + name: str, + attribute: str, + ): """Initialize the sensor.""" self._device = device self._attr_unique_id = f"{self._device.device_id}_{name}" @@ -73,7 +79,7 @@ def device_info(self): def available(self) -> bool: """Return True if roller and hub is available.""" # FIXME - #return self._device.online and self._device.hub.online + # return self._device.online and self._device.hub.online return True async def async_added_to_hass(self): @@ -86,6 +92,7 @@ async def async_will_remove_from_hass(self): # The opposite of async_added_to_hass. Remove any registered call backs here. self._device.remove_callback(self.async_write_ha_state) + class BinarySensorBase(BinarySensorEntity): """Base representation of a Sensor.""" @@ -109,7 +116,7 @@ def device_info(self): @property def available(self) -> bool: """Return True if roller and hub is available.""" - #return self._roller.online and self._roller.hub.online + # return self._roller.online and self._roller.hub.online # FIXME return True @@ -123,9 +130,17 @@ async def async_will_remove_from_hass(self): # The opposite of async_added_to_hass. Remove any registered call backs here. self._device.remove_callback(self.async_write_ha_state) + class GenericBinarySensor(BinarySensorBase): """Generic representation of a Binary Sensor.""" - def __init__(self, device: TydomDevice, device_class: BinarySensorDeviceClass, name: str, attribute: str): + + def __init__( + self, + device: TydomDevice, + device_class: BinarySensorDeviceClass, + name: str, + attribute: str, + ): """Initialize the sensor.""" super().__init__(device) self._attr_unique_id = f"{self._device.device_id}_{name}" @@ -139,6 +154,7 @@ def is_on(self): """Return the state of the sensor.""" return getattr(self._device, self._attribute) + class HATydom(Entity): """Representation of a Tydom.""" @@ -163,7 +179,7 @@ def device_info(self): def available(self) -> bool: """Return True if roller and hub is available.""" # FIXME - #return self._device.online and self._device.hub.online + # return self._device.online and self._device.hub.online return True async def async_added_to_hass(self): @@ -176,6 +192,7 @@ async def async_will_remove_from_hass(self): # The opposite of async_added_to_hass. Remove any registered call backs here. self._device.remove_callback(self.async_write_ha_state) + class HAEnergy(Entity): """Representation of an energy sensor""" @@ -237,21 +254,34 @@ async def async_will_remove_from_hass(self) -> None: @property def device_info(self): """Return information to link this entity with the correct device.""" - return {"identifiers": {(DOMAIN, self._energy.device_id)}, "name": self._energy.device_name} + return { + "identifiers": {(DOMAIN, self._energy.device_id)}, + "name": self._energy.device_name, + } def get_sensors(self): """Get available sensors for this entity""" sensors = [] for attribute, value in self._energy.__dict__.items(): - if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + if ( + attribute[:1] != "_" + and value is not None + and attribute not in self._registered_sensors + ): sensor_class = None if attribute in self.sensor_classes: sensor_class = self.sensor_classes[attribute] if isinstance(value, bool): - sensors.append(GenericBinarySensor(self._energy, sensor_class, attribute, attribute)) - else : - sensors.append(GenericSensor(self._energy, sensor_class, attribute, attribute)) + sensors.append( + GenericBinarySensor( + self._energy, sensor_class, attribute, attribute + ) + ) + else: + sensors.append( + GenericSensor(self._energy, sensor_class, attribute, attribute) + ) self._registered_sensors.append(attribute) return sensors @@ -269,17 +299,19 @@ class HACover(CoverEntity): # imported above, we can tell HA the features that are supported by this entity. # If the supported features were dynamic (ie: different depending on the external # device it connected to), then this should be function with an @property decorator. - supported_features = SUPPORT_SET_POSITION | SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + supported_features = ( + SUPPORT_SET_POSITION | SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + ) device_class = CoverDeviceClass.SHUTTER sensor_classes = { - "batt_defect": BinarySensorDeviceClass.PROBLEM, - "thermic_defect": BinarySensorDeviceClass.PROBLEM, - "up_defect": BinarySensorDeviceClass.PROBLEM, - "down_defect": BinarySensorDeviceClass.PROBLEM, - "obstacle_defect": BinarySensorDeviceClass.PROBLEM, - "intrusion": BinarySensorDeviceClass.PROBLEM, - } + "batt_defect": BinarySensorDeviceClass.PROBLEM, + "thermic_defect": BinarySensorDeviceClass.PROBLEM, + "up_defect": BinarySensorDeviceClass.PROBLEM, + "down_defect": BinarySensorDeviceClass.PROBLEM, + "obstacle_defect": BinarySensorDeviceClass.PROBLEM, + "intrusion": BinarySensorDeviceClass.PROBLEM, + } def __init__(self, shutter: TydomShutter) -> None: """Initialize the sensor.""" @@ -340,9 +372,9 @@ def device_info(self) -> DeviceInfo: "identifiers": {(DOMAIN, self._shutter.device_id)}, # If desired, the name for the device could be different to the entity "name": self.name, - #"sw_version": self._shutter.firmware_version, - #"model": self._shutter.model, - #"manufacturer": self._shutter.hub.manufacturer, + # "sw_version": self._shutter.firmware_version, + # "model": self._shutter.model, + # "manufacturer": self._shutter.hub.manufacturer, } # This property is important to let HA know if this entity is online or not. @@ -372,42 +404,32 @@ def is_closed(self) -> bool: """Return if the cover is closed, same as position 0.""" return self._shutter.position == 0 - #@property - #def is_closing(self) -> bool: + # @property + # def is_closing(self) -> bool: # """Return if the cover is closing or not.""" # return self._shutter.moving < 0 - #@property - #def is_opening(self) -> bool: + # @property + # def is_opening(self) -> bool: # """Return if the cover is opening or not.""" # return self._shutter.moving > 0 - - - #self.on_fav_pos = None - #self.up_defect = None - #self.down_defect = None - #self.obstacle_defect = None - #self.intrusion = None - #self.batt_defect = None - - @property - def is_thermic_defect(self) -> bool: - """Return the thermic_defect status""" - return self._shutter.thermic_defect - # These methods allow HA to tell the actual device what to do. In this case, move # the cover to the desired position, or open and close it all the way. async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - await self._shutter.set_position(100) + await self._shutter.up() async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - await self._shutter.set_position(0) + await self._shutter.down() + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + await self._shutter.stop() async def async_set_cover_position(self, **kwargs: Any) -> None: - """Close the cover.""" + """Set the cover's position.""" await self._shutter.set_position(kwargs[ATTR_POSITION]) def get_sensors(self) -> list: @@ -415,31 +437,39 @@ def get_sensors(self) -> list: sensors = [] for attribute, value in self._shutter.__dict__.items(): - if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + if ( + attribute[:1] != "_" + and value is not None + and attribute not in self._registered_sensors + ): sensor_class = None if attribute in self.sensor_classes: sensor_class = self.sensor_classes[attribute] if isinstance(value, bool): - sensors.append(GenericBinarySensor(self._shutter, sensor_class, attribute, attribute)) + sensors.append( + GenericBinarySensor( + self._shutter, sensor_class, attribute, attribute + ) + ) else: - sensors.append(GenericSensor(self._shutter, sensor_class, attribute, attribute)) + sensors.append( + GenericSensor(self._shutter, sensor_class, attribute, attribute) + ) self._registered_sensors.append(attribute) return sensors - class HASmoke(BinarySensorEntity): """Representation of an smoke sensor""" + should_poll = False device_class = None supported_features = None device_class = BinarySensorDeviceClass.PROBLEM - sensor_classes = { - "batt_defect" : BinarySensorDeviceClass.PROBLEM - } + sensor_classes = {"batt_defect": BinarySensorDeviceClass.PROBLEM} def __init__(self, smoke: TydomSmoke) -> None: self._device = smoke @@ -458,7 +488,8 @@ def device_info(self): """Return information to link this entity with the correct device.""" return { "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.device_name} + "name": self._device.device_name, + } async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" @@ -480,18 +511,29 @@ def get_sensors(self): sensors = [] for attribute, value in self._device.__dict__.items(): - if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + if ( + attribute[:1] != "_" + and value is not None + and attribute not in self._registered_sensors + ): sensor_class = None if attribute in self.sensor_classes: sensor_class = self.sensor_classes[attribute] if isinstance(value, bool): - sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericBinarySensor( + self._device, sensor_class, attribute, attribute + ) + ) else: - sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericSensor(self._device, sensor_class, attribute, attribute) + ) self._registered_sensors.append(attribute) return sensors + class HaClimate(ClimateEntity): """A climate entity.""" @@ -504,7 +546,7 @@ class HaClimate(ClimateEntity): "TempSensorShortCut": BinarySensorDeviceClass.PROBLEM, "ProductionDefect": BinarySensorDeviceClass.PROBLEM, "BatteryCmdDefect": BinarySensorDeviceClass.PROBLEM, - } + } DICT_HA_TO_DD = { HVACMode.AUTO: "todo", HVACMode.COOL: "todo", @@ -523,7 +565,10 @@ def __init__(self, device: TydomBoiler) -> None: self._device = device self._attr_unique_id = f"{self._device.device_id}_climate" self._attr_name = self._device.device_name - self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] # , HVACMode.AUTO, HVACMode.COOL, + self._attr_hvac_modes = [ + HVACMode.OFF, + HVACMode.HEAT, + ] # , HVACMode.AUTO, HVACMode.COOL, self._registered_sensors = [] @property @@ -532,15 +577,15 @@ def supported_features(self) -> ClimateEntityFeature: features = ClimateEntityFeature(0) features = features | ClimateEntityFeature.TARGET_TEMPERATURE - #set_req = self.gateway.const.SetReq - #if set_req.V_HVAC_SPEED in self._values: + # set_req = self.gateway.const.SetReq + # if set_req.V_HVAC_SPEED in self._values: # features = features | ClimateEntityFeature.FAN_MODE - #if ( + # if ( # set_req.V_HVAC_SETPOINT_COOL in self._values # and set_req.V_HVAC_SETPOINT_HEAT in self._values - #): + # ): # features = features | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - #else: + # else: # features = features | ClimateEntityFeature.TARGET_TEMPERATURE return features @@ -561,7 +606,7 @@ def temperature_unit(self) -> str: def hvac_mode(self) -> HVACMode: """Return the current operation (e.g. heat, cool, idle).""" # FIXME - #return self._device.hvacMode + # return self._device.hvacMode return HVACMode.HEAT @property @@ -604,18 +649,29 @@ def get_sensors(self): sensors = [] for attribute, value in self._device.__dict__.items(): - if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + if ( + attribute[:1] != "_" + and value is not None + and attribute not in self._registered_sensors + ): sensor_class = None if attribute in self.sensor_classes: sensor_class = self.sensor_classes[attribute] if isinstance(value, bool): - sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericBinarySensor( + self._device, sensor_class, attribute, attribute + ) + ) else: - sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericSensor(self._device, sensor_class, attribute, attribute) + ) self._registered_sensors.append(attribute) return sensors + class HaWindow(CoverEntity): """Representation of a Cover.""" @@ -626,7 +682,6 @@ class HaWindow(CoverEntity): sensor_classes = { "battDefect": BinarySensorDeviceClass.PROBLEM, "intrusionDetect": BinarySensorDeviceClass.PROBLEM, - } def __init__(self, device: TydomWindow) -> None: @@ -645,7 +700,6 @@ async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" self._device.remove_callback(self.async_write_ha_state) - @property def device_info(self) -> DeviceInfo: """Information about this entity/device.""" @@ -664,18 +718,29 @@ def get_sensors(self): sensors = [] for attribute, value in self._device.__dict__.items(): - if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + if ( + attribute[:1] != "_" + and value is not None + and attribute not in self._registered_sensors + ): sensor_class = None if attribute in self.sensor_classes: sensor_class = self.sensor_classes[attribute] if isinstance(value, bool): - sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericBinarySensor( + self._device, sensor_class, attribute, attribute + ) + ) else: - sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericSensor(self._device, sensor_class, attribute, attribute) + ) self._registered_sensors.append(attribute) return sensors + class HaDoor(LockEntity, CoverEntity): """Representation of a Cover.""" @@ -722,18 +787,29 @@ def get_sensors(self): sensors = [] for attribute, value in self._device.__dict__.items(): - if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + if ( + attribute[:1] != "_" + and value is not None + and attribute not in self._registered_sensors + ): sensor_class = None if attribute in self.sensor_classes: sensor_class = self.sensor_classes[attribute] if isinstance(value, bool): - sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericBinarySensor( + self._device, sensor_class, attribute, attribute + ) + ) else: - sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericSensor(self._device, sensor_class, attribute, attribute) + ) self._registered_sensors.append(attribute) return sensors + class HaGate(CoverEntity): """Representation of a Cover.""" @@ -771,18 +847,29 @@ def get_sensors(self): sensors = [] for attribute, value in self._device.__dict__.items(): - if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + if ( + attribute[:1] != "_" + and value is not None + and attribute not in self._registered_sensors + ): sensor_class = None if attribute in self.sensor_classes: sensor_class = self.sensor_classes[attribute] if isinstance(value, bool): - sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericBinarySensor( + self._device, sensor_class, attribute, attribute + ) + ) else: - sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericSensor(self._device, sensor_class, attribute, attribute) + ) self._registered_sensors.append(attribute) return sensors + class HaGarage(CoverEntity): """Representation of a Cover.""" @@ -820,18 +907,29 @@ def get_sensors(self): sensors = [] for attribute, value in self._device.__dict__.items(): - if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + if ( + attribute[:1] != "_" + and value is not None + and attribute not in self._registered_sensors + ): sensor_class = None if attribute in self.sensor_classes: sensor_class = self.sensor_classes[attribute] if isinstance(value, bool): - sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericBinarySensor( + self._device, sensor_class, attribute, attribute + ) + ) else: - sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericSensor(self._device, sensor_class, attribute, attribute) + ) self._registered_sensors.append(attribute) return sensors + class HaLight(LightEntity): """Representation of a Light.""" @@ -868,14 +966,24 @@ def get_sensors(self): sensors = [] for attribute, value in self._device.__dict__.items(): - if attribute[:1] != '_' and value is not None and attribute not in self._registered_sensors: + if ( + attribute[:1] != "_" + and value is not None + and attribute not in self._registered_sensors + ): sensor_class = None if attribute in self.sensor_classes: sensor_class = self.sensor_classes[attribute] if isinstance(value, bool): - sensors.append(GenericBinarySensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericBinarySensor( + self._device, sensor_class, attribute, attribute + ) + ) else: - sensors.append(GenericSensor(self._device, sensor_class, attribute, attribute)) + sensors.append( + GenericSensor(self._device, sensor_class, attribute, attribute) + ) self._registered_sensors.append(attribute) return sensors diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 4e7dcdd..ebe399b 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -47,7 +47,9 @@ def __init__( self._hass = hass self._name = mac self._id = "Tydom-" + mac - self.device_info = TydomBaseEntity(None, None, None, None, None, None, None, None, None, None, None, False) + self.device_info = TydomBaseEntity( + None, None, None, None, None, None, None, None, None, None, None, False + ) self.devices = {} self.ha_devices = {} self.add_cover_callback = None @@ -55,6 +57,7 @@ def __init__( self.add_climate_callback = None self.add_light_callback = None self.add_lock_callback = None + self.add_light_callback = None self._tydom_client = TydomClient( hass=self._hass, @@ -62,13 +65,13 @@ def __init__( host=self._host, password=self._pass, alarm_pin=self._pin, - event_callback=self.handle_event + event_callback=self.handle_event, ) self.rollers = [ - # Roller(f"{self._id}_1", f"{self._name} 1", self), - # Roller(f"{self._id}_2", f"{self._name} 2", self), - # Roller(f"{self._id}_3", f"{self._name} 3", self), + # Roller(f"{self._id}_1", f"{self._name} 1", self), + # Roller(f"{self._id}_2", f"{self._name} 2", self), + # Roller(f"{self._id}_3", f"{self._name} 3", self), ] self.online = True @@ -108,13 +111,19 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: if isinstance(device, TydomBaseEntity): await self.device_info.update_device(device) else: - logger.error("*** publish_updates for device : %s", device) + logger.warn("*** publish_updates for device : %s", device) if device.device_id not in self.devices: self.devices[device.device_id] = device await self.create_ha_device(device) else: - logger.warn("update device %s : %s", device.device_id, self.devices[device.device_id]) - await self.update_ha_device(self.devices[device.device_id], device) + logger.warn( + "update device %s : %s", + device.device_id, + self.devices[device.device_id], + ) + await self.update_ha_device( + self.devices[device.device_id], device + ) async def create_ha_device(self, device): """Create a new HA device""" @@ -202,7 +211,12 @@ async def create_ha_device(self, device): if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case _: - logger.error("unsupported device type (%s) %s for device %s", type(device), device.device_type, device.device_id) + logger.error( + "unsupported device type (%s) %s for device %s", + type(device), + device.device_type, + device.device_id, + ) return async def update_ha_device(self, stored_device, device): @@ -214,7 +228,6 @@ async def update_ha_device(self, stored_device, device): # add new sensors self.add_sensor_callback(new_sensors) - async def ping(self) -> None: """Periodically send pings""" logger.info("Sending ping") @@ -263,7 +276,7 @@ async def set_position(self, position: int) -> None: Set dummy cover to the given position. State is announced a random number of seconds later. """ - logger.error("set roller position") + logger.error("set roller position (hub)") self._target_position = position # Update the moving status, and broadcast the update diff --git a/custom_components/deltadore-tydom/light.py b/custom_components/deltadore-tydom/light.py new file mode 100644 index 0000000..9c9a1a9 --- /dev/null +++ b/custom_components/deltadore-tydom/light.py @@ -0,0 +1,25 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, LOGGER + + +# This function is called as part of the __init__.async_setup_entry (via the +# hass.config_entries.async_forward_entry_setup call) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add cover for passed config_entry in HA.""" + LOGGER.error("***** async_setup_entry *****") + # The hub is loaded from the associated hass.data entry that was created in the + # __init__.async_setup_entry function + hub = hass.data[DOMAIN][config_entry.entry_id] + hub.add_light_callback = async_add_entities diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index c0267f7..dd503a3 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -213,6 +213,7 @@ device_endpoint = dict() device_type = dict() + class MessageHandler: """Handle incomming Tydom messages""" @@ -354,45 +355,94 @@ async def parse_msg_info(self, parsed): boot_reference = parsed["bootReference"] boot_version = parsed["bootVersion"] update_available = parsed["updateAvailable"] - return [TydomBaseEntity(product_name, main_version_sw, main_version_hw, main_id, main_reference, key_version_sw, key_version_hw, key_version_stack, key_reference, boot_reference, boot_version, update_available)] + return [ + TydomBaseEntity( + product_name, + main_version_sw, + main_version_hw, + main_id, + main_reference, + key_version_sw, + key_version_hw, + key_version_stack, + key_reference, + boot_reference, + boot_version, + update_available, + ) + ] @staticmethod - async def get_device(last_usage, uid, name, endpoint = None, data = None) -> TydomDevice: + async def get_device( + tydom_client, last_usage, uid, device_id, name, endpoint=None, data=None + ) -> TydomDevice: """Get device class from its last usage""" - #FIXME voir: class CoverDeviceClass(StrEnum): - # Refer to the cover dev docs for device class descriptions - #AWNING = "awning" - #BLIND = "blind" - #CURTAIN = "curtain" - #DAMPER = "damper" - #DOOR = "door" - #GARAGE = "garage" - #GATE = "gate" - #SHADE = "shade" - #SHUTTER = "shutter" - #WINDOW = "window" + # FIXME voir: class CoverDeviceClass(StrEnum): + # Refer to the cover dev docs for device class descriptions + # AWNING = "awning" + # BLIND = "blind" + # CURTAIN = "curtain" + # DAMPER = "damper" + # DOOR = "door" + # GARAGE = "garage" + # GATE = "gate" + # SHADE = "shade" + # SHUTTER = "shutter" + # WINDOW = "window" match last_usage: case "shutter" | "klineShutter": - return TydomShutter(uid, name, last_usage, endpoint, data) + return TydomShutter( + tydom_client, uid, device_id, name, last_usage, endpoint, data + ) case "window" | "windowFrench" | "windowSliding" | "klineWindowFrench" | "klineWindowSliding": - return TydomWindow(uid, name, last_usage, endpoint, data) + return TydomWindow( + tydom_client, uid, device_id, name, last_usage, endpoint, data + ) case "belmDoor" | "klineDoor": - return TydomDoor(uid, name, last_usage, endpoint, data) + return TydomDoor( + tydom_client, uid, device_id, name, last_usage, endpoint, data + ) case "garage_door": - return TydomGarage(uid, name, last_usage, endpoint, data) + return TydomGarage( + tydom_client, uid, device_id, name, last_usage, endpoint, data + ) case "gate": - return TydomGate(uid, name, last_usage, endpoint, data) + return TydomGate( + tydom_client, + uid, + device_id, + name, + last_usage, + endpoint, + data, + ) case "light": - return TydomLight(uid, name, last_usage, endpoint, data) + return TydomLight( + tydom_client, + uid, + device_id, + name, + last_usage, + endpoint, + data, + ) case "conso": - return TydomEnergy(uid, name, last_usage, endpoint, data) + return TydomEnergy( + tydom_client, uid, device_id, name, last_usage, endpoint, data + ) case "smoke": - return TydomSmoke(uid, name, last_usage, endpoint, data) + return TydomSmoke( + tydom_client, uid, device_id, name, last_usage, endpoint, data + ) case "boiler": - return TydomBoiler(uid, name, last_usage, endpoint, data) + return TydomBoiler( + tydom_client, uid, device_id, name, last_usage, endpoint, data + ) case "alarm": - return TydoAlarm(uid, name, last_usage, endpoint, data) + return TydomAlarm( + tydom_client, uid, device_id, name, last_usage, endpoint, data + ) case _: logger.warn("Unknown usage : %s", last_usage) return @@ -404,8 +454,8 @@ async def parse_config_data(parsed): for i in parsed["endpoints"]: device_unique_id = str(i["id_endpoint"]) + "_" + str(i["id_device"]) - #device = await MessageHandler.get_device(i["last_usage"], device_unique_id, i["name"], i["id_endpoint"], None) - #if device is not None: + # device = await MessageHandler.get_device(i["last_usage"], device_unique_id, i["name"], i["id_endpoint"], None) + # if device is not None: # devices.append(device) if ( @@ -583,7 +633,6 @@ async def parse_devices_data(self, parsed): attr_light[element_name] = element_value if type_of_id == "shutter" or type_of_id == "klineShutter": - if ( element_name in deviceCoverKeywords and element_validity == "upToDate" @@ -766,7 +815,15 @@ async def parse_devices_data(self, parsed): logger.error("msg_data error in parsing !") logger.error(e) - device = await MessageHandler.get_device(type_of_id, unique_id, name_of_id, endpoint_id, data) + device = await MessageHandler.get_device( + self.tydom_client, + type_of_id, + unique_id, + device_id, + name_of_id, + endpoint_id, + data, + ) if device is not None: devices.append(device) diff --git a/custom_components/deltadore-tydom/tydom/README.md b/custom_components/deltadore-tydom/tydom/README.md new file mode 100644 index 0000000..261e21d --- /dev/null +++ b/custom_components/deltadore-tydom/tydom/README.md @@ -0,0 +1,34 @@ +# How it Tydom working ? + +## Authentication +GET request to https://deltadoreadb2ciot.b2clogin.com/deltadoreadb2ciot.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_AccountProviderROPC_SignIn + +It gives the authorization endpoint and what's supported. we retrieve the token endpoint URL from there : https://deltadoreadb2ciot.b2clogin.com/deltadoreadb2ciot.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_accountproviderropc_signin + +POST request to the token endpoint with : +Content-Type header = multipart form data encoded credentials, grant type, client_id and scope : +{ + "username": "email", + "password": "password", + "grant_type": DELTADORE_AUTH_GRANT_TYPE, + "client_id": DELTADORE_AUTH_CLIENTID, + "scope": DELTADORE_AUTH_SCOPE +} + +we retrieve the access_token from the response +we can now query the Delta Dore API using the bearer token + +get the tydom password: +https://prod.iotdeltadore.com/sitesmanagement/api/v1/sites?gateway_mac=xxx + +{"count":1,"sites":[{"id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","creation_date":"2022-09-06T11:39:51.19+02:00","gateway":{"id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","creation_date":"22022-09-06T11:39:51.19+02:00","hashes":[],"mac":"001Axxxxxxxx","password":"xxxx"},"cameras":[]}]} + +Get the list of devices, endpoints, attributes, services : +POST request to https://pilotage.iotdeltadore.com/pilotageservice/api/v1/control/gateways +{ + "id": "001Axxxxxxxx" +} + +The id is the mac address + + diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index 59dd056..d23bb67 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -33,8 +33,9 @@ class TydomClientApiClientCommunicationError(TydomClientApiClientError): class TydomClientApiClientAuthenticationError(TydomClientApiClientError): """Exception to indicate an authentication error.""" -#proxy = None -proxy = "http://proxy.rd.francetelecom.fr:8080/" + +proxy = None + class TydomClient: """Tydom API Client.""" @@ -46,7 +47,7 @@ def __init__( password: str, alarm_pin: str = None, host: str = MEDIATION_URL, - event_callback=None + event_callback=None, ) -> None: logger.debug("Initializing TydomClient Class") @@ -71,7 +72,9 @@ def __init__( self._cmd_prefix = "" self._ping_timeout = None - self._message_handler = MessageHandler(tydom_client=self, cmd_prefix=self._cmd_prefix) + self._message_handler = MessageHandler( + tydom_client=self, cmd_prefix=self._cmd_prefix + ) @staticmethod async def async_get_credentials( @@ -81,9 +84,7 @@ async def async_get_credentials( try: async with async_timeout.timeout(10): response = await session.request( - method="GET", - url=DELTADORE_AUTH_URL, - proxy=proxy + method="GET", url=DELTADORE_AUTH_URL, proxy=proxy ) logger.debug( @@ -112,7 +113,7 @@ async def async_get_credentials( url=signin_url, headers={"Content-Type": ct_header}, data=body, - proxy=proxy + proxy=proxy, ) logger.debug( @@ -130,7 +131,7 @@ async def async_get_credentials( method="GET", url=DELTADORE_API_SITES + macaddress, headers={"Authorization": f"Bearer {access_token}"}, - proxy=proxy + proxy=proxy, ) logger.debug( @@ -144,15 +145,14 @@ async def async_get_credentials( response.close() await session.close() - if ( - "sites" in json_response - and len(json_response["sites"]) > 0 - ): + if "sites" in json_response and len(json_response["sites"]) > 0: for site in json_response["sites"]: if "gateway" in site and site["gateway"]["mac"] == macaddress: password = json_response["sites"][0]["gateway"]["password"] return password - raise TydomClientApiClientAuthenticationError("Tydom credentials not found") + raise TydomClientApiClientAuthenticationError( + "Tydom credentials not found" + ) except asyncio.TimeoutError as exception: raise TydomClientApiClientCommunicationError( "Timeout error fetching information", @@ -187,7 +187,7 @@ async def async_connect(self) -> ClientWebSocketResponse: url=f"https://{self._host}:443/mediation/client?mac={self._mac}&appli=1", headers=http_headers, json=None, - proxy=proxy + proxy=proxy, ) logger.debug( "response status : %s\nheaders : %s\ncontent : %s", @@ -207,7 +207,6 @@ async def async_connect(self) -> ClientWebSocketResponse: else: raise TydomClientApiClientError("Could't find auth nonce") - http_headers = {} http_headers["Authorization"] = self.build_digest_headers( re_matcher.group(1) @@ -219,7 +218,7 @@ async def async_connect(self) -> ClientWebSocketResponse: headers=http_headers, autoping=True, heartbeat=2, - proxy=proxy + proxy=proxy, ) return connection @@ -254,10 +253,13 @@ async def consume_messages(self): if self._connection.closed: await self._connection.close() await asyncio.sleep(10) - self._connection = await self.async_connect() + self.listen_tydom(await self.async_connect()) + # self._connection = await self.async_connect() msg = await self._connection.receive() - logger.info("Incomming message - type : %s - message : %s", msg.type, msg.data) + logger.info( + "Incomming message - type : %s - message : %s", msg.type, msg.data + ) incoming_bytes_str = cast(bytes, msg.data) return await self._message_handler.incoming_triage(incoming_bytes_str) @@ -429,7 +431,11 @@ async def put_devices_data(self, device_id, endpoint_id, name, value): ) a_bytes = bytes(str_request, "ascii") logger.debug("Sending message to tydom (%s %s)", "PUT data", body) - await self._connection.send(a_bytes) + # self._connection.send_bytes + # self._connection.send_json + # self._connection.send_str + # await self._connection.send(a_bytes) + await self._connection.send_bytes(a_bytes) return 0 async def put_alarm_cdata(self, device_id, alarm_id=None, value=None, zone_id=None): diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index 991e216..da2a2b8 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -4,10 +4,25 @@ logger = logging.getLogger(__name__) + class TydomBaseEntity: """Tydom entity base class.""" - def __init__(self, product_name, main_version_sw, main_version_hw, main_id, main_reference, - key_version_sw, key_version_hw, key_version_stack, key_reference, boot_reference, boot_version, update_available): + + def __init__( + self, + product_name, + main_version_sw, + main_version_hw, + main_id, + main_reference, + key_version_sw, + key_version_hw, + key_version_stack, + key_reference, + boot_reference, + boot_version, + update_available, + ): self.product_name = product_name self.main_version_sw = main_version_sw self.main_version_hw = main_version_hw @@ -55,19 +70,21 @@ async def publish_updates(self) -> None: callback() -class TydomDevice(): +class TydomDevice: """represents a generic device""" - def __init__(self, uid, name, device_type, endpoint, data): + def __init__(self, tydom_client, uid, device_id, name, device_type, endpoint, data): + self._tydom_client = tydom_client self._uid = uid + self._id = device_id self._name = name self._type = device_type self._endpoint = endpoint self._callbacks = set() + for key in data: setattr(self, key, data[key]) - def register_callback(self, callback: Callable[[], None]) -> None: """Register callback, called when Roller changes state.""" self._callbacks.add(callback) @@ -100,8 +117,8 @@ async def update_device(self, device): """Update the device values from another device""" logger.debug("Update device %s", device.device_id) for attribute, value in device.__dict__.items(): - if attribute[:1] != '_' and value is not None: - setattr(self, attribute, value) + if attribute[:1] != "_" and value is not None: + setattr(self, attribute, value) await self.publish_updates() # In a real implementation, this library would call it's call backs when it was @@ -111,69 +128,76 @@ async def publish_updates(self) -> None: for callback in self._callbacks: callback() + class TydomShutter(TydomDevice): """Represents a shutter""" - def __init__(self, uid, name, device_type, endpoint, data): - super().__init__(uid, name, device_type, endpoint, data) + async def down(self) -> None: + """Tell cover to go down""" + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "positionCmd", "DOWN" + ) -class TydomEnergy(TydomDevice): - """Represents an energy sensor (for example TYWATT)""" + async def up(self) -> None: + """Tell cover to go up""" + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "positionCmd", "UP" + ) + + async def stop(self) -> None: + """Tell cover to stop moving""" + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "positionCmd", "STOP" + ) + + async def set_position(self, position: int) -> None: + """ + Set cover to the given position. + """ + logger.error("set roller position (device) to : %s", position) - def __init__(self, uid, name, device_type, endpoint, data): - logger.info("TydomEnergy : data %s", data) + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "position", str(position) + ) - super().__init__(uid, name, device_type, endpoint, data) + +class TydomEnergy(TydomDevice): + """Represents an energy sensor (for example TYWATT)""" class TydomSmoke(TydomDevice): """Represents an smoke detector sensor""" - def __init__(self, uid, name, device_type, endpoint, data): - logger.info("TydomSmoke : data %s", data) - super().__init__(uid, name, device_type, endpoint, data) class TydomBoiler(TydomDevice): """represents a boiler""" - def __init__(self, uid, name, device_type, endpoint, data): - logger.info("TydomBoiler : data %s", data) - # {'authorization': 'HEATING', 'setpoint': 19.0, 'thermicLevel': None, 'hvacMode': 'NORMAL', 'timeDelay': 0, 'temperature': 21.35, 'tempoOn': False, 'antifrostOn': False, 'loadSheddingOn': False, 'openingDetected': False, 'presenceDetected': False, 'absence': False, 'productionDefect': False, 'batteryCmdDefect': False, 'tempSensorDefect': False, 'tempSensorShortCut': False, 'tempSensorOpenCirc': False, 'boostOn': False, 'anticipCoeff': 30} - - super().__init__(uid, name, device_type, endpoint, data) class TydomWindow(TydomDevice): """represents a window""" - def __init__(self, uid, name, device_type, endpoint, data): - logger.info("TydomWindow : data %s", data) - super().__init__(uid, name, device_type, endpoint, data) + class TydomDoor(TydomDevice): """represents a door""" - def __init__(self, uid, name, device_type, endpoint, data): - logger.info("TydomDoor : data %s", data) - super().__init__(uid, name, device_type, endpoint, data) + class TydomGate(TydomDevice): - """represents a door""" - def __init__(self, uid, name, device_type, endpoint, data): - logger.info("TydomGate : data %s", data) - super().__init__(uid, name, device_type, endpoint, data) + """represents a gate""" + class TydomGarage(TydomDevice): - """represents a door""" - def __init__(self, uid, name, device_type, endpoint, data): + """represents a garage door""" + + def __init__(self, tydom_client, uid, device_id, name, device_type, endpoint, data): logger.info("TydomGarage : data %s", data) - super().__init__(uid, name, device_type, endpoint, data) + super().__init__( + tydom_client, uid, device_id, name, device_type, endpoint, data + ) + class TydomLight(TydomDevice): - """represents a door""" - def __init__(self, uid, name, device_type, endpoint, data): - logger.info("TydomLight : data %s", data) - super().__init__(uid, name, device_type, endpoint, data) + """represents a light""" + -class TydoAlarm(TydomDevice): +class TydomAlarm(TydomDevice): """represents an alarm""" - def __init__(self, uid, name, device_type, endpoint, data): - logger.info("TydomAlarm : data %s", data) - super().__init__(uid, name, device_type, endpoint, data) From 245a067d4118cc473ace348b03c536f30fbc622f Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:36:11 +0000 Subject: [PATCH 30/74] cleanup --- .../integration_blueprint/__init__.py | 55 ------- .../integration_blueprint/api.py | 90 ---------- .../integration_blueprint/binary_sensor.py | 50 ------ .../integration_blueprint/config_flow.py | 80 --------- .../integration_blueprint/const.py | 9 - .../integration_blueprint/coordinator.py | 49 ------ .../integration_blueprint/entity.py | 25 --- .../integration_blueprint/manifest.json | 12 -- .../integration_blueprint/sensor.py | 46 ------ .../integration_blueprint/switch.py | 56 ------- .../translations/en.json | 18 -- custom_components/livebox/__init__.py | 79 --------- custom_components/livebox/binary_sensor.py | 93 ----------- custom_components/livebox/bridge.py | 154 ------------------ custom_components/livebox/button.py | 51 ------ custom_components/livebox/config_flow.py | 136 ---------------- custom_components/livebox/const.py | 78 --------- custom_components/livebox/coordinator.py | 51 ------ custom_components/livebox/device_tracker.py | 98 ----------- custom_components/livebox/manifest.json | 17 -- custom_components/livebox/sensor.py | 61 ------- custom_components/livebox/services.yaml | 13 -- custom_components/livebox/strings.json | 33 ---- custom_components/livebox/switch.py | 87 ---------- .../livebox/translations/en.json | 34 ---- .../livebox/translations/fr.json | 34 ---- .../livebox/translations/nb.json | 34 ---- 27 files changed, 1543 deletions(-) delete mode 100644 custom_components/integration_blueprint/__init__.py delete mode 100644 custom_components/integration_blueprint/api.py delete mode 100644 custom_components/integration_blueprint/binary_sensor.py delete mode 100644 custom_components/integration_blueprint/config_flow.py delete mode 100644 custom_components/integration_blueprint/const.py delete mode 100644 custom_components/integration_blueprint/coordinator.py delete mode 100644 custom_components/integration_blueprint/entity.py delete mode 100644 custom_components/integration_blueprint/manifest.json delete mode 100644 custom_components/integration_blueprint/sensor.py delete mode 100644 custom_components/integration_blueprint/switch.py delete mode 100644 custom_components/integration_blueprint/translations/en.json delete mode 100644 custom_components/livebox/__init__.py delete mode 100644 custom_components/livebox/binary_sensor.py delete mode 100644 custom_components/livebox/bridge.py delete mode 100644 custom_components/livebox/button.py delete mode 100644 custom_components/livebox/config_flow.py delete mode 100644 custom_components/livebox/const.py delete mode 100644 custom_components/livebox/coordinator.py delete mode 100644 custom_components/livebox/device_tracker.py delete mode 100644 custom_components/livebox/manifest.json delete mode 100644 custom_components/livebox/sensor.py delete mode 100644 custom_components/livebox/services.yaml delete mode 100644 custom_components/livebox/strings.json delete mode 100644 custom_components/livebox/switch.py delete mode 100644 custom_components/livebox/translations/en.json delete mode 100644 custom_components/livebox/translations/fr.json delete mode 100644 custom_components/livebox/translations/nb.json diff --git a/custom_components/integration_blueprint/__init__.py b/custom_components/integration_blueprint/__init__.py deleted file mode 100644 index a9adfdc..0000000 --- a/custom_components/integration_blueprint/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Custom integration to integrate integration_blueprint with Home Assistant. - -For more details about this integration, please refer to -https://github.com/ludeeus/integration_blueprint -""" -from __future__ import annotations - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -from .api import IntegrationBlueprintApiClient -from .const import DOMAIN -from .coordinator import BlueprintDataUpdateCoordinator - -PLATFORMS: list[Platform] = [ - Platform.SENSOR, - Platform.BINARY_SENSOR, - Platform.SWITCH, -] - - -# https://developers.home-assistant.io/docs/config_entries_index/#setting-up-an-entry -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up this integration using UI.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator = BlueprintDataUpdateCoordinator( - hass=hass, - client=IntegrationBlueprintApiClient( - username=entry.data[CONF_USERNAME], - password=entry.data[CONF_PASSWORD], - session=async_get_clientsession(hass), - ), - ) - # https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities - await coordinator.async_config_entry_first_refresh() - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(async_reload_entry)) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Handle removal of an entry.""" - if unloaded := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unloaded - - -async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Reload config entry.""" - await async_unload_entry(hass, entry) - await async_setup_entry(hass, entry) diff --git a/custom_components/integration_blueprint/api.py b/custom_components/integration_blueprint/api.py deleted file mode 100644 index a738040..0000000 --- a/custom_components/integration_blueprint/api.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Sample API Client.""" -from __future__ import annotations - -import asyncio -import socket - -import aiohttp -import async_timeout - - -class IntegrationBlueprintApiClientError(Exception): - """Exception to indicate a general API error.""" - - -class IntegrationBlueprintApiClientCommunicationError( - IntegrationBlueprintApiClientError -): - """Exception to indicate a communication error.""" - - -class IntegrationBlueprintApiClientAuthenticationError( - IntegrationBlueprintApiClientError -): - """Exception to indicate an authentication error.""" - - -class IntegrationBlueprintApiClient: - """Sample API Client.""" - - def __init__( - self, - username: str, - password: str, - session: aiohttp.ClientSession, - ) -> None: - """Sample API Client.""" - self._username = username - self._password = password - self._session = session - - async def async_get_data(self) -> any: - """Get data from the API.""" - return await self._api_wrapper( - method="get", url="https://jsonplaceholder.typicode.com/posts/1" - ) - - async def async_set_title(self, value: str) -> any: - """Get data from the API.""" - return await self._api_wrapper( - method="patch", - url="https://jsonplaceholder.typicode.com/posts/1", - data={"title": value}, - headers={"Content-type": "application/json; charset=UTF-8"}, - ) - - async def _api_wrapper( - self, - method: str, - url: str, - data: dict | None = None, - headers: dict | None = None, - ) -> any: - """Get information from the API.""" - try: - async with async_timeout.timeout(10): - response = await self._session.request( - method=method, - url=url, - headers=headers, - json=data, - ) - if response.status in (401, 403): - raise IntegrationBlueprintApiClientAuthenticationError( - "Invalid credentials", - ) - response.raise_for_status() - return await response.json() - - except asyncio.TimeoutError as exception: - raise IntegrationBlueprintApiClientCommunicationError( - "Timeout error fetching information", - ) from exception - except (aiohttp.ClientError, socket.gaierror) as exception: - raise IntegrationBlueprintApiClientCommunicationError( - "Error fetching information", - ) from exception - except Exception as exception: # pylint: disable=broad-except - raise IntegrationBlueprintApiClientError( - "Something really wrong happened!" - ) from exception diff --git a/custom_components/integration_blueprint/binary_sensor.py b/custom_components/integration_blueprint/binary_sensor.py deleted file mode 100644 index fff5b21..0000000 --- a/custom_components/integration_blueprint/binary_sensor.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Binary sensor platform for integration_blueprint.""" -from __future__ import annotations - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, - BinarySensorEntityDescription, -) - -from .const import DOMAIN -from .coordinator import BlueprintDataUpdateCoordinator -from .entity import IntegrationBlueprintEntity - -ENTITY_DESCRIPTIONS = ( - BinarySensorEntityDescription( - key="integration_blueprint", - name="Integration Blueprint Binary Sensor", - device_class=BinarySensorDeviceClass.CONNECTIVITY, - ), -) - - -async def async_setup_entry(hass, entry, async_add_devices): - """Set up the binary_sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_devices( - IntegrationBlueprintBinarySensor( - coordinator=coordinator, - entity_description=entity_description, - ) - for entity_description in ENTITY_DESCRIPTIONS - ) - - -class IntegrationBlueprintBinarySensor(IntegrationBlueprintEntity, BinarySensorEntity): - """integration_blueprint binary_sensor class.""" - - def __init__( - self, - coordinator: BlueprintDataUpdateCoordinator, - entity_description: BinarySensorEntityDescription, - ) -> None: - """Initialize the binary_sensor class.""" - super().__init__(coordinator) - self.entity_description = entity_description - - @property - def is_on(self) -> bool: - """Return true if the binary_sensor is on.""" - return self.coordinator.data.get("title", "") == "foo" diff --git a/custom_components/integration_blueprint/config_flow.py b/custom_components/integration_blueprint/config_flow.py deleted file mode 100644 index a474163..0000000 --- a/custom_components/integration_blueprint/config_flow.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Adds config flow for Blueprint.""" -from __future__ import annotations - -import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import selector -from homeassistant.helpers.aiohttp_client import async_create_clientsession - -from .api import ( - IntegrationBlueprintApiClient, - IntegrationBlueprintApiClientAuthenticationError, - IntegrationBlueprintApiClientCommunicationError, - IntegrationBlueprintApiClientError, -) -from .const import DOMAIN, LOGGER - - -class BlueprintFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Config flow for Blueprint.""" - - VERSION = 1 - - async def async_step_user( - self, - user_input: dict | None = None, - ) -> config_entries.FlowResult: - """Handle a flow initialized by the user.""" - _errors = {} - if user_input is not None: - try: - await self._test_credentials( - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - ) - except IntegrationBlueprintApiClientAuthenticationError as exception: - LOGGER.warning(exception) - _errors["base"] = "auth" - except IntegrationBlueprintApiClientCommunicationError as exception: - LOGGER.error(exception) - _errors["base"] = "connection" - except IntegrationBlueprintApiClientError as exception: - LOGGER.exception(exception) - _errors["base"] = "unknown" - else: - return self.async_create_entry( - title=user_input[CONF_USERNAME], - data=user_input, - ) - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required( - CONF_USERNAME, - default=(user_input or {}).get(CONF_USERNAME), - ): selector.TextSelector( - selector.TextSelectorConfig( - type=selector.TextSelectorType.TEXT - ), - ), - vol.Required(CONF_PASSWORD): selector.TextSelector( - selector.TextSelectorConfig( - type=selector.TextSelectorType.PASSWORD - ), - ), - } - ), - errors=_errors, - ) - - async def _test_credentials(self, username: str, password: str) -> None: - """Validate credentials.""" - client = IntegrationBlueprintApiClient( - username=username, - password=password, - session=async_create_clientsession(self.hass), - ) - await client.async_get_data() diff --git a/custom_components/integration_blueprint/const.py b/custom_components/integration_blueprint/const.py deleted file mode 100644 index 66c28f3..0000000 --- a/custom_components/integration_blueprint/const.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Constants for integration_blueprint.""" -from logging import Logger, getLogger - -LOGGER: Logger = getLogger(__package__) - -NAME = "Integration blueprint" -DOMAIN = "integration_blueprint" -VERSION = "0.0.0" -ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/" diff --git a/custom_components/integration_blueprint/coordinator.py b/custom_components/integration_blueprint/coordinator.py deleted file mode 100644 index d427a1a..0000000 --- a/custom_components/integration_blueprint/coordinator.py +++ /dev/null @@ -1,49 +0,0 @@ -"""DataUpdateCoordinator for integration_blueprint.""" -from __future__ import annotations - -from datetime import timedelta - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import ( - DataUpdateCoordinator, - UpdateFailed, -) -from homeassistant.exceptions import ConfigEntryAuthFailed - -from .api import ( - IntegrationBlueprintApiClient, - IntegrationBlueprintApiClientAuthenticationError, - IntegrationBlueprintApiClientError, -) -from .const import DOMAIN, LOGGER - - -# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities -class BlueprintDataUpdateCoordinator(DataUpdateCoordinator): - """Class to manage fetching data from the API.""" - - config_entry: ConfigEntry - - def __init__( - self, - hass: HomeAssistant, - client: IntegrationBlueprintApiClient, - ) -> None: - """Initialize.""" - self.client = client - super().__init__( - hass=hass, - logger=LOGGER, - name=DOMAIN, - update_interval=timedelta(minutes=5), - ) - - async def _async_update_data(self): - """Update data via library.""" - try: - return await self.client.async_get_data() - except IntegrationBlueprintApiClientAuthenticationError as exception: - raise ConfigEntryAuthFailed(exception) from exception - except IntegrationBlueprintApiClientError as exception: - raise UpdateFailed(exception) from exception diff --git a/custom_components/integration_blueprint/entity.py b/custom_components/integration_blueprint/entity.py deleted file mode 100644 index 4325227..0000000 --- a/custom_components/integration_blueprint/entity.py +++ /dev/null @@ -1,25 +0,0 @@ -"""BlueprintEntity class.""" -from __future__ import annotations - -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import ATTRIBUTION, DOMAIN, NAME, VERSION -from .coordinator import BlueprintDataUpdateCoordinator - - -class IntegrationBlueprintEntity(CoordinatorEntity): - """BlueprintEntity class.""" - - _attr_attribution = ATTRIBUTION - - def __init__(self, coordinator: BlueprintDataUpdateCoordinator) -> None: - """Initialize.""" - super().__init__(coordinator) - self._attr_unique_id = coordinator.config_entry.entry_id - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self.unique_id)}, - name=NAME, - model=VERSION, - manufacturer=NAME, - ) diff --git a/custom_components/integration_blueprint/manifest.json b/custom_components/integration_blueprint/manifest.json deleted file mode 100644 index 817cd7b..0000000 --- a/custom_components/integration_blueprint/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "integration_blueprint", - "name": "Integration blueprint", - "codeowners": [ - "@ludeeus" - ], - "config_flow": true, - "documentation": "https://github.com/ludeeus/integration_blueprint", - "iot_class": "cloud_polling", - "issue_tracker": "https://github.com/ludeeus/integration_blueprint/issues", - "version": "0.0.0" -} \ No newline at end of file diff --git a/custom_components/integration_blueprint/sensor.py b/custom_components/integration_blueprint/sensor.py deleted file mode 100644 index 06201fe..0000000 --- a/custom_components/integration_blueprint/sensor.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Sensor platform for integration_blueprint.""" -from __future__ import annotations - -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription - -from .const import DOMAIN -from .coordinator import BlueprintDataUpdateCoordinator -from .entity import IntegrationBlueprintEntity - -ENTITY_DESCRIPTIONS = ( - SensorEntityDescription( - key="integration_blueprint", - name="Integration Sensor", - icon="mdi:format-quote-close", - ), -) - - -async def async_setup_entry(hass, entry, async_add_devices): - """Set up the sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_devices( - IntegrationBlueprintSensor( - coordinator=coordinator, - entity_description=entity_description, - ) - for entity_description in ENTITY_DESCRIPTIONS - ) - - -class IntegrationBlueprintSensor(IntegrationBlueprintEntity, SensorEntity): - """integration_blueprint Sensor class.""" - - def __init__( - self, - coordinator: BlueprintDataUpdateCoordinator, - entity_description: SensorEntityDescription, - ) -> None: - """Initialize the sensor class.""" - super().__init__(coordinator) - self.entity_description = entity_description - - @property - def native_value(self) -> str: - """Return the native value of the sensor.""" - return self.coordinator.data.get("body") diff --git a/custom_components/integration_blueprint/switch.py b/custom_components/integration_blueprint/switch.py deleted file mode 100644 index 33340a2..0000000 --- a/custom_components/integration_blueprint/switch.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Switch platform for integration_blueprint.""" -from __future__ import annotations - -from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription - -from .const import DOMAIN -from .coordinator import BlueprintDataUpdateCoordinator -from .entity import IntegrationBlueprintEntity - -ENTITY_DESCRIPTIONS = ( - SwitchEntityDescription( - key="integration_blueprint", - name="Integration Switch", - icon="mdi:format-quote-close", - ), -) - - -async def async_setup_entry(hass, entry, async_add_devices): - """Set up the sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_devices( - IntegrationBlueprintSwitch( - coordinator=coordinator, - entity_description=entity_description, - ) - for entity_description in ENTITY_DESCRIPTIONS - ) - - -class IntegrationBlueprintSwitch(IntegrationBlueprintEntity, SwitchEntity): - """integration_blueprint switch class.""" - - def __init__( - self, - coordinator: BlueprintDataUpdateCoordinator, - entity_description: SwitchEntityDescription, - ) -> None: - """Initialize the switch class.""" - super().__init__(coordinator) - self.entity_description = entity_description - - @property - def is_on(self) -> bool: - """Return true if the switch is on.""" - return self.coordinator.data.get("title", "") == "foo" - - async def async_turn_on(self, **_: any) -> None: - """Turn on the switch.""" - await self.coordinator.api.async_set_title("bar") - await self.coordinator.async_request_refresh() - - async def async_turn_off(self, **_: any) -> None: - """Turn off the switch.""" - await self.coordinator.api.async_set_title("foo") - await self.coordinator.async_request_refresh() diff --git a/custom_components/integration_blueprint/translations/en.json b/custom_components/integration_blueprint/translations/en.json deleted file mode 100644 index 049f7a4..0000000 --- a/custom_components/integration_blueprint/translations/en.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "If you need help with the configuration have a look here: https://github.com/ludeeus/integration_blueprint", - "data": { - "username": "Username", - "password": "Password" - } - } - }, - "error": { - "auth": "Username/Password is wrong.", - "connection": "Unable to connect to the server.", - "unknown": "Unknown error occurred." - } - } -} \ No newline at end of file diff --git a/custom_components/livebox/__init__.py b/custom_components/livebox/__init__.py deleted file mode 100644 index 365eb5f..0000000 --- a/custom_components/livebox/__init__.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Orange Livebox.""" -import logging - -import voluptuous as vol - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers import device_registry as dr - -from .const import ( - CALLID, - CONF_TRACKING_TIMEOUT, - COORDINATOR, - DOMAIN, - LIVEBOX_API, - LIVEBOX_ID, - PLATFORMS, -) -from .coordinator import LiveboxDataUpdateCoordinator - -CALLMISSED_SCHEMA = vol.Schema({vol.Optional(CALLID): str}) - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Livebox as config entry.""" - hass.data.setdefault(DOMAIN, {}) - - coordinator = LiveboxDataUpdateCoordinator(hass, entry) - await coordinator.async_config_entry_first_refresh() - if (infos := coordinator.data.get("infos")) is None: - raise PlatformNotReady - - device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - identifiers={(DOMAIN, infos.get("SerialNumber"))}, - manufacturer=infos.get("Manufacturer"), - name=infos.get("ProductClass"), - model=infos.get("ModelName"), - sw_version=infos.get("SoftwareVersion"), - configuration_url="http://{}:{}".format( - entry.data.get("host"), entry.data.get("port") - ), - ) - - entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - hass.data[DOMAIN][entry.entry_id] = { - LIVEBOX_ID: entry.unique_id, - COORDINATOR: coordinator, - LIVEBOX_API: coordinator.bridge.api, - CONF_TRACKING_TIMEOUT: entry.options.get(CONF_TRACKING_TIMEOUT, 0), - } - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - async def async_remove_cmissed(call) -> None: - await coordinator.bridge.async_remove_cmissed(call) - await coordinator.async_refresh() - - hass.services.async_register( - DOMAIN, "remove_call_missed", async_remove_cmissed, schema=CALLMISSED_SCHEMA - ) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok - - -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): - """Reload device tracker if change option.""" - await hass.config_entries.async_reload(entry.entry_id) diff --git a/custom_components/livebox/binary_sensor.py b/custom_components/livebox/binary_sensor.py deleted file mode 100644 index 333f170..0000000 --- a/custom_components/livebox/binary_sensor.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Livebox binary sensor entities.""" -import logging -from datetime import datetime, timedelta - -from homeassistant.components.binary_sensor import ( - BinarySensorEntity, - BinarySensorDeviceClass, -) -from homeassistant.helpers.entity import EntityCategory -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import COORDINATOR, DOMAIN, LIVEBOX_ID, MISSED_ICON -from .coordinator import LiveboxDataUpdateCoordinator - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Defer binary sensor setup to the shared sensor module.""" - datas = hass.data[DOMAIN][config_entry.entry_id] - box_id = datas[LIVEBOX_ID] - coordinator = datas[COORDINATOR] - async_add_entities( - [WanStatus(coordinator, box_id), CallMissed(coordinator, box_id)], True - ) - - -class WanStatus(CoordinatorEntity[LiveboxDataUpdateCoordinator], BinarySensorEntity): - """Wan status sensor.""" - - _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY - _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_has_entity_name = True - _attr_name = "WAN Status" - - def __init__(self, coordinator, box_id): - """Initialize the sensor.""" - super().__init__(coordinator) - self._attr_unique_id = f"{box_id}_connectivity" - self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - wstatus = self.coordinator.data.get("wan_status", {}).get("data", {}) - return wstatus.get("WanState") == "up" - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - wstatus = self.coordinator.data.get("wan_status", {}).get("data", {}) - uptime = datetime.today() - timedelta( - seconds=self.coordinator.data["infos"].get("UpTime") - ) - _attributs = { - "link_type": wstatus.get("LinkType"), - "link_state": wstatus.get("LinkState"), - "last_connection_error": wstatus.get("LastConnectionError"), - "wan_ipaddress": wstatus.get("IPAddress"), - "wan_ipv6address": wstatus.get("IPv6Address"), - "uptime": uptime, - } - cwired = self.coordinator.data.get("count_wired_devices") - if cwired > 0: - _attributs.update({"wired clients": cwired}) - cwireless = self.coordinator.data.get("count_wireless_devices") - if cwireless > 0: - _attributs.update({"wireless clients": cwireless}) - - return _attributs - - -class CallMissed(CoordinatorEntity[LiveboxDataUpdateCoordinator], BinarySensorEntity): - """Call missed sensor.""" - - _attr_name = "Call missed" - _attr_icon = MISSED_ICON - _attr_has_entity_name = True - - def __init__(self, coordinator, box_id): - """Initialize the sensor.""" - super().__init__(coordinator) - self._attr_unique_id = f"{box_id}_callmissed" - self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return len(self.coordinator.data.get("cmissed").get("call missed")) > 0 - - @property - def extra_state_attributes(self): - """Return attributs.""" - return self.coordinator.data.get("cmissed") diff --git a/custom_components/livebox/bridge.py b/custom_components/livebox/bridge.py deleted file mode 100644 index fb0bcb5..0000000 --- a/custom_components/livebox/bridge.py +++ /dev/null @@ -1,154 +0,0 @@ -"""Collect datas information from livebox.""" -import logging -from datetime import datetime - -from aiosysbus import AIOSysbus -from aiosysbus.exceptions import ( - AuthorizationError, - HttpRequestError, - InsufficientPermissionsError, - LiveboxException, - NotOpenError, -) -from homeassistant.util.dt import ( - UTC, - DEFAULT_TIME_ZONE, -) -from .const import CALLID - -_LOGGER = logging.getLogger(__name__) - - -class BridgeData: - """Simplification of API calls.""" - - def __init__(self, hass): - """Init parameters.""" - self.hass = hass - self.api = None - self.count_wired_devices = 0 - self.count_wireless_devices = 0 - - async def async_connect(self, **kwargs): - """Connect at livebox.""" - self.api = AIOSysbus( - username=kwargs.get("username"), - password=kwargs.get("password"), - host=kwargs.get("host"), - port=kwargs.get("port"), - ) - - try: - await self.hass.async_add_executor_job(self.api.connect) - await self.hass.async_add_executor_job(self.api.get_permissions) - except AuthorizationError as error: - _LOGGER.error("Error Authorization (%s)", error) - raise AuthorizationError from error - except NotOpenError as error: - _LOGGER.error("Error Not open (%s)", error) - raise NotOpenError from error - except LiveboxException as error: - _LOGGER.error("Error Unknown (%s)", error) - raise LiveboxException from error - except InsufficientPermissionsError as error: - _LOGGER.error("Error Insufficient Permissions (%s)", error) - raise InsufficientPermissionsError from error - - async def async_make_request(self, call_api, **kwargs): - """Make request for API.""" - try: - return await self.hass.async_add_executor_job(call_api, kwargs) - except HttpRequestError as error: - _LOGGER.error("HTTP Request (%s)", error) - raise LiveboxException from error - except LiveboxException as error: - _LOGGER.error("Error Unknown (%s)", error) - raise LiveboxException from error - - async def async_get_devices(self, lan_tracking=False): - """Get all devices.""" - devices_tracker = {} - parameters = { - "expression": { - "wifi": 'wifi && (edev || hnid) and .PhysAddress!=""', - "eth": 'eth && (edev || hnid) and .PhysAddress!=""', - } - } - devices = await self.async_make_request( - self.api.devices.get_devices, **parameters - ) - devices_status_wireless = devices.get("status", {}).get("wifi", {}) - self.count_wireless_devices = len(devices_status_wireless) - for device in devices_status_wireless: - if device.get("Key"): - devices_tracker.setdefault(device.get("Key"), {}).update(device) - - if lan_tracking: - devices_status_wired = devices.get("status", {}).get("eth", {}) - self.count_wired_devices = len(devices_status_wired) - for device in devices_status_wired: - if device.get("Key"): - devices_tracker.setdefault(device.get("Key"), {}).update(device) - - return devices_tracker - - async def async_get_infos(self): - """Get router infos.""" - infos = await self.async_make_request(self.api.deviceinfo.get_deviceinfo) - return infos.get("status", {}) - - async def async_get_wan_status(self): - """Get status.""" - wan_status = await self.async_make_request(self.api.system.get_wanstatus) - return wan_status - - async def async_get_caller_missed(self): - """Get caller missed.""" - cmisseds = [] - calls = await self.async_make_request( - self.api.call.get_voiceapplication_calllist - ) - - for call in calls.get("status", {}): - if call["callType"] != "succeeded": - utc_dt = datetime.strptime(call["startTime"], "%Y-%m-%dT%H:%M:%SZ") - local_dt = utc_dt.replace(tzinfo=UTC).astimezone(tz=DEFAULT_TIME_ZONE) - cmisseds.append( - { - "phone_number": call["remoteNumber"], - "date": str(local_dt), - "callId": call["callId"], - } - ) - - return {"call missed": cmisseds} - - async def async_get_dsl_status(self): - """Get dsl status.""" - parameters = {"mibs": "dsl", "flag": "", "traverse": "down"} - dsl_status = await self.async_make_request( - self.api.connection.get_data_MIBS, **parameters - ) - return dsl_status.get("status", {}).get("dsl", {}).get("dsl0", {}) - - async def async_get_nmc(self): - """Get dsl status.""" - nmc = await self.async_make_request(self.api.system.get_nmc) - return nmc.get("status", {}) - - async def async_get_wifi(self): - """Get dsl status.""" - wifi = await self.async_make_request(self.api.wifi.get_wifi) - return wifi.get("status", {}).get("Enable") is True - - async def async_get_guest_wifi(self): - """Get Guest Wifi status.""" - guest_wifi = await self.async_make_request(self.api.guestwifi.get_guest_wifi) - return guest_wifi.get("status", {}).get("Enable") is True - - async def async_remove_cmissed(self, call) -> None: - """Remove call missed.""" - await self.async_make_request( - self.api.call.get_voiceapplication_clearlist, - **{CALLID: call.data.get(CALLID)}, - ) diff --git a/custom_components/livebox/button.py b/custom_components/livebox/button.py deleted file mode 100644 index 4c8ce2e..0000000 --- a/custom_components/livebox/button.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Button for Livebox router.""" -import logging - -from homeassistant.components.button import ButtonEntity - -from .const import DOMAIN, LIVEBOX_API, LIVEBOX_ID, RESTART_ICON, RING_ICON - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the sensors.""" - box_id = hass.data[DOMAIN][config_entry.entry_id][LIVEBOX_ID] - api = hass.data[DOMAIN][config_entry.entry_id][LIVEBOX_API] - async_add_entities([RestartButton(box_id, api), RingButton(box_id, api)], True) - - -class RestartButton(ButtonEntity): - """Representation of a livebox sensor.""" - - _attr_name = "Livebox restart" - _attr_icon = RESTART_ICON - _attr_has_entity_name = True - - def __init__(self, box_id, api): - """Initialize the sensor.""" - self._api = api - self._attr_unique_id = f"{box_id}_restart" - self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} - - async def async_press(self) -> None: - """Handle the button press.""" - await self.hass.async_add_executor_job(self._api.system.reboot) - - -class RingButton(ButtonEntity): - """Representation of a livebox sensor.""" - - _attr_name = "Ring your phone" - _attr_icon = RING_ICON - _attr_has_entity_name = True - - def __init__(self, box_id, api): - """Initialize the sensor.""" - self._api = api - self._attr_unique_id = f"{box_id}_ring" - self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} - - async def async_press(self) -> None: - """Handle the button press.""" - await self.hass.async_add_executor_job(self._api.call.set_voiceapplication_ring) diff --git a/custom_components/livebox/config_flow.py b/custom_components/livebox/config_flow.py deleted file mode 100644 index 341357a..0000000 --- a/custom_components/livebox/config_flow.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Config flow to configure Livebox.""" -import logging -from urllib.parse import urlparse - -import voluptuous as vol -from aiosysbus.exceptions import ( - AuthorizationError, - InsufficientPermissionsError, - LiveboxException, - NotOpenError, -) -from homeassistant import config_entries -from homeassistant.components import ssdp -from homeassistant.components.ssdp import ATTR_SSDP_UDN, ATTR_SSDP_USN -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_UNIQUE_ID, - CONF_USERNAME, -) -from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv - -from .bridge import BridgeData -from .const import ( - CONF_LAN_TRACKING, - CONF_TRACKING_TIMEOUT, - DEFAULT_HOST, - DEFAULT_LAN_TRACKING, - DEFAULT_PORT, - DEFAULT_TRACKING_TIMEOUT, - DEFAULT_USERNAME, - DOMAIN, -) - -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST, default=DEFAULT_HOST): str, - vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, - } -) - -_LOGGER = logging.getLogger(__name__) - - -class LiveboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a Livebox config flow.""" - - VERSION = 1 - - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get option flow.""" - return LiveboxOptionsFlowHandler(config_entry) - - async def async_step_import(self, import_config): - """Import a config entry from configuration.yaml.""" - return await self.async_step_user(import_config) - - async def async_step_user(self, user_input=None): - """Handle a flow initialized by the user.""" - errors = {} - if user_input is not None and user_input.get(CONF_USERNAME) is not None: - try: - bridge = BridgeData(self.hass) - await bridge.async_connect(**user_input) - infos = await bridge.async_get_infos() - await self.async_set_unique_id(infos["SerialNumber"]) - self._abort_if_unique_id_configured() - except AuthorizationError: - errors["base"] = "login_inccorect" - except InsufficientPermissionsError: - errors["base"] = "insufficient_permission" - except NotOpenError: - errors["base"] = "cannot_connect" - except LiveboxException: - errors["base"] = "unknown" - else: - return self.async_create_entry( - title=infos["ProductClass"], data=user_input - ) - - return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors - ) - - async def async_step_ssdp(self, discovery_info): - """Handle a discovered device.""" - hostname = urlparse(discovery_info.ssdp_location).hostname - friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] - unique_id = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - - user_input = { - CONF_HOST: hostname, - CONF_NAME: friendly_name, - CONF_UNIQUE_ID: unique_id, - ATTR_SSDP_USN: discovery_info.ssdp_usn, - ATTR_SSDP_UDN: discovery_info.ssdp_udn, - } - return await self.async_step_user(user_input) - - -class LiveboxOptionsFlowHandler(config_entries.OptionsFlow): - """Handle option.""" - - def __init__(self, config_entry): - """Initialize the options flow.""" - self.config_entry = config_entry - self._lan_tracking = self.config_entry.options.get( - CONF_LAN_TRACKING, DEFAULT_LAN_TRACKING - ) - self._tracking_timeout = self.config_entry.options.get( - CONF_TRACKING_TIMEOUT, DEFAULT_TRACKING_TIMEOUT - ) - - async def async_step_init(self, user_input=None): - """Handle a flow initialized by the user.""" - options_schema = vol.Schema( - { - vol.Required(CONF_LAN_TRACKING, default=self._lan_tracking): bool, - vol.Required( - CONF_TRACKING_TIMEOUT, default=self._tracking_timeout - ): int, - }, - ) - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form(step_id="init", data_schema=options_schema) diff --git a/custom_components/livebox/const.py b/custom_components/livebox/const.py deleted file mode 100644 index f0fa0b0..0000000 --- a/custom_components/livebox/const.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Constants for the Livebox component.""" -from __future__ import annotations - -from dataclasses import dataclass -from typing import Final -from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND -from homeassistant.components.sensor import ( - STATE_CLASS_MEASUREMENT, - SensorEntityDescription, -) - -DOMAIN = "livebox" -COORDINATOR = "coordinator" -UNSUB_LISTENER = "unsubscribe_listener" -LIVEBOX_ID = "id" -LIVEBOX_API = "api" -PLATFORMS = ["sensor", "binary_sensor", "device_tracker", "switch", "button"] - -TEMPLATE_SENSOR = "Orange Livebox" - -DEFAULT_USERNAME = "admin" -DEFAULT_HOST = "192.168.1.1" -DEFAULT_PORT = 80 - -CALLID = "callId" - -CONF_LAN_TRACKING = "lan_tracking" -DEFAULT_LAN_TRACKING = False - -CONF_TRACKING_TIMEOUT = "timeout_tracking" -DEFAULT_TRACKING_TIMEOUT = 300 - -UPLOAD_ICON = "mdi:upload-network" -DOWNLOAD_ICON = "mdi:download-network" -MISSED_ICON = "mdi:phone-alert" -RESTART_ICON = "mdi:restart-alert" -RING_ICON = "mdi:phone-classic" -GUESTWIFI_ICON = "mdi:wifi-lock-open" - - -@dataclass -class FlowSensorEntityDescription(SensorEntityDescription): - """Represents an Flow Sensor.""" - - current_rate: str | None = None - attr: dict | None = None - - -SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( - FlowSensorEntityDescription( - key="down", - name="Orange Livebox Download speed", - icon=DOWNLOAD_ICON, - current_rate="DownstreamCurrRate", - native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, - state_class=STATE_CLASS_MEASUREMENT, - attr={ - "downstream_maxrate": "DownstreamMaxRate", - "downstream_lineattenuation": "DownstreamLineAttenuation", - "downstream_noisemargin": "DownstreamNoiseMargin", - "downstream_power": "DownstreamPower", - }, - ), - FlowSensorEntityDescription( - key="up", - name="Orange Livebox Upload speed", - icon=UPLOAD_ICON, - current_rate="UpstreamCurrRate", - native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, - state_class=STATE_CLASS_MEASUREMENT, - attr={ - "upstream_maxrate": "UpstreamMaxRate", - "upstream_lineattenuation": "UpstreamLineAttenuation", - "upstream_noisemargin": "UpstreamNoiseMargin", - "upstream_power": "UpstreamPower", - }, - ), -) diff --git a/custom_components/livebox/coordinator.py b/custom_components/livebox/coordinator.py deleted file mode 100644 index 3cf0809..0000000 --- a/custom_components/livebox/coordinator.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Corddinator for Livebox.""" -from __future__ import annotations - -from datetime import timedelta -import logging - -from aiosysbus.exceptions import LiveboxException - -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .bridge import BridgeData -from .const import DOMAIN, CONF_LAN_TRACKING - -_LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=1) - - -class LiveboxDataUpdateCoordinator(DataUpdateCoordinator): - """Define an object to fetch datas.""" - - def __init__( - self, - hass: HomeAssistant, - config_entry, - ) -> None: - """Class to manage fetching data API.""" - self.bridge = BridgeData(hass) - self.config_entry = config_entry - self.api = None - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) - - async def _async_update_data(self) -> dict: - """Fetch datas.""" - try: - lan_tracking = self.config_entry.options.get(CONF_LAN_TRACKING, False) - self.api = await self.bridge.async_connect(**self.config_entry.data) - return { - "cmissed": await self.bridge.async_get_caller_missed(), - "devices": await self.bridge.async_get_devices(lan_tracking), - "dsl_status": await self.bridge.async_get_dsl_status(), - "infos": await self.bridge.async_get_infos(), - "nmc": await self.bridge.async_get_nmc(), - "wan_status": await self.bridge.async_get_wan_status(), - "wifi": await self.bridge.async_get_wifi(), - "guest_wifi": await self.bridge.async_get_guest_wifi(), - "count_wired_devices": self.bridge.count_wired_devices, - "count_wireless_devices": self.bridge.count_wireless_devices, - } - except LiveboxException as error: - raise LiveboxException(error) from error diff --git a/custom_components/livebox/device_tracker.py b/custom_components/livebox/device_tracker.py deleted file mode 100644 index ea91544..0000000 --- a/custom_components/livebox/device_tracker.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Support for the Livebox platform.""" -import logging -from datetime import datetime, timedelta - -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER -from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from . import CONF_TRACKING_TIMEOUT, COORDINATOR, DOMAIN, LIVEBOX_ID -from .coordinator import LiveboxDataUpdateCoordinator - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up device tracker from config entry.""" - datas = hass.data[DOMAIN][config_entry.entry_id] - box_id = datas[LIVEBOX_ID] - coordinator = datas[COORDINATOR] - timeout = datas[CONF_TRACKING_TIMEOUT] - - device_trackers = coordinator.data["devices"] - entities = [ - LiveboxDeviceScannerEntity(key, box_id, coordinator, timeout) - for key, device in device_trackers.items() - if "IPAddress" and "PhysAddress" in device - ] - async_add_entities(entities, True) - - -class LiveboxDeviceScannerEntity( - CoordinatorEntity[LiveboxDataUpdateCoordinator], ScannerEntity -): - """Represent a tracked device.""" - - _attr_has_entity_name = True - - def __init__(self, key, bridge_id, coordinator, timeout): - """Initialize the device tracker.""" - super().__init__(coordinator) - self.box_id = bridge_id - self.key = key - self._device = coordinator.data.get("devices", {}).get(key, {}) - self._timeout_tracking = timeout - self._old_status = datetime.today() - - self._attr_name = self._device.get("Name") - self._attr_unique_id = key - - @property - def is_connected(self): - """Return true if the device is connected to the network.""" - status = ( - self.coordinator.data.get("devices", {}) - .get(self.unique_id, {}) - .get("Active") - ) - if status is True: - self._old_status = datetime.today() + timedelta( - seconds=self._timeout_tracking - ) - if status is False and self._old_status > datetime.today(): - _LOGGER.debug("%s will be disconnected at %s", self.name, self._old_status) - return True - - return status - - @property - def source_type(self): - """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_ROUTER - - @property - def ip_address(self): - """Return ip address.""" - device = self.coordinator.data["devices"].get(self.unique_id, {}) - return device.get("IPAddress") - - @property - def mac_address(self): - """Return mac address.""" - return self.key - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self.name, - "identifiers": {(DOMAIN, self.unique_id)}, - "via_device": (DOMAIN, self.box_id), - } - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return { - "first_seen": self._device.get("FirstSeen"), - } diff --git a/custom_components/livebox/manifest.json b/custom_components/livebox/manifest.json deleted file mode 100644 index ae826b7..0000000 --- a/custom_components/livebox/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "domain": "livebox", - "name": "Orange Livebox", - "codeowners": ["@cyr-ius"], - "config_flow": true, - "dependencies": ["ssdp"], - "documentation": "https://github.com/cyr-ius/hass-livebox-component", - "iot_class": "local_polling", - "issue_tracker": "https://github.com/cyr-ius/hass-livebox-component/issues", - "loggers": ["aiosysbus"], - "requirements": ["aiosysbus==0.2.1"], - "ssdp": [{ - "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:2", - "friendlyName": "Orange Livebox" - }], - "version": "1.8.6" -} diff --git a/custom_components/livebox/sensor.py b/custom_components/livebox/sensor.py deleted file mode 100644 index 0b7e3f1..0000000 --- a/custom_components/livebox/sensor.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Sensor for Livebox router.""" -import logging - -from homeassistant.components.sensor import SensorEntity -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import COORDINATOR, DOMAIN, LIVEBOX_ID, SENSOR_TYPES -from .coordinator import LiveboxDataUpdateCoordinator - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the sensors.""" - datas = hass.data[DOMAIN][config_entry.entry_id] - box_id = datas[LIVEBOX_ID] - coordinator = datas[COORDINATOR] - nmc = coordinator.data["nmc"] - entities = [ - FlowSensor( - coordinator, - box_id, - description, - ) - for description in SENSOR_TYPES - ] - if nmc.get("WanMode") is not None and "ETHERNET" not in nmc["WanMode"].upper(): - async_add_entities(entities, True) - - -class FlowSensor(CoordinatorEntity[LiveboxDataUpdateCoordinator], SensorEntity): - """Representation of a livebox sensor.""" - - _attr_has_entity_name = True - - def __init__(self, coordinator, box_id, description): - """Initialize the sensor.""" - super().__init__(coordinator) - self._attributs = description.attr - self._current = description.current_rate - self.entity_description = description - self._attr_unique_id = f"{box_id}_{self._current}" - self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} - - @property - def native_value(self): - """Return the native value of the device.""" - if self.coordinator.data["dsl_status"].get(self._current): - return round( - self.coordinator.data["dsl_status"][self._current] / 1000, - 2, - ) - return None - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - attributs = {} - for key, value in self._attributs.items(): - attributs[key] = self.coordinator.data["dsl_status"].get(value) - return attributs diff --git a/custom_components/livebox/services.yaml b/custom_components/livebox/services.yaml deleted file mode 100644 index 3d8b51a..0000000 --- a/custom_components/livebox/services.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Livebox service entries description. - -reboot: - # Description of the service - description: Restart the Livebox. - -remove_call_missed: - description: Remove call missed - fields: - callId: - name: Call Id - description: ID of the call (information in the attribute field of the binary sensor). Value to put in quotation marks. If you omit so that it deletes all calls - example: '"666"' diff --git a/custom_components/livebox/strings.json b/custom_components/livebox/strings.json deleted file mode 100644 index ec1e6ac..0000000 --- a/custom_components/livebox/strings.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "[%key:components::livebox::config::step::user::title%]", - "data": { - "host": "[%key:common::config_flow::data::host%]", - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]", - "port": "[%key:common::config_flow::data::port%]" - } - } - }, - "error": { - "login_inccorect": "[%key:components::livebox::config::error::login_inccorect%]", - "insufficient_permission": "[%key:components::livebox::config::error::insufficient_permission%]", - "unknown": "[%key:common::config_flow::error::unknown%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - } - }, - "options": { - "step": { - "init": { - "title": "[%key:components::livebox::options::step::init::title%]", - "description": "[%key:components::livebox::options::step::init::description%]", - "data": { - "lan_tracking": "[%key:components::livebox::options::step::init::data::lan_tracking%]", - "timeout_tracking": "[%key:components::livebox::options::step::init::data::timeout_tracking%]" - } - } - } - } -} diff --git a/custom_components/livebox/switch.py b/custom_components/livebox/switch.py deleted file mode 100644 index 8fc8159..0000000 --- a/custom_components/livebox/switch.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Sensor for Livebox router.""" -import logging - -from homeassistant.components.switch import SwitchEntity -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import COORDINATOR, DOMAIN, GUESTWIFI_ICON, LIVEBOX_API, LIVEBOX_ID -from .coordinator import LiveboxDataUpdateCoordinator - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the sensors.""" - datas = hass.data[DOMAIN][config_entry.entry_id] - box_id = datas[LIVEBOX_ID] - api = datas[LIVEBOX_API] - coordinator = datas[COORDINATOR] - async_add_entities([WifiSwitch(coordinator, box_id, api)], True) - async_add_entities([GuestWifiSwitch(coordinator, box_id, api)], True) - - -class WifiSwitch(CoordinatorEntity[LiveboxDataUpdateCoordinator], SwitchEntity): - """Representation of a livebox sensor.""" - - _attr_name = "Wifi switch" - _attr_has_entity_name = True - - def __init__(self, coordinator, box_id, api): - """Initialize the sensor.""" - super().__init__(coordinator) - self._api = api - self._attr_unique_id = f"{box_id}_wifi" - self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} - - @property - def is_on(self): - """Return true if device is on.""" - return self.coordinator.data.get("wifi") - - async def async_turn_on(self): - """Turn the switch on.""" - parameters = {"Enable": "true", "Status": "true"} - await self.hass.async_add_executor_job(self._api.wifi.set_wifi, parameters) - await self.coordinator.async_request_refresh() - - async def async_turn_off(self): - """Turn the switch off.""" - parameters = {"Enable": "false", "Status": "false"} - await self.hass.async_add_executor_job(self._api.wifi.set_wifi, parameters) - await self.coordinator.async_request_refresh() - - -class GuestWifiSwitch(CoordinatorEntity, SwitchEntity): - """Representation of a livebox sensor.""" - - _attr_name = "Guest Wifi switch" - _attr_icon = GUESTWIFI_ICON - _attr_has_entity_name = True - - def __init__(self, coordinator, box_id, api): - """Initialize the sensor.""" - super().__init__(coordinator) - self._api = api - self._attr_unique_id = f"{box_id}_guest_wifi" - self._attr_device_info = {"identifiers": {(DOMAIN, box_id)}} - - @property - def is_on(self): - """Return true if device is on.""" - return self.coordinator.data.get("guest_wifi") - - async def async_turn_on(self): - """Turn the switch on.""" - parameters = {"Enable": "true", "Status": "true"} - await self.hass.async_add_executor_job( - self._api.guestwifi.set_guest_wifi, parameters - ) - await self.coordinator.async_request_refresh() - - async def async_turn_off(self): - """Turn the switch off.""" - parameters = {"Enable": "false", "Status": "false"} - await self.hass.async_add_executor_job( - self._api.guestwifi.set_guest_wifi, parameters - ) - await self.coordinator.async_request_refresh() diff --git a/custom_components/livebox/translations/en.json b/custom_components/livebox/translations/en.json deleted file mode 100644 index 1e12417..0000000 --- a/custom_components/livebox/translations/en.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "title": "Orange Livebox", - "step": { - "user": { - "title": "Check your livebox", - "data": { - "host": "IP Address", - "username": "Username", - "password": "Password", - "port": "Port" - } - } - }, - "error": { - "login_inccorect": "User or password inccorect", - "insufficient_permission": "Insufficient permission.", - "unknown": "Unknown error occurred", - "cannot_connect": "Unable to connect to the livebox" - } - }, - "options": { - "step": { - "init": { - "title": "Device Tracking - Track devices on lan", - "description": "you want tracking yours wired devices in addition to wireless devices", - "data": { - "lan_tracking": "Wired tracking", - "timeout_tracking": "Timeout tracking" - } - } - } - } -} diff --git a/custom_components/livebox/translations/fr.json b/custom_components/livebox/translations/fr.json deleted file mode 100644 index d354c76..0000000 --- a/custom_components/livebox/translations/fr.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "title": "Orange Livebox", - "step": { - "user": { - "title": "Vérification de votre livebox.", - "data": { - "host": "Adresse IP", - "username": "Utilisateur", - "password": "Mot de passe", - "port": "Port" - } - } - }, - "error": { - "login_inccorect": "Utilisateur ou mot de passe incorrect.", - "insufficient_permission": "Permission insuffisante.", - "unknown": "Erreur inconnue.", - "cannot_connect": "Impossible de se connecter à la Livebox." - } - }, - "options": { - "step": { - "init": { - "title": "Suivre les équipements filaires", - "description": "Pour suivre les équipements filaires en plus des équipements Wifi", - "data": { - "lan_tracking": "Equipements Filaires", - "timeout_tracking": "Délai avant de considérer un équipement absent" - } - } - } - } -} diff --git a/custom_components/livebox/translations/nb.json b/custom_components/livebox/translations/nb.json deleted file mode 100644 index e7da10b..0000000 --- a/custom_components/livebox/translations/nb.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "title": "Orange Livebox", - "step": { - "user": { - "title": "Sjekk liveboxen din", - "data": { - "host": "IP Address", - "username": "Brukernavn", - "password": "Passord", - "port": "Port" - } - } - }, - "error": { - "login_inccorect": "Feil bruker eller passord", - "insufficient_permission": "Utilstrekkelig tillatelse.", - "unknown": "Det oppstod en ukjent feil", - "cannot_connect": "Kan ikke koble til liveboxen" - } - }, - "options": { - "step": { - "init": { - "title": "Enhetssporing - Spor enheter på lan", - "description": "du vil spore dine kablede enheter i tillegg til trådløse enheter", - "data": { - "lan_tracking": "Kablet sporing", - "timeout_tracking": "Tid før overvejelse om manglende udstyr" - } - } - } - } -} From 9d303e3009bf0a8fdab1e102e9c84d7fb048d3c9 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:39:25 +0000 Subject: [PATCH 31/74] update readme --- README.md | 89 ++++++++++++++++++++++++++++------------------- README_EXAMPLE.md | 66 ----------------------------------- 2 files changed, 54 insertions(+), 101 deletions(-) delete mode 100644 README_EXAMPLE.md diff --git a/README.md b/README.md index a6db09e..4702163 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,66 @@ -# Notice +# Delta Dore Tydom -The component and platforms in this repository are not meant to be used by a -user, but as a "blueprint" that custom component developers can build -upon, to make more awesome stuff. +[![License][license-shield]](LICENSE) -HAVE FUN! 😎 +[![BuyMeCoffee][buymecoffeebadge]][buymecoffee] -## Why? +This a *custom component* for [Home Assistant](https://www.home-assistant.io/). -This is simple, by having custom_components look (README + structure) the same -it is easier for developers to help each other and for users to start using them. +The `Delta Dore Tydom` integration allows you to observe and control [Delta Dore Tydom smart home gateway](https://www.deltadore.fr/). -If you are a developer and you want to add things to this "blueprint" that you think more -developers will have use for, please open a PR to add it :) +This integration can work in local mode or cloud mode depending on how the integration is configured (see Configuration part) -## What? +![GitHub release](https://img.shields.io/github/release/CyrilP/hass-deltadore-tydom-component) +[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) -This repository contains multiple files, here is a overview: +**This integration will set up the following platforms.** -File | Purpose | Documentation --- | -- | -- -`.devcontainer.json` | Used for development/testing with Visual Studio Code. | [Documentation](https://code.visualstudio.com/docs/remote/containers) -`.github/ISSUE_TEMPLATE/*.yml` | Templates for the issue tracker | [Documentation](https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository) -`.vscode/tasks.json` | Tasks for the devcontainer. | [Documentation](https://code.visualstudio.com/docs/editor/tasks) -`custom_components/integration_blueprint/*` | Integration files, this is where everything happens. | [Documentation](https://developers.home-assistant.io/docs/creating_component_index) -`CONTRIBUTING.md` | Guidelines on how to contribute. | [Documentation](https://help.github.com/en/github/building-a-strong-community/setting-guidelines-for-repository-contributors) -`LICENSE` | The license file for the project. | [Documentation](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/licensing-a-repository) -`README.md` | The file you are reading now, should contain info about the integration, installation and configuration instructions. | [Documentation](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax) -`requirements.txt` | Python packages used for development/lint/testing this integration. | [Documentation](https://pip.pypa.io/en/stable/user_guide/#requirements-files) +Platform | Description +-- | -- +`binary_sensor` | Show something `True` or `False`. +`sensor` | Show info from blueprint API. +`switch` | Switch something `True` or `False`. -## How? +## Installation -1. Create a new repository in GitHub, using this repository as a template by clicking the "Use this template" button in the GitHub UI. -1. Open your new repository in Visual Studio Code devcontainer (Preferably with the "`Dev Containers: Clone Repository in Named Container Volume...`" option). -1. Rename all instances of the `integration_blueprint` to `custom_components/` (e.g. `custom_components/awesome_integration`). -1. Rename all instances of the `Integration Blueprint` to `` (e.g. `Awesome Integration`). -1. Run the `scrtipts/develop` to start HA and test out your new integration. +The preferred way to install the Delta Dore Tydom integration is by addig it using HACS. -## Next steps +Add your device via the Integration menu -These are some next steps you may want to look into: -- Add tests to your integration, [`pytest-homeassistant-custom-component`](https://github.com/MatthewFlamm/pytest-homeassistant-custom-component) can help you get started. -- Add brand images (logo/icon) to https://github.com/home-assistant/brands. -- Create your first release. -- Share your integration on the [Home Assistant Forum](https://community.home-assistant.io/). -- Submit your integration to the [HACS](https://hacs.xyz/docs/publish/start). +[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=deltadore-tydom) + +Manual method : + +1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). +1. If you do not have a `custom_components` directory (folder) there, you need to create it. +1. In the `custom_components` directory (folder) create a new folder called `deltadore-tydom`. +1. Download _all_ the files from the `custom_components/deltadore-tydom/` directory (folder) in this repository. +1. Place the files you downloaded in the new directory (folder) you created. +1. Restart Home Assistant +1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Delta Dore Tydom" + +## Configuration is done in the UI + + +The hostname/ip can be : +* The hostname/ip of your Tydom (local mode only). An access to the cloud is done to retrieve the Tydom credentials +* mediation.tydom.com. Using this configuration makes the integration work through the cloud + +The Mac address is the Mac of you Tydom + +Email/Password are you Dela Dore credentials + +The alarm PIN is optional and used to set your alarm mode + +## Contributions are welcome! + +If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md) + +*** + +[integration_blueprint]: https://github.com/CyrilP/hass-deltadore-tydom-component +[buymecoffee]: https://www.buymeacoffee.com/cyrilp +[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge +[exampleimg]: example.png +[forum]: https://community.home-assistant.io/ +[license-shield]: https://img.shields.io/github/license/CyrilP/hass-deltadore-tydom-component.svg?style=for-the-badge diff --git a/README_EXAMPLE.md b/README_EXAMPLE.md deleted file mode 100644 index 9a14959..0000000 --- a/README_EXAMPLE.md +++ /dev/null @@ -1,66 +0,0 @@ -# Delta Dore Tydom - -[![License][license-shield]](LICENSE) - -[![BuyMeCoffee][buymecoffeebadge]][buymecoffee] - -This a *custom component* for [Home Assistant](https://www.home-assistant.io/). - -The `Delta Dore Tydom` integration allows you to observe and control [Delta Dore Tydom smart home gateway](https://www.deltadore.fr/). - -This integration can work in local mode or cloud mode depending on how the integration is configured (see Configuration part) - -![GitHub release](https://img.shields.io/github/release/CyrilP/hass-deltadore-tydom-component) -[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) - -**This integration will set up the following platforms.** - -Platform | Description --- | -- -`binary_sensor` | Show something `True` or `False`. -`sensor` | Show info from blueprint API. -`switch` | Switch something `True` or `False`. - -## Installation - -The preferred way to install the Delta Dore Tydom integration is by addig it using HACS. - -Add your device via the Integration menu - -[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=deltadore-tydom) - -Manual method : - -1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). -1. If you do not have a `custom_components` directory (folder) there, you need to create it. -1. In the `custom_components` directory (folder) create a new folder called `deltadore-tydom`. -1. Download _all_ the files from the `custom_components/deltadore-tydom/` directory (folder) in this repository. -1. Place the files you downloaded in the new directory (folder) you created. -1. Restart Home Assistant -1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Delta Dore Tydom" - -## Configuration is done in the UI - - -The hostname/ip can be : -* The hostname/ip of your Tydom (local mode only). An access to the cloud is done to retrieve the Tydom credentials -* mediation.tydom.com. Using this configuration makes the integration work through the cloud - -The Mac address is the Mac of you Tydom - -Email/Password are you Dela Dore credentials - -The alarm PIN is optional and used to set your alarm mode - -## Contributions are welcome! - -If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md) - -*** - -[integration_blueprint]: https://github.com/CyrilP/hass-deltadore-tydom-component -[buymecoffee]: https://www.buymeacoffee.com/cyrilp -[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge -[exampleimg]: example.png -[forum]: https://community.home-assistant.io/ -[license-shield]: https://img.shields.io/github/license/CyrilP/hass-deltadore-tydom-component.svg?style=for-the-badge From 2db1656598b1441f4f71e9fb58cafe415caa767c Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:50:29 +0000 Subject: [PATCH 32/74] add areas calls --- .../deltadore-tydom/tydom/tydom_client.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index d23bb67..b7a40e6 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -377,6 +377,24 @@ async def get_devices_cmeta(self): req = "GET" await self.send_message(method=req, msg=msg_type) + async def get_areas_meta(self): + """Get areas metadata""" + msg_type = "/areas/meta" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + async def get_areas_cmeta(self): + """Get areas metadata""" + msg_type = "/areas/cmeta" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + async def get_areas_data(self): + """Get areas metadata""" + msg_type = "/areas/data" + req = "GET" + await self.send_message(method=req, msg=msg_type) + async def get_data(self): """Get all config/metadata/data""" await self.get_configs_file() From 6d03d7cd27794e664ccd285d7c268765f044f00a Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Mon, 21 Aug 2023 12:02:15 +0200 Subject: [PATCH 33/74] add cover tilt, try api mode, implement other messages --- .../deltadore-tydom/ha_entities.py | 46 +++++++++++++++++-- custom_components/deltadore-tydom/hub.py | 9 ++-- .../deltadore-tydom/tydom/MessageHandler.py | 2 +- .../deltadore-tydom/tydom/README.md | 29 ++++++++++-- .../deltadore-tydom/tydom/tydom_client.py | 40 +++++++++++++++- .../deltadore-tydom/tydom/tydom_devices.py | 37 +++++++++++++-- 6 files changed, 148 insertions(+), 15 deletions(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 0586cae..d0e25d3 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -24,10 +24,15 @@ from homeassistant.helpers.entity import Entity, DeviceInfo, Entity from homeassistant.components.cover import ( ATTR_POSITION, + ATTR_TILT_POSITION, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_STOP, + SUPPORT_SET_TILT_POSITION, + SUPPORT_OPEN_TILT, + SUPPORT_CLOSE_TILT, + SUPPORT_STOP_TILT, CoverEntity, CoverDeviceClass, ) @@ -299,9 +304,7 @@ class HACover(CoverEntity): # imported above, we can tell HA the features that are supported by this entity. # If the supported features were dynamic (ie: different depending on the external # device it connected to), then this should be function with an @property decorator. - supported_features = ( - SUPPORT_SET_POSITION | SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP - ) + supported_features = 0 device_class = CoverDeviceClass.SHUTTER sensor_classes = { @@ -330,6 +333,22 @@ def __init__(self, shutter: TydomShutter) -> None: # entity screens, and used to build the Entity ID that's used is automations etc. self._attr_name = self._shutter.device_name self._registered_sensors = [] + if hasattr(shutter, "position"): + self.supported_features = ( + self.supported_features + | SUPPORT_SET_POSITION + | SUPPORT_OPEN + | SUPPORT_CLOSE + | SUPPORT_STOP + ) + if hasattr(shutter, "slope"): + self.supported_features = ( + self.supported_features + | SUPPORT_SET_TILT_POSITION + | SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_STOP_TILT + ) async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" @@ -404,6 +423,11 @@ def is_closed(self) -> bool: """Return if the cover is closed, same as position 0.""" return self._shutter.position == 0 + @property + def current_cover_tilt_position(self): + """Return the current tilt position of the cover.""" + return self._shutter.slope + # @property # def is_closing(self) -> bool: # """Return if the cover is closing or not.""" @@ -432,6 +456,22 @@ async def async_set_cover_position(self, **kwargs: Any) -> None: """Set the cover's position.""" await self._shutter.set_position(kwargs[ATTR_POSITION]) + async def async_open_cover_tilt(self, **kwargs): + """Open the cover tilt.""" + await self._shutter.slope_open() + + async def async_close_cover_tilt(self, **kwargs): + """Close the cover tilt.""" + await self._shutter.slope_close() + + async def async_set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position.""" + await self._shutter.set_slope_position(kwargs[ATTR_TILT_POSITION]) + + async def async_stop_cover_tilt(self, **kwargs): + """Stop the cover tilt.""" + await self._shutter.slope_stop() + def get_sensors(self) -> list: """Get available sensors for this entity""" sensors = [] diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index ebe399b..6ff0783 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession from .tydom.tydom_client import TydomClient -from .tydom.tydom_devices import TydomBaseEntity +from .tydom.tydom_devices import Tydom from .ha_entities import * logger = logging.getLogger(__name__) @@ -47,7 +47,7 @@ def __init__( self._hass = hass self._name = mac self._id = "Tydom-" + mac - self.device_info = TydomBaseEntity( + self.device_info = Tydom( None, None, None, None, None, None, None, None, None, None, None, False ) self.devices = {} @@ -108,7 +108,7 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: if devices is not None: for device in devices: logger.info("*** device %s", device) - if isinstance(device, TydomBaseEntity): + if isinstance(device, Tydom): await self.device_info.update_device(device) else: logger.warn("*** publish_updates for device : %s", device) @@ -237,7 +237,8 @@ async def ping(self) -> None: async def async_trigger_firmware_update(self) -> None: """Trigger firmware update""" - logger.info("Installing update...") + logger.info("Installing firmware update...") + self._tydom_client.update_firmware() class Roller: diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index dd503a3..7881347 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -356,7 +356,7 @@ async def parse_msg_info(self, parsed): boot_version = parsed["bootVersion"] update_available = parsed["updateAvailable"] return [ - TydomBaseEntity( + Tydom( product_name, main_version_sw, main_version_hw, diff --git a/custom_components/deltadore-tydom/tydom/README.md b/custom_components/deltadore-tydom/tydom/README.md index 261e21d..dd3aa70 100644 --- a/custom_components/deltadore-tydom/tydom/README.md +++ b/custom_components/deltadore-tydom/tydom/README.md @@ -1,4 +1,4 @@ -# How it Tydom working ? +# How is Tydom working ? ## Authentication GET request to https://deltadoreadb2ciot.b2clogin.com/deltadoreadb2ciot.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_AccountProviderROPC_SignIn @@ -30,5 +30,28 @@ POST request to https://pilotage.iotdeltadore.com/pilotageservice/api/v1/control } The id is the mac address - - +this is also available using websockets. + +connection to Tydom/cloud mediation server +GET request to https://host:443/mediation/client?mac=001Axxxxxxxx&appli=1 +host is either tydom ip or mediation server address (depending on mode used) +This first request is used to get the digest authentication informations and to indicate the server that we want to upgrade connection to websockets using request headers + +GET request to wss://host:443/mediation/client?mac=001Axxxxxxxx&appli=1 +this request is sent with digest authentication challenge answer + +We now have a websocket connection to send commands + +Init flow used by the app : +GET /ping +GET /info +PUT /configs/gateway/api_mode +GET /configs/gateway/geoloc +GET /configs/gateway/local_claim +GET /devices/meta +GET /areas/meta +GET /devices/cmeta +GET /areas/cmeta +GET /devices/data +GET /areas/data +POST /refresh/all diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index b7a40e6..a797684 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -241,9 +241,23 @@ async def listen_tydom(self, connection: ClientWebSocketResponse): """Listen for Tydom messages""" logger.info("Listen for Tydom messages") self._connection = connection + await self.ping() await self.get_info() + await self.put_api_mode() + # await self.get_geoloc() + # await self.get_local_claim() + # await self.get_devices_meta() + # await self.get_areas_meta() + # await self.get_devices_cmeta() + # await self.get_areas_cmeta() + # await self.get_devices_data() + # await self.get_areas_data() + # await self.post_refresh() + + # await self.get_info() await self.post_refresh() await self.get_configs_file() + await self.get_devices_meta() await self.get_devices_cmeta() await self.get_devices_data() @@ -327,6 +341,24 @@ async def get_info(self): req = "GET" await self.send_message(method=req, msg=msg_type) + async def get_local_claim(self): + """Ask some information from Tydom""" + msg_type = "/configs/gateway/local_claim" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + async def get_geoloc(self): + """Ask some information from Tydom""" + msg_type = "/configs/gateway/geoloc" + req = "GET" + await self.send_message(method=req, msg=msg_type) + + async def put_api_mode(self): + """Use Tydom API mode ?""" + msg_type = "/configs/gateway/api_mode" + req = "PUT" + await self.send_message(method=req, msg=msg_type) + async def post_refresh(self): """Refresh (all)""" msg_type = "/refresh/all" @@ -366,7 +398,7 @@ async def get_devices_data(self): await self.get_poll_device_data(url) async def get_configs_file(self): - """List the device to get the endpoint id""" + """List the devices to get the endpoint id""" msg_type = "/configs/file" req = "GET" await self.send_message(method=req, msg=msg_type) @@ -519,3 +551,9 @@ async def put_alarm_cdata(self, device_id, alarm_id=None, value=None, zone_id=No logger.error(a_bytes) except BaseException: logger.error("put_alarm_cdata ERROR !", exc_info=True) + + async def update_firmware(self): + """Update Tydom firmware""" + msg_type = "/configs/gateway/update" + req = "PUT" + await self.send_message(method=req, msg=msg_type) diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index da2a2b8..e4c08d6 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -5,8 +5,8 @@ logger = logging.getLogger(__name__) -class TydomBaseEntity: - """Tydom entity base class.""" +class Tydom: + """Tydom""" def __init__( self, @@ -81,7 +81,6 @@ def __init__(self, tydom_client, uid, device_id, name, device_type, endpoint, da self._type = device_type self._endpoint = endpoint self._callbacks = set() - for key in data: setattr(self, key, data[key]) @@ -160,6 +159,38 @@ async def set_position(self, position: int) -> None: self._id, self._endpoint, "position", str(position) ) + # FIXME replace command + async def slope_open(self) -> None: + """Tell the cover to tilt open""" + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "positionCmd", "DOWN" + ) + + # FIXME replace command + async def slope_close(self) -> None: + """Tell the cover to tilt closed""" + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "positionCmd", "UP" + ) + + # FIXME replace command + async def slope_stop(self) -> None: + """Tell the cover to stop tilt""" + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "positionCmd", "STOP" + ) + + # FIXME replace command + async def set_slope_position(self, position: int) -> None: + """ + Set cover to the given position. + """ + logger.error("set roller tilt position (device) to : %s", position) + + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "position", str(position) + ) + class TydomEnergy(TydomDevice): """Represents an energy sensor (for example TYWATT)""" From cf7cb515e38c2e01c103b9c6b91e2862477cc3b2 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Mon, 21 Aug 2023 12:58:10 +0200 Subject: [PATCH 34/74] update documentation --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 4702163..135f0b7 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,17 @@ Platform | Description `binary_sensor` | Show something `True` or `False`. `sensor` | Show info from blueprint API. `switch` | Switch something `True` or `False`. +`cover` | controls an opening or cover. +`climate` | controls temperature, humidity, or fans. +`light` | controls a light. +`alarm_control_panel` | controls an alarm. + +Tested hardware +- Cover (Up/Down/Stop) +- Tywatt 5400 +- Tyxal+ DFR +- K-Line DVI +- Typass ATL (zones temperatures, target temperature, mode, power usage) ## Installation From 26d84ab31574dcc6159e98d73386913e214351cc Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Mon, 21 Aug 2023 12:58:34 +0200 Subject: [PATCH 35/74] remove useless code --- custom_components/deltadore-tydom/tydom/tydom_client.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index a797684..8a51501 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -427,12 +427,6 @@ async def get_areas_data(self): req = "GET" await self.send_message(method=req, msg=msg_type) - async def get_data(self): - """Get all config/metadata/data""" - await self.get_configs_file() - await self.get_devices_cmeta() - await self.get_devices_data() - async def get_device_data(self, id): """Give order to endpoint""" # 10 here is the endpoint = the device (shutter in this case) to open. From f19cfbf41970f4c9620057396f561d112201536b Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Mon, 21 Aug 2023 13:57:46 +0200 Subject: [PATCH 36/74] fix slope --- custom_components/deltadore-tydom/ha_entities.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index d0e25d3..79f0646 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -426,7 +426,10 @@ def is_closed(self) -> bool: @property def current_cover_tilt_position(self): """Return the current tilt position of the cover.""" - return self._shutter.slope + if hasattr(self._shutter, "slope"): + return self._shutter.slope + else: + return None # @property # def is_closing(self) -> bool: From 060452669ac96a4a7c13c8d2f9c8c35f8594c541 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Wed, 6 Sep 2023 18:40:40 +0200 Subject: [PATCH 37/74] updates --- custom_components/deltadore-tydom/__init__.py | 15 ++- .../deltadore-tydom/tydom/MessageHandler.py | 116 +++++++++++++++--- .../deltadore-tydom/tydom/tydom_client.py | 13 +- 3 files changed, 118 insertions(+), 26 deletions(-) diff --git a/custom_components/deltadore-tydom/__init__.py b/custom_components/deltadore-tydom/__init__.py index 9e76ae9..6159c65 100644 --- a/custom_components/deltadore-tydom/__init__.py +++ b/custom_components/deltadore-tydom/__init__.py @@ -11,7 +11,15 @@ # List of platforms to support. There should be a matching .py file for each, # eg and -PLATFORMS: list[str] = [Platform.CLIMATE, Platform.COVER, Platform.SENSOR, Platform.LOCK, Platform.LIGHT, Platform.UPDATE] +PLATFORMS: list[str] = [ + # Platform.ALARM_CONTROL_PANEL, + Platform.CLIMATE, + Platform.COVER, + Platform.SENSOR, + Platform.LOCK, + Platform.LIGHT, + Platform.UPDATE, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -36,13 +44,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_create_background_task( target=tydom_hub.setup(connection), hass=hass, name="Tydom" ) - #entry.async_create_background_task( + # entry.async_create_background_task( # target=tydom_hub.ping(connection), hass=hass, name="Tydom ping" - #) + # ) except Exception as err: raise ConfigEntryNotReady from err - # This creates each HA object for each platform your device requires. # It's done by calling the `async_setup_entry` function in each platform module. await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 7881347..877fe09 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -5,6 +5,7 @@ from io import BytesIO import traceback import urllib3 +import re from .tydom_devices import * @@ -221,15 +222,84 @@ def __init__(self, tydom_client, cmd_prefix): self.tydom_client = tydom_client self.cmd_prefix = cmd_prefix + @staticmethod + def get_uri_origin(data) -> str: + """Extract Uri-Origin from Tydom messages if present""" + uri_origin = "" + re_matcher = re.match( + ".*Uri-Origin: ([a-zA-Z0-9\\-._~:/?#\\[\\]@!$&'\\(\\)\\*\\+,;%=]+).*", + data, + ) + + if re_matcher: + # logger.info("///// Uri-Origin : %s", re_matcher.group(1)) + uri_origin = re_matcher.group(1) + # else: + # logger.info("///// no match") + return uri_origin + + @staticmethod + def get_http_request_line(data) -> str: + """Extract Http request line""" + request_line = "" + re_matcher = re.match( + "b'(.*)HTTP/1.1", + data, + ) + if re_matcher: + # logger.info("///// PUT : %s", re_matcher.group(1)) + request_line = re_matcher.group(1) + # else: + # logger.info("///// no match") + return request_line + async def incoming_triage(self, bytes_str): """Identify message type and dispatch the result""" incoming = None first = str(bytes_str[:40]) + + # Find Uri-Origin in header if available + uri_origin = MessageHandler.get_uri_origin(str(bytes_str)) + + # Find http request line before http response + http_request_line = MessageHandler.get_http_request_line(str(bytes_str)) + try: - if "Uri-Origin: /refresh/all" in first in first: + if len(http_request_line) > 0: + logger.debug("%s detected !", http_request_line) + try: + try: + incoming = self.parse_put_response(bytes_str) + except BaseException: + # Tywatt response starts at 7 + incoming = self.parse_put_response(bytes_str, 7) + return await self.parse_response( + incoming, uri_origin, http_request_line + ) + except BaseException: + logger.error("Error when parsing tydom message (%s)", bytes_str) + return None + elif len(uri_origin) > 0: + response = self.response_from_bytes(bytes_str[len(self.cmd_prefix) :]) + incoming = response.data.decode("utf-8") + try: + return await self.parse_response( + incoming, uri_origin, http_request_line + ) + except BaseException: + logger.error("Error when parsing tydom message (%s)", bytes_str) + return None + else: + logger.warning("Unknown tydom message type received (%s)", bytes_str) + return None + + """ + if "/refresh/all" in uri_origin: pass - elif ("PUT /devices/data" in first) or ("/devices/cdata" in first): + elif ("PUT /devices/data" in http_request_line) or ( + "/devices/cdata" in http_request_line + ): logger.debug("PUT /devices/data message detected !") try: try: @@ -245,9 +315,7 @@ async def incoming_triage(self, bytes_str): return None elif "scn" in first: try: - # FIXME - # incoming = get(bytes_str) - incoming = first + incoming = str(bytes_str) scenarii = await self.parse_response(incoming) logger.debug("Scenarii message processed") return scenarii @@ -256,7 +324,7 @@ async def incoming_triage(self, bytes_str): "Error when parsing Scenarii tydom message (%s)", bytes_str ) return None - elif "POST" in first: + elif "POST" in http_request_line: try: incoming = self.parse_put_response(bytes_str) post = await self.parse_response(incoming) @@ -267,6 +335,8 @@ async def incoming_triage(self, bytes_str): "Error when parsing POST tydom message (%s)", bytes_str ) return None + elif "/devices/meta" in uri_origin: + pass elif "HTTP/1.1" in first: response = self.response_from_bytes(bytes_str[len(self.cmd_prefix) :]) incoming = response.data.decode("utf-8") @@ -279,34 +349,44 @@ async def incoming_triage(self, bytes_str): return None else: logger.warning("Unknown tydom message type received (%s)", bytes_str) - return None + return None """ - except Exception as e: - logger.error("Technical error when parsing tydom message (%s)", bytes_str) + except Exception as ex: + logger.error( + "Technical error when parsing tydom message (%s) : %s", bytes_str, ex + ) logger.debug("Incoming payload (%s)", incoming) - logger.debug("exception : %s", e) + logger.debug("exception : %s", ex) return None # Basic response parsing. Typically GET responses + instanciate covers and # alarm class for updating data - async def parse_response(self, incoming): + async def parse_response(self, incoming, uri_origin, http_request_line): data = incoming msg_type = None first = str(data[:40]) if data != "": - if "id_catalog" in data: + if "/configs/file" in uri_origin: msg_type = "msg_config" - elif "cmetadata" in data: + elif "/devices/cmeta" in uri_origin: msg_type = "msg_cmetadata" + elif "/configs/gateway/api_mode" in uri_origin: + msg_type = "msg_api_mode" + elif "/groups/file" in uri_origin: + msg_type = "msg_groups" + elif "/devices/meta" in uri_origin: + msg_type = "msg_metadata" + elif "/scenarios/file" in uri_origin: + msg_type = "msg_scenarios" elif "cdata" in data: msg_type = "msg_cdata" - elif "id" in first: - msg_type = "msg_data" elif "doctype" in first: msg_type = "msg_html" - elif "productName" in first: + elif "/info" in uri_origin: msg_type = "msg_info" + elif "id" in first: + msg_type = "msg_data" if msg_type is None: logger.warning("Unknown message type received %s", data) @@ -391,7 +471,7 @@ async def get_device( # SHUTTER = "shutter" # WINDOW = "window" match last_usage: - case "shutter" | "klineShutter": + case "shutter" | "klineShutter" | "awning": return TydomShutter( tydom_client, uid, device_id, name, last_usage, endpoint, data ) @@ -435,7 +515,7 @@ async def get_device( return TydomSmoke( tydom_client, uid, device_id, name, last_usage, endpoint, data ) - case "boiler": + case "boiler" | "sh_hvac": return TydomBoiler( tydom_client, uid, device_id, name, last_usage, endpoint, data ) diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index 8a51501..dc3a459 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -255,12 +255,15 @@ async def listen_tydom(self, connection: ClientWebSocketResponse): # await self.post_refresh() # await self.get_info() + await self.get_groups() await self.post_refresh() await self.get_configs_file() await self.get_devices_meta() await self.get_devices_cmeta() await self.get_devices_data() + await self.get_scenarii() + async def consume_messages(self): """Read and parse incomming messages""" try: @@ -459,6 +462,12 @@ async def get_scenarii(self): req = "GET" await self.send_message(method=req, msg=msg_type) + async def get_groups(self): + """Get the groups""" + msg_type = "/groups/file" + req = "GET" + await self.send_message(method=req, msg=msg_type) + async def put_devices_data(self, device_id, endpoint_id, name, value): """Give order (name + value) to endpoint""" # For shutter, value is the percentage of closing @@ -475,10 +484,6 @@ async def put_devices_data(self, device_id, endpoint_id, name, value): ) a_bytes = bytes(str_request, "ascii") logger.debug("Sending message to tydom (%s %s)", "PUT data", body) - # self._connection.send_bytes - # self._connection.send_json - # self._connection.send_str - # await self._connection.send(a_bytes) await self._connection.send_bytes(a_bytes) return 0 From 9555eb73e8d43b3ebe0302b4727211d35b172dec Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Sun, 17 Sep 2023 23:10:37 +0200 Subject: [PATCH 38/74] various updates --- README.md | 9 +- config/configuration.yaml | 3 +- .../deltadore-tydom/alarm_control_panel.py | 86 ++++++ .../deltadore-tydom/config_flow.py | 91 ++++++- .../deltadore-tydom/ha_entities.py | 174 +++++++++++-- .../deltadore-tydom/manifest.json | 2 +- .../deltadore-tydom/translations/en.json | 19 +- .../deltadore-tydom/tydom/MessageHandler.py | 244 ++++++++---------- .../deltadore-tydom/tydom/tydom_client.py | 63 +++-- .../deltadore-tydom/tydom/tydom_devices.py | 10 + 10 files changed, 495 insertions(+), 206 deletions(-) create mode 100644 custom_components/deltadore-tydom/alarm_control_panel.py diff --git a/README.md b/README.md index 135f0b7..fb482dd 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This a *custom component* for [Home Assistant](https://www.home-assistant.io/). The `Delta Dore Tydom` integration allows you to observe and control [Delta Dore Tydom smart home gateway](https://www.deltadore.fr/). This integration can work in local mode or cloud mode depending on how the integration is configured (see Configuration part) +The Delta Dore gateway can be detected using dhcp discovery. ![GitHub release](https://img.shields.io/github/release/CyrilP/hass-deltadore-tydom-component) [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) @@ -18,19 +19,21 @@ This integration can work in local mode or cloud mode depending on how the integ Platform | Description -- | -- `binary_sensor` | Show something `True` or `False`. -`sensor` | Show info from blueprint API. +`sensor` | Show info. `switch` | Switch something `True` or `False`. `cover` | controls an opening or cover. `climate` | controls temperature, humidity, or fans. `light` | controls a light. `alarm_control_panel` | controls an alarm. +`update` | firmware update + +**This integration has been tested with the following hardware.** -Tested hardware - Cover (Up/Down/Stop) - Tywatt 5400 - Tyxal+ DFR - K-Line DVI -- Typass ATL (zones temperatures, target temperature, mode, power usage) +- Typass ATL (zones temperatures, target temperature, mode, presets, water/heat power usage) ## Installation diff --git a/config/configuration.yaml b/config/configuration.yaml index 9e4088c..b15b6b7 100644 --- a/config/configuration.yaml +++ b/config/configuration.yaml @@ -3,7 +3,8 @@ default_config: # https://www.home-assistant.io/integrations/logger/ logger: - default: warn + default: info logs: custom_components.deltadore-tydom: debug +# demo: diff --git a/custom_components/deltadore-tydom/alarm_control_panel.py b/custom_components/deltadore-tydom/alarm_control_panel.py new file mode 100644 index 0000000..a8a458e --- /dev/null +++ b/custom_components/deltadore-tydom/alarm_control_panel.py @@ -0,0 +1,86 @@ +"""Platform for alarm control panel integration.""" +from __future__ import annotations + +import datetime + +from homeassistant.components.manual.alarm_control_panel import ManualAlarm +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_ARMING_TIME, + CONF_DELAY_TIME, + CONF_TRIGGER_TIME, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Demo alarm control panel platform.""" + async_add_entities( + [ + ManualAlarm( # type:ignore[no-untyped-call] + hass, + "Security", + "1234", + None, + True, + False, + { + STATE_ALARM_ARMED_AWAY: { + CONF_ARMING_TIME: datetime.timedelta(seconds=5), + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_ARMED_HOME: { + CONF_ARMING_TIME: datetime.timedelta(seconds=5), + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_ARMED_NIGHT: { + CONF_ARMING_TIME: datetime.timedelta(seconds=5), + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_ARMED_VACATION: { + CONF_ARMING_TIME: datetime.timedelta(seconds=5), + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_DISARMED: { + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_ARMED_CUSTOM_BYPASS: { + CONF_ARMING_TIME: datetime.timedelta(seconds=5), + CONF_DELAY_TIME: datetime.timedelta(seconds=0), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_TRIGGERED: { + CONF_ARMING_TIME: datetime.timedelta(seconds=5) + }, + }, + ) + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index 7a9bfaa..7afb514 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -7,11 +7,11 @@ import voluptuous as vol - +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from homeassistant import config_entries, exceptions -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_EMAIL, CONF_PASSWORD, CONF_PIN +from homeassistant.const import CONF_NAME, CONF_HOST, CONF_MAC, CONF_EMAIL, CONF_PASSWORD, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.components import dhcp @@ -204,29 +204,94 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: ), errors=_errors, ) + + @property + def _name(self) -> str | None: + return self.context.get(CONF_NAME) + + @_name.setter + def _name(self, value: str) -> None: + self.context[CONF_NAME] = value + self.context["title_placeholders"] = {"name": self._name} async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo): """Handle the discovery from dhcp.""" self._discovered_host = discovery_info.ip - self._discovered_mac = discovery_info.macaddress - return await self._async_handle_discovery() - - async def _async_handle_discovery(self): - self.context[CONF_HOST] = self._discovered_host - self.context[CONF_MAC] = self._discovered_mac + self._discovered_mac = discovery_info.macaddress.upper() + self._name = discovery_info.hostname.upper() + await self.async_set_unique_id(format_mac(discovery_info.macaddress)) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) return await self.async_step_discovery_confirm() async def async_step_discovery_confirm(self, user_input=None): """Confirm discovery.""" + + _errors = {} + if user_input is not None: + try: + await validate_input(self.hass, user_input) + # Ensure it's working as expected + + tydom_hub = hub.Hub( + self.hass, + user_input[CONF_HOST], + user_input[CONF_MAC], + user_input[CONF_PASSWORD], + None, + ) + await tydom_hub.test_credentials() + + except CannotConnect: + _errors["base"] = "cannot_connect" + except InvalidHost: + # The error string is set here, and should be translated. + # This example does not currently cover translations, see the + # comments on `DATA_SCHEMA` for further details. + # Set the error on the `host` field, not the entire form. + _errors[CONF_HOST] = "invalid_host" + except InvalidMacAddress: + _errors[CONF_MAC] = "invalid_macaddress" + except InvalidEmail: + _errors[CONF_EMAIL] = "invalid_email" + except InvalidPassword: + _errors[CONF_PASSWORD] = "invalid_password" + except TydomClientApiClientCommunicationError: + traceback.print_exc() + _errors["base"] = "communication_error" + except TydomClientApiClientAuthenticationError: + traceback.print_exc() + _errors["base"] = "authentication_error" + except TydomClientApiClientError: + traceback.print_exc() + _errors["base"] = "unknown" + + except Exception: # pylint: disable=broad-except + traceback.print_exc() + LOGGER.exception("Unexpected exception") + _errors["base"] = "unknown" + else: + + await self.async_set_unique_id(user_input[CONF_MAC]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title="Tydom-" + user_input[CONF_MAC][6:], data=user_input + ) + + user_input = user_input or {} return self.async_show_form( - step_id="user", + step_id="discovery_confirm", + description_placeholders={"name": self._name}, data_schema=vol.Schema( { - vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, - vol.Required(CONF_MAC, default=user_input.get(CONF_MAC, "")): str, + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, self._discovered_host)): str, + vol.Required(CONF_MAC, default=user_input.get(CONF_MAC, self._discovered_mac)): str, vol.Required( - CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") - ): str, + CONF_EMAIL, default=user_input.get(CONF_EMAIL) + ): cv.string, + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD) + ): cv.string, vol.Optional(CONF_PIN, default=user_input.get(CONF_PIN, "")): str, } ), diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 79f0646..eebaec3 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -19,9 +19,17 @@ HVACAction, HVACMode, ) -from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.const import ( + PERCENTAGE, + ATTR_TEMPERATURE, + UnitOfTemperature, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfElectricCurrent, +) -from homeassistant.helpers.entity import Entity, DeviceInfo, Entity +from homeassistant.helpers.entity import Entity, DeviceInfo from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -36,7 +44,7 @@ CoverEntity, CoverDeviceClass, ) -from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass, SensorEntity from homeassistant.components.light import LightEntity from homeassistant.components.lock import LockEntity @@ -45,7 +53,7 @@ from .const import DOMAIN, LOGGER -class GenericSensor(Entity): +class GenericSensor(SensorEntity): """Representation of a generic sensor""" should_poll = False @@ -54,8 +62,10 @@ def __init__( self, device: TydomDevice, device_class: SensorDeviceClass, + state_class: SensorStateClass, name: str, attribute: str, + unit_of_measurement ): """Initialize the sensor.""" self._device = device @@ -63,6 +73,8 @@ def __init__( self._attr_name = f"{self._device.device_name} {name}" self._attribute = attribute self._attr_device_class = device_class + self._attr_state_class = state_class + self._attr_native_unit_of_measurement = unit_of_measurement @property def state(self): @@ -231,6 +243,50 @@ class HAEnergy(Entity): "outTemperature": SensorDeviceClass.TEMPERATURE, } + + #MEASUREMENT = "measurement" + """The state represents a measurement in present time.""" + #TOTAL = "total" + """The state represents a total amount. + For example: net energy consumption""" + #TOTAL_INCREASING = "total_increasing" + """The state represents a monotonically increasing total. + For example: an amount of consumed gas""" + + state_classes = { + "energyTotIndexWatt" : SensorStateClass.TOTAL_INCREASING, + "energyIndexECSWatt" : SensorStateClass.TOTAL_INCREASING, + "energyIndexHeatGas" : SensorStateClass.TOTAL_INCREASING, + } + + units = { + "energyInstantTotElec": UnitOfElectricCurrent.AMPERE, + "energyInstantTotElec_Min": UnitOfElectricCurrent.AMPERE, + "energyInstantTotElec_Max": UnitOfElectricCurrent.AMPERE, + "energyScaleTotElec_Min": UnitOfElectricCurrent.AMPERE, + "energyScaleTotElec_Max": UnitOfElectricCurrent.AMPERE, + "energyInstantTotElecP": UnitOfPower.WATT, + "energyInstantTotElec_P_Min": UnitOfPower.WATT, + "energyInstantTotElec_P_Max": UnitOfPower.WATT, + "energyScaleTotElec_P_Min": UnitOfPower.WATT, + "energyScaleTotElec_P_Max": UnitOfPower.WATT, + "energyInstantTi1P": UnitOfPower.WATT, + "energyInstantTi1P_Min": UnitOfPower.WATT, + "energyInstantTi1P_Max": UnitOfPower.WATT, + "energyScaleTi1P_Min": UnitOfPower.WATT, + "energyScaleTi1P_Max": UnitOfPower.WATT, + "energyInstantTi1I": UnitOfElectricCurrent.AMPERE, + "energyInstantTi1I_Min": UnitOfElectricCurrent.AMPERE, + "energyInstantTi1I_Max": UnitOfElectricCurrent.AMPERE, + "energyScaleTi1I_Min": UnitOfElectricCurrent.AMPERE, + "energyScaleTi1I_Max": UnitOfElectricCurrent.AMPERE, + "energyTotIndexWatt": UnitOfEnergy.WATT_HOUR, + "energyIndexHeatWatt": UnitOfEnergy.WATT_HOUR, + "energyIndexECSWatt": UnitOfEnergy.WATT_HOUR, + "energyIndexHeatGas": UnitOfEnergy.WATT_HOUR, + "outTemperature": UnitOfTemperature.CELSIUS, + } + def __init__(self, energy: TydomEnergy) -> None: self._energy = energy self._attr_unique_id = f"{self._energy.device_id}_energy" @@ -277,6 +333,15 @@ def get_sensors(self): sensor_class = None if attribute in self.sensor_classes: sensor_class = self.sensor_classes[attribute] + + state_class = None + if attribute in self.state_classes: + state_class = self.state_classes[attribute] + + unit = None + if attribute in self.units: + unit = self.units[attribute] + if isinstance(value, bool): sensors.append( GenericBinarySensor( @@ -285,7 +350,7 @@ def get_sensors(self): ) else: sensors.append( - GenericSensor(self._energy, sensor_class, attribute, attribute) + GenericSensor(self._energy, sensor_class, state_class, attribute, attribute, unit) ) self._registered_sensors.append(attribute) @@ -496,7 +561,7 @@ def get_sensors(self) -> list: ) else: sensors.append( - GenericSensor(self._shutter, sensor_class, attribute, attribute) + GenericSensor(self._shutter, sensor_class, None, attribute, attribute, None) ) self._registered_sensors.append(attribute) @@ -570,7 +635,7 @@ def get_sensors(self): ) else: sensors.append( - GenericSensor(self._device, sensor_class, attribute, attribute) + GenericSensor(self._device, sensor_class, None, attribute, attribute, None) ) self._registered_sensors.append(attribute) @@ -580,27 +645,32 @@ def get_sensors(self): class HaClimate(ClimateEntity): """A climate entity.""" - _attr_should_poll = False should_poll = False sensor_classes = { + "temperature": SensorDeviceClass.TEMPERATURE, "TempSensorDefect": BinarySensorDeviceClass.PROBLEM, "TempSensorOpenCirc": BinarySensorDeviceClass.PROBLEM, "TempSensorShortCut": BinarySensorDeviceClass.PROBLEM, "ProductionDefect": BinarySensorDeviceClass.PROBLEM, "BatteryCmdDefect": BinarySensorDeviceClass.PROBLEM, } - DICT_HA_TO_DD = { - HVACMode.AUTO: "todo", - HVACMode.COOL: "todo", - HVACMode.HEAT: "todo", - HVACMode.OFF: "todo", + + units = { + "temperature": UnitOfTemperature.CELSIUS, + } + + DICT_MODES_HA_TO_DD = { + HVACMode.AUTO: None, + HVACMode.COOL: None, + HVACMode.HEAT: "HEATING", + HVACMode.OFF: "STOP", } - DICT_DD_TO_HA = { - "todo": HVACMode.AUTO, - "todo": HVACMode.COOL, - "todo": HVACMode.HEAT, - "todo": HVACMode.OFF, + DICT_MODES_DD__TO_HA = { + # "": HVACMode.AUTO, + # "": HVACMode.COOL, + "HEATING": HVACMode.HEAT, + "STOP": HVACMode.OFF, } def __init__(self, device: TydomBoiler) -> None: @@ -608,18 +678,48 @@ def __init__(self, device: TydomBoiler) -> None: self._device = device self._attr_unique_id = f"{self._device.device_id}_climate" self._attr_name = self._device.device_name + self._attr_hvac_modes = [ HVACMode.OFF, HVACMode.HEAT, ] # , HVACMode.AUTO, HVACMode.COOL, self._registered_sensors = [] + self._attr_preset_modes = ["NORMAL", "STOP", "ANTI_FROST"] + #self._attr_preset_mode = "STOP" + self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] + # self._attr_hvac_mode = DICT_MODES_DD__TO_HA[] + """ + { + "name":"authorization", + "type":"string", + "permission":"rw", + "validity":"STATUS_POLLING", + "enum_values":[ + "STOP", + "HEATING" + ] + + + { + "name":"hvacMode", + "type":"string", + "permission":"rw", + "validity":"DATA_POLLING", + "enum_values":[ + "NORMAL", + "STOP", + "ANTI_FROST" + ] + + """ + @property def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" features = ClimateEntityFeature(0) - features = features | ClimateEntityFeature.TARGET_TEMPERATURE + features = features | ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE # set_req = self.gateway.const.SetReq # if set_req.V_HVAC_SPEED in self._values: # features = features | ClimateEntityFeature.FAN_MODE @@ -649,8 +749,13 @@ def temperature_unit(self) -> str: def hvac_mode(self) -> HVACMode: """Return the current operation (e.g. heat, cool, idle).""" # FIXME - # return self._device.hvacMode - return HVACMode.HEAT + return self.DICT_MODES_DD__TO_HA[self._device.authorization] + + @property + def preset_mode(self) -> HVACMode: + """Return the current operation (e.g. heat, cool, idle).""" + # FIXME + return self._device.hvacMode @property def current_temperature(self) -> float | None: @@ -666,10 +771,19 @@ def target_temperature(self) -> float | None: async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" + # FIXME + self._device.set_hvac_mode(self.DICT_MODES_HA_TO_DD[hvac_mode]) logger.warn("SET HVAC MODE") + async def async_set_preset_mode(self, preset_mode): + """Set new target preset mode.""" + # FIXME + self._device.set_preset_mode(preset_mode) + logger.warn("SET preset MODE") + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" + # FIXME logger.warn("SET TEMPERATURE") async def async_added_to_hass(self) -> None: @@ -700,6 +814,12 @@ def get_sensors(self): sensor_class = None if attribute in self.sensor_classes: sensor_class = self.sensor_classes[attribute] + + unit = None + if attribute in self.units: + unit = self.units[attribute] + + if isinstance(value, bool): sensors.append( GenericBinarySensor( @@ -708,7 +828,7 @@ def get_sensors(self): ) else: sensors.append( - GenericSensor(self._device, sensor_class, attribute, attribute) + GenericSensor(self._device, sensor_class, None, attribute, attribute, unit) ) self._registered_sensors.append(attribute) @@ -777,7 +897,7 @@ def get_sensors(self): ) else: sensors.append( - GenericSensor(self._device, sensor_class, attribute, attribute) + GenericSensor(self._device, sensor_class, None, attribute, attribute, None) ) self._registered_sensors.append(attribute) @@ -846,7 +966,7 @@ def get_sensors(self): ) else: sensors.append( - GenericSensor(self._device, sensor_class, attribute, attribute) + GenericSensor(self._device, sensor_class, None, attribute, attribute, None) ) self._registered_sensors.append(attribute) @@ -906,7 +1026,7 @@ def get_sensors(self): ) else: sensors.append( - GenericSensor(self._device, sensor_class, attribute, attribute) + GenericSensor(self._device, sensor_class, None, attribute, attribute, None) ) self._registered_sensors.append(attribute) @@ -966,7 +1086,7 @@ def get_sensors(self): ) else: sensors.append( - GenericSensor(self._device, sensor_class, attribute, attribute) + GenericSensor(self._device, sensor_class, None, attribute, attribute, None) ) self._registered_sensors.append(attribute) @@ -1025,7 +1145,7 @@ def get_sensors(self): ) else: sensors.append( - GenericSensor(self._device, sensor_class, attribute, attribute) + GenericSensor(self._device, sensor_class, None, attribute, attribute, None) ) self._registered_sensors.append(attribute) diff --git a/custom_components/deltadore-tydom/manifest.json b/custom_components/deltadore-tydom/manifest.json index 07e73b6..8ef2cd8 100644 --- a/custom_components/deltadore-tydom/manifest.json +++ b/custom_components/deltadore-tydom/manifest.json @@ -12,7 +12,7 @@ ], "dhcp": [ { - "hostname": "TYDOM-*", + "hostname": "tydom-*", "macaddress": "001A25*" } ], diff --git a/custom_components/deltadore-tydom/translations/en.json b/custom_components/deltadore-tydom/translations/en.json index 3557e0c..2c1304b 100644 --- a/custom_components/deltadore-tydom/translations/en.json +++ b/custom_components/deltadore-tydom/translations/en.json @@ -11,6 +11,17 @@ "password": "Password", "pin": "Alarm PIN" } + }, + "discovery_confirm": { + "title": "Delta Dore Tydom Configuration", + "description": "If you need help with the configuration go to: https://github.com/CyrilP/hass-deltadore-tydom-component", + "data": { + "host": "IP or hostname", + "mac": "MAC address", + "email": "Email", + "password": "Password", + "pin": "Alarm PIN" + } } }, "error": { @@ -21,7 +32,13 @@ "invalid_password": "Password is invalid" }, "abort": { + "discover_timeout": "Unable to discover devices", + "no_bridges": "No device discovered", + "all_configured": "All device are already configured", + "unknown": "Unknown error occurred", + "cannot_connect": "Unable to connect to the device", "already_configured": "Device is already configured" } - } + }, + "flow_title": "{name}" } \ No newline at end of file diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 877fe09..125c63d 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -1,5 +1,4 @@ import json -import logging from http.client import HTTPResponse from http.server import BaseHTTPRequestHandler from io import BytesIO @@ -9,7 +8,7 @@ from .tydom_devices import * -logger = logging.getLogger(__name__) +from ..const import LOGGER # Dicts deviceAlarmKeywords = [ @@ -232,32 +231,32 @@ def get_uri_origin(data) -> str: ) if re_matcher: - # logger.info("///// Uri-Origin : %s", re_matcher.group(1)) + # LOGGER.info("///// Uri-Origin : %s", re_matcher.group(1)) uri_origin = re_matcher.group(1) # else: - # logger.info("///// no match") + # LOGGER.info("///// no match") return uri_origin @staticmethod def get_http_request_line(data) -> str: """Extract Http request line""" + clean_data = data.replace('\\x02', '') request_line = "" re_matcher = re.match( "b'(.*)HTTP/1.1", - data, + clean_data, ) if re_matcher: - # logger.info("///// PUT : %s", re_matcher.group(1)) + # LOGGER.info("///// PUT : %s", re_matcher.group(1)) request_line = re_matcher.group(1) # else: - # logger.info("///// no match") - return request_line + # LOGGER.info("///// no match") + return request_line.strip() async def incoming_triage(self, bytes_str): """Identify message type and dispatch the result""" incoming = None - first = str(bytes_str[:40]) # Find Uri-Origin in header if available uri_origin = MessageHandler.get_uri_origin(str(bytes_str)) @@ -266,8 +265,8 @@ async def incoming_triage(self, bytes_str): http_request_line = MessageHandler.get_http_request_line(str(bytes_str)) try: - if len(http_request_line) > 0: - logger.debug("%s detected !", http_request_line) + if http_request_line is not None and len(http_request_line) > 0: + LOGGER.debug("%s detected !", http_request_line) try: try: incoming = self.parse_put_response(bytes_str) @@ -278,7 +277,8 @@ async def incoming_triage(self, bytes_str): incoming, uri_origin, http_request_line ) except BaseException: - logger.error("Error when parsing tydom message (%s)", bytes_str) + LOGGER.error("Error when parsing tydom message (%s)", bytes_str) + LOGGER.exception("Error when parsing tydom message") return None elif len(uri_origin) > 0: response = self.response_from_bytes(bytes_str[len(self.cmd_prefix) :]) @@ -288,75 +288,22 @@ async def incoming_triage(self, bytes_str): incoming, uri_origin, http_request_line ) except BaseException: - logger.error("Error when parsing tydom message (%s)", bytes_str) + LOGGER.error("Error when parsing tydom message (%s)", bytes_str) return None else: - logger.warning("Unknown tydom message type received (%s)", bytes_str) + LOGGER.warning("Unknown tydom message type received (%s)", bytes_str) return None - """ - if "/refresh/all" in uri_origin: - pass - elif ("PUT /devices/data" in http_request_line) or ( - "/devices/cdata" in http_request_line - ): - logger.debug("PUT /devices/data message detected !") - try: - try: - incoming = self.parse_put_response(bytes_str) - except BaseException: - # Tywatt response starts at 7 - incoming = self.parse_put_response(bytes_str, 7) - return await self.parse_response(incoming) - except BaseException: - logger.error( - "Error when parsing devices/data tydom message (%s)", bytes_str - ) - return None - elif "scn" in first: - try: - incoming = str(bytes_str) - scenarii = await self.parse_response(incoming) - logger.debug("Scenarii message processed") - return scenarii - except BaseException: - logger.error( - "Error when parsing Scenarii tydom message (%s)", bytes_str - ) - return None - elif "POST" in http_request_line: - try: - incoming = self.parse_put_response(bytes_str) - post = await self.parse_response(incoming) - logger.debug("POST message processed") - return post - except BaseException: - logger.error( - "Error when parsing POST tydom message (%s)", bytes_str - ) - return None - elif "/devices/meta" in uri_origin: - pass - elif "HTTP/1.1" in first: - response = self.response_from_bytes(bytes_str[len(self.cmd_prefix) :]) - incoming = response.data.decode("utf-8") - try: - return await self.parse_response(incoming) - except BaseException: - logger.error( - "Error when parsing HTTP/1.1 tydom message (%s)", bytes_str - ) - return None - else: - logger.warning("Unknown tydom message type received (%s)", bytes_str) - return None """ - except Exception as ex: - logger.error( + LOGGER.exception("exception") + LOGGER.error( "Technical error when parsing tydom message (%s) : %s", bytes_str, ex ) - logger.debug("Incoming payload (%s)", incoming) - logger.debug("exception : %s", ex) + LOGGER.debug("Incoming payload (%s)", incoming) + LOGGER.debug("exception : %s", ex) + raise Exception( + "Something really wrong happened!" + ) from ex return None # Basic response parsing. Typically GET responses + instanciate covers and @@ -389,9 +336,9 @@ async def parse_response(self, incoming, uri_origin, http_request_line): msg_type = "msg_data" if msg_type is None: - logger.warning("Unknown message type received %s", data) + LOGGER.warning("Unknown message type received %s", data) else: - logger.debug("Message received detected as (%s)", msg_type) + LOGGER.debug("Message received detected as (%s)", msg_type) try: if msg_type == "msg_config": parsed = json.loads(data) @@ -410,19 +357,20 @@ async def parse_response(self, incoming, uri_origin, http_request_line): return await self.parse_devices_cdata(parsed=parsed) elif msg_type == "msg_html": - logger.debug("HTML Response ?") + LOGGER.debug("HTML Response ?") elif msg_type == "msg_info": parsed = json.loads(data) return await self.parse_msg_info(parsed) except Exception as e: - logger.error("Error on parsing tydom response (%s)", e) + LOGGER.error("Error on parsing tydom response (%s)", e) + LOGGER.exception("Error on parsing tydom response") traceback.print_exception(e) - logger.debug("Incoming data parsed with success") + LOGGER.debug("Incoming data parsed with success") async def parse_msg_info(self, parsed): - logger.debug("parse_msg_info : %s", parsed) + LOGGER.debug("parse_msg_info : %s", parsed) product_name = parsed["productName"] main_version_sw = parsed["mainVersionSW"] main_version_hw = parsed["mainVersionHW"] @@ -458,6 +406,8 @@ async def get_device( ) -> TydomDevice: """Get device class from its last usage""" + LOGGER.error("- %s", name) + # FIXME voir: class CoverDeviceClass(StrEnum): # Refer to the cover dev docs for device class descriptions # AWNING = "awning" @@ -511,11 +461,11 @@ async def get_device( return TydomEnergy( tydom_client, uid, device_id, name, last_usage, endpoint, data ) - case "smoke": + case "sensorDFR": return TydomSmoke( tydom_client, uid, device_id, name, last_usage, endpoint, data ) - case "boiler" | "sh_hvac": + case "boiler" | "sh_hvac" | "electric": return TydomBoiler( tydom_client, uid, device_id, name, last_usage, endpoint, data ) @@ -524,12 +474,13 @@ async def get_device( tydom_client, uid, device_id, name, last_usage, endpoint, data ) case _: - logger.warn("Unknown usage : %s", last_usage) + # TODO generic sensor ? + LOGGER.warn("Unknown usage : %s", last_usage) return @staticmethod async def parse_config_data(parsed): - logger.debug("parse_config_data : %s", parsed) + LOGGER.debug("parse_config_data : %s", parsed) devices = [] for i in parsed["endpoints"]: device_unique_id = str(i["id_endpoint"]) + "_" + str(i["id_device"]) @@ -538,6 +489,12 @@ async def parse_config_data(parsed): # if device is not None: # devices.append(device) + LOGGER.warning(" config_data : %s - %s", device_unique_id, i["name"]) + + device_name[device_unique_id] = i["name"] + device_type[device_unique_id] = i["last_usage"] + device_endpoint[device_unique_id] = i["id_endpoint"] + if ( i["last_usage"] == "shutter" or i["last_usage"] == "klineShutter" @@ -552,41 +509,28 @@ async def parse_config_data(parsed): or i["last_usage"] == "garage_door" or i["last_usage"] == "gate" ): - device_name[device_unique_id] = i["name"] - device_type[device_unique_id] = i["last_usage"] - device_endpoint[device_unique_id] = i["id_endpoint"] + pass if i["last_usage"] == "boiler" or i["last_usage"] == "conso": - device_name[device_unique_id] = i["name"] - device_type[device_unique_id] = i["last_usage"] - device_endpoint[device_unique_id] = i["id_endpoint"] - + pass if i["last_usage"] == "alarm": device_name[device_unique_id] = "Tyxal Alarm" - device_type[device_unique_id] = "alarm" - device_endpoint[device_unique_id] = i["id_endpoint"] if i["last_usage"] == "electric": - device_name[device_unique_id] = i["name"] - device_type[device_unique_id] = "boiler" - device_endpoint[device_unique_id] = i["id_endpoint"] + pass if i["last_usage"] == "sensorDFR": - device_name[device_unique_id] = i["name"] - device_type[device_unique_id] = "smoke" - device_endpoint[device_unique_id] = i["id_endpoint"] + pass if i["last_usage"] == "": - device_name[device_unique_id] = i["name"] device_type[device_unique_id] = "unknown" - device_endpoint[device_unique_id] = i["id_endpoint"] - logger.debug("Configuration updated") - logger.debug("devices : %s", devices) + LOGGER.debug("Configuration updated") + LOGGER.debug("devices : %s", devices) return devices async def parse_cmeta_data(self, parsed): - logger.debug("parse_cmeta_data : %s", parsed) + LOGGER.debug("parse_cmeta_data : %s", parsed) for i in parsed: for endpoint in i["endpoints"]: if len(endpoint["cmetadata"]) > 0: @@ -613,7 +557,7 @@ async def parse_cmeta_data(self, parsed): + "&reset=false" ) self.tydom_client.add_poll_device_url(url) - logger.debug("Add poll device : %s", url) + LOGGER.debug("Add poll device : %s", url) elif elem["name"] == "energyInstant": device_name[unique_id] = "Tywatt" device_type[unique_id] = "conso" @@ -632,7 +576,7 @@ async def parse_cmeta_data(self, parsed): + "&reset=false" ) self.tydom_client.add_poll_device_url(url) - logger.debug("Add poll device : " + url) + LOGGER.debug("Add poll device : " + url) elif elem["name"] == "energyDistrib": device_name[unique_id] = "Tywatt" device_type[unique_id] = "conso" @@ -650,17 +594,59 @@ async def parse_cmeta_data(self, parsed): + src ) self.tydom_client.add_poll_device_url(url) - logger.debug("Add poll device : " + url) + LOGGER.debug("Add poll device : " + url) - logger.debug("Metadata configuration updated") + LOGGER.debug("Metadata configuration updated") async def parse_devices_data(self, parsed): - logger.debug("parse_devices_data : %s", parsed) + LOGGER.debug("parse_devices_data : %s", parsed) devices = [] for i in parsed: for endpoint in i["endpoints"]: if endpoint["error"] == 0 and len(endpoint["data"]) > 0: + + try: + device_id = i["id"] + endpoint_id = endpoint["id"] + unique_id = str(endpoint_id) + "_" + str(device_id) + name_of_id = self.get_name_from_id(unique_id) + type_of_id = self.get_type_from_id(unique_id) + + data = {} + + for elem in endpoint["data"]: + element_name = elem["name"] + element_value = elem["value"] + element_validity = elem["validity"] + + if element_validity == "upToDate": + data[element_name] = element_value + + # Create the device + device = await MessageHandler.get_device( + self.tydom_client, + type_of_id, + unique_id, + device_id, + name_of_id, + endpoint_id, + data, + ) + if device is not None: + devices.append(device) + LOGGER.info( + "Device update (id=%s, endpoint=%s, name=%s, type=%s)", + device_id, + endpoint_id, + name_of_id, + type_of_id, + ) + except Exception as e: + LOGGER.error("msg_data error in parsing !") + LOGGER.error(e) + + """ try: attr_alarm = {} attr_cover = {} @@ -677,7 +663,7 @@ async def parse_devices_data(self, parsed): name_of_id = self.get_name_from_id(unique_id) type_of_id = self.get_type_from_id(unique_id) - logger.info( + LOGGER.info( "Device update (id=%s, endpoint=%s, name=%s, type=%s)", device_id, endpoint_id, @@ -692,7 +678,7 @@ async def parse_devices_data(self, parsed): element_value = elem["value"] element_validity = elem["validity"] - if element_validity == "upToDate": + if element_validity == "upToDate" and element_name != "name": data[element_name] = element_value print_id = name_of_id if len(name_of_id) != 0 else device_id @@ -892,20 +878,11 @@ async def parse_devices_data(self, parsed): attr_ukn[element_name] = element_value except Exception as e: - logger.error("msg_data error in parsing !") - logger.error(e) - - device = await MessageHandler.get_device( - self.tydom_client, - type_of_id, - unique_id, - device_id, - name_of_id, - endpoint_id, - data, - ) - if device is not None: - devices.append(device) + LOGGER.error("msg_data error in parsing !") + LOGGER.error(e) + """ + + """ if ( "device_type" in attr_cover @@ -1041,7 +1018,7 @@ async def parse_devices_data(self, parsed): state = "disarmed" if sos_state: - logger.warning("SOS !") + LOGGER.warning("SOS !") if not (state is None): # alarm = Alarm( @@ -1054,15 +1031,16 @@ async def parse_devices_data(self, parsed): pass except Exception as e: - logger.error("Error in alarm parsing !") - logger.error(e) + LOGGER.error("Error in alarm parsing !") + LOGGER.error(e) pass else: pass + """ return devices async def parse_devices_cdata(self, parsed): - logger.debug("parse_devices_data : %s", parsed) + LOGGER.debug("parse_devices_data : %s", parsed) for i in parsed: for endpoint in i["endpoints"]: if endpoint["error"] == 0 and len(endpoint["cdata"]) > 0: @@ -1072,7 +1050,7 @@ async def parse_devices_cdata(self, parsed): unique_id = str(endpoint_id) + "_" + str(device_id) name_of_id = self.get_name_from_id(unique_id) type_of_id = self.get_type_from_id(unique_id) - logger.info( + LOGGER.info( "Device configured (id=%s, endpoint=%s, name=%s, type=%s)", device_id, endpoint_id, @@ -1161,7 +1139,7 @@ async def parse_devices_cdata(self, parsed): # await new_conso.update() except Exception as e: - logger.error("Error when parsing msg_cdata (%s)", e) + LOGGER.error("Error when parsing msg_cdata (%s)", e) # PUT response DIRTY parsing def parse_put_response(self, bytes_str, start=6): @@ -1201,7 +1179,7 @@ def get_type_from_id(self, id): if id in device_type.keys(): device_type_detected = device_type[id] else: - logger.warning("Unknown device type (%s)", id) + LOGGER.warning("Unknown device type (%s)", id) return device_type_detected # Get pretty name for a device id @@ -1210,7 +1188,7 @@ def get_name_from_id(self, id): if id in device_name.keys(): name = device_name[id] else: - logger.warning("Unknown device name (%s)", id) + LOGGER.warning("Unknown device name (%s)", id) return name diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index dc3a459..86837ab 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -11,7 +11,7 @@ from typing import cast from urllib3 import encode_multipart_formdata -from aiohttp import ClientWebSocketResponse, ClientSession +from aiohttp import ClientWebSocketResponse, ClientSession, WSMsgType from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import * @@ -19,8 +19,7 @@ from requests.auth import HTTPDigestAuth -logger = logging.getLogger(__name__) - +from ..const import LOGGER class TydomClientApiClientError(Exception): """Exception to indicate a general API error.""" @@ -49,7 +48,7 @@ def __init__( host: str = MEDIATION_URL, event_callback=None, ) -> None: - logger.debug("Initializing TydomClient Class") + LOGGER.debug("Initializing TydomClient Class") self._hass = hass self._password = password @@ -64,11 +63,11 @@ def __init__( self.current_poll_index = 0 if self._remote_mode: - logger.info("Configure remote mode (%s)", self._host) + LOGGER.info("Configure remote mode (%s)", self._host) self._cmd_prefix = "\x02" self._ping_timeout = 40 else: - logger.info("Configure local mode (%s)", self._host) + LOGGER.info("Configure local mode (%s)", self._host) self._cmd_prefix = "" self._ping_timeout = None @@ -87,7 +86,7 @@ async def async_get_credentials( method="GET", url=DELTADORE_AUTH_URL, proxy=proxy ) - logger.debug( + LOGGER.debug( "response status for openid-config: %s\nheaders : %s\ncontent : %s", response.status, response.headers, @@ -97,7 +96,7 @@ async def async_get_credentials( json_response = await response.json() response.close() signin_url = json_response["token_endpoint"] - logger.info("signin_url : %s", signin_url) + LOGGER.info("signin_url : %s", signin_url) body, ct_header = encode_multipart_formdata( { @@ -116,7 +115,7 @@ async def async_get_credentials( proxy=proxy, ) - logger.debug( + LOGGER.debug( "response status for signin : %s\nheaders : %s\ncontent : %s", response.status, response.headers, @@ -134,7 +133,7 @@ async def async_get_credentials( proxy=proxy, ) - logger.debug( + LOGGER.debug( "response status for https://prod.iotdeltadore.com/sitesmanagement/api/v1/sites?gateway_mac= : %s\nheaders : %s\ncontent : %s", response.status, response.headers, @@ -144,7 +143,6 @@ async def async_get_credentials( json_response = await response.json() response.close() - await session.close() if "sites" in json_response and len(json_response["sites"]) > 0: for site in json_response["sites"]: if "gateway" in site and site["gateway"]["mac"] == macaddress: @@ -189,7 +187,7 @@ async def async_connect(self) -> ClientWebSocketResponse: json=None, proxy=proxy, ) - logger.debug( + LOGGER.debug( "response status : %s\nheaders : %s\ncontent : %s", response.status, response.headers, @@ -203,7 +201,7 @@ async def async_connect(self) -> ClientWebSocketResponse: response.close() if re_matcher: - logger.info("nonce : %s", re_matcher.group(1)) + LOGGER.info("nonce : %s", re_matcher.group(1)) else: raise TydomClientApiClientError("Could't find auth nonce") @@ -239,7 +237,7 @@ async def async_connect(self) -> ClientWebSocketResponse: async def listen_tydom(self, connection: ClientWebSocketResponse): """Listen for Tydom messages""" - logger.info("Listen for Tydom messages") + LOGGER.info("Listen for Tydom messages") self._connection = connection await self.ping() await self.get_info() @@ -270,19 +268,30 @@ async def consume_messages(self): if self._connection.closed: await self._connection.close() await asyncio.sleep(10) - self.listen_tydom(await self.async_connect()) + await self.listen_tydom(await self.async_connect()) # self._connection = await self.async_connect() msg = await self._connection.receive() - logger.info( + LOGGER.info( "Incomming message - type : %s - message : %s", msg.type, msg.data ) + + if msg.type == WSMsgType.CLOSE or msg.type == WSMsgType.CLOSED or msg.type == WSMsgType.CLOSING: + LOGGER.debug("Close message type received") + return None + elif msg.type == WSMsgType.ERROR: + LOGGER.debug("Error message type received") + return None + elif msg.type == WSMsgType.PING or msg.type == WSMsgType.PONG: + LOGGER.debug("Ping/Pong message type received") + return None + incoming_bytes_str = cast(bytes, msg.data) return await self._message_handler.incoming_triage(incoming_bytes_str) except Exception as e: - logger.warning("Unable to handle message: %s", e) + LOGGER.exception("Unable to handle message") return None def build_digest_headers(self, nonce): @@ -313,7 +322,7 @@ async def send_message(self, method, msg): + " HTTP/1.1\r\nContent-Length: 0\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" ) a_bytes = bytes(message, "ascii") - logger.debug( + LOGGER.debug( "Sending message to tydom (%s %s)", method, msg if "pwd" not in msg else "***", @@ -322,7 +331,7 @@ async def send_message(self, method, msg): if self._connection is not None: await self._connection.send_bytes(a_bytes) else: - logger.warning( + LOGGER.warning( "Cannot send message to Tydom because no connection has been established yet" ) @@ -383,7 +392,7 @@ async def ping(self): msg_type = "/ping" req = "GET" await self.send_message(method=req, msg=msg_type) - logger.debug("Ping") + LOGGER.debug("Ping") async def get_devices_meta(self): """Get all devices metadata""" @@ -442,7 +451,7 @@ async def get_device_data(self, id): await self._connection.send(a_bytes) async def get_poll_device_data(self, url): - logger.error("poll device data %s", url) + LOGGER.error("poll device data %s", url) msg_type = url req = "GET" await self.send_message(method=req, msg=msg_type) @@ -483,7 +492,7 @@ async def put_devices_data(self, device_id, endpoint_id, name, value): + "\r\n\r\n" ) a_bytes = bytes(str_request, "ascii") - logger.debug("Sending message to tydom (%s %s)", "PUT data", body) + LOGGER.debug("Sending message to tydom (%s %s)", "PUT data", body) await self._connection.send_bytes(a_bytes) return 0 @@ -504,7 +513,7 @@ async def put_alarm_cdata(self, device_id, alarm_id=None, value=None, zone_id=No # zones if self._alarm_pin is None: - logger.warning("Tydom alarm pin is not set!") + LOGGER.warning("Tydom alarm pin is not set!") try: if zone_id is None: @@ -540,16 +549,16 @@ async def put_alarm_cdata(self, device_id, alarm_id=None, value=None, zone_id=No ) a_bytes = bytes(str_request, "ascii") - logger.debug("Sending message to tydom (%s %s)", "PUT cdata", body) + LOGGER.debug("Sending message to tydom (%s %s)", "PUT cdata", body) try: await self._connection.send(a_bytes) return 0 except BaseException: - logger.error("put_alarm_cdata ERROR !", exc_info=True) - logger.error(a_bytes) + LOGGER.error("put_alarm_cdata ERROR !", exc_info=True) + LOGGER.error(a_bytes) except BaseException: - logger.error("put_alarm_cdata ERROR !", exc_info=True) + LOGGER.error("put_alarm_cdata ERROR !", exc_info=True) async def update_firmware(self): """Update Tydom firmware""" diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index e4c08d6..2bc6907 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -202,6 +202,16 @@ class TydomSmoke(TydomDevice): class TydomBoiler(TydomDevice): """represents a boiler""" + def set_hvac_mode(self, mode): + """Set hvac mode (STOP/HEATING)""" + logger.info("setting mode to %s", mode) + # authorization + + def set_preset_mode(self, mode): + """Set preset mode (NORMAL/STOP/ANTI_FROST)""" + logger.info("setting preset to %s", mode) + # hvacMode + class TydomWindow(TydomDevice): From e972b5aec811f65b2ef447c4075c3daef04d6c0f Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Mon, 18 Sep 2023 13:44:52 +0200 Subject: [PATCH 39/74] set some sensors as DIAGNOSTIC --- custom_components/deltadore-tydom/ha_entities.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index eebaec3..3507e71 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -27,6 +27,7 @@ UnitOfEnergy, UnitOfPower, UnitOfElectricCurrent, + EntityCategory, ) from homeassistant.helpers.entity import Entity, DeviceInfo @@ -75,6 +76,8 @@ def __init__( self._attr_device_class = device_class self._attr_state_class = state_class self._attr_native_unit_of_measurement = unit_of_measurement + if name == "config" or name == "supervisionMode": + self._attr_entity_category = EntityCategory.DIAGNOSTIC @property def state(self): From 7aa0e6235300e49b1e05fe1b628e6536c9ef305c Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 19 Sep 2023 20:44:49 +0200 Subject: [PATCH 40/74] fix multiple dhcp discoveries --- custom_components/deltadore-tydom/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index 7afb514..3414b4e 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -219,8 +219,8 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo): self._discovered_host = discovery_info.ip self._discovered_mac = discovery_info.macaddress.upper() self._name = discovery_info.hostname.upper() - await self.async_set_unique_id(format_mac(discovery_info.macaddress)) - self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + await self.async_set_unique_id(discovery_info.macaddress.upper()) + self._abort_if_unique_id_configured() return await self.async_step_discovery_confirm() async def async_step_discovery_confirm(self, user_input=None): From b4bfa5d4e98face816eb481b1ad6f7e247c1cf68 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Sun, 24 Sep 2023 14:26:46 +0200 Subject: [PATCH 41/74] cleanup --- custom_components/deltadore-tydom/cover.py | 3 --- custom_components/deltadore-tydom/sensor.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/custom_components/deltadore-tydom/cover.py b/custom_components/deltadore-tydom/cover.py index 47588b8..cae445e 100644 --- a/custom_components/deltadore-tydom/cover.py +++ b/custom_components/deltadore-tydom/cover.py @@ -35,9 +35,6 @@ async def async_setup_entry( hub = hass.data[DOMAIN][config_entry.entry_id] hub.add_cover_callback = async_add_entities - # Add all entities to HA - async_add_entities(HelloWorldCover(roller) for roller in hub.rollers) - # This entire class could be written to extend a base class to ensure common attributes # are kept identical/in sync. It's broken apart here between the Cover and Sensors to diff --git a/custom_components/deltadore-tydom/sensor.py b/custom_components/deltadore-tydom/sensor.py index 5b922ef..f0c6670 100644 --- a/custom_components/deltadore-tydom/sensor.py +++ b/custom_components/deltadore-tydom/sensor.py @@ -28,9 +28,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hub.add_sensor_callback = async_add_entities new_devices = [] - for roller in hub.rollers: - new_devices.append(BatterySensor(roller)) - new_devices.append(IlluminanceSensor(roller)) if new_devices: async_add_entities(new_devices) From 4b9750cca8748d37c1b4654f0cf1a84475b3250a Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Wed, 4 Oct 2023 21:32:12 +0200 Subject: [PATCH 42/74] add tydom bridge, various updates --- .../deltadore-tydom/ha_entities.py | 156 ++++++++++++++---- custom_components/deltadore-tydom/hub.py | 149 +++-------------- .../deltadore-tydom/tydom/MessageHandler.py | 42 ++--- .../deltadore-tydom/tydom/tydom_client.py | 2 + .../deltadore-tydom/tydom/tydom_devices.py | 84 ++-------- custom_components/deltadore-tydom/update.py | 13 +- 6 files changed, 192 insertions(+), 254 deletions(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 3507e71..e4073e0 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -58,6 +58,21 @@ class GenericSensor(SensorEntity): """Representation of a generic sensor""" should_poll = False + diagnostic_attrs = [ + "config", + "supervisionMode", + "bootReference", + "bootVersion", + "keyReference", + "keyVersionHW", + "keyVersionStack", + "keyVersionSW", + "mainId", + "mainReference", + "mainVersionHW", + "productName", + "mac" + ] def __init__( self, @@ -71,12 +86,12 @@ def __init__( """Initialize the sensor.""" self._device = device self._attr_unique_id = f"{self._device.device_id}_{name}" - self._attr_name = f"{self._device.device_name} {name}" + self._attr_name = name self._attribute = attribute self._attr_device_class = device_class self._attr_state_class = state_class self._attr_native_unit_of_measurement = unit_of_measurement - if name == "config" or name == "supervisionMode": + if name in self.diagnostic_attrs: self._attr_entity_category = EntityCategory.DIAGNOSTIC @property @@ -131,15 +146,6 @@ def device_info(self): """Return information to link this entity with the correct device.""" return {"identifiers": {(DOMAIN, self._device.device_id)}} - # This property is important to let HA know if this entity is online or not. - # If an entity is offline (return False), the UI will refelect this. - @property - def available(self) -> bool: - """Return True if roller and hub is available.""" - # return self._roller.online and self._roller.hub.online - # FIXME - return True - async def async_added_to_hass(self): """Run when this Entity has been added to HA.""" # Sensors should also register callbacks to HA when their state changes @@ -164,7 +170,7 @@ def __init__( """Initialize the sensor.""" super().__init__(device) self._attr_unique_id = f"{self._device.device_id}_{name}" - self._attr_name = f"{self._device.device_name} {name}" + self._attr_name = name self._attribute = attribute self._attr_device_class = device_class @@ -178,11 +184,66 @@ def is_on(self): class HATydom(Entity): """Representation of a Tydom.""" + _attr_has_entity_name = True + _attr_entity_category = EntityCategory.DIAGNOSTIC + entity_description: str + should_poll = False + device_class = None + supported_features = None - def __init__(self, device: TydomEnergy): - """Initialize the sensor.""" - self._device = device + sensor_classes = { + "update_available": BinarySensorDeviceClass.UPDATE + } + + filtered_attrs = [ + "absence.json", + "anticip.json", + "bdd_mig.json", + "bdd.json", + "bioclim.json", + "collect.json", + "config.json", + "data_config.json", + "gateway.dat", + "gateway.dat", + "groups.json", + "info_col.json", + "info_mig.json", + "mom_api.json", + "mom.json", + "scenario.json", + "site.json", + "trigger.json", + "TYDOM.dat", + ] + + def __init__(self, device: Tydom) -> None: + self.device = device + self._attr_unique_id = f"{self.device.device_id}" + self._attr_name = self.device.device_name + self._registered_sensors = [] + + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._device.update_available + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + # Importantly for a push integration, the module that will be getting updates + # needs to notify HA of changes. The dummy device has a registercallback + # method, so to this we add the 'self.async_write_ha_state' method, to be + # called where ever there are changes. + # The call back registration is done once this entity is registered with HA + # (rather than in the __init__) + self.device.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self.device.remove_callback(self.async_write_ha_state) # To link this entity to the cover device, this property must return an # identifiers value matching that used in the cover, but no other information such @@ -191,7 +252,14 @@ def __init__(self, device: TydomEnergy): @property def device_info(self): """Return information to link this entity with the correct device.""" - return {"identifiers": {(DOMAIN, self._device.device_id)}} + return { + "identifiers": {(DOMAIN, self.device.device_id)}, + "name": self.device.device_id, + "manufacturer": "Delta Dore", + "sw_version": self.device.mainVersionSW, + "model": self.device.productName, + } + # This property is important to let HA know if this entity is online or not. # If an entity is offline (return False), the UI will refelect this. @@ -202,15 +270,37 @@ def available(self) -> bool: # return self._device.online and self._device.hub.online return True - async def async_added_to_hass(self): - """Run when this Entity has been added to HA.""" - # Sensors should also register callbacks to HA when their state changes - self._device.register_callback(self.async_write_ha_state) - async def async_will_remove_from_hass(self): - """Entity being removed from hass.""" - # The opposite of async_added_to_hass. Remove any registered call backs here. - self._device.remove_callback(self.async_write_ha_state) + def get_sensors(self) -> list: + """Get available sensors for this entity""" + sensors = [] + + for attribute, value in self.device.__dict__.items(): + if ( + attribute[:1] != "_" + and value is not None + and attribute not in self._registered_sensors + ): + sensor_class = None + if attribute in self.filtered_attrs: + continue + if attribute in self.sensor_classes: + LOGGER.warn("sensor class for %s", attribute) + LOGGER.warn("sensor class for %s = %s", attribute, self.sensor_classes[attribute]) + sensor_class = self.sensor_classes[attribute] + if isinstance(value, bool): + sensors.append( + GenericBinarySensor( + self.device, sensor_class, attribute, attribute + ) + ) + else: + sensors.append( + GenericSensor(self.device, sensor_class, None, attribute, attribute, None) + ) + self._registered_sensors.append(attribute) + + return sensors class HAEnergy(Entity): @@ -257,9 +347,10 @@ class HAEnergy(Entity): For example: an amount of consumed gas""" state_classes = { - "energyTotIndexWatt" : SensorStateClass.TOTAL_INCREASING, - "energyIndexECSWatt" : SensorStateClass.TOTAL_INCREASING, - "energyIndexHeatGas" : SensorStateClass.TOTAL_INCREASING, + "energyTotIndexWatt": SensorStateClass.TOTAL_INCREASING, + "energyIndexECSWatt": SensorStateClass.TOTAL_INCREASING, + "energyIndexHeatWatt": SensorStateClass.TOTAL_INCREASING, + "energyIndexHeatGas": SensorStateClass.TOTAL_INCREASING, } units = { @@ -575,10 +666,8 @@ class HASmoke(BinarySensorEntity): """Representation of an smoke sensor""" should_poll = False - device_class = None - supported_features = None - device_class = BinarySensorDeviceClass.PROBLEM + supported_features = None sensor_classes = {"batt_defect": BinarySensorDeviceClass.PROBLEM} @@ -600,6 +689,7 @@ def device_info(self): return { "identifiers": {(DOMAIN, self._device.device_id)}, "name": self._device.device_name, + "manufacturer": "Delta Dore", } async def async_added_to_hass(self) -> None: @@ -752,7 +842,11 @@ def temperature_unit(self) -> str: def hvac_mode(self) -> HVACMode: """Return the current operation (e.g. heat, cool, idle).""" # FIXME - return self.DICT_MODES_DD__TO_HA[self._device.authorization] + if (hasattr(self._device, 'authorization')): + return self.DICT_MODES_DD__TO_HA[self._device.authorization] + else: + return None + @property def preset_mode(self) -> HVACMode: diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 6ff0783..cad6b53 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -46,10 +46,8 @@ def __init__( self._pin = alarmpin self._hass = hass self._name = mac - self._id = "Tydom-" + mac - self.device_info = Tydom( - None, None, None, None, None, None, None, None, None, None, None, False - ) + self._id = "Tydom-" + mac[6:] + self.device_info = Tydom(None, None, None, None, None, None, None) self.devices = {} self.ha_devices = {} self.add_cover_callback = None @@ -61,6 +59,7 @@ def __init__( self._tydom_client = TydomClient( hass=self._hass, + id=self._id, mac=self._mac, host=self._host, password=self._pass, @@ -68,11 +67,6 @@ def __init__( event_callback=self.handle_event, ) - self.rollers = [ - # Roller(f"{self._id}_1", f"{self._name} 1", self), - # Roller(f"{self._id}_2", f"{self._name} 2", self), - # Roller(f"{self._id}_3", f"{self._name} 3", self), - ] self.online = True @property @@ -107,28 +101,35 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: devices = await self._tydom_client.consume_messages() if devices is not None: for device in devices: - logger.info("*** device %s", device) - if isinstance(device, Tydom): - await self.device_info.update_device(device) + if device.device_id not in self.devices: + self.devices[device.device_id] = device + await self.create_ha_device(device) else: - logger.warn("*** publish_updates for device : %s", device) - if device.device_id not in self.devices: - self.devices[device.device_id] = device - await self.create_ha_device(device) - else: - logger.warn( - "update device %s : %s", - device.device_id, - self.devices[device.device_id], - ) - await self.update_ha_device( - self.devices[device.device_id], device - ) + logger.warn( + "update device %s : %s", + device.device_id, + self.devices[device.device_id], + ) + await self.update_ha_device( + self.devices[device.device_id], device + ) async def create_ha_device(self, device): """Create a new HA device""" logger.warn("device type %s", device.device_type) match device: + case Tydom(): + + logger.info("Create Tydom gateway %s", device.device_id) + self.devices[device.device_id] = self.device_info + await self.device_info.update_device(device) + ha_device = HATydom(self.device_info) + + self.ha_devices[self.device_info.device_id] = ha_device + if self.add_sensor_callback is not None: + self.add_sensor_callback([ha_device]) + if self.add_sensor_callback is not None: + self.add_sensor_callback(ha_device.get_sensors()) case TydomShutter(): logger.warn("Create cover %s", device.device_id) ha_device = HACover(device) @@ -239,101 +240,3 @@ async def async_trigger_firmware_update(self) -> None: """Trigger firmware update""" logger.info("Installing firmware update...") self._tydom_client.update_firmware() - - -class Roller: - """Dummy roller (device for HA) for Hello World example.""" - - def __init__(self, rollerid: str, name: str, hub: Hub) -> None: - """Init dummy roller.""" - self._id = rollerid - self.hub = hub - self.name = name - self._callbacks = set() - self._loop = asyncio.get_event_loop() - self._target_position = 100 - self._current_position = 100 - # Reports if the roller is moving up or down. - # >0 is up, <0 is down. This very much just for demonstration. - self.moving = 0 - - # Some static information about this device - self.firmware_version = f"0.0.{random.randint(1, 9)}" - self.model = "Test Device" - - @property - def roller_id(self) -> str: - """Return ID for roller.""" - return self._id - - @property - def position(self): - """Return position for roller.""" - logger.error("get roller position") - return self._current_position - - async def set_position(self, position: int) -> None: - """ - Set dummy cover to the given position. - State is announced a random number of seconds later. - """ - logger.error("set roller position (hub)") - self._target_position = position - - # Update the moving status, and broadcast the update - self.moving = position - 50 - await self.publish_updates() - - self._loop.create_task(self.delayed_update()) - - async def delayed_update(self) -> None: - """Publish updates, with a random delay to emulate interaction with device.""" - logger.error("delayed_update") - await asyncio.sleep(random.randint(1, 10)) - self.moving = 0 - await self.publish_updates() - - def register_callback(self, callback: Callable[[], None]) -> None: - """Register callback, called when Roller changes state.""" - logger.error("register_callback %s", callback) - self._callbacks.add(callback) - - def remove_callback(self, callback: Callable[[], None]) -> None: - """Remove previously registered callback.""" - logger.error("remove_callback") - self._callbacks.discard(callback) - - # In a real implementation, this library would call it's call backs when it was - # notified of any state changeds for the relevant device. - async def publish_updates(self) -> None: - """Schedule call all registered callbacks.""" - logger.error("publish_updates") - self._current_position = self._target_position - for callback in self._callbacks: - callback() - - @property - def online(self) -> float: - """Roller is online.""" - logger.error("online") - # The dummy roller is offline about 10% of the time. Returns True if online, - # False if offline. - return random.random() > 0.1 - - @property - def battery_level(self) -> int: - """Battery level as a percentage.""" - logger.error("battery_level") - return random.randint(0, 100) - - @property - def battery_voltage(self) -> float: - """Return a random voltage roughly that of a 12v battery.""" - logger.error("battery_voltage") - return round(random.random() * 3 + 10, 2) - - @property - def illuminance(self) -> int: - """Return a sample illuminance in lux.""" - logger.error("illuminance") - return random.randint(0, 500) diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 125c63d..98ae64f 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -371,33 +371,23 @@ async def parse_response(self, incoming, uri_origin, http_request_line): async def parse_msg_info(self, parsed): LOGGER.debug("parse_msg_info : %s", parsed) - product_name = parsed["productName"] - main_version_sw = parsed["mainVersionSW"] - main_version_hw = parsed["mainVersionHW"] - main_id = parsed["mainId"] - main_reference = parsed["mainReference"] - key_version_sw = parsed["keyVersionSW"] - key_version_hw = parsed["keyVersionHW"] - key_version_stack = parsed["keyVersionStack"] - key_reference = parsed["keyReference"] - boot_reference = parsed["bootReference"] - boot_version = parsed["bootVersion"] - update_available = parsed["updateAvailable"] + return [ - Tydom( - product_name, - main_version_sw, - main_version_hw, - main_id, - main_reference, - key_version_sw, - key_version_hw, - key_version_stack, - key_reference, - boot_reference, - boot_version, - update_available, - ) + Tydom(self.tydom_client, self.tydom_client.id, self.tydom_client.id, self.tydom_client.id, "Tydom Gateway", None, parsed) + #Tydom( + # product_name, + # main_version_sw, + # main_version_hw, + # main_id, + # main_reference, + # key_version_sw, + # key_version_hw, + # key_version_stack, + # key_reference, + # boot_reference, + # boot_version, + # update_available, + #) ] @staticmethod diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index 86837ab..09f12a1 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -42,6 +42,7 @@ class TydomClient: def __init__( self, hass, + id: str, mac: str, password: str, alarm_pin: str = None, @@ -51,6 +52,7 @@ def __init__( LOGGER.debug("Initializing TydomClient Class") self._hass = hass + self.id = id self._password = password self._mac = mac self._host = host diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index 2bc6907..d49eb4b 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -4,72 +4,6 @@ logger = logging.getLogger(__name__) - -class Tydom: - """Tydom""" - - def __init__( - self, - product_name, - main_version_sw, - main_version_hw, - main_id, - main_reference, - key_version_sw, - key_version_hw, - key_version_stack, - key_reference, - boot_reference, - boot_version, - update_available, - ): - self.product_name = product_name - self.main_version_sw = main_version_sw - self.main_version_hw = main_version_hw - self.main_id = main_id - self.main_reference = main_reference - self.key_version_sw = key_version_sw - self.key_version_hw = key_version_hw - self.key_version_stack = key_version_stack - self.key_reference = key_reference - self.boot_reference = boot_reference - self.boot_version = boot_version - self.update_available = update_available - self._callbacks = set() - - def register_callback(self, callback: Callable[[], None]) -> None: - """Register callback, called when Roller changes state.""" - self._callbacks.add(callback) - - def remove_callback(self, callback: Callable[[], None]) -> None: - """Remove previously registered callback.""" - self._callbacks.discard(callback) - - async def update_device(self, updated_entity): - """Update the device values from another device""" - logger.error("update Tydom ") - self.product_name = updated_entity.product_name - self.main_version_sw = updated_entity.main_version_sw - self.main_version_hw = updated_entity.main_version_hw - self.main_id = updated_entity.main_id - self.main_reference = updated_entity.main_reference - self.key_version_sw = updated_entity.key_version_sw - self.key_version_hw = updated_entity.key_version_hw - self.key_version_stack = updated_entity.key_version_stack - self.key_reference = updated_entity.key_reference - self.boot_reference = updated_entity.boot_reference - self.boot_version = updated_entity.boot_version - self.update_available = updated_entity.update_available - await self.publish_updates() - - # In a real implementation, this library would call it's call backs when it was - # notified of any state changeds for the relevant device. - async def publish_updates(self) -> None: - """Schedule call all registered callbacks.""" - for callback in self._callbacks: - callback() - - class TydomDevice: """represents a generic device""" @@ -81,8 +15,17 @@ def __init__(self, tydom_client, uid, device_id, name, device_type, endpoint, da self._type = device_type self._endpoint = endpoint self._callbacks = set() - for key in data: - setattr(self, key, data[key]) + if data is not None: + for key in data: + + if isinstance(data[key], dict): + logger.warning("type of %s : %s", key, type(data[key])) + logger.warning("%s => %s", key, data[key]) + elif isinstance(data[key], list): + logger.warning("type of %s : %s", key, type(data[key])) + logger.warning("%s => %s", key, data[key]) + else: + setattr(self, key, data[key]) def register_callback(self, callback: Callable[[], None]) -> None: """Register callback, called when Roller changes state.""" @@ -116,7 +59,7 @@ async def update_device(self, device): """Update the device values from another device""" logger.debug("Update device %s", device.device_id) for attribute, value in device.__dict__.items(): - if attribute[:1] != "_" and value is not None: + if (attribute == "_uid" or attribute[:1] != "_") and value is not None: setattr(self, attribute, value) await self.publish_updates() @@ -128,6 +71,9 @@ async def publish_updates(self) -> None: callback() +class Tydom(TydomDevice): + """Tydom Gateway""" + class TydomShutter(TydomDevice): """Represents a shutter""" diff --git a/custom_components/deltadore-tydom/update.py b/custom_components/deltadore-tydom/update.py index fc3fdd9..2ec7c91 100644 --- a/custom_components/deltadore-tydom/update.py +++ b/custom_components/deltadore-tydom/update.py @@ -35,7 +35,7 @@ def __init__( device_friendly_name: str, ) -> None: """Init Tydom connectivity class.""" - self._attr_name = f"{device_friendly_name} Tydom" + self._attr_name = f"{device_friendly_name}" self._attr_unique_id = f"{hub.hub_id}-update" self._hub = hub @@ -60,16 +60,19 @@ def installed_version(self) -> str | None: if self._hub.device_info is None: return None # return self._hub.current_firmware - return self._hub.device_info.main_version_sw + if hasattr (self._hub.device_info, "mainVersionSW"): + return self._hub.device_info.mainVersionSW + else: + return None @property def latest_version(self) -> str | None: """Latest version available for install.""" - if self._hub.device_info is not None: - if self._hub.device_info.update_available: + if self._hub.device_info is not None and hasattr (self._hub.device_info, "mainVersionSW"): + if self._hub.device_info.updateAvailable: # return version based on today's date for update version return date.today().strftime("%y.%m.%d") - return self._hub.device_info.main_version_sw + return self._hub.device_info.mainVersionSW # FIXME : return correct version on update return None From 72e7356eef68c2703551c761a3fa9ad08b94bbc9 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Mon, 16 Oct 2023 19:54:20 +0200 Subject: [PATCH 43/74] cleanup --- custom_components/deltadore-tydom/sensor.py | 122 +------------------- 1 file changed, 1 insertion(+), 121 deletions(-) diff --git a/custom_components/deltadore-tydom/sensor.py b/custom_components/deltadore-tydom/sensor.py index f0c6670..d8876ff 100644 --- a/custom_components/deltadore-tydom/sensor.py +++ b/custom_components/deltadore-tydom/sensor.py @@ -1,130 +1,10 @@ """Platform for sensor integration.""" -# This file shows the setup for the sensors associated with the cover. -# They are setup in the same way with the call to the async_setup_entry function -# via HA from the module __init__. Each sensor has a device_class, this tells HA how -# to display it in the UI (for know types). The unit_of_measurement property tells HA -# what the unit is, so it can display the correct range. For predefined types (such as -# battery), the unit_of_measurement should match what's expected. -import random -from homeassistant.const import ( - ATTR_VOLTAGE, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_ILLUMINANCE, - PERCENTAGE, -) -from homeassistant.helpers.entity import Entity +import random from .const import DOMAIN - -# See cover.py for more details. -# Note how both entities for each roller sensor (battry and illuminance) are added at -# the same time to the same list. This way only a single async_add_devices call is -# required. async def async_setup_entry(hass, config_entry, async_add_entities): """Add sensors for passed config_entry in HA.""" hub = hass.data[DOMAIN][config_entry.entry_id] hub.add_sensor_callback = async_add_entities - - new_devices = [] - if new_devices: - async_add_entities(new_devices) - - -# This base class shows the common properties and methods for a sensor as used in this -# example. See each sensor for further details about properties and methods that -# have been overridden. -class SensorBase(Entity): - """Base representation of a Hello World Sensor.""" - - should_poll = False - - def __init__(self, roller): - """Initialize the sensor.""" - self._roller = roller - - # To link this entity to the cover device, this property must return an - # identifiers value matching that used in the cover, but no other information such - # as name. If name is returned, this entity will then also become a device in the - # HA UI. - @property - def device_info(self): - """Return information to link this entity with the correct device.""" - return {"identifiers": {(DOMAIN, self._roller.roller_id)}} - - # This property is important to let HA know if this entity is online or not. - # If an entity is offline (return False), the UI will refelect this. - @property - def available(self) -> bool: - """Return True if roller and hub is available.""" - return self._roller.online and self._roller.hub.online - - async def async_added_to_hass(self): - """Run when this Entity has been added to HA.""" - # Sensors should also register callbacks to HA when their state changes - self._roller.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self): - """Entity being removed from hass.""" - # The opposite of async_added_to_hass. Remove any registered call backs here. - self._roller.remove_callback(self.async_write_ha_state) - - -class BatterySensor(SensorBase): - """Representation of a Sensor.""" - - # The class of this device. Note the value should come from the homeassistant.const - # module. More information on the available devices classes can be seen here: - # https://developers.home-assistant.io/docs/core/entity/sensor - device_class = DEVICE_CLASS_BATTERY - - # The unit of measurement for this entity. As it's a DEVICE_CLASS_BATTERY, this - # should be PERCENTAGE. A number of units are supported by HA, for some - # examples, see: - # https://developers.home-assistant.io/docs/core/entity/sensor#available-device-classes - _attr_unit_of_measurement = PERCENTAGE - - def __init__(self, roller): - """Initialize the sensor.""" - super().__init__(roller) - - # As per the sensor, this must be a unique value within this domain. This is done - # by using the device ID, and appending "_battery" - self._attr_unique_id = f"{self._roller.roller_id}_battery" - - # The name of the entity - self._attr_name = f"{self._roller.name} Battery" - - self._state = random.randint(0, 100) - - # The value of this sensor. As this is a DEVICE_CLASS_BATTERY, this value must be - # the battery level as a percentage (between 0 and 100) - @property - def state(self): - """Return the state of the sensor.""" - return self._roller.battery_level - - -# This is another sensor, but more simple compared to the battery above. See the -# comments above for how each field works. -class IlluminanceSensor(SensorBase): - """Representation of a Sensor.""" - - device_class = DEVICE_CLASS_ILLUMINANCE - _attr_unit_of_measurement = "lx" - - def __init__(self, roller): - """Initialize the sensor.""" - super().__init__(roller) - # As per the sensor, this must be a unique value within this domain. This is done - # by using the device ID, and appending "_battery" - self._attr_unique_id = f"{self._roller.roller_id}_illuminance" - - # The name of the entity - self._attr_name = f"{self._roller.name} Illuminance" - - @property - def state(self): - """Return the state of the sensor.""" - return self._roller.illuminance From d7a127281fc064f3a243c9561232544eacd0af5c Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Mon, 16 Oct 2023 19:56:22 +0200 Subject: [PATCH 44/74] fix gateway, rename device --- .../deltadore-tydom/ha_entities.py | 183 +++++++----------- 1 file changed, 72 insertions(+), 111 deletions(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index e4073e0..3512f0f 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -71,7 +71,10 @@ class GenericSensor(SensorEntity): "mainReference", "mainVersionHW", "productName", - "mac" + "mac", + "jobsMP", + "softPlan", + "softVersion", ] def __init__( @@ -182,10 +185,10 @@ def is_on(self): class HATydom(Entity): - """Representation of a Tydom.""" + """Representation of a Tydom Gateway.""" - _attr_has_entity_name = True - _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = False + _attr_entity_category = None entity_description: str should_poll = False @@ -220,16 +223,10 @@ class HATydom(Entity): def __init__(self, device: Tydom) -> None: self.device = device - self._attr_unique_id = f"{self.device.device_id}" + self._attr_unique_id = f"{self.device.device_id}_gateway" self._attr_name = self.device.device_name self._registered_sensors = [] - - @property - def is_on(self): - """Return the state of the sensor.""" - return self._device.update_available - async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" # Importantly for a push integration, the module that will be getting updates @@ -304,7 +301,11 @@ def get_sensors(self) -> list: class HAEnergy(Entity): - """Representation of an energy sensor""" + """Representation of an Energy sensor""" + + _attr_has_entity_name = False + _attr_entity_category = None + entity_description: str should_poll = False device_class = None @@ -336,16 +337,6 @@ class HAEnergy(Entity): "outTemperature": SensorDeviceClass.TEMPERATURE, } - - #MEASUREMENT = "measurement" - """The state represents a measurement in present time.""" - #TOTAL = "total" - """The state represents a total amount. - For example: net energy consumption""" - #TOTAL_INCREASING = "total_increasing" - """The state represents a monotonically increasing total. - For example: an amount of consumed gas""" - state_classes = { "energyTotIndexWatt": SensorStateClass.TOTAL_INCREASING, "energyIndexECSWatt": SensorStateClass.TOTAL_INCREASING, @@ -381,11 +372,20 @@ class HAEnergy(Entity): "outTemperature": UnitOfTemperature.CELSIUS, } - def __init__(self, energy: TydomEnergy) -> None: - self._energy = energy - self._attr_unique_id = f"{self._energy.device_id}_energy" - self._attr_name = self._energy.device_name + def __init__(self, device: TydomEnergy) -> None: + self._device = device + self._attr_unique_id = f"{self._device.device_id}_energy" + self._attr_name = self._device.device_name self._registered_sensors = [] + + # This property is important to let HA know if this entity is online or not. + # If an entity is offline (return False), the UI will refelect this. + @property + def available(self) -> bool: + """Return True if roller and hub is available.""" + # FIXME + # return self._device.online and self._device.hub.online + return True async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" @@ -395,12 +395,12 @@ async def async_added_to_hass(self) -> None: # called where ever there are changes. # The call back registration is done once this entity is registered with HA # (rather than in the __init__) - self._energy.register_callback(self.async_write_ha_state) + self._device.register_callback(self.async_write_ha_state) async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" # The opposite of async_added_to_hass. Remove any registered call backs here. - self._energy.remove_callback(self.async_write_ha_state) + self._device.remove_callback(self.async_write_ha_state) # To link this entity to the cover device, this property must return an # identifiers value matching that used in the cover, but no other information such @@ -410,15 +410,15 @@ async def async_will_remove_from_hass(self) -> None: def device_info(self): """Return information to link this entity with the correct device.""" return { - "identifiers": {(DOMAIN, self._energy.device_id)}, - "name": self._energy.device_name, + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.device_name, } - + def get_sensors(self): """Get available sensors for this entity""" sensors = [] - for attribute, value in self._energy.__dict__.items(): + for attribute, value in self._device.__dict__.items(): if ( attribute[:1] != "_" and value is not None @@ -439,12 +439,12 @@ def get_sensors(self): if isinstance(value, bool): sensors.append( GenericBinarySensor( - self._energy, sensor_class, attribute, attribute + self._device, sensor_class, attribute, attribute ) ) else: sensors.append( - GenericSensor(self._energy, sensor_class, state_class, attribute, attribute, unit) + GenericSensor(self._device, sensor_class, state_class, attribute, attribute, unit) ) self._registered_sensors.append(attribute) @@ -475,24 +475,24 @@ class HACover(CoverEntity): "intrusion": BinarySensorDeviceClass.PROBLEM, } - def __init__(self, shutter: TydomShutter) -> None: + def __init__(self, device: TydomShutter) -> None: """Initialize the sensor.""" # Usual setup is done here. Callbacks are added in async_added_to_hass. - self._shutter = shutter + self._device = device # A unique_id for this entity with in this domain. This means for example if you # have a sensor on this cover, you must ensure the value returned is unique, # which is done here by appending "_cover". For more information, see: # https://developers.home-assistant.io/docs/entity_registry_index/#unique-id-requirements # Note: This is NOT used to generate the user visible Entity ID used in automations. - self._attr_unique_id = f"{self._shutter.device_id}_cover" + self._attr_unique_id = f"{self._device.device_id}_cover" # This is the name for this *entity*, the "name" attribute from "device_info" # is used as the device name for device screens in the UI. This name is used on # entity screens, and used to build the Entity ID that's used is automations etc. - self._attr_name = self._shutter.device_name + self._attr_name = self._device.device_name self._registered_sensors = [] - if hasattr(shutter, "position"): + if hasattr(device, "position"): self.supported_features = ( self.supported_features | SUPPORT_SET_POSITION @@ -500,7 +500,7 @@ def __init__(self, shutter: TydomShutter) -> None: | SUPPORT_CLOSE | SUPPORT_STOP ) - if hasattr(shutter, "slope"): + if hasattr(device, "slope"): self.supported_features = ( self.supported_features | SUPPORT_SET_TILT_POSITION @@ -517,12 +517,12 @@ async def async_added_to_hass(self) -> None: # called where ever there are changes. # The call back registration is done once this entity is registered with HA # (rather than in the __init__) - self._shutter.register_callback(self.async_write_ha_state) + self._device.register_callback(self.async_write_ha_state) async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" # The opposite of async_added_to_hass. Remove any registered call backs here. - self._shutter.remove_callback(self.async_write_ha_state) + self._device.remove_callback(self.async_write_ha_state) # Information about the devices that is partially visible in the UI. # The most critical thing here is to give this entity a name so it is displayed @@ -547,12 +547,9 @@ async def async_will_remove_from_hass(self) -> None: def device_info(self) -> DeviceInfo: """Information about this entity/device.""" return { - "identifiers": {(DOMAIN, self._shutter.device_id)}, + "identifiers": {(DOMAIN, self._device.device_id)}, # If desired, the name for the device could be different to the entity "name": self.name, - # "sw_version": self._shutter.firmware_version, - # "model": self._shutter.model, - # "manufacturer": self._shutter.hub.manufacturer, } # This property is important to let HA know if this entity is online or not. @@ -560,7 +557,7 @@ def device_info(self) -> DeviceInfo: @property def available(self) -> bool: """Return True if roller and hub is available.""" - # return self._shutter.online and self._shutter.hub.online + # return self._device.online and self._device.hub.online # FIXME return True @@ -575,70 +572,70 @@ def available(self) -> bool: @property def current_cover_position(self): """Return the current position of the cover.""" - return self._shutter.position + return self._device.position @property def is_closed(self) -> bool: """Return if the cover is closed, same as position 0.""" - return self._shutter.position == 0 + return self._device.position == 0 @property def current_cover_tilt_position(self): """Return the current tilt position of the cover.""" - if hasattr(self._shutter, "slope"): - return self._shutter.slope + if hasattr(self._device, "slope"): + return self._device.slope else: return None # @property # def is_closing(self) -> bool: # """Return if the cover is closing or not.""" - # return self._shutter.moving < 0 + # return self._device.moving < 0 # @property # def is_opening(self) -> bool: # """Return if the cover is opening or not.""" - # return self._shutter.moving > 0 + # return self._device.moving > 0 # These methods allow HA to tell the actual device what to do. In this case, move # the cover to the desired position, or open and close it all the way. async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - await self._shutter.up() + await self._device.up() async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - await self._shutter.down() + await self._device.down() async def async_stop_cover(self, **kwargs): """Stop the cover.""" - await self._shutter.stop() + await self._device.stop() async def async_set_cover_position(self, **kwargs: Any) -> None: """Set the cover's position.""" - await self._shutter.set_position(kwargs[ATTR_POSITION]) + await self._device.set_position(kwargs[ATTR_POSITION]) async def async_open_cover_tilt(self, **kwargs): """Open the cover tilt.""" - await self._shutter.slope_open() + await self._device.slope_open() async def async_close_cover_tilt(self, **kwargs): """Close the cover tilt.""" - await self._shutter.slope_close() + await self._device.slope_close() async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - await self._shutter.set_slope_position(kwargs[ATTR_TILT_POSITION]) + await self._device.set_slope_position(kwargs[ATTR_TILT_POSITION]) async def async_stop_cover_tilt(self, **kwargs): """Stop the cover tilt.""" - await self._shutter.slope_stop() + await self._device.slope_stop() def get_sensors(self) -> list: """Get available sensors for this entity""" sensors = [] - for attribute, value in self._shutter.__dict__.items(): + for attribute, value in self._device.__dict__.items(): if ( attribute[:1] != "_" and value is not None @@ -650,12 +647,12 @@ def get_sensors(self) -> list: if isinstance(value, bool): sensors.append( GenericBinarySensor( - self._shutter, sensor_class, attribute, attribute + self._device, sensor_class, attribute, attribute ) ) else: sensors.append( - GenericSensor(self._shutter, sensor_class, None, attribute, attribute, None) + GenericSensor(self._device, sensor_class, None, attribute, attribute, None) ) self._registered_sensors.append(attribute) @@ -663,20 +660,21 @@ def get_sensors(self) -> list: class HASmoke(BinarySensorEntity): - """Representation of an smoke sensor""" + """Representation of an Smoke sensor""" should_poll = False - device_class = BinarySensorDeviceClass.PROBLEM supported_features = None sensor_classes = {"batt_defect": BinarySensorDeviceClass.PROBLEM} - def __init__(self, smoke: TydomSmoke) -> None: - self._device = smoke + def __init__(self, device: TydomSmoke) -> None: + self._device = device self._attr_unique_id = f"{self._device.device_id}_smoke_defect" self._attr_name = self._device.device_name self._state = False self._registered_sensors = [] + self._attr_device_class = BinarySensorDeviceClass.SMOKE + @property def is_on(self): @@ -775,54 +773,17 @@ def __init__(self, device: TydomBoiler) -> None: self._attr_hvac_modes = [ HVACMode.OFF, HVACMode.HEAT, - ] # , HVACMode.AUTO, HVACMode.COOL, + ] self._registered_sensors = [] self._attr_preset_modes = ["NORMAL", "STOP", "ANTI_FROST"] - #self._attr_preset_mode = "STOP" self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] - # self._attr_hvac_mode = DICT_MODES_DD__TO_HA[] - """ - { - "name":"authorization", - "type":"string", - "permission":"rw", - "validity":"STATUS_POLLING", - "enum_values":[ - "STOP", - "HEATING" - ] - - - { - "name":"hvacMode", - "type":"string", - "permission":"rw", - "validity":"DATA_POLLING", - "enum_values":[ - "NORMAL", - "STOP", - "ANTI_FROST" - ] - - """ - + @property def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" features = ClimateEntityFeature(0) - features = features | ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE - # set_req = self.gateway.const.SetReq - # if set_req.V_HVAC_SPEED in self._values: - # features = features | ClimateEntityFeature.FAN_MODE - # if ( - # set_req.V_HVAC_SETPOINT_COOL in self._values - # and set_req.V_HVAC_SETPOINT_HEAT in self._values - # ): - # features = features | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - # else: - # features = features | ClimateEntityFeature.TARGET_TEMPERATURE return features @property @@ -933,7 +894,7 @@ def get_sensors(self): class HaWindow(CoverEntity): - """Representation of a Cover.""" + """Representation of a Window.""" should_poll = False supported_features = None @@ -1002,7 +963,7 @@ def get_sensors(self): class HaDoor(LockEntity, CoverEntity): - """Representation of a Cover.""" + """Representation of a Door.""" should_poll = False supported_features = None @@ -1071,7 +1032,7 @@ def get_sensors(self): class HaGate(CoverEntity): - """Representation of a Cover.""" + """Representation of a Gate.""" should_poll = False supported_features = None @@ -1131,7 +1092,7 @@ def get_sensors(self): class HaGarage(CoverEntity): - """Representation of a Cover.""" + """Representation of a Garage door.""" should_poll = False supported_features = None From d5bb8e169a96f42d33c5e606bb2872087893e6fa Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Mon, 16 Oct 2023 21:58:43 +0200 Subject: [PATCH 45/74] register conso and gateway manually --- .../deltadore-tydom/ha_entities.py | 27 ++++++++++++++++--- custom_components/deltadore-tydom/hub.py | 8 ++---- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 3512f0f..96cb2e6 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -1,6 +1,7 @@ """Home assistant entites""" from typing import Any +from homeassistant.helpers import device_registry as dr from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -221,12 +222,24 @@ class HATydom(Entity): "TYDOM.dat", ] - def __init__(self, device: Tydom) -> None: + def __init__(self, device: Tydom, hass) -> None: self.device = device - self._attr_unique_id = f"{self.device.device_id}_gateway" + self._attr_unique_id = f"{self.device.device_id}" self._attr_name = self.device.device_name self._registered_sensors = [] + device_registry = dr.async_get(hass) + + device_registry.async_get_or_create( + config_entry_id=self.device.device_id, + identifiers={(DOMAIN, self.device.device_id)}, + name=self.device.device_id, + manufacturer="Delta Dore", + model=self.device.productName, + sw_version=self.device.mainVersionSW, + + ) + async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" # Importantly for a push integration, the module that will be getting updates @@ -372,11 +385,19 @@ class HAEnergy(Entity): "outTemperature": UnitOfTemperature.CELSIUS, } - def __init__(self, device: TydomEnergy) -> None: + def __init__(self, device: TydomEnergy, hass) -> None: self._device = device self._attr_unique_id = f"{self._device.device_id}_energy" self._attr_name = self._device.device_name self._registered_sensors = [] + + device_registry = dr.async_get(hass) + + device_registry.async_get_or_create( + config_entry_id=self._device.device_id, + identifiers={(DOMAIN, self._device.device_id)}, + name=self._device.device_name, + ) # This property is important to let HA know if this entity is online or not. # If an entity is offline (return False), the UI will refelect this. diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index cad6b53..976bed0 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -123,11 +123,9 @@ async def create_ha_device(self, device): logger.info("Create Tydom gateway %s", device.device_id) self.devices[device.device_id] = self.device_info await self.device_info.update_device(device) - ha_device = HATydom(self.device_info) + ha_device = HATydom(self.device_info, self._hass) self.ha_devices[self.device_info.device_id] = ha_device - if self.add_sensor_callback is not None: - self.add_sensor_callback([ha_device]) if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case TydomShutter(): @@ -140,10 +138,8 @@ async def create_ha_device(self, device): self.add_sensor_callback(ha_device.get_sensors()) case TydomEnergy(): logger.warn("Create conso %s", device.device_id) - ha_device = HAEnergy(device) + ha_device = HAEnergy(device, self._hass) self.ha_devices[device.device_id] = ha_device - if self.add_sensor_callback is not None: - self.add_sensor_callback([ha_device]) if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) From b67c2ff211247f167f4251265558b7598da66fae Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 17 Oct 2023 07:47:38 +0200 Subject: [PATCH 46/74] refactor --- .../deltadore-tydom/ha_entities.py | 641 +++--------------- 1 file changed, 106 insertions(+), 535 deletions(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 96cb2e6..91ad32e 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -54,6 +54,67 @@ from .const import DOMAIN, LOGGER +class HAEntity: + + sensor_classes = {} + state_classes = {} + units = {} + filtered_attrs = {} + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + + self._device.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + self._device.remove_callback(self.async_write_ha_state) + + @property + def available(self) -> bool: + """Return True if roller and hub is available.""" + # FIXME + # return self._device.online and self._device.hub.online + return True + + def get_sensors(self): + """Get available sensors for this entity""" + sensors = [] + + for attribute, value in self._device.__dict__.items(): + if ( + attribute[:1] != "_" + and value is not None + and attribute not in self._registered_sensors + ): + if attribute in self.filtered_attrs: + continue + sensor_class = None + if attribute in self.sensor_classes: + sensor_class = self.sensor_classes[attribute] + + state_class = None + if attribute in self.state_classes: + state_class = self.state_classes[attribute] + + unit = None + if attribute in self.units: + unit = self.units[attribute] + + if isinstance(value, bool): + sensors.append( + GenericBinarySensor( + self._device, sensor_class, attribute, attribute + ) + ) + else: + sensors.append( + GenericSensor(self._device, sensor_class, state_class, attribute, attribute, unit) + ) + self._registered_sensors.append(attribute) + + return sensors + class GenericSensor(SensorEntity): """Representation of a generic sensor""" @@ -103,22 +164,16 @@ def state(self): """Return the state of the sensor.""" return getattr(self._device, self._attribute) - # To link this entity to the cover device, this property must return an - # identifiers value matching that used in the cover, but no other information such - # as name. If name is returned, this entity will then also become a device in the - # HA UI. @property def device_info(self): """Return information to link this entity with the correct device.""" return {"identifiers": {(DOMAIN, self._device.device_id)}} - # This property is important to let HA know if this entity is online or not. - # If an entity is offline (return False), the UI will refelect this. @property def available(self) -> bool: - """Return True if roller and hub is available.""" + """Return True if hub is available.""" # FIXME - # return self._device.online and self._device.hub.online + # return self._device.online return True async def async_added_to_hass(self): @@ -141,10 +196,6 @@ def __init__(self, device: TydomDevice): """Initialize the sensor.""" self._device = device - # To link this entity to the cover device, this property must return an - # identifiers value matching that used in the cover, but no other information such - # as name. If name is returned, this entity will then also become a device in the - # HA UI. @property def device_info(self): """Return information to link this entity with the correct device.""" @@ -178,6 +229,13 @@ def __init__( self._attribute = attribute self._attr_device_class = device_class + @property + def available(self) -> bool: + """Return True if hub is available.""" + # FIXME + # return self._device.online + return True + # The value of this sensor. @property def is_on(self): @@ -185,7 +243,7 @@ def is_on(self): return getattr(self._device, self._attribute) -class HATydom(Entity): +class HATydom(Entity, HAEntity): """Representation of a Tydom Gateway.""" _attr_has_entity_name = False @@ -223,97 +281,35 @@ class HATydom(Entity): ] def __init__(self, device: Tydom, hass) -> None: - self.device = device - self._attr_unique_id = f"{self.device.device_id}" - self._attr_name = self.device.device_name + self._device = device + self._attr_unique_id = f"{self._device.device_id}" + self._attr_name = self._device.device_name self._registered_sensors = [] device_registry = dr.async_get(hass) device_registry.async_get_or_create( - config_entry_id=self.device.device_id, - identifiers={(DOMAIN, self.device.device_id)}, - name=self.device.device_id, + config_entry_id=self._device.device_id, + identifiers={(DOMAIN, self._device.device_id)}, + name=self._device.device_id, manufacturer="Delta Dore", - model=self.device.productName, - sw_version=self.device.mainVersionSW, + model=self._device.productName, + sw_version=self._device.mainVersionSW, ) - async def async_added_to_hass(self) -> None: - """Run when this Entity has been added to HA.""" - # Importantly for a push integration, the module that will be getting updates - # needs to notify HA of changes. The dummy device has a registercallback - # method, so to this we add the 'self.async_write_ha_state' method, to be - # called where ever there are changes. - # The call back registration is done once this entity is registered with HA - # (rather than in the __init__) - self.device.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self) -> None: - """Entity being removed from hass.""" - # The opposite of async_added_to_hass. Remove any registered call backs here. - self.device.remove_callback(self.async_write_ha_state) - - # To link this entity to the cover device, this property must return an - # identifiers value matching that used in the cover, but no other information such - # as name. If name is returned, this entity will then also become a device in the - # HA UI. @property def device_info(self): """Return information to link this entity with the correct device.""" return { "identifiers": {(DOMAIN, self.device.device_id)}, - "name": self.device.device_id, + "name": self._device.device_id, "manufacturer": "Delta Dore", - "sw_version": self.device.mainVersionSW, - "model": self.device.productName, + "sw_version": self._device.mainVersionSW, + "model": self._device.productName, } - - # This property is important to let HA know if this entity is online or not. - # If an entity is offline (return False), the UI will refelect this. - @property - def available(self) -> bool: - """Return True if roller and hub is available.""" - # FIXME - # return self._device.online and self._device.hub.online - return True - - - def get_sensors(self) -> list: - """Get available sensors for this entity""" - sensors = [] - - for attribute, value in self.device.__dict__.items(): - if ( - attribute[:1] != "_" - and value is not None - and attribute not in self._registered_sensors - ): - sensor_class = None - if attribute in self.filtered_attrs: - continue - if attribute in self.sensor_classes: - LOGGER.warn("sensor class for %s", attribute) - LOGGER.warn("sensor class for %s = %s", attribute, self.sensor_classes[attribute]) - sensor_class = self.sensor_classes[attribute] - if isinstance(value, bool): - sensors.append( - GenericBinarySensor( - self.device, sensor_class, attribute, attribute - ) - ) - else: - sensors.append( - GenericSensor(self.device, sensor_class, None, attribute, attribute, None) - ) - self._registered_sensors.append(attribute) - - return sensors - - -class HAEnergy(Entity): +class HAEnergy(Entity, HAEntity): """Representation of an Energy sensor""" _attr_has_entity_name = False @@ -343,6 +339,7 @@ class HAEnergy(Entity): "energyInstantTi1I": SensorDeviceClass.CURRENT, "energyInstantTi1I_Min": SensorDeviceClass.CURRENT, "energyInstantTi1I_Max": SensorDeviceClass.CURRENT, + "energyIndexTi1": SensorDeviceClass.ENERGY, "energyTotIndexWatt": SensorDeviceClass.ENERGY, "energyIndexHeatWatt": SensorDeviceClass.ENERGY, "energyIndexECSWatt": SensorDeviceClass.ENERGY, @@ -351,6 +348,7 @@ class HAEnergy(Entity): } state_classes = { + "energyIndexTi1": SensorStateClass.TOTAL_INCREASING, "energyTotIndexWatt": SensorStateClass.TOTAL_INCREASING, "energyIndexECSWatt": SensorStateClass.TOTAL_INCREASING, "energyIndexHeatWatt": SensorStateClass.TOTAL_INCREASING, @@ -378,6 +376,7 @@ class HAEnergy(Entity): "energyInstantTi1I_Max": UnitOfElectricCurrent.AMPERE, "energyScaleTi1I_Min": UnitOfElectricCurrent.AMPERE, "energyScaleTi1I_Max": UnitOfElectricCurrent.AMPERE, + "energyIndexTi1": UnitOfEnergy.WATT_HOUR, "energyTotIndexWatt": UnitOfEnergy.WATT_HOUR, "energyIndexHeatWatt": UnitOfEnergy.WATT_HOUR, "energyIndexECSWatt": UnitOfEnergy.WATT_HOUR, @@ -393,97 +392,34 @@ def __init__(self, device: TydomEnergy, hass) -> None: device_registry = dr.async_get(hass) + sw_version = None + if hasattr(self._device, "softVersion"): + sw_version = self._device.softVersion + device_registry.async_get_or_create( config_entry_id=self._device.device_id, identifiers={(DOMAIN, self._device.device_id)}, name=self._device.device_name, + sw_version= sw_version, ) - # This property is important to let HA know if this entity is online or not. - # If an entity is offline (return False), the UI will refelect this. - @property - def available(self) -> bool: - """Return True if roller and hub is available.""" - # FIXME - # return self._device.online and self._device.hub.online - return True - - async def async_added_to_hass(self) -> None: - """Run when this Entity has been added to HA.""" - # Importantly for a push integration, the module that will be getting updates - # needs to notify HA of changes. The dummy device has a registercallback - # method, so to this we add the 'self.async_write_ha_state' method, to be - # called where ever there are changes. - # The call back registration is done once this entity is registered with HA - # (rather than in the __init__) - self._device.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self) -> None: - """Entity being removed from hass.""" - # The opposite of async_added_to_hass. Remove any registered call backs here. - self._device.remove_callback(self.async_write_ha_state) - - # To link this entity to the cover device, this property must return an - # identifiers value matching that used in the cover, but no other information such - # as name. If name is returned, this entity will then also become a device in the - # HA UI. @property def device_info(self): """Return information to link this entity with the correct device.""" + sw_version = None + if hasattr(self._device, "softVersion"): + sw_version = self._device.softVersion return { "identifiers": {(DOMAIN, self._device.device_id)}, "name": self._device.device_name, + "sw_version": sw_version, } - - def get_sensors(self): - """Get available sensors for this entity""" - sensors = [] - - for attribute, value in self._device.__dict__.items(): - if ( - attribute[:1] != "_" - and value is not None - and attribute not in self._registered_sensors - ): - sensor_class = None - if attribute in self.sensor_classes: - sensor_class = self.sensor_classes[attribute] - state_class = None - if attribute in self.state_classes: - state_class = self.state_classes[attribute] - - unit = None - if attribute in self.units: - unit = self.units[attribute] - - if isinstance(value, bool): - sensors.append( - GenericBinarySensor( - self._device, sensor_class, attribute, attribute - ) - ) - else: - sensors.append( - GenericSensor(self._device, sensor_class, state_class, attribute, attribute, unit) - ) - self._registered_sensors.append(attribute) - - return sensors - - -# This entire class could be written to extend a base class to ensure common attributes -# are kept identical/in sync. It's broken apart here between the Cover and Sensors to -# be explicit about what is returned, and the comments outline where the overlap is. -class HACover(CoverEntity): +class HACover(CoverEntity, HAEntity): """Representation of a Cover.""" - # Our dummy class is PUSH, so we tell HA that it should not be polled + should_poll = False - # The supported features of a cover are done using a bitmask. Using the constants - # imported above, we can tell HA the features that are supported by this entity. - # If the supported features were dynamic (ie: different depending on the external - # device it connected to), then this should be function with an @property decorator. supported_features = 0 device_class = CoverDeviceClass.SHUTTER @@ -498,19 +434,9 @@ class HACover(CoverEntity): def __init__(self, device: TydomShutter) -> None: """Initialize the sensor.""" - # Usual setup is done here. Callbacks are added in async_added_to_hass. - self._device = device - # A unique_id for this entity with in this domain. This means for example if you - # have a sensor on this cover, you must ensure the value returned is unique, - # which is done here by appending "_cover". For more information, see: - # https://developers.home-assistant.io/docs/entity_registry_index/#unique-id-requirements - # Note: This is NOT used to generate the user visible Entity ID used in automations. + self._device = device self._attr_unique_id = f"{self._device.device_id}_cover" - - # This is the name for this *entity*, the "name" attribute from "device_info" - # is used as the device name for device screens in the UI. This name is used on - # entity screens, and used to build the Entity ID that's used is automations etc. self._attr_name = self._device.device_name self._registered_sensors = [] if hasattr(device, "position"): @@ -530,66 +456,14 @@ def __init__(self, device: TydomShutter) -> None: | SUPPORT_STOP_TILT ) - async def async_added_to_hass(self) -> None: - """Run when this Entity has been added to HA.""" - # Importantly for a push integration, the module that will be getting updates - # needs to notify HA of changes. The dummy device has a registercallback - # method, so to this we add the 'self.async_write_ha_state' method, to be - # called where ever there are changes. - # The call back registration is done once this entity is registered with HA - # (rather than in the __init__) - self._device.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self) -> None: - """Entity being removed from hass.""" - # The opposite of async_added_to_hass. Remove any registered call backs here. - self._device.remove_callback(self.async_write_ha_state) - - # Information about the devices that is partially visible in the UI. - # The most critical thing here is to give this entity a name so it is displayed - # as a "device" in the HA UI. This name is used on the Devices overview table, - # and the initial screen when the device is added (rather than the entity name - # property below). You can then associate other Entities (eg: a battery - # sensor) with this device, so it shows more like a unified element in the UI. - # For example, an associated battery sensor will be displayed in the right most - # column in the Configuration > Devices view for a device. - # To associate an entity with this device, the device_info must also return an - # identical "identifiers" attribute, but not return a name attribute. - # See the sensors.py file for the corresponding example setup. - # Additional meta data can also be returned here, including sw_version (displayed - # as Firmware), model and manufacturer (displayed as by ) - # shown on the device info screen. The Manufacturer and model also have their - # respective columns on the Devices overview table. Note: Many of these must be - # set when the device is first added, and they are not always automatically - # refreshed by HA from it's internal cache. - # For more information see: - # https://developers.home-assistant.io/docs/device_registry_index/#device-properties @property def device_info(self) -> DeviceInfo: """Information about this entity/device.""" return { "identifiers": {(DOMAIN, self._device.device_id)}, - # If desired, the name for the device could be different to the entity "name": self.name, } - # This property is important to let HA know if this entity is online or not. - # If an entity is offline (return False), the UI will refelect this. - @property - def available(self) -> bool: - """Return True if roller and hub is available.""" - # return self._device.online and self._device.hub.online - # FIXME - return True - - # The following properties are how HA knows the current state of the device. - # These must return a value from memory, not make a live query to the device/hub - # etc when called (hence they are properties). For a push based integration, - # HA is notified of changes via the async_write_ha_state call. See the __init__ - # method for hos this is implemented in this example. - # The properties that are expected for a cover are based on the supported_features - # property of the object. In the case of a cover, see the following for more - # details: https://developers.home-assistant.io/docs/core/entity/cover/ @property def current_cover_position(self): """Return the current position of the cover.""" @@ -652,35 +526,8 @@ async def async_stop_cover_tilt(self, **kwargs): """Stop the cover tilt.""" await self._device.slope_stop() - def get_sensors(self) -> list: - """Get available sensors for this entity""" - sensors = [] - - for attribute, value in self._device.__dict__.items(): - if ( - attribute[:1] != "_" - and value is not None - and attribute not in self._registered_sensors - ): - sensor_class = None - if attribute in self.sensor_classes: - sensor_class = self.sensor_classes[attribute] - if isinstance(value, bool): - sensors.append( - GenericBinarySensor( - self._device, sensor_class, attribute, attribute - ) - ) - else: - sensors.append( - GenericSensor(self._device, sensor_class, None, attribute, attribute, None) - ) - self._registered_sensors.append(attribute) - - return sensors - -class HASmoke(BinarySensorEntity): +class HASmoke(BinarySensorEntity, HAEntity): """Representation of an Smoke sensor""" should_poll = False @@ -711,50 +558,7 @@ def device_info(self): "manufacturer": "Delta Dore", } - async def async_added_to_hass(self) -> None: - """Run when this Entity has been added to HA.""" - # Importantly for a push integration, the module that will be getting updates - # needs to notify HA of changes. The dummy device has a registercallback - # method, so to this we add the 'self.async_write_ha_state' method, to be - # called where ever there are changes. - # The call back registration is done once this entity is registered with HA - # (rather than in the __init__) - self._device.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self) -> None: - """Entity being removed from hass.""" - # The opposite of async_added_to_hass. Remove any registered call backs here. - self._device.remove_callback(self.async_write_ha_state) - - def get_sensors(self): - """Get available sensors for this entity""" - sensors = [] - - for attribute, value in self._device.__dict__.items(): - if ( - attribute[:1] != "_" - and value is not None - and attribute not in self._registered_sensors - ): - sensor_class = None - if attribute in self.sensor_classes: - sensor_class = self.sensor_classes[attribute] - if isinstance(value, bool): - sensors.append( - GenericBinarySensor( - self._device, sensor_class, attribute, attribute - ) - ) - else: - sensors.append( - GenericSensor(self._device, sensor_class, None, attribute, attribute, None) - ) - self._registered_sensors.append(attribute) - - return sensors - - -class HaClimate(ClimateEntity): +class HaClimate(ClimateEntity, HAEntity): """A climate entity.""" should_poll = False @@ -829,7 +633,6 @@ def hvac_mode(self) -> HVACMode: else: return None - @property def preset_mode(self) -> HVACMode: """Return the current operation (e.g. heat, cool, idle).""" @@ -865,56 +668,7 @@ async def async_set_temperature(self, **kwargs): # FIXME logger.warn("SET TEMPERATURE") - async def async_added_to_hass(self) -> None: - """Run when this Entity has been added to HA.""" - # Importantly for a push integration, the module that will be getting updates - # needs to notify HA of changes. The dummy device has a registercallback - # method, so to this we add the 'self.async_write_ha_state' method, to be - # called where ever there are changes. - # The call back registration is done once this entity is registered with HA - # (rather than in the __init__) - self._device.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self) -> None: - """Entity being removed from hass.""" - # The opposite of async_added_to_hass. Remove any registered call backs here. - self._device.remove_callback(self.async_write_ha_state) - - def get_sensors(self): - """Get available sensors for this entity""" - sensors = [] - - for attribute, value in self._device.__dict__.items(): - if ( - attribute[:1] != "_" - and value is not None - and attribute not in self._registered_sensors - ): - sensor_class = None - if attribute in self.sensor_classes: - sensor_class = self.sensor_classes[attribute] - - unit = None - if attribute in self.units: - unit = self.units[attribute] - - - if isinstance(value, bool): - sensors.append( - GenericBinarySensor( - self._device, sensor_class, attribute, attribute - ) - ) - else: - sensors.append( - GenericSensor(self._device, sensor_class, None, attribute, attribute, unit) - ) - self._registered_sensors.append(attribute) - - return sensors - - -class HaWindow(CoverEntity): +class HaWindow(CoverEntity, HAEntity): """Representation of a Window.""" should_poll = False @@ -933,15 +687,6 @@ def __init__(self, device: TydomWindow) -> None: self._attr_name = self._device.device_name self._registered_sensors = [] - async def async_added_to_hass(self) -> None: - """Run when this Entity has been added to HA.""" - - self._device.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self) -> None: - """Entity being removed from hass.""" - self._device.remove_callback(self.async_write_ha_state) - @property def device_info(self) -> DeviceInfo: """Information about this entity/device.""" @@ -955,35 +700,7 @@ def is_closed(self) -> bool: """Return if the window is closed""" return self._device.openState == "LOCKED" - def get_sensors(self): - """Get available sensors for this entity""" - sensors = [] - - for attribute, value in self._device.__dict__.items(): - if ( - attribute[:1] != "_" - and value is not None - and attribute not in self._registered_sensors - ): - sensor_class = None - if attribute in self.sensor_classes: - sensor_class = self.sensor_classes[attribute] - if isinstance(value, bool): - sensors.append( - GenericBinarySensor( - self._device, sensor_class, attribute, attribute - ) - ) - else: - sensors.append( - GenericSensor(self._device, sensor_class, None, attribute, attribute, None) - ) - self._registered_sensors.append(attribute) - - return sensors - - -class HaDoor(LockEntity, CoverEntity): +class HaDoor(LockEntity, HAEntity): """Representation of a Door.""" should_poll = False @@ -1002,15 +719,6 @@ def __init__(self, device: TydomDoor) -> None: self._attr_name = self._device.device_name self._registered_sensors = [] - async def async_added_to_hass(self) -> None: - """Run when this Entity has been added to HA.""" - - self._device.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self) -> None: - """Entity being removed from hass.""" - self._device.remove_callback(self.async_write_ha_state) - @property def device_info(self) -> DeviceInfo: """Information about this entity/device.""" @@ -1020,39 +728,11 @@ def device_info(self) -> DeviceInfo: } @property - def is_closed(self) -> bool: + def is_locked(self) -> bool: """Return if the door is closed""" return self._device.openState == "LOCKED" - def get_sensors(self): - """Get available sensors for this entity""" - sensors = [] - - for attribute, value in self._device.__dict__.items(): - if ( - attribute[:1] != "_" - and value is not None - and attribute not in self._registered_sensors - ): - sensor_class = None - if attribute in self.sensor_classes: - sensor_class = self.sensor_classes[attribute] - if isinstance(value, bool): - sensors.append( - GenericBinarySensor( - self._device, sensor_class, attribute, attribute - ) - ) - else: - sensors.append( - GenericSensor(self._device, sensor_class, None, attribute, attribute, None) - ) - self._registered_sensors.append(attribute) - - return sensors - - -class HaGate(CoverEntity): +class HaGate(CoverEntity, HAEntity): """Representation of a Gate.""" should_poll = False @@ -1067,15 +747,6 @@ def __init__(self, device: TydomGate) -> None: self._attr_name = self._device.device_name self._registered_sensors = [] - async def async_added_to_hass(self) -> None: - """Run when this Entity has been added to HA.""" - - self._device.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self) -> None: - """Entity being removed from hass.""" - self._device.remove_callback(self.async_write_ha_state) - @property def device_info(self) -> DeviceInfo: """Information about this entity/device.""" @@ -1084,35 +755,7 @@ def device_info(self) -> DeviceInfo: "name": self.name, } - def get_sensors(self): - """Get available sensors for this entity""" - sensors = [] - - for attribute, value in self._device.__dict__.items(): - if ( - attribute[:1] != "_" - and value is not None - and attribute not in self._registered_sensors - ): - sensor_class = None - if attribute in self.sensor_classes: - sensor_class = self.sensor_classes[attribute] - if isinstance(value, bool): - sensors.append( - GenericBinarySensor( - self._device, sensor_class, attribute, attribute - ) - ) - else: - sensors.append( - GenericSensor(self._device, sensor_class, None, attribute, attribute, None) - ) - self._registered_sensors.append(attribute) - - return sensors - - -class HaGarage(CoverEntity): +class HaGarage(CoverEntity, HAEntity): """Representation of a Garage door.""" should_poll = False @@ -1127,15 +770,6 @@ def __init__(self, device: TydomGarage) -> None: self._attr_name = self._device.device_name self._registered_sensors = [] - async def async_added_to_hass(self) -> None: - """Run when this Entity has been added to HA.""" - - self._device.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self) -> None: - """Entity being removed from hass.""" - self._device.remove_callback(self.async_write_ha_state) - @property def device_info(self) -> DeviceInfo: """Information about this entity/device.""" @@ -1144,35 +778,7 @@ def device_info(self) -> DeviceInfo: "name": self.name, } - def get_sensors(self): - """Get available sensors for this entity""" - sensors = [] - - for attribute, value in self._device.__dict__.items(): - if ( - attribute[:1] != "_" - and value is not None - and attribute not in self._registered_sensors - ): - sensor_class = None - if attribute in self.sensor_classes: - sensor_class = self.sensor_classes[attribute] - if isinstance(value, bool): - sensors.append( - GenericBinarySensor( - self._device, sensor_class, attribute, attribute - ) - ) - else: - sensors.append( - GenericSensor(self._device, sensor_class, None, attribute, attribute, None) - ) - self._registered_sensors.append(attribute) - - return sensors - - -class HaLight(LightEntity): +class HaLight(LightEntity, HAEntity): """Representation of a Light.""" should_poll = False @@ -1186,15 +792,6 @@ def __init__(self, device: TydomLight) -> None: self._attr_name = self._device.device_name self._registered_sensors = [] - async def async_added_to_hass(self) -> None: - """Run when this Entity has been added to HA.""" - - self._device.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self) -> None: - """Entity being removed from hass.""" - self._device.remove_callback(self.async_write_ha_state) - @property def device_info(self) -> DeviceInfo: """Information about this entity/device.""" @@ -1203,29 +800,3 @@ def device_info(self) -> DeviceInfo: "name": self.name, } - def get_sensors(self): - """Get available sensors for this entity""" - sensors = [] - - for attribute, value in self._device.__dict__.items(): - if ( - attribute[:1] != "_" - and value is not None - and attribute not in self._registered_sensors - ): - sensor_class = None - if attribute in self.sensor_classes: - sensor_class = self.sensor_classes[attribute] - if isinstance(value, bool): - sensors.append( - GenericBinarySensor( - self._device, sensor_class, attribute, attribute - ) - ) - else: - sensors.append( - GenericSensor(self._device, sensor_class, None, attribute, attribute, None) - ) - self._registered_sensors.append(attribute) - - return sensors From ef31c29a18097231be942780803e2bd587845e08 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 17 Oct 2023 07:49:03 +0200 Subject: [PATCH 47/74] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb482dd..91e08e2 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Platform | Description - Cover (Up/Down/Stop) - Tywatt 5400 - Tyxal+ DFR -- K-Line DVI +- K-Line DVI (windows, door) - Typass ATL (zones temperatures, target temperature, mode, presets, water/heat power usage) ## Installation From 9d3577066212fea1b17b5a9e823d591167d66d4a Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 17 Oct 2023 07:49:51 +0200 Subject: [PATCH 48/74] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 91e08e2..be6dd0e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Platform | Description `cover` | controls an opening or cover. `climate` | controls temperature, humidity, or fans. `light` | controls a light. +`lock` | controls a lock. `alarm_control_panel` | controls an alarm. `update` | firmware update From 1d0c036f540ad9a0316422b4bfb25dfc39a28a7e Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 17 Oct 2023 19:34:07 +0200 Subject: [PATCH 49/74] update tydom password --- custom_components/deltadore-tydom/__init__.py | 4 +-- .../deltadore-tydom/config_flow.py | 25 +++++++++++++------ custom_components/deltadore-tydom/const.py | 2 ++ 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/custom_components/deltadore-tydom/__init__.py b/custom_components/deltadore-tydom/__init__.py index 6159c65..48ccdde 100644 --- a/custom_components/deltadore-tydom/__init__.py +++ b/custom_components/deltadore-tydom/__init__.py @@ -7,7 +7,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from . import hub -from .const import DOMAIN +from .const import DOMAIN, CONF_TYDOM_PASSWORD # List of platforms to support. There should be a matching .py file for each, # eg and @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, entry.data[CONF_HOST], entry.data[CONF_MAC], - entry.data[CONF_PASSWORD], + entry.data[CONF_TYDOM_PASSWORD], pin, ) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = tydom_hub diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index 3414b4e..f3f7c56 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.components import dhcp -from .const import DOMAIN, LOGGER +from .const import DOMAIN, LOGGER, CONF_TYDOM_PASSWORD from . import hub from .tydom.tydom_client import ( TydomClientApiClientCommunicationError, @@ -73,7 +73,7 @@ async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: # This is a simple example to show an error in the UI for a short hostname # The exceptions are defined at the end of this file, and are used in the # `async_step_user` method below. - + LOGGER.debug("validating input: %s", data) if not host_valid(data[CONF_HOST]): raise InvalidHost @@ -92,16 +92,17 @@ async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: data[CONF_PASSWORD], data[CONF_MAC], ) - data[CONF_PASSWORD] = password + data[CONF_TYDOM_PASSWORD] = password pin = None if CONF_PIN in data: pin = data[CONF_PIN] - + LOGGER.debug("Input is valid.") return { CONF_HOST: data[CONF_HOST], CONF_MAC: data[CONF_MAC], CONF_PASSWORD: data[CONF_PASSWORD], + CONF_TYDOM_PASSWORD: password, CONF_PIN: pin, } @@ -136,14 +137,14 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: _errors = {} if user_input is not None: try: - await validate_input(self.hass, user_input) + user_input = await validate_input(self.hass, user_input) # Ensure it's working as expected tydom_hub = hub.Hub( self.hass, user_input[CONF_HOST], user_input[CONF_MAC], - user_input[CONF_PASSWORD], + user_input[CONF_TYDOM_PASSWORD], None, ) await tydom_hub.test_credentials() @@ -158,27 +159,35 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: # comments on `DATA_SCHEMA` for further details. # Set the error on the `host` field, not the entire form. _errors[CONF_HOST] = "invalid_host" + LOGGER.error("Invalid host: %s", user_input[CONF_HOST]) except InvalidMacAddress: _errors[CONF_MAC] = "invalid_macaddress" + LOGGER.error("Invalid MAC: %s", user_input[CONF_MAC]) except InvalidEmail: _errors[CONF_EMAIL] = "invalid_email" + LOGGER.error("Invalid email: %s", user_input[CONF_EMAIL]) except InvalidPassword: _errors[CONF_PASSWORD] = "invalid_password" + LOGGER.error("Invalid password") except TydomClientApiClientCommunicationError: traceback.print_exc() _errors["base"] = "communication_error" + LOGGER.exception("Communication error") except TydomClientApiClientAuthenticationError: traceback.print_exc() _errors["base"] = "authentication_error" + LOGGER.exception("Authentication error") except TydomClientApiClientError: traceback.print_exc() _errors["base"] = "unknown" + LOGGER.exception("Unknown error") except Exception: # pylint: disable=broad-except traceback.print_exc() LOGGER.exception("Unexpected exception") _errors["base"] = "unknown" else: + user_input[CONF_PASSWORD] = password return self.async_create_entry( title="Tydom-" + user_input[CONF_MAC], data=user_input ) @@ -229,14 +238,14 @@ async def async_step_discovery_confirm(self, user_input=None): _errors = {} if user_input is not None: try: - await validate_input(self.hass, user_input) + user_input = await validate_input(self.hass, user_input) # Ensure it's working as expected tydom_hub = hub.Hub( self.hass, user_input[CONF_HOST], user_input[CONF_MAC], - user_input[CONF_PASSWORD], + user_input[CONF_TYDOM_PASSWORD], None, ) await tydom_hub.test_credentials() diff --git a/custom_components/deltadore-tydom/const.py b/custom_components/deltadore-tydom/const.py index c57107a..4cef298 100644 --- a/custom_components/deltadore-tydom/const.py +++ b/custom_components/deltadore-tydom/const.py @@ -9,3 +9,5 @@ NAME = "Integration blueprint" VERSION = "0.0.1" ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/" + +CONF_TYDOM_PASSWORD = "tydom_password" \ No newline at end of file From 310bdf52854336edb351386d9b043e0049008b81 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Sun, 22 Oct 2023 12:07:08 +0200 Subject: [PATCH 50/74] cleanup logs --- custom_components/deltadore-tydom/climate.py | 1 - custom_components/deltadore-tydom/cover.py | 1 - .../deltadore-tydom/ha_entities.py | 25 +++++--------- custom_components/deltadore-tydom/hub.py | 34 ++++++++----------- custom_components/deltadore-tydom/light.py | 1 - custom_components/deltadore-tydom/lock.py | 1 - .../deltadore-tydom/tydom/tydom_devices.py | 31 ++++++++++++----- 7 files changed, 47 insertions(+), 47 deletions(-) diff --git a/custom_components/deltadore-tydom/climate.py b/custom_components/deltadore-tydom/climate.py index b517562..8bb874a 100644 --- a/custom_components/deltadore-tydom/climate.py +++ b/custom_components/deltadore-tydom/climate.py @@ -29,7 +29,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add cover for passed config_entry in HA.""" - LOGGER.error("***** async_setup_entry *****") # The hub is loaded from the associated hass.data entry that was created in the # __init__.async_setup_entry function hub = hass.data[DOMAIN][config_entry.entry_id] diff --git a/custom_components/deltadore-tydom/cover.py b/custom_components/deltadore-tydom/cover.py index cae445e..1f6d4bc 100644 --- a/custom_components/deltadore-tydom/cover.py +++ b/custom_components/deltadore-tydom/cover.py @@ -29,7 +29,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add cover for passed config_entry in HA.""" - LOGGER.error("***** async_setup_entry *****") # The hub is loaded from the associated hass.data entry that was created in the # __init__.async_setup_entry function hub = hass.data[DOMAIN][config_entry.entry_id] diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 91ad32e..2bae1eb 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -63,7 +63,6 @@ class HAEntity: async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" - self._device.register_callback(self.async_write_ha_state) async def async_will_remove_from_hass(self) -> None: @@ -242,7 +241,6 @@ def is_on(self): """Return the state of the sensor.""" return getattr(self._device, self._attribute) - class HATydom(Entity, HAEntity): """Representation of a Tydom Gateway.""" @@ -561,7 +559,7 @@ def device_info(self): class HaClimate(ClimateEntity, HAEntity): """A climate entity.""" - should_poll = False + should_poll = True sensor_classes = { "temperature": SensorDeviceClass.TEMPERATURE, @@ -582,7 +580,7 @@ class HaClimate(ClimateEntity, HAEntity): HVACMode.HEAT: "HEATING", HVACMode.OFF: "STOP", } - DICT_MODES_DD__TO_HA = { + DICT_MODES_DD_TO_HA = { # "": HVACMode.AUTO, # "": HVACMode.COOL, "HEATING": HVACMode.HEAT, @@ -627,16 +625,16 @@ def temperature_unit(self) -> str: @property def hvac_mode(self) -> HVACMode: """Return the current operation (e.g. heat, cool, idle).""" - # FIXME if (hasattr(self._device, 'authorization')): - return self.DICT_MODES_DD__TO_HA[self._device.authorization] + LOGGER.debug("hvac_mode = %s", self.DICT_MODES_DD_TO_HA[self._device.authorization]) + return self.DICT_MODES_DD_TO_HA[self._device.authorization] else: return None @property def preset_mode(self) -> HVACMode: """Return the current operation (e.g. heat, cool, idle).""" - # FIXME + LOGGER.debug("preset_mode = %s", self._device.hvacMode) return self._device.hvacMode @property @@ -653,20 +651,15 @@ def target_temperature(self) -> float | None: async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" - # FIXME - self._device.set_hvac_mode(self.DICT_MODES_HA_TO_DD[hvac_mode]) - logger.warn("SET HVAC MODE") + await self._device.set_hvac_mode(self.DICT_MODES_HA_TO_DD[hvac_mode]) async def async_set_preset_mode(self, preset_mode): """Set new target preset mode.""" - # FIXME - self._device.set_preset_mode(preset_mode) - logger.warn("SET preset MODE") + await self._device.set_preset_mode(preset_mode) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, target_temperature): """Set new target temperature.""" - # FIXME - logger.warn("SET TEMPERATURE") + await self._device.set_temperature(target_temperature) class HaWindow(CoverEntity, HAEntity): """Representation of a Window.""" diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 976bed0..ea8d124 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -19,8 +19,7 @@ from .tydom.tydom_devices import Tydom from .ha_entities import * -logger = logging.getLogger(__name__) - +from .const import LOGGER class Hub: """Hub for Delta Dore Tydom.""" @@ -96,7 +95,7 @@ async def test_credentials(self) -> None: async def setup(self, connection: ClientWebSocketResponse) -> None: """Listen to tydom events.""" - logger.info("Listen to tydom events") + LOGGER.debug("Listen to tydom events") while True: devices = await self._tydom_client.consume_messages() if devices is not None: @@ -105,7 +104,7 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: self.devices[device.device_id] = device await self.create_ha_device(device) else: - logger.warn( + LOGGER.debug( "update device %s : %s", device.device_id, self.devices[device.device_id], @@ -116,11 +115,9 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: async def create_ha_device(self, device): """Create a new HA device""" - logger.warn("device type %s", device.device_type) match device: case Tydom(): - - logger.info("Create Tydom gateway %s", device.device_id) + LOGGER.debug("Create Tydom gateway %s", device.device_id) self.devices[device.device_id] = self.device_info await self.device_info.update_device(device) ha_device = HATydom(self.device_info, self._hass) @@ -129,7 +126,7 @@ async def create_ha_device(self, device): if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case TydomShutter(): - logger.warn("Create cover %s", device.device_id) + LOGGER.debug("Create cover %s", device.device_id) ha_device = HACover(device) self.ha_devices[device.device_id] = ha_device if self.add_cover_callback is not None: @@ -137,7 +134,7 @@ async def create_ha_device(self, device): if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case TydomEnergy(): - logger.warn("Create conso %s", device.device_id) + LOGGER.debug("Create conso %s", device.device_id) ha_device = HAEnergy(device, self._hass) self.ha_devices[device.device_id] = ha_device @@ -145,7 +142,7 @@ async def create_ha_device(self, device): self.add_sensor_callback(ha_device.get_sensors()) case TydomSmoke(): - logger.warn("Create smoke %s", device.device_id) + LOGGER.debug("Create smoke %s", device.device_id) ha_device = HASmoke(device) self.ha_devices[device.device_id] = ha_device if self.add_sensor_callback is not None: @@ -154,7 +151,7 @@ async def create_ha_device(self, device): if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case TydomBoiler(): - logger.warn("Create boiler %s", device.device_id) + LOGGER.debug("Create boiler %s", device.device_id) ha_device = HaClimate(device) self.ha_devices[device.device_id] = ha_device if self.add_climate_callback is not None: @@ -163,7 +160,7 @@ async def create_ha_device(self, device): if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case TydomWindow(): - logger.warn("Create window %s", device.device_id) + LOGGER.debug("Create window %s", device.device_id) ha_device = HaWindow(device) self.ha_devices[device.device_id] = ha_device if self.add_cover_callback is not None: @@ -172,7 +169,7 @@ async def create_ha_device(self, device): if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case TydomDoor(): - logger.warn("Create door %s", device.device_id) + LOGGER.debug("Create door %s", device.device_id) ha_device = HaDoor(device) self.ha_devices[device.device_id] = ha_device if self.add_cover_callback is not None: @@ -181,7 +178,7 @@ async def create_ha_device(self, device): if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case TydomGate(): - logger.warn("Create gate %s", device.device_id) + LOGGER.debug("Create gate %s", device.device_id) ha_device = HaGate(device) self.ha_devices[device.device_id] = ha_device if self.add_cover_callback is not None: @@ -190,7 +187,7 @@ async def create_ha_device(self, device): if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case TydomGarage(): - logger.warn("Create garage %s", device.device_id) + LOGGER.debug("Create garage %s", device.device_id) ha_device = HaGarage(device) self.ha_devices[device.device_id] = ha_device if self.add_cover_callback is not None: @@ -199,7 +196,7 @@ async def create_ha_device(self, device): if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case TydomLight(): - logger.warn("Create light %s", device.device_id) + LOGGER.debug("Create light %s", device.device_id) ha_device = HaLight(device) self.ha_devices[device.device_id] = ha_device if self.add_light_callback is not None: @@ -208,7 +205,7 @@ async def create_ha_device(self, device): if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) case _: - logger.error( + LOGGER.error( "unsupported device type (%s) %s for device %s", type(device), device.device_type, @@ -227,12 +224,11 @@ async def update_ha_device(self, stored_device, device): async def ping(self) -> None: """Periodically send pings""" - logger.info("Sending ping") while True: await self._tydom_client.ping() await asyncio.sleep(10) async def async_trigger_firmware_update(self) -> None: """Trigger firmware update""" - logger.info("Installing firmware update...") + LOGGER.debug("Installing firmware update...") self._tydom_client.update_firmware() diff --git a/custom_components/deltadore-tydom/light.py b/custom_components/deltadore-tydom/light.py index 9c9a1a9..d2c1c54 100644 --- a/custom_components/deltadore-tydom/light.py +++ b/custom_components/deltadore-tydom/light.py @@ -18,7 +18,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add cover for passed config_entry in HA.""" - LOGGER.error("***** async_setup_entry *****") # The hub is loaded from the associated hass.data entry that was created in the # __init__.async_setup_entry function hub = hass.data[DOMAIN][config_entry.entry_id] diff --git a/custom_components/deltadore-tydom/lock.py b/custom_components/deltadore-tydom/lock.py index f330270..e363e03 100644 --- a/custom_components/deltadore-tydom/lock.py +++ b/custom_components/deltadore-tydom/lock.py @@ -18,7 +18,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add cover for passed config_entry in HA.""" - LOGGER.error("***** async_setup_entry *****") # The hub is loaded from the associated hass.data entry that was created in the # __init__.async_setup_entry function hub = hass.data[DOMAIN][config_entry.entry_id] diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index d49eb4b..e36f624 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -1,6 +1,7 @@ """Support for Tydom classes""" from typing import Callable import logging +from ..const import LOGGER logger = logging.getLogger(__name__) @@ -28,7 +29,7 @@ def __init__(self, tydom_client, uid, device_id, name, device_type, endpoint, da setattr(self, key, data[key]) def register_callback(self, callback: Callable[[], None]) -> None: - """Register callback, called when Roller changes state.""" + """Register callback, called when state changes.""" self._callbacks.add(callback) def remove_callback(self, callback: Callable[[], None]) -> None: @@ -59,6 +60,8 @@ async def update_device(self, device): """Update the device values from another device""" logger.debug("Update device %s", device.device_id) for attribute, value in device.__dict__.items(): +# if device._type == "boiler": +# LOGGER.debug("updating device attr %s=>%s", attribute, value) if (attribute == "_uid" or attribute[:1] != "_") and value is not None: setattr(self, attribute, value) await self.publish_updates() @@ -68,6 +71,7 @@ async def update_device(self, device): async def publish_updates(self) -> None: """Schedule call all registered callbacks.""" for callback in self._callbacks: +# LOGGER.debug("calling callback%s", callback) callback() @@ -148,16 +152,27 @@ class TydomSmoke(TydomDevice): class TydomBoiler(TydomDevice): """represents a boiler""" - def set_hvac_mode(self, mode): + async def set_hvac_mode(self, mode): """Set hvac mode (STOP/HEATING)""" - logger.info("setting mode to %s", mode) - # authorization + logger.error("setting mode to %s", mode) + LOGGER.debug("setting mode to %s", mode) + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "authorization", mode + ) - def set_preset_mode(self, mode): + async def set_preset_mode(self, mode): """Set preset mode (NORMAL/STOP/ANTI_FROST)""" - logger.info("setting preset to %s", mode) - # hvacMode - + logger.error("setting preset to %s", mode) + LOGGER.debug("setting preset to %s", mode) + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "hvacMode", mode + ) + async def set_temperature(self, temperature): + """Set target temperature""" + logger.error("setting target temperature to %s", temperature) + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "setpoint", temperature + ) class TydomWindow(TydomDevice): From 3a5cc680bbf827fd21fc9feedbeff11917c6eab5 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Sun, 22 Oct 2023 12:13:50 +0200 Subject: [PATCH 51/74] update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index be6dd0e..fb5105d 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Platform | Description - Tywatt 5400 - Tyxal+ DFR - K-Line DVI (windows, door) -- Typass ATL (zones temperatures, target temperature, mode, presets, water/heat power usage) +- Typass ATL (zones temperatures, target temperature, mode, presets, water/heat power usage) with Tybox 5101 ## Installation From 7b41f3efd3c86417af34b2cf72ef519811d69968 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Sun, 22 Oct 2023 14:00:52 +0200 Subject: [PATCH 52/74] fix set boiler temperature --- custom_components/deltadore-tydom/ha_entities.py | 4 ++-- custom_components/deltadore-tydom/tydom/tydom_devices.py | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 2bae1eb..46c1db6 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -657,9 +657,9 @@ async def async_set_preset_mode(self, preset_mode): """Set new target preset mode.""" await self._device.set_preset_mode(preset_mode) - async def async_set_temperature(self, target_temperature): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - await self._device.set_temperature(target_temperature) + await self._device.set_temperature(str(kwargs.get(ATTR_TEMPERATURE))) class HaWindow(CoverEntity, HAEntity): """Representation of a Window.""" diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index e36f624..6e16b9e 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -60,18 +60,13 @@ async def update_device(self, device): """Update the device values from another device""" logger.debug("Update device %s", device.device_id) for attribute, value in device.__dict__.items(): -# if device._type == "boiler": -# LOGGER.debug("updating device attr %s=>%s", attribute, value) if (attribute == "_uid" or attribute[:1] != "_") and value is not None: setattr(self, attribute, value) await self.publish_updates() - # In a real implementation, this library would call it's call backs when it was - # notified of any state changeds for the relevant device. async def publish_updates(self) -> None: """Schedule call all registered callbacks.""" for callback in self._callbacks: -# LOGGER.debug("calling callback%s", callback) callback() From db169220680f8ccfc21380fc2562d808de729883 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Sun, 22 Oct 2023 18:29:57 +0200 Subject: [PATCH 53/74] get metadata for devices, cleanup, min/max temp for climate entity, fix climate entity refresh --- .../deltadore-tydom/ha_entities.py | 12 +- custom_components/deltadore-tydom/hub.py | 2 +- .../deltadore-tydom/tydom/MessageHandler.py | 655 ++---------------- .../deltadore-tydom/tydom/tydom_devices.py | 8 +- 4 files changed, 56 insertions(+), 621 deletions(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 46c1db6..d613df4 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -293,7 +293,6 @@ def __init__(self, device: Tydom, hass) -> None: manufacturer="Delta Dore", model=self._device.productName, sw_version=self._device.mainVersionSW, - ) @property @@ -559,7 +558,7 @@ def device_info(self): class HaClimate(ClimateEntity, HAEntity): """A climate entity.""" - should_poll = True + should_poll = False sensor_classes = { "temperature": SensorDeviceClass.TEMPERATURE, @@ -590,6 +589,7 @@ class HaClimate(ClimateEntity, HAEntity): def __init__(self, device: TydomBoiler) -> None: super().__init__() self._device = device + self._device._ha_device = self self._attr_unique_id = f"{self._device.device_id}_climate" self._attr_name = self._device.device_name @@ -601,6 +601,12 @@ def __init__(self, device: TydomBoiler) -> None: self._attr_preset_modes = ["NORMAL", "STOP", "ANTI_FROST"] self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] + + if "min" in self._device._metadata["setpoint"]: + self._attr_min_temp = self._device._metadata["setpoint"]["min"] + + if "max" in self._device._metadata["setpoint"]: + self._attr_max_temp = self._device._metadata["setpoint"]["max"] @property def supported_features(self) -> ClimateEntityFeature: @@ -651,7 +657,7 @@ def target_temperature(self) -> float | None: async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" - await self._device.set_hvac_mode(self.DICT_MODES_HA_TO_DD[hvac_mode]) + await self._device.set_hvac_mode(self.DICT_MODES_HA_TO_DD[hvac_mode]) async def async_set_preset_mode(self, preset_mode): """Set new target preset mode.""" diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index ea8d124..a4d9576 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -46,7 +46,7 @@ def __init__( self._hass = hass self._name = mac self._id = "Tydom-" + mac[6:] - self.device_info = Tydom(None, None, None, None, None, None, None) + self.device_info = Tydom(None, None, None, None, None, None, None, None) self.devices = {} self.ha_devices = {} self.add_cover_callback = None diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 98ae64f..4db0d14 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -10,209 +10,11 @@ from ..const import LOGGER -# Dicts -deviceAlarmKeywords = [ - "alarmMode", - "alarmState", - "alarmSOS", - "zone1State", - "zone2State", - "zone3State", - "zone4State", - "zone5State", - "zone6State", - "zone7State", - "zone8State", - "gsmLevel", - "inactiveProduct", - "zone1State", - "liveCheckRunning", - "networkDefect", - "unitAutoProtect", - "unitBatteryDefect", - "unackedEvent", - "alarmTechnical", - "systAutoProtect", - "systBatteryDefect", - "systSupervisionDefect", - "systOpenIssue", - "systTechnicalDefect", - "videoLinkDefect", - "outTemperature", - "kernelUpToDate", - "irv1State", - "irv2State", - "irv3State", - "irv4State", - "simDefect", - "remoteSurveyDefect", - "systSectorDefect", -] -deviceAlarmDetailsKeywords = [ - "alarmSOS", - "zone1State", - "zone2State", - "zone3State", - "zone4State", - "zone5State", - "zone6State", - "zone7State", - "zone8State", - "gsmLevel", - "inactiveProduct", - "zone1State", - "liveCheckRunning", - "networkDefect", - "unitAutoProtect", - "unitBatteryDefect", - "unackedEvent", - "alarmTechnical", - "systAutoProtect", - "systBatteryDefect", - "systSupervisionDefect", - "systOpenIssue", - "systTechnicalDefect", - "videoLinkDefect", - "outTemperature", -] - -deviceLightKeywords = [ - "level", - "onFavPos", - "thermicDefect", - "battDefect", - "loadDefect", - "cmdDefect", - "onPresenceDetected", - "onDusk", -] -deviceLightDetailsKeywords = [ - "onFavPos", - "thermicDefect", - "battDefect", - "loadDefect", - "cmdDefect", - "onPresenceDetected", - "onDusk", -] - -deviceDoorKeywords = ["openState", "intrusionDetect"] -deviceDoorDetailsKeywords = [ - "onFavPos", - "thermicDefect", - "obstacleDefect", - "intrusion", - "battDefect", -] - -deviceCoverKeywords = [ - "position", - "slope", - "onFavPos", - "thermicDefect", - "obstacleDefect", - "intrusion", - "battDefect", -] -deviceCoverDetailsKeywords = [ - "onFavPos", - "thermicDefect", - "obstacleDefect", - "intrusion", - "battDefect", - "position", - "slope", -] - -deviceBoilerKeywords = [ - "thermicLevel", - "delayThermicLevel", - "temperature", - "authorization", - "hvacMode", - "timeDelay", - "tempoOn", - "antifrostOn", - "openingDetected", - "presenceDetected", - "absence", - "loadSheddingOn", - "setpoint", - "delaySetpoint", - "anticipCoeff", - "outTemperature", -] - -deviceSwitchKeywords = ["thermicDefect"] -deviceSwitchDetailsKeywords = ["thermicDefect"] - -deviceMotionKeywords = ["motionDetect"] -deviceMotionDetailsKeywords = ["motionDetect"] - -device_conso_classes = { - "energyInstantTotElec": "current", - "energyInstantTotElec_Min": "current", - "energyInstantTotElec_Max": "current", - "energyScaleTotElec_Min": "current", - "energyScaleTotElec_Max": "current", - "energyInstantTotElecP": "power", - "energyInstantTotElec_P_Min": "power", - "energyInstantTotElec_P_Max": "power", - "energyScaleTotElec_P_Min": "power", - "energyScaleTotElec_P_Max": "power", - "energyInstantTi1P": "power", - "energyInstantTi1P_Min": "power", - "energyInstantTi1P_Max": "power", - "energyScaleTi1P_Min": "power", - "energyScaleTi1P_Max": "power", - "energyInstantTi1I": "current", - "energyInstantTi1I_Min": "current", - "energyInstantTi1I_Max": "current", - "energyScaleTi1I_Min": "current", - "energyScaleTi1I_Max": "current", - "energyTotIndexWatt": "energy", - "energyIndexHeatWatt": "energy", - "energyIndexECSWatt": "energy", - "energyIndexHeatGas": "energy", - "outTemperature": "temperature", -} - -device_conso_unit_of_measurement = { - "energyInstantTotElec": "A", - "energyInstantTotElec_Min": "A", - "energyInstantTotElec_Max": "A", - "energyScaleTotElec_Min": "A", - "energyScaleTotElec_Max": "A", - "energyInstantTotElecP": "W", - "energyInstantTotElec_P_Min": "W", - "energyInstantTotElec_P_Max": "W", - "energyScaleTotElec_P_Min": "W", - "energyScaleTotElec_P_Max": "W", - "energyInstantTi1P": "W", - "energyInstantTi1P_Min": "W", - "energyInstantTi1P_Max": "W", - "energyScaleTi1P_Min": "W", - "energyScaleTi1P_Max": "W", - "energyInstantTi1I": "A", - "energyInstantTi1I_Min": "A", - "energyInstantTi1I_Max": "A", - "energyScaleTi1I_Min": "A", - "energyScaleTi1I_Max": "A", - "energyTotIndexWatt": "Wh", - "energyIndexHeatWatt": "Wh", - "energyIndexECSWatt": "Wh", - "energyIndexHeatGas": "Wh", - "outTemperature": "C", -} -device_conso_keywords = device_conso_classes.keys() - -deviceSmokeKeywords = ["techSmokeDefect"] - # Device dict for parsing device_name = dict() device_endpoint = dict() device_type = dict() - +device_metadata = dict() class MessageHandler: """Handle incomming Tydom messages""" @@ -356,6 +158,10 @@ async def parse_response(self, incoming, uri_origin, http_request_line): parsed = json.loads(data) return await self.parse_devices_cdata(parsed=parsed) + elif msg_type == "msg_metadata": + parsed = json.loads(data) + return await self.parse_devices_metadata(parsed=parsed) + elif msg_type == "msg_html": LOGGER.debug("HTML Response ?") @@ -369,25 +175,33 @@ async def parse_response(self, incoming, uri_origin, http_request_line): traceback.print_exception(e) LOGGER.debug("Incoming data parsed with success") + async def parse_devices_metadata(self, parsed): + LOGGER.debug("metadata : %s", parsed) + #LOGGER.error("metadata : %s", parsed) + for device in parsed: + id = device["id"] + for endpoint in device["endpoints"]: + id_endpoint = endpoint["id"] + device_unique_id = str(id_endpoint) + "_" + str(id) + device_metadata[device_unique_id] = dict() + LOGGER.error("metadata unique id : %s", device_unique_id) + for metadata in endpoint["metadata"]: + metadata_name = metadata["name"] + device_metadata[device_unique_id][metadata_name] = dict() + LOGGER.error("\tmetadata name : %s", metadata_name) + for meta in metadata: + if meta == "name": + continue + LOGGER.error("\t\tmetadata meta : %s => %s", meta, metadata[meta]) + device_metadata[device_unique_id][metadata_name][meta] = metadata[meta] + LOGGER.error("metadata result %s", device_metadata) + return [] + async def parse_msg_info(self, parsed): LOGGER.debug("parse_msg_info : %s", parsed) return [ - Tydom(self.tydom_client, self.tydom_client.id, self.tydom_client.id, self.tydom_client.id, "Tydom Gateway", None, parsed) - #Tydom( - # product_name, - # main_version_sw, - # main_version_hw, - # main_id, - # main_reference, - # key_version_sw, - # key_version_hw, - # key_version_stack, - # key_reference, - # boot_reference, - # boot_version, - # update_available, - #) + Tydom(self.tydom_client, self.tydom_client.id, self.tydom_client.id, self.tydom_client.id, "Tydom Gateway", None, None, parsed) ] @staticmethod @@ -413,19 +227,19 @@ async def get_device( match last_usage: case "shutter" | "klineShutter" | "awning": return TydomShutter( - tydom_client, uid, device_id, name, last_usage, endpoint, data + tydom_client, uid, device_id, name, last_usage, endpoint, None, data ) case "window" | "windowFrench" | "windowSliding" | "klineWindowFrench" | "klineWindowSliding": return TydomWindow( - tydom_client, uid, device_id, name, last_usage, endpoint, data + tydom_client, uid, device_id, name, last_usage, endpoint, device_metadata[uid], data ) case "belmDoor" | "klineDoor": return TydomDoor( - tydom_client, uid, device_id, name, last_usage, endpoint, data + tydom_client, uid, device_id, name, last_usage, endpoint, device_metadata[uid], data ) case "garage_door": return TydomGarage( - tydom_client, uid, device_id, name, last_usage, endpoint, data + tydom_client, uid, device_id, name, last_usage, endpoint, device_metadata[uid], data ) case "gate": return TydomGate( @@ -435,6 +249,7 @@ async def get_device( name, last_usage, endpoint, + device_metadata[uid], data, ) case "light": @@ -445,23 +260,24 @@ async def get_device( name, last_usage, endpoint, + device_metadata[uid], data, ) case "conso": return TydomEnergy( - tydom_client, uid, device_id, name, last_usage, endpoint, data + tydom_client, uid, device_id, name, last_usage, endpoint, device_metadata[uid], data ) case "sensorDFR": return TydomSmoke( - tydom_client, uid, device_id, name, last_usage, endpoint, data + tydom_client, uid, device_id, name, last_usage, endpoint, device_metadata[uid], data ) case "boiler" | "sh_hvac" | "electric": return TydomBoiler( - tydom_client, uid, device_id, name, last_usage, endpoint, data + tydom_client, uid, device_id, name, last_usage, endpoint, device_metadata[uid], data ) case "alarm": return TydomAlarm( - tydom_client, uid, device_id, name, last_usage, endpoint, data + tydom_client, uid, device_id, name, last_usage, endpoint, device_metadata[uid], data ) case _: # TODO generic sensor ? @@ -633,400 +449,7 @@ async def parse_devices_data(self, parsed): type_of_id, ) except Exception as e: - LOGGER.error("msg_data error in parsing !") - LOGGER.error(e) - - """ - try: - attr_alarm = {} - attr_cover = {} - attr_door = {} - attr_ukn = {} - attr_window = {} - attr_light = {} - attr_gate = {} - attr_boiler = {} - attr_smoke = {} - device_id = i["id"] - endpoint_id = endpoint["id"] - unique_id = str(endpoint_id) + "_" + str(device_id) - name_of_id = self.get_name_from_id(unique_id) - type_of_id = self.get_type_from_id(unique_id) - - LOGGER.info( - "Device update (id=%s, endpoint=%s, name=%s, type=%s)", - device_id, - endpoint_id, - name_of_id, - type_of_id, - ) - - data = {} - - for elem in endpoint["data"]: - element_name = elem["name"] - element_value = elem["value"] - element_validity = elem["validity"] - - if element_validity == "upToDate" and element_name != "name": - data[element_name] = element_value - - print_id = name_of_id if len(name_of_id) != 0 else device_id - - if type_of_id == "light": - if ( - element_name in deviceLightKeywords - and element_validity == "upToDate" - ): - attr_light["device_id"] = device_id - attr_light["endpoint_id"] = endpoint_id - attr_light["id"] = ( - str(device_id) + "_" + str(endpoint_id) - ) - attr_light["light_name"] = print_id - attr_light["name"] = print_id - attr_light["device_type"] = "light" - attr_light[element_name] = element_value - - if type_of_id == "shutter" or type_of_id == "klineShutter": - if ( - element_name in deviceCoverKeywords - and element_validity == "upToDate" - ): - attr_cover["device_id"] = device_id - attr_cover["endpoint_id"] = endpoint_id - attr_cover["id"] = ( - str(device_id) + "_" + str(endpoint_id) - ) - attr_cover["cover_name"] = print_id - attr_cover["name"] = print_id - attr_cover["device_type"] = "cover" - - if element_name == "slope": - attr_cover["tilt"] = element_value - else: - attr_cover[element_name] = element_value - - if type_of_id == "belmDoor" or type_of_id == "klineDoor": - if ( - element_name in deviceDoorKeywords - and element_validity == "upToDate" - ): - attr_door["device_id"] = device_id - attr_door["endpoint_id"] = endpoint_id - attr_door["id"] = ( - str(device_id) + "_" + str(endpoint_id) - ) - attr_door["door_name"] = print_id - attr_door["name"] = print_id - attr_door["device_type"] = "sensor" - attr_door["element_name"] = element_name - attr_door[element_name] = element_value - - if ( - type_of_id == "windowFrench" - or type_of_id == "window" - or type_of_id == "windowSliding" - or type_of_id == "klineWindowFrench" - or type_of_id == "klineWindowSliding" - ): - if ( - element_name in deviceDoorKeywords - and element_validity == "upToDate" - ): - attr_window["device_id"] = device_id - attr_window["endpoint_id"] = endpoint_id - attr_window["id"] = ( - str(device_id) + "_" + str(endpoint_id) - ) - attr_window["door_name"] = print_id - attr_window["name"] = print_id - attr_window["device_type"] = "sensor" - attr_window["element_name"] = element_name - attr_window[element_name] = element_value - - if type_of_id == "boiler": - if ( - element_name in deviceBoilerKeywords - and element_validity == "upToDate" - ): - attr_boiler["device_id"] = device_id - attr_boiler["endpoint_id"] = endpoint_id - attr_boiler["id"] = ( - str(device_id) + "_" + str(endpoint_id) - ) - # attr_boiler['boiler_name'] = print_id - attr_boiler["name"] = print_id - attr_boiler["device_type"] = "climate" - attr_boiler[element_name] = element_value - - if type_of_id == "alarm": - if ( - element_name in deviceAlarmKeywords - and element_validity == "upToDate" - ): - attr_alarm["device_id"] = device_id - attr_alarm["endpoint_id"] = endpoint_id - attr_alarm["id"] = ( - str(device_id) + "_" + str(endpoint_id) - ) - attr_alarm["alarm_name"] = "Tyxal Alarm" - attr_alarm["name"] = "Tyxal Alarm" - attr_alarm["device_type"] = "alarm_control_panel" - attr_alarm[element_name] = element_value - - if type_of_id == "garage_door" or type_of_id == "gate": - if ( - element_name in deviceSwitchKeywords - and element_validity == "upToDate" - ): - attr_gate["device_id"] = device_id - attr_gate["endpoint_id"] = endpoint_id - attr_gate["id"] = ( - str(device_id) + "_" + str(endpoint_id) - ) - attr_gate["switch_name"] = print_id - attr_gate["name"] = print_id - attr_gate["device_type"] = "switch" - attr_gate[element_name] = element_value - - if type_of_id == "conso": - if ( - element_name in device_conso_keywords - and element_validity == "upToDate" - ): - attr_conso = { - "device_id": device_id, - "endpoint_id": endpoint_id, - "id": str(device_id) + "_" + str(endpoint_id), - "name": print_id, - "device_type": "sensor", - element_name: element_value, - } - - if element_name in device_conso_classes: - attr_conso[ - "device_class" - ] = device_conso_classes[element_name] - - if element_name in device_conso_unit_of_measurement: - attr_conso[ - "unit_of_measurement" - ] = device_conso_unit_of_measurement[ - element_name - ] - - # new_conso = Sensor( - # elem_name=element_name, - # tydom_attributes_payload=attr_conso, - # mqtt=self.mqtt_client, - # ) - # await new_conso.update() - - if type_of_id == "smoke": - if ( - element_name in deviceSmokeKeywords - and element_validity == "upToDate" - ): - attr_smoke["device_id"] = device_id - attr_smoke["device_class"] = "smoke" - attr_smoke["endpoint_id"] = endpoint_id - attr_smoke["id"] = ( - str(device_id) + "_" + str(endpoint_id) - ) - attr_smoke["name"] = print_id - attr_smoke["device_type"] = "sensor" - attr_smoke["element_name"] = element_name - attr_smoke[element_name] = element_value - - if type_of_id == "unknown": - if ( - element_name in deviceMotionKeywords - and element_validity == "upToDate" - ): - attr_ukn["device_id"] = device_id - attr_ukn["endpoint_id"] = endpoint_id - attr_ukn["id"] = ( - str(device_id) + "_" + str(endpoint_id) - ) - attr_ukn["name"] = print_id - attr_ukn["device_type"] = "sensor" - attr_ukn["element_name"] = element_name - attr_ukn[element_name] = element_value - elif ( - element_name in deviceDoorKeywords - and element_validity == "upToDate" - ): - attr_ukn["device_id"] = device_id - attr_ukn["endpoint_id"] = endpoint_id - attr_ukn["id"] = ( - str(device_id) + "_" + str(endpoint_id) - ) - attr_ukn["name"] = print_id - attr_ukn["device_type"] = "sensor" - attr_ukn["element_name"] = element_name - attr_ukn[element_name] = element_value - - except Exception as e: - LOGGER.error("msg_data error in parsing !") - LOGGER.error(e) - """ - - """ - - if ( - "device_type" in attr_cover - and attr_cover["device_type"] == "cover" - ): - # new_cover = Cover( - # tydom_attributes=attr_cover, mqtt=self.mqtt_client - # ) - # await new_cover.update() - pass - elif ( - "device_type" in attr_door - and attr_door["device_type"] == "sensor" - ): - # new_door = Sensor( - # elem_name=attr_door["element_name"], - # tydom_attributes_payload=attr_door, - # mqtt=self.mqtt_client, - # ) - # await new_door.update() - pass - elif ( - "device_type" in attr_window - and attr_window["device_type"] == "sensor" - ): - # new_window = Sensor( - # elem_name=attr_window["element_name"], - # tydom_attributes_payload=attr_window, - # mqtt=self.mqtt_client, - # ) - # await new_window.update() - pass - elif ( - "device_type" in attr_light - and attr_light["device_type"] == "light" - ): - # new_light = Light( - # tydom_attributes=attr_light, mqtt=self.mqtt_client - # ) - # await new_light.update() - pass - elif ( - "device_type" in attr_boiler - and attr_boiler["device_type"] == "climate" - ): - # new_boiler = Boiler( - # tydom_attributes=attr_boiler, - # tydom_client=self.tydom_client, - # mqtt=self.mqtt_client, - # ) - # await new_boiler.update() - pass - elif ( - "device_type" in attr_gate - and attr_gate["device_type"] == "switch" - ): - # new_gate = Switch( - # tydom_attributes=attr_gate, mqtt=self.mqtt_client - # ) - # await new_gate.update() - pass - elif ( - "device_type" in attr_smoke - and attr_smoke["device_type"] == "sensor" - ): - # new_smoke = Sensor( - # elem_name=attr_smoke["element_name"], - # tydom_attributes_payload=attr_smoke, - # mqtt=self.mqtt_client, - # ) - # await new_smoke.update() - pass - elif ( - "device_type" in attr_ukn - and attr_ukn["device_type"] == "sensor" - ): - # new_ukn = Sensor( - # elem_name=attr_ukn["element_name"], - # tydom_attributes_payload=attr_ukn, - # mqtt=self.mqtt_client, - # ) - # await new_ukn.update() - pass - - # Get last known state (for alarm) # NEW METHOD - elif ( - "device_type" in attr_alarm - and attr_alarm["device_type"] == "alarm_control_panel" - ): - state = None - sos_state = False - try: - if ( - "alarmState" in attr_alarm - and attr_alarm["alarmState"] == "ON" - ) or ( - "alarmState" in attr_alarm and attr_alarm["alarmState"] - ) == "QUIET": - state = "triggered" - - elif ( - "alarmState" in attr_alarm - and attr_alarm["alarmState"] == "DELAYED" - ): - state = "pending" - - if ( - "alarmSOS" in attr_alarm - and attr_alarm["alarmSOS"] == "true" - ): - state = "triggered" - sos_state = True - - elif ( - "alarmMode" in attr_alarm - and attr_alarm["alarmMode"] == "ON" - ): - state = "armed_away" - elif ( - "alarmMode" in attr_alarm - and attr_alarm["alarmMode"] == "ZONE" - ): - state = "armed_home" - elif ( - "alarmMode" in attr_alarm - and attr_alarm["alarmMode"] == "OFF" - ): - state = "disarmed" - elif ( - "alarmMode" in attr_alarm - and attr_alarm["alarmMode"] == "MAINTENANCE" - ): - state = "disarmed" - - if sos_state: - LOGGER.warning("SOS !") - - if not (state is None): - # alarm = Alarm( - # current_state=state, - # alarm_pin=self.tydom_client.alarm_pin, - # tydom_attributes=attr_alarm, - # mqtt=self.mqtt_client, - # ) - # await alarm.update() - pass - - except Exception as e: - LOGGER.error("Error in alarm parsing !") - LOGGER.error(e) - pass - else: - pass - """ + LOGGER.exception("msg_data error in parsing !") return devices async def parse_devices_cdata(self, parsed): diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index 6e16b9e..dc5f89e 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -8,13 +8,14 @@ class TydomDevice: """represents a generic device""" - def __init__(self, tydom_client, uid, device_id, name, device_type, endpoint, data): + def __init__(self, tydom_client, uid, device_id, name, device_type, endpoint, metadata, data): self._tydom_client = tydom_client self._uid = uid self._id = device_id self._name = name self._type = device_type self._endpoint = endpoint + self._metadata = metadata self._callbacks = set() if data is not None: for key in data: @@ -63,6 +64,8 @@ async def update_device(self, device): if (attribute == "_uid" or attribute[:1] != "_") and value is not None: setattr(self, attribute, value) await self.publish_updates() + if hasattr(self,"_ha_device"): + self._ha_device.async_write_ha_state() async def publish_updates(self) -> None: """Schedule call all registered callbacks.""" @@ -146,6 +149,9 @@ class TydomSmoke(TydomDevice): class TydomBoiler(TydomDevice): + + _ha_device = None + """represents a boiler""" async def set_hvac_mode(self, mode): """Set hvac mode (STOP/HEATING)""" From b5875b0d30074f316c6db5ea9bc140bd10a01085 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Sun, 22 Oct 2023 19:37:29 +0200 Subject: [PATCH 54/74] =?UTF-8?q?=C2=A0cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deltadore-tydom/tydom/tydom_devices.py | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index dc5f89e..97535ab 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -3,8 +3,6 @@ import logging from ..const import LOGGER -logger = logging.getLogger(__name__) - class TydomDevice: """represents a generic device""" @@ -19,13 +17,12 @@ def __init__(self, tydom_client, uid, device_id, name, device_type, endpoint, me self._callbacks = set() if data is not None: for key in data: - if isinstance(data[key], dict): - logger.warning("type of %s : %s", key, type(data[key])) - logger.warning("%s => %s", key, data[key]) + LOGGER.debug("type of %s : %s", key, type(data[key])) + LOGGER.debug("%s => %s", key, data[key]) elif isinstance(data[key], list): - logger.warning("type of %s : %s", key, type(data[key])) - logger.warning("%s => %s", key, data[key]) + LOGGER.debug("type of %s : %s", key, type(data[key])) + LOGGER.debug("%s => %s", key, data[key]) else: setattr(self, key, data[key]) @@ -101,8 +98,6 @@ async def set_position(self, position: int) -> None: """ Set cover to the given position. """ - logger.error("set roller position (device) to : %s", position) - await self._tydom_client.put_devices_data( self._id, self._endpoint, "position", str(position) ) @@ -155,22 +150,19 @@ class TydomBoiler(TydomDevice): """represents a boiler""" async def set_hvac_mode(self, mode): """Set hvac mode (STOP/HEATING)""" - logger.error("setting mode to %s", mode) - LOGGER.debug("setting mode to %s", mode) + LOGGER.error("setting mode to %s", mode) await self._tydom_client.put_devices_data( self._id, self._endpoint, "authorization", mode ) async def set_preset_mode(self, mode): """Set preset mode (NORMAL/STOP/ANTI_FROST)""" - logger.error("setting preset to %s", mode) - LOGGER.debug("setting preset to %s", mode) + LOGGER.error("setting preset to %s", mode) await self._tydom_client.put_devices_data( self._id, self._endpoint, "hvacMode", mode ) async def set_temperature(self, temperature): """Set target temperature""" - logger.error("setting target temperature to %s", temperature) await self._tydom_client.put_devices_data( self._id, self._endpoint, "setpoint", temperature ) From b9de69f40470746ce8c1c506c9c15a68b693d073 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Sun, 22 Oct 2023 19:42:37 +0200 Subject: [PATCH 55/74] =?UTF-8?q?=C2=A0cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/hub.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index a4d9576..0063aa6 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -1,11 +1,6 @@ """A demonstration 'hub' that connects several devices.""" from __future__ import annotations -# In a real implementation, this would be in an external library that's on PyPI. -# The PyPI package needs to be included in the `requirements` section of manifest.json -# See https://developers.home-assistant.io/docs/creating_integration_manifest -# for more information. -# This dummy hub always returns 3 rollers. import asyncio import random import logging From 0d36daf3eb082fc6b6b0c62471bb491ceb546cf3 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Sun, 22 Oct 2023 19:48:08 +0200 Subject: [PATCH 56/74] =?UTF-8?q?=C2=A0cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deltadore-tydom/tydom/tydom_devices.py | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index 97535ab..bd3e2f6 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -56,7 +56,7 @@ def device_endpoint(self) -> str: async def update_device(self, device): """Update the device values from another device""" - logger.debug("Update device %s", device.device_id) + LOGGER.debug("Update device %s", device.device_id) for attribute, value in device.__dict__.items(): if (attribute == "_uid" or attribute[:1] != "_") and value is not None: setattr(self, attribute, value) @@ -128,7 +128,7 @@ async def set_slope_position(self, position: int) -> None: """ Set cover to the given position. """ - logger.error("set roller tilt position (device) to : %s", position) + LOGGER.debug("set roller tilt position (device) to : %s", position) await self._tydom_client.put_devices_data( self._id, self._endpoint, "position", str(position) @@ -150,14 +150,14 @@ class TydomBoiler(TydomDevice): """represents a boiler""" async def set_hvac_mode(self, mode): """Set hvac mode (STOP/HEATING)""" - LOGGER.error("setting mode to %s", mode) + LOGGER.debug("setting mode to %s", mode) await self._tydom_client.put_devices_data( self._id, self._endpoint, "authorization", mode ) async def set_preset_mode(self, mode): """Set preset mode (NORMAL/STOP/ANTI_FROST)""" - LOGGER.error("setting preset to %s", mode) + LOGGER.debug("setting preset to %s", mode) await self._tydom_client.put_devices_data( self._id, self._endpoint, "hvacMode", mode ) @@ -167,32 +167,20 @@ async def set_temperature(self, temperature): self._id, self._endpoint, "setpoint", temperature ) - class TydomWindow(TydomDevice): """represents a window""" - class TydomDoor(TydomDevice): """represents a door""" - class TydomGate(TydomDevice): """represents a gate""" - class TydomGarage(TydomDevice): """represents a garage door""" - def __init__(self, tydom_client, uid, device_id, name, device_type, endpoint, data): - logger.info("TydomGarage : data %s", data) - super().__init__( - tydom_client, uid, device_id, name, device_type, endpoint, data - ) - - class TydomLight(TydomDevice): """represents a light""" - class TydomAlarm(TydomDevice): """represents an alarm""" From 50ef37c3f41e9a41479633865d1df0021f94c69a Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Mon, 30 Oct 2023 19:57:21 +0100 Subject: [PATCH 57/74] =?UTF-8?q?=C2=A0fix=20climate,=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- .../deltadore-tydom/config_flow.py | 37 +---- custom_components/deltadore-tydom/cover.py | 133 +----------------- .../deltadore-tydom/ha_entities.py | 20 +-- .../deltadore-tydom/tydom/MessageHandler.py | 9 +- .../deltadore-tydom/tydom/tydom_client.py | 32 ++++- .../deltadore-tydom/tydom/tydom_devices.py | 94 +++++++++++-- 7 files changed, 131 insertions(+), 198 deletions(-) diff --git a/README.md b/README.md index fb5105d..314871c 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,9 @@ Platform | Description - Tywatt 5400 - Tyxal+ DFR - K-Line DVI (windows, door) -- Typass ATL (zones temperatures, target temperature, mode, presets, water/heat power usage) with Tybox 5101 +- Typass ATL (zones temperatures, target temperature, mode (Auto mode is used for antifrost), water/heat power usage) with Tybox 5101 + +Some other functions may also work or only report attributes. ## Installation diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index f3f7c56..13d3980 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -23,18 +23,6 @@ TydomClientApiClientError, ) -# This is the schema that used to display the UI to the user. This simple -# schema has a single required host field, but it could include a number of fields -# such as username, password etc. See other components in the HA core code for -# further examples. -# Note the input displayed to the user will be translated. See the -# translations/.json file and strings.json. See here for further information: -# https://developers.home-assistant.io/docs/config_entries_config_flow_handler/#translations -# At the time of writing I found the translations created by the scaffold didn't -# quite work as documented and always gave me the "Lokalise key references" string -# (in square brackets), rather than the actual translated value. I did not attempt to -# figure this out or look further into it. - DATA_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): cv.string, @@ -45,7 +33,6 @@ } ) - def host_valid(host) -> bool: """Return True if hostname or IP address is valid""" try: @@ -55,10 +42,8 @@ def host_valid(host) -> bool: disallowed = re.compile(r"[^a-zA-Z\d\-]") return all(x and not disallowed.search(x) for x in host.split(".")) - regex = re.compile(r"([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+") - def email_valid(email) -> bool: """Return True if email is valid""" return re.fullmatch(regex, email) @@ -70,9 +55,6 @@ async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """ # Validate the data can be used to set up a connection. - # This is a simple example to show an error in the UI for a short hostname - # The exceptions are defined at the end of this file, and are used in the - # `async_step_user` method below. LOGGER.debug("validating input: %s", data) if not host_valid(data[CONF_HOST]): raise InvalidHost @@ -111,11 +93,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Tydom.""" VERSION = 1 - - # Pick one of the available connection classes in homeassistant/config_entries.py - # This tells HA if it should be asking for updates, or it'll be notified of updates - # automatically. This example uses PUSH, as the dummy hub will notify HA of - # changes. CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): @@ -187,13 +164,15 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: LOGGER.exception("Unexpected exception") _errors["base"] = "unknown" else: - user_input[CONF_PASSWORD] = password + await self.async_set_unique_id(user_input[CONF_MAC]) + self._abort_if_unique_id_configured() + return self.async_create_entry( - title="Tydom-" + user_input[CONF_MAC], data=user_input + title="Tydom-" + user_input[CONF_MAC][6:], data=user_input ) user_input = user_input or {} - # If there is no user input or there were errors, show the form again, including any errors that were found with the input. + return self.async_show_form( step_id="user", data_schema=vol.Schema( @@ -240,7 +219,6 @@ async def async_step_discovery_confirm(self, user_input=None): try: user_input = await validate_input(self.hass, user_input) # Ensure it's working as expected - tydom_hub = hub.Hub( self.hass, user_input[CONF_HOST], @@ -253,10 +231,6 @@ async def async_step_discovery_confirm(self, user_input=None): except CannotConnect: _errors["base"] = "cannot_connect" except InvalidHost: - # The error string is set here, and should be translated. - # This example does not currently cover translations, see the - # comments on `DATA_SCHEMA` for further details. - # Set the error on the `host` field, not the entire form. _errors[CONF_HOST] = "invalid_host" except InvalidMacAddress: _errors[CONF_MAC] = "invalid_macaddress" @@ -279,7 +253,6 @@ async def async_step_discovery_confirm(self, user_input=None): LOGGER.exception("Unexpected exception") _errors["base"] = "unknown" else: - await self.async_set_unique_id(user_input[CONF_MAC]) self._abort_if_unique_id_configured() diff --git a/custom_components/deltadore-tydom/cover.py b/custom_components/deltadore-tydom/cover.py index 1f6d4bc..86e1431 100644 --- a/custom_components/deltadore-tydom/cover.py +++ b/custom_components/deltadore-tydom/cover.py @@ -21,142 +21,11 @@ from .const import DOMAIN, LOGGER -# This function is called as part of the __init__.async_setup_entry (via the -# hass.config_entries.async_forward_entry_setup call) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Add cover for passed config_entry in HA.""" - # The hub is loaded from the associated hass.data entry that was created in the - # __init__.async_setup_entry function hub = hass.data[DOMAIN][config_entry.entry_id] - hub.add_cover_callback = async_add_entities - - -# This entire class could be written to extend a base class to ensure common attributes -# are kept identical/in sync. It's broken apart here between the Cover and Sensors to -# be explicit about what is returned, and the comments outline where the overlap is. -class HelloWorldCover(CoverEntity): - """Representation of a dummy Cover.""" - - # Our dummy class is PUSH, so we tell HA that it should not be polled - should_poll = False - # The supported features of a cover are done using a bitmask. Using the constants - # imported above, we can tell HA the features that are supported by this entity. - # If the supported features were dynamic (ie: different depending on the external - # device it connected to), then this should be function with an @property decorator. - supported_features = SUPPORT_SET_POSITION | SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP - - def __init__(self, roller) -> None: - """Initialize the sensor.""" - # Usual setup is done here. Callbacks are added in async_added_to_hass. - self._roller = roller - - # A unique_id for this entity with in this domain. This means for example if you - # have a sensor on this cover, you must ensure the value returned is unique, - # which is done here by appending "_cover". For more information, see: - # https://developers.home-assistant.io/docs/entity_registry_index/#unique-id-requirements - # Note: This is NOT used to generate the user visible Entity ID used in automations. - self._attr_unique_id = f"{self._roller.roller_id}_cover" - - # This is the name for this *entity*, the "name" attribute from "device_info" - # is used as the device name for device screens in the UI. This name is used on - # entity screens, and used to build the Entity ID that's used is automations etc. - self._attr_name = self._roller.name - - async def async_added_to_hass(self) -> None: - """Run when this Entity has been added to HA.""" - # Importantly for a push integration, the module that will be getting updates - # needs to notify HA of changes. The dummy device has a registercallback - # method, so to this we add the 'self.async_write_ha_state' method, to be - # called where ever there are changes. - # The call back registration is done once this entity is registered with HA - # (rather than in the __init__) - self._roller.register_callback(self.async_write_ha_state) - - async def async_will_remove_from_hass(self) -> None: - """Entity being removed from hass.""" - # The opposite of async_added_to_hass. Remove any registered call backs here. - self._roller.remove_callback(self.async_write_ha_state) - - # Information about the devices that is partially visible in the UI. - # The most critical thing here is to give this entity a name so it is displayed - # as a "device" in the HA UI. This name is used on the Devices overview table, - # and the initial screen when the device is added (rather than the entity name - # property below). You can then associate other Entities (eg: a battery - # sensor) with this device, so it shows more like a unified element in the UI. - # For example, an associated battery sensor will be displayed in the right most - # column in the Configuration > Devices view for a device. - # To associate an entity with this device, the device_info must also return an - # identical "identifiers" attribute, but not return a name attribute. - # See the sensors.py file for the corresponding example setup. - # Additional meta data can also be returned here, including sw_version (displayed - # as Firmware), model and manufacturer (displayed as by ) - # shown on the device info screen. The Manufacturer and model also have their - # respective columns on the Devices overview table. Note: Many of these must be - # set when the device is first added, and they are not always automatically - # refreshed by HA from it's internal cache. - # For more information see: - # https://developers.home-assistant.io/docs/device_registry_index/#device-properties - @property - def device_info(self) -> DeviceInfo: - """Information about this entity/device.""" - return { - "identifiers": {(DOMAIN, self._roller.roller_id)}, - # If desired, the name for the device could be different to the entity - "name": self.name, - "sw_version": self._roller.firmware_version, - "model": self._roller.model, - "manufacturer": self._roller.hub.manufacturer, - } - - # This property is important to let HA know if this entity is online or not. - # If an entity is offline (return False), the UI will refelect this. - @property - def available(self) -> bool: - """Return True if roller and hub is available.""" - return self._roller.online and self._roller.hub.online - - # The following properties are how HA knows the current state of the device. - # These must return a value from memory, not make a live query to the device/hub - # etc when called (hence they are properties). For a push based integration, - # HA is notified of changes via the async_write_ha_state call. See the __init__ - # method for hos this is implemented in this example. - # The properties that are expected for a cover are based on the supported_features - # property of the object. In the case of a cover, see the following for more - # details: https://developers.home-assistant.io/docs/core/entity/cover/ - @property - def current_cover_position(self): - """Return the current position of the cover.""" - return self._roller.position - - @property - def is_closed(self) -> bool: - """Return if the cover is closed, same as position 0.""" - return self._roller.position == 0 - - @property - def is_closing(self) -> bool: - """Return if the cover is closing or not.""" - return self._roller.moving < 0 - - @property - def is_opening(self) -> bool: - """Return if the cover is opening or not.""" - return self._roller.moving > 0 - - # These methods allow HA to tell the actual device what to do. In this case, move - # the cover to the desired position, or open and close it all the way. - async def async_open_cover(self, **kwargs: Any) -> None: - """Open the cover.""" - await self._roller.set_position(100) - - async def async_close_cover(self, **kwargs: Any) -> None: - """Close the cover.""" - await self._roller.set_position(0) - - async def async_set_cover_position(self, **kwargs: Any) -> None: - """Close the cover.""" - await self._roller.set_position(kwargs[ATTR_POSITION]) + hub.add_cover_callback = async_add_entities \ No newline at end of file diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index d613df4..386936c 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -574,15 +574,16 @@ class HaClimate(ClimateEntity, HAEntity): } DICT_MODES_HA_TO_DD = { - HVACMode.AUTO: None, + HVACMode.AUTO: "ANTI_FROST", HVACMode.COOL: None, - HVACMode.HEAT: "HEATING", + HVACMode.HEAT: "NORMAL", HVACMode.OFF: "STOP", } DICT_MODES_DD_TO_HA = { # "": HVACMode.AUTO, # "": HVACMode.COOL, - "HEATING": HVACMode.HEAT, + "ANTI_FROST": HVACMode.AUTO, + "NORMAL": HVACMode.HEAT, "STOP": HVACMode.OFF, } @@ -593,15 +594,14 @@ def __init__(self, device: TydomBoiler) -> None: self._attr_unique_id = f"{self._device.device_id}_climate" self._attr_name = self._device.device_name + # self._attr_preset_modes = ["NORMAL", "STOP", "ANTI_FROST"] self._attr_hvac_modes = [ HVACMode.OFF, HVACMode.HEAT, + HVACMode.AUTO, ] self._registered_sensors = [] - self._attr_preset_modes = ["NORMAL", "STOP", "ANTI_FROST"] - self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] - if "min" in self._device._metadata["setpoint"]: self._attr_min_temp = self._device._metadata["setpoint"]["min"] @@ -612,7 +612,7 @@ def __init__(self, device: TydomBoiler) -> None: def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" features = ClimateEntityFeature(0) - features = features | ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + features = features | ClimateEntityFeature.TARGET_TEMPERATURE #| ClimateEntityFeature.PRESET_MODE return features @property @@ -631,9 +631,9 @@ def temperature_unit(self) -> str: @property def hvac_mode(self) -> HVACMode: """Return the current operation (e.g. heat, cool, idle).""" - if (hasattr(self._device, 'authorization')): - LOGGER.debug("hvac_mode = %s", self.DICT_MODES_DD_TO_HA[self._device.authorization]) - return self.DICT_MODES_DD_TO_HA[self._device.authorization] + if (hasattr(self._device, 'hvacMode')): + LOGGER.debug("hvac_mode = %s", self.DICT_MODES_DD_TO_HA[self._device.hvacMode]) + return self.DICT_MODES_DD_TO_HA[self._device.hvacMode] else: return None diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 4db0d14..33559c6 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -177,24 +177,19 @@ async def parse_response(self, incoming, uri_origin, http_request_line): async def parse_devices_metadata(self, parsed): LOGGER.debug("metadata : %s", parsed) - #LOGGER.error("metadata : %s", parsed) for device in parsed: id = device["id"] for endpoint in device["endpoints"]: id_endpoint = endpoint["id"] device_unique_id = str(id_endpoint) + "_" + str(id) device_metadata[device_unique_id] = dict() - LOGGER.error("metadata unique id : %s", device_unique_id) for metadata in endpoint["metadata"]: metadata_name = metadata["name"] device_metadata[device_unique_id][metadata_name] = dict() - LOGGER.error("\tmetadata name : %s", metadata_name) for meta in metadata: if meta == "name": continue - LOGGER.error("\t\tmetadata meta : %s => %s", meta, metadata[meta]) device_metadata[device_unique_id][metadata_name][meta] = metadata[meta] - LOGGER.error("metadata result %s", device_metadata) return [] async def parse_msg_info(self, parsed): @@ -210,8 +205,6 @@ async def get_device( ) -> TydomDevice: """Get device class from its last usage""" - LOGGER.error("- %s", name) - # FIXME voir: class CoverDeviceClass(StrEnum): # Refer to the cover dev docs for device class descriptions # AWNING = "awning" @@ -295,7 +288,7 @@ async def parse_config_data(parsed): # if device is not None: # devices.append(device) - LOGGER.warning(" config_data : %s - %s", device_unique_id, i["name"]) + LOGGER.debug("config_data device parsed : %s - %s", device_unique_id, i["name"]) device_name[device_unique_id] = i["name"] device_type[device_unique_id] = i["last_usage"] diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index 09f12a1..7c88978 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -479,10 +479,40 @@ async def get_groups(self): req = "GET" await self.send_message(method=req, msg=msg_type) + async def put_data(self, path, name, value): + """Give order (name + value) to path""" + body: str + if value is None: + body = '{"' + name + '":"null}' + elif type(value)==bool or type(value)==int: + body = '{"' + name + '":"' + str(value).lower() + '}' + else: + body = '{"' + name + '":"' + value + '"}' + + str_request = ( + self._cmd_prefix + + f"PUT {path} HTTP/1.1\r\nContent-Length: " + + str(len(body)) + + "\r\nContent-Type: application/json; charset=UTF-8\r\nTransac-Id: 0\r\n\r\n" + + body + + "\r\n\r\n" + ) + a_bytes = bytes(str_request, "ascii") + LOGGER.debug("Sending message to tydom (%s %s)", "PUT data", body) + await self._connection.send_bytes(a_bytes) + return 0 + async def put_devices_data(self, device_id, endpoint_id, name, value): """Give order (name + value) to endpoint""" # For shutter, value is the percentage of closing - body = '[{"name":"' + name + '","value":"' + value + '"}]' + body: str + if value is None: + body = '[{"name":"' + name + '","value":null}]' + elif type(value)==bool: + body = '[{"name":"' + name + '","value":' + str(value).lower() + '}]' + else: + body = '[{"name":"' + name + '","value":"' + value + '"}]' + # endpoint_id is the endpoint = the device (shutter in this case) to # open. str_request = ( diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index bd3e2f6..5e47c26 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -125,9 +125,7 @@ async def slope_stop(self) -> None: # FIXME replace command async def set_slope_position(self, position: int) -> None: - """ - Set cover to the given position. - """ + """ Set cover to the given position. """ LOGGER.debug("set roller tilt position (device) to : %s", position) await self._tydom_client.put_devices_data( @@ -149,18 +147,86 @@ class TydomBoiler(TydomDevice): """represents a boiler""" async def set_hvac_mode(self, mode): - """Set hvac mode (STOP/HEATING)""" - LOGGER.debug("setting mode to %s", mode) - await self._tydom_client.put_devices_data( - self._id, self._endpoint, "authorization", mode - ) + """Set hvac mode (ANTI_FROST/NORMAL/STOP)""" + LOGGER.debug("setting hvac mode to %s", mode) + if mode == "ANTI_FROST": + #await self._tydom_client.put_devices_data( + # self._id, self._endpoint, "thermicLevel", None + #) + #await self._tydom_client.put_devices_data( + # self._id, self._endpoint, "authorization", mode + #) + #await self._tydom_client.put_devices_data( + # self._id, self._endpoint, "antifrostOn", False + #) + #await self.set_temperature("19.0") + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "setpoint", None + ) + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "thermicLevel", "STOP" + ) + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "hvacMode", "ANTI_FROST" + ) + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "antifrostOn", True + ) + await self._tydom_client.put_data( + "/home/absence", "to", -1 + ) + await self._tydom_client.put_data( + "/events/home/absence", "to", -1 + ) + await self._tydom_client.put_data( + "/events/home/absence", "actions", "in" + ) + elif mode == "NORMAL": + if self.hvacMode == "ANTI_FROST": + await self._tydom_client.put_data( + "/home/absence", "to", 0 + ) + await self._tydom_client.put_data( + "/events/home/absence", "to", 0 + ) + await self._tydom_client.put_data( + "/events/home/absence", "actions", "in" + ) + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "hvacMode", "NORMAL" + ) + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "authorization", "HEATING" + ) + await self.set_temperature("19.0") + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "antifrostOn", False + ) + elif mode == "STOP": + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "hvacMode", "STOP" + ) + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "authorization", "STOP" + ) + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "thermicLevel", "STOP" + ) + await self._tydom_client.put_devices_data( + self._id, self._endpoint, "setpoint", None + ) + else: + LOGGER.error("Unknown hvac mode: %s", mode) + #await self._tydom_client.put_devices_data( + # self._id, self._endpoint, "thermicLevel", "STOP" + #) + #await self._tydom_client.put_devices_data( + # self._id, self._endpoint, "antifrostOn", True + #) + #await self._tydom_client.put_devices_data( + # self._id, self._endpoint, "authorization", mode + #) - async def set_preset_mode(self, mode): - """Set preset mode (NORMAL/STOP/ANTI_FROST)""" - LOGGER.debug("setting preset to %s", mode) - await self._tydom_client.put_devices_data( - self._id, self._endpoint, "hvacMode", mode - ) async def set_temperature(self, temperature): """Set target temperature""" await self._tydom_client.put_devices_data( From 6b2599e37d34cedfddd6e93bfa77c8729c9b7a80 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 31 Oct 2023 10:49:45 +0100 Subject: [PATCH 58/74] =?UTF-8?q?=C2=A0fix=20ruff=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/climate.py | 21 +----- .../deltadore-tydom/config_flow.py | 9 +-- custom_components/deltadore-tydom/const.py | 2 +- custom_components/deltadore-tydom/cover.py | 15 +---- .../deltadore-tydom/ha_entities.py | 36 +++------- custom_components/deltadore-tydom/hub.py | 45 ++++++++----- custom_components/deltadore-tydom/light.py | 8 +-- custom_components/deltadore-tydom/lock.py | 8 +-- custom_components/deltadore-tydom/sensor.py | 2 - .../deltadore-tydom/tydom/MessageHandler.py | 42 ++++++++---- .../deltadore-tydom/tydom/tydom_client.py | 65 +++++++++++-------- .../deltadore-tydom/tydom/tydom_devices.py | 10 ++- custom_components/deltadore-tydom/update.py | 4 +- 13 files changed, 124 insertions(+), 143 deletions(-) diff --git a/custom_components/deltadore-tydom/climate.py b/custom_components/deltadore-tydom/climate.py index 8bb874a..4162bb5 100644 --- a/custom_components/deltadore-tydom/climate.py +++ b/custom_components/deltadore-tydom/climate.py @@ -1,36 +1,19 @@ """Platform for sensor integration.""" from __future__ import annotations -from typing import Any - -# These constants are relevant to the type of entity we are using. -# See below for how they are used. -from homeassistant.components.cover import ( - ATTR_POSITION, - SUPPORT_CLOSE, - SUPPORT_OPEN, - SUPPORT_SET_POSITION, - SUPPORT_STOP, - CoverEntity, -) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity import DeviceInfo -from .const import DOMAIN, LOGGER +from .const import DOMAIN -# This function is called as part of the __init__.async_setup_entry (via the -# hass.config_entries.async_forward_entry_setup call) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Add cover for passed config_entry in HA.""" - # The hub is loaded from the associated hass.data entry that was created in the - # __init__.async_setup_entry function + """Add climate for passed config_entry in HA.""" hub = hass.data[DOMAIN][config_entry.entry_id] hub.add_climate_callback = async_add_entities diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index 13d3980..3bf84c5 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -7,7 +7,6 @@ import voluptuous as vol -from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from homeassistant import config_entries, exceptions @@ -34,7 +33,7 @@ ) def host_valid(host) -> bool: - """Return True if hostname or IP address is valid""" + """Return True if hostname or IP address is valid.""" try: if ipaddress.ip_address(host).version == (4 or 6): return True @@ -45,12 +44,13 @@ def host_valid(host) -> bool: regex = re.compile(r"([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+") def email_valid(email) -> bool: - """Return True if email is valid""" + """Return True if email is valid.""" return re.fullmatch(regex, email) async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. + Data has the keys from DATA_SCHEMA with values provided by the user. """ # Validate the data can be used to set up a connection. @@ -96,6 +96,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): + """Initialize config flow.""" self._discovered_host = None self._discovered_mac = None @@ -192,7 +193,7 @@ async def async_step_user(self, user_input=None) -> config_entries.FlowResult: ), errors=_errors, ) - + @property def _name(self) -> str | None: return self.context.get(CONF_NAME) diff --git a/custom_components/deltadore-tydom/const.py b/custom_components/deltadore-tydom/const.py index 4cef298..59bed61 100644 --- a/custom_components/deltadore-tydom/const.py +++ b/custom_components/deltadore-tydom/const.py @@ -10,4 +10,4 @@ VERSION = "0.0.1" ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/" -CONF_TYDOM_PASSWORD = "tydom_password" \ No newline at end of file +CONF_TYDOM_PASSWORD = "tydom_password" diff --git a/custom_components/deltadore-tydom/cover.py b/custom_components/deltadore-tydom/cover.py index 86e1431..1ec3352 100644 --- a/custom_components/deltadore-tydom/cover.py +++ b/custom_components/deltadore-tydom/cover.py @@ -1,24 +1,11 @@ """Platform for sensor integration.""" from __future__ import annotations -from typing import Any - -# These constants are relevant to the type of entity we are using. -# See below for how they are used. -from homeassistant.components.cover import ( - ATTR_POSITION, - SUPPORT_CLOSE, - SUPPORT_OPEN, - SUPPORT_SET_POSITION, - SUPPORT_STOP, - CoverEntity, -) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity import DeviceInfo -from .const import DOMAIN, LOGGER +from .const import DOMAIN async def async_setup_entry( diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 386936c..8937336 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -8,23 +8,13 @@ ) from homeassistant.components.climate import ( - ATTR_HVAC_MODE, - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, - FAN_OFF, - FAN_ON, - PRESET_ECO, - PRESET_NONE, ClimateEntity, ClimateEntityFeature, - HVACAction, HVACMode, ) from homeassistant.const import ( - PERCENTAGE, ATTR_TEMPERATURE, UnitOfTemperature, - UnitOfElectricPotential, UnitOfEnergy, UnitOfPower, UnitOfElectricCurrent, @@ -75,9 +65,9 @@ def available(self) -> bool: # FIXME # return self._device.online and self._device.hub.online return True - + def get_sensors(self): - """Get available sensors for this entity""" + """Get available sensors for this entity.""" sensors = [] for attribute, value in self._device.__dict__.items(): @@ -116,7 +106,7 @@ def get_sensors(self): class GenericSensor(SensorEntity): - """Representation of a generic sensor""" + """Representation of a generic sensor.""" should_poll = False diagnostic_attrs = [ @@ -294,7 +284,7 @@ def __init__(self, device: Tydom, hass) -> None: model=self._device.productName, sw_version=self._device.mainVersionSW, ) - + @property def device_info(self): """Return information to link this entity with the correct device.""" @@ -399,7 +389,7 @@ def __init__(self, device: TydomEnergy, hass) -> None: name=self._device.device_name, sw_version= sw_version, ) - + @property def device_info(self): """Return information to link this entity with the correct device.""" @@ -415,7 +405,6 @@ def device_info(self): class HACover(CoverEntity, HAEntity): """Representation of a Cover.""" - should_poll = False supported_features = 0 device_class = CoverDeviceClass.SHUTTER @@ -588,6 +577,7 @@ class HaClimate(ClimateEntity, HAEntity): } def __init__(self, device: TydomBoiler) -> None: + """Initialize Climate.""" super().__init__() self._device = device self._device._ha_device = self @@ -604,10 +594,10 @@ def __init__(self, device: TydomBoiler) -> None: if "min" in self._device._metadata["setpoint"]: self._attr_min_temp = self._device._metadata["setpoint"]["min"] - + if "max" in self._device._metadata["setpoint"]: self._attr_max_temp = self._device._metadata["setpoint"]["max"] - + @property def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" @@ -636,12 +626,6 @@ def hvac_mode(self) -> HVACMode: return self.DICT_MODES_DD_TO_HA[self._device.hvacMode] else: return None - - @property - def preset_mode(self) -> HVACMode: - """Return the current operation (e.g. heat, cool, idle).""" - LOGGER.debug("preset_mode = %s", self._device.hvacMode) - return self._device.hvacMode @property def current_temperature(self) -> float | None: @@ -696,7 +680,7 @@ def device_info(self) -> DeviceInfo: @property def is_closed(self) -> bool: - """Return if the window is closed""" + """Return if the window is closed.""" return self._device.openState == "LOCKED" class HaDoor(LockEntity, HAEntity): @@ -728,7 +712,7 @@ def device_info(self) -> DeviceInfo: @property def is_locked(self) -> bool: - """Return if the door is closed""" + """Return if the door is closed.""" return self._device.openState == "LOCKED" class HaGate(CoverEntity, HAEntity): diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 0063aa6..5dd2e23 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -2,17 +2,32 @@ from __future__ import annotations import asyncio -import random -import logging -import time -from typing import Callable from aiohttp import ClientWebSocketResponse, ClientSession from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_create_clientsession from .tydom.tydom_client import TydomClient from .tydom.tydom_devices import Tydom -from .ha_entities import * +from .ha_entities import ( + HATydom, + HACover, + HAEnergy, + HASmoke, + HaClimate, + HaWindow, + HaDoor, + HaGate, + HaGarage, + HaLight, + TydomShutter, + TydomEnergy, + TydomSmoke, + TydomBoiler, + TydomWindow, + TydomDoor, + TydomGate, + TydomGarage, + TydomLight, +) from .const import LOGGER @@ -22,7 +37,7 @@ class Hub: manufacturer = "Delta Dore" def handle_event(self, event): - """Event callback""" + """Event callback.""" pass def __init__( @@ -69,7 +84,7 @@ def hub_id(self) -> str: return self._id async def connect(self) -> ClientWebSocketResponse: - """Connect to Tydom""" + """Connect to Tydom.""" connection = await self._tydom_client.async_connect() await self._tydom_client.listen_tydom(connection) return connection @@ -78,7 +93,7 @@ async def connect(self) -> ClientWebSocketResponse: async def get_tydom_credentials( session: ClientSession, email: str, password: str, macaddress: str ): - """Get Tydom credentials""" + """Get Tydom credentials.""" return await TydomClient.async_get_credentials( session, email, password, macaddress ) @@ -109,14 +124,14 @@ async def setup(self, connection: ClientWebSocketResponse) -> None: ) async def create_ha_device(self, device): - """Create a new HA device""" + """Create a new HA device.""" match device: - case Tydom(): + case Tydom(): LOGGER.debug("Create Tydom gateway %s", device.device_id) self.devices[device.device_id] = self.device_info await self.device_info.update_device(device) ha_device = HATydom(self.device_info, self._hass) - + self.ha_devices[self.device_info.device_id] = ha_device if self.add_sensor_callback is not None: self.add_sensor_callback(ha_device.get_sensors()) @@ -209,7 +224,7 @@ async def create_ha_device(self, device): return async def update_ha_device(self, stored_device, device): - """Update HA device values""" + """Update HA device values.""" await stored_device.update_device(device) ha_device = self.ha_devices[device.device_id] new_sensors = ha_device.get_sensors() @@ -218,12 +233,12 @@ async def update_ha_device(self, stored_device, device): self.add_sensor_callback(new_sensors) async def ping(self) -> None: - """Periodically send pings""" + """Periodically send pings.""" while True: await self._tydom_client.ping() await asyncio.sleep(10) async def async_trigger_firmware_update(self) -> None: - """Trigger firmware update""" + """Trigger firmware update.""" LOGGER.debug("Installing firmware update...") self._tydom_client.update_firmware() diff --git a/custom_components/deltadore-tydom/light.py b/custom_components/deltadore-tydom/light.py index d2c1c54..d3da065 100644 --- a/custom_components/deltadore-tydom/light.py +++ b/custom_components/deltadore-tydom/light.py @@ -1,24 +1,18 @@ """Platform for sensor integration.""" from __future__ import annotations -from typing import Any - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, LOGGER +from .const import DOMAIN -# This function is called as part of the __init__.async_setup_entry (via the -# hass.config_entries.async_forward_entry_setup call) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Add cover for passed config_entry in HA.""" - # The hub is loaded from the associated hass.data entry that was created in the - # __init__.async_setup_entry function hub = hass.data[DOMAIN][config_entry.entry_id] hub.add_light_callback = async_add_entities diff --git a/custom_components/deltadore-tydom/lock.py b/custom_components/deltadore-tydom/lock.py index e363e03..d586718 100644 --- a/custom_components/deltadore-tydom/lock.py +++ b/custom_components/deltadore-tydom/lock.py @@ -7,19 +7,15 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, LOGGER +from .const import DOMAIN -# This function is called as part of the __init__.async_setup_entry (via the -# hass.config_entries.async_forward_entry_setup call) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Add cover for passed config_entry in HA.""" - # The hub is loaded from the associated hass.data entry that was created in the - # __init__.async_setup_entry function + """Add lock for passed config_entry in HA.""" hub = hass.data[DOMAIN][config_entry.entry_id] hub.add_lock_callback = async_add_entities diff --git a/custom_components/deltadore-tydom/sensor.py b/custom_components/deltadore-tydom/sensor.py index d8876ff..9cf5ba0 100644 --- a/custom_components/deltadore-tydom/sensor.py +++ b/custom_components/deltadore-tydom/sensor.py @@ -1,7 +1,5 @@ """Platform for sensor integration.""" -import random - from .const import DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 33559c6..6579b61 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -1,3 +1,4 @@ +"""Tydom message parsing.""" import json from http.client import HTTPResponse from http.server import BaseHTTPRequestHandler @@ -11,13 +12,13 @@ from ..const import LOGGER # Device dict for parsing -device_name = dict() -device_endpoint = dict() -device_type = dict() -device_metadata = dict() +device_name = {} +device_endpoint = {} +device_type = {} +device_metadata = {} class MessageHandler: - """Handle incomming Tydom messages""" + """Handle incomming Tydom messages.""" def __init__(self, tydom_client, cmd_prefix): self.tydom_client = tydom_client @@ -25,7 +26,7 @@ def __init__(self, tydom_client, cmd_prefix): @staticmethod def get_uri_origin(data) -> str: - """Extract Uri-Origin from Tydom messages if present""" + """Extract Uri-Origin from Tydom messages if present.""" uri_origin = "" re_matcher = re.match( ".*Uri-Origin: ([a-zA-Z0-9\\-._~:/?#\\[\\]@!$&'\\(\\)\\*\\+,;%=]+).*", @@ -41,7 +42,7 @@ def get_uri_origin(data) -> str: @staticmethod def get_http_request_line(data) -> str: - """Extract Http request line""" + """Extract Http request line.""" clean_data = data.replace('\\x02', '') request_line = "" re_matcher = re.match( @@ -56,7 +57,7 @@ def get_http_request_line(data) -> str: return request_line.strip() async def incoming_triage(self, bytes_str): - """Identify message type and dispatch the result""" + """Identify message type and dispatch the result.""" incoming = None @@ -176,6 +177,7 @@ async def parse_response(self, incoming, uri_origin, http_request_line): LOGGER.debug("Incoming data parsed with success") async def parse_devices_metadata(self, parsed): + """Parse metadata.""" LOGGER.debug("metadata : %s", parsed) for device in parsed: id = device["id"] @@ -193,6 +195,7 @@ async def parse_devices_metadata(self, parsed): return [] async def parse_msg_info(self, parsed): + """Parse message info.""" LOGGER.debug("parse_msg_info : %s", parsed) return [ @@ -203,7 +206,7 @@ async def parse_msg_info(self, parsed): async def get_device( tydom_client, last_usage, uid, device_id, name, endpoint=None, data=None ) -> TydomDevice: - """Get device class from its last usage""" + """Get device class from its last usage.""" # FIXME voir: class CoverDeviceClass(StrEnum): # Refer to the cover dev docs for device class descriptions @@ -279,6 +282,7 @@ async def get_device( @staticmethod async def parse_config_data(parsed): + """Parse config data.""" LOGGER.debug("parse_config_data : %s", parsed) devices = [] for i in parsed["endpoints"]: @@ -329,6 +333,7 @@ async def parse_config_data(parsed): return devices async def parse_cmeta_data(self, parsed): + """Parse cmeta data.""" LOGGER.debug("parse_cmeta_data : %s", parsed) for i in parsed: for endpoint in i["endpoints"]: @@ -398,6 +403,7 @@ async def parse_cmeta_data(self, parsed): LOGGER.debug("Metadata configuration updated") async def parse_devices_data(self, parsed): + """Parse device data.""" LOGGER.debug("parse_devices_data : %s", parsed) devices = [] @@ -441,11 +447,12 @@ async def parse_devices_data(self, parsed): name_of_id, type_of_id, ) - except Exception as e: + except Exception: LOGGER.exception("msg_data error in parsing !") return devices async def parse_devices_cdata(self, parsed): + """Parse devices cdata.""" LOGGER.debug("parse_devices_data : %s", parsed) for i in parsed: for endpoint in i["endpoints"]: @@ -549,13 +556,14 @@ async def parse_devices_cdata(self, parsed): # PUT response DIRTY parsing def parse_put_response(self, bytes_str, start=6): + """Parse PUT response.""" # TODO : Find a cooler way to parse nicely the PUT HTTP response resp = bytes_str[len(self.cmd_prefix) :].decode("utf-8") fields = resp.split("\r\n") fields = fields[start:] # ignore the PUT / HTTP/1.1 end_parsing = False i = 0 - output = str() + output = "" while not end_parsing: field = fields[i] if len(field) == 0 or field == "0": @@ -570,6 +578,7 @@ def parse_put_response(self, bytes_str, start=6): @staticmethod def response_from_bytes(data): + """Get HTTPResponse from bytes.""" sock = BytesIOSocket(data) response = HTTPResponse(sock) response.begin() @@ -577,12 +586,14 @@ def response_from_bytes(data): @staticmethod def put_response_from_bytes(data): + """Get HTTPResponse from bytes.""" request = HTTPRequest(data) return request def get_type_from_id(self, id): + """Get device type from id.""" device_type_detected = "" - if id in device_type.keys(): + if id in device_type: device_type_detected = device_type[id] else: LOGGER.warning("Unknown device type (%s)", id) @@ -590,8 +601,9 @@ def get_type_from_id(self, id): # Get pretty name for a device id def get_name_from_id(self, id): + """Get device name from id.""" name = "" - if id in device_name.keys(): + if id in device_name: name = device_name[id] else: LOGGER.warning("Unknown device name (%s)", id) @@ -600,18 +612,22 @@ def get_name_from_id(self, id): class BytesIOSocket: def __init__(self, content): + """Initialize a BytesIOSocket.""" self.handle = BytesIO(content) def makefile(self, mode): + """get handle.""" return self.handle class HTTPRequest(BaseHTTPRequestHandler): def __init__(self, request_text): + """Initialize a HTTPRequest.""" self.raw_requestline = request_text self.error_code = self.error_message = None self.parse_request() def send_error(self, code, message): + """Set error code and message.""" self.error_code = code self.error_message = message diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index 7c88978..c0ecc9b 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -1,6 +1,5 @@ """Tydom API Client.""" import os -import logging import asyncio import socket import base64 @@ -14,7 +13,13 @@ from aiohttp import ClientWebSocketResponse, ClientSession, WSMsgType from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import * +from .const import ( + MEDIATION_URL, + DELTADORE_AUTH_URL, + DELTADORE_AUTH_GRANT_TYPE, + DELTADORE_AUTH_CLIENTID, + DELTADORE_AUTH_SCOPE, + DELTADORE_API_SITES) from .MessageHandler import MessageHandler from requests.auth import HTTPDigestAuth @@ -49,6 +54,7 @@ def __init__( host: str = MEDIATION_URL, event_callback=None, ) -> None: + """Initialize client.""" LOGGER.debug("Initializing TydomClient Class") self._hass = hass @@ -81,7 +87,7 @@ def __init__( async def async_get_credentials( session: ClientSession, email: str, password: str, macaddress: str ): - """get tydom credentials from Delta Dore""" + """Get tydom credentials from Delta Dore.""" try: async with async_timeout.timeout(10): response = await session.request( @@ -238,7 +244,7 @@ async def async_connect(self) -> ClientWebSocketResponse: ) from exception async def listen_tydom(self, connection: ClientWebSocketResponse): - """Listen for Tydom messages""" + """Listen for Tydom messages.""" LOGGER.info("Listen for Tydom messages") self._connection = connection await self.ping() @@ -265,7 +271,7 @@ async def listen_tydom(self, connection: ClientWebSocketResponse): await self.get_scenarii() async def consume_messages(self): - """Read and parse incomming messages""" + """Read and parse incomming messages.""" try: if self._connection.closed: await self._connection.close() @@ -292,7 +298,7 @@ async def consume_messages(self): return await self._message_handler.incoming_triage(incoming_bytes_str) - except Exception as e: + except Exception: LOGGER.exception("Unable to handle message") return None @@ -315,7 +321,7 @@ def build_digest_headers(self, nonce): return digest async def send_message(self, method, msg): - """Send Generic message to Tydom""" + """Send Generic message to Tydom.""" message = ( self._cmd_prefix + method @@ -350,31 +356,31 @@ def generate_random_key(): # Tydom messages # ######################## async def get_info(self): - """Ask some information from Tydom""" + """Ask some information from Tydom.""" msg_type = "/info" req = "GET" await self.send_message(method=req, msg=msg_type) async def get_local_claim(self): - """Ask some information from Tydom""" + """Ask some information from Tydom.""" msg_type = "/configs/gateway/local_claim" req = "GET" await self.send_message(method=req, msg=msg_type) async def get_geoloc(self): - """Ask some information from Tydom""" + """Ask some information from Tydom.""" msg_type = "/configs/gateway/geoloc" req = "GET" await self.send_message(method=req, msg=msg_type) async def put_api_mode(self): - """Use Tydom API mode ?""" + """Use Tydom API mode.""" msg_type = "/configs/gateway/api_mode" req = "PUT" await self.send_message(method=req, msg=msg_type) async def post_refresh(self): - """Refresh (all)""" + """Refresh (all).""" msg_type = "/refresh/all" req = "POST" await self.send_message(method=req, msg=msg_type) @@ -390,20 +396,20 @@ async def post_refresh(self): ) async def ping(self): - """Send a ping (pong should be returned)""" + """Send a ping (pong should be returned).""" msg_type = "/ping" req = "GET" await self.send_message(method=req, msg=msg_type) LOGGER.debug("Ping") async def get_devices_meta(self): - """Get all devices metadata""" + """Get all devices metadata.""" msg_type = "/devices/meta" req = "GET" await self.send_message(method=req, msg=msg_type) async def get_devices_data(self): - """Get all devices data""" + """Get all devices data.""" msg_type = "/devices/data" req = "GET" await self.send_message(method=req, msg=msg_type) @@ -412,37 +418,37 @@ async def get_devices_data(self): await self.get_poll_device_data(url) async def get_configs_file(self): - """List the devices to get the endpoint id""" + """List the devices to get the endpoint id.""" msg_type = "/configs/file" req = "GET" await self.send_message(method=req, msg=msg_type) async def get_devices_cmeta(self): - """Get metadata configuration to list poll devices (like Tywatt)""" + """Get metadata configuration to list poll devices (like Tywatt).""" msg_type = "/devices/cmeta" req = "GET" await self.send_message(method=req, msg=msg_type) async def get_areas_meta(self): - """Get areas metadata""" + """Get areas metadata.""" msg_type = "/areas/meta" req = "GET" await self.send_message(method=req, msg=msg_type) async def get_areas_cmeta(self): - """Get areas metadata""" + """Get areas metadata.""" msg_type = "/areas/cmeta" req = "GET" await self.send_message(method=req, msg=msg_type) async def get_areas_data(self): - """Get areas metadata""" + """Get areas metadata.""" msg_type = "/areas/data" req = "GET" await self.send_message(method=req, msg=msg_type) async def get_device_data(self, id): - """Give order to endpoint""" + """Give order to endpoint.""" # 10 here is the endpoint = the device (shutter in this case) to open. device_id = str(id) str_request = ( @@ -453,34 +459,36 @@ async def get_device_data(self, id): await self._connection.send(a_bytes) async def get_poll_device_data(self, url): + """Poll a device (probably unused).""" LOGGER.error("poll device data %s", url) msg_type = url req = "GET" await self.send_message(method=req, msg=msg_type) def add_poll_device_url(self, url): + """Add a device for polling (probably unused).""" self.poll_device_urls.append(url) async def get_moments(self): - """Get the moments (programs)""" + """Get the moments (programs).""" msg_type = "/moments/file" req = "GET" await self.send_message(method=req, msg=msg_type) async def get_scenarii(self): - """Get the scenarios""" + """Get the scenarios.""" msg_type = "/scenarios/file" req = "GET" await self.send_message(method=req, msg=msg_type) async def get_groups(self): - """Get the groups""" + """Get the groups.""" msg_type = "/groups/file" req = "GET" await self.send_message(method=req, msg=msg_type) async def put_data(self, path, name, value): - """Give order (name + value) to path""" + """Give order (name + value) to path.""" body: str if value is None: body = '{"' + name + '":"null}' @@ -503,7 +511,7 @@ async def put_data(self, path, name, value): return 0 async def put_devices_data(self, device_id, endpoint_id, name, value): - """Give order (name + value) to endpoint""" + """Give order (name + value) to endpoint.""" # For shutter, value is the percentage of closing body: str if value is None: @@ -512,7 +520,7 @@ async def put_devices_data(self, device_id, endpoint_id, name, value): body = '[{"name":"' + name + '","value":' + str(value).lower() + '}]' else: body = '[{"name":"' + name + '","value":"' + value + '"}]' - + # endpoint_id is the endpoint = the device (shutter in this case) to # open. str_request = ( @@ -529,6 +537,7 @@ async def put_devices_data(self, device_id, endpoint_id, name, value): return 0 async def put_alarm_cdata(self, device_id, alarm_id=None, value=None, zone_id=None): + """Configure alarm mode.""" # Credits to @mgcrea on github ! # AWAY # "PUT /devices/{}/endpoints/{}/cdata?name=alarmCmd HTTP/1.1\r\ncontent-length: 29\r\ncontent-type: application/json; charset=utf-8\r\ntransac-id: request_124\r\n\r\n\r\n{"value":"ON","pwd":{}}\r\n\r\n" # HOME "PUT /devices/{}/endpoints/{}/cdata?name=zoneCmd HTTP/1.1\r\ncontent-length: 41\r\ncontent-type: application/json; charset=utf-8\r\ntransac-id: request_46\r\n\r\n\r\n{"value":"ON","pwd":"{}","zones":[1]}\r\n\r\n" @@ -593,7 +602,7 @@ async def put_alarm_cdata(self, device_id, alarm_id=None, value=None, zone_id=No LOGGER.error("put_alarm_cdata ERROR !", exc_info=True) async def update_firmware(self): - """Update Tydom firmware""" + """Update Tydom firmware.""" msg_type = "/configs/gateway/update" req = "PUT" await self.send_message(method=req, msg=msg_type) diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index 5e47c26..36f0074 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -1,6 +1,5 @@ """Support for Tydom classes""" -from typing import Callable -import logging +from collections.abc import Callable from ..const import LOGGER class TydomDevice: @@ -95,8 +94,7 @@ async def stop(self) -> None: ) async def set_position(self, position: int) -> None: - """ - Set cover to the given position. + """Set cover to the given position. """ await self._tydom_client.put_devices_data( self._id, self._endpoint, "position", str(position) @@ -125,7 +123,7 @@ async def slope_stop(self) -> None: # FIXME replace command async def set_slope_position(self, position: int) -> None: - """ Set cover to the given position. """ + """Set cover to the given position.""" LOGGER.debug("set roller tilt position (device) to : %s", position) await self._tydom_client.put_devices_data( @@ -142,7 +140,7 @@ class TydomSmoke(TydomDevice): class TydomBoiler(TydomDevice): - + _ha_device = None """represents a boiler""" diff --git a/custom_components/deltadore-tydom/update.py b/custom_components/deltadore-tydom/update.py index 2ec7c91..2adeab4 100644 --- a/custom_components/deltadore-tydom/update.py +++ b/custom_components/deltadore-tydom/update.py @@ -62,7 +62,7 @@ def installed_version(self) -> str | None: # return self._hub.current_firmware if hasattr (self._hub.device_info, "mainVersionSW"): return self._hub.device_info.mainVersionSW - else: + else: return None @property @@ -80,4 +80,4 @@ async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" - await self._hub.async_trigger_firmware_update() \ No newline at end of file + await self._hub.async_trigger_firmware_update() From baba5a1def201edc9792eb37b4eae266718aa6fd Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 31 Oct 2023 10:55:39 +0100 Subject: [PATCH 59/74] =?UTF-8?q?=C2=A0fix=20ruff=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deltadore-tydom/tydom/tydom_devices.py | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/custom_components/deltadore-tydom/tydom/tydom_devices.py b/custom_components/deltadore-tydom/tydom/tydom_devices.py index 36f0074..77b2035 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_devices.py +++ b/custom_components/deltadore-tydom/tydom/tydom_devices.py @@ -1,11 +1,12 @@ -"""Support for Tydom classes""" +"""Support for Tydom classes.""" from collections.abc import Callable from ..const import LOGGER class TydomDevice: - """represents a generic device""" + """represents a generic device.""" def __init__(self, tydom_client, uid, device_id, name, device_type, endpoint, metadata, data): + """Initialize a TydomDevice.""" self._tydom_client = tydom_client self._uid = uid self._id = device_id @@ -40,21 +41,21 @@ def device_id(self) -> str: @property def device_name(self) -> str: - """Return name for device""" + """Return name for device.""" return self._name @property def device_type(self) -> str: - """Return type for device""" + """Return type for device.""" return self._type @property def device_endpoint(self) -> str: - """Return endpoint for device""" + """Return endpoint for device.""" return self._endpoint async def update_device(self, device): - """Update the device values from another device""" + """Update the device values from another device.""" LOGGER.debug("Update device %s", device.device_id) for attribute, value in device.__dict__.items(): if (attribute == "_uid" or attribute[:1] != "_") and value is not None: @@ -70,53 +71,52 @@ async def publish_updates(self) -> None: class Tydom(TydomDevice): - """Tydom Gateway""" + """Tydom Gateway.""" class TydomShutter(TydomDevice): - """Represents a shutter""" + """Represents a shutter.""" async def down(self) -> None: - """Tell cover to go down""" + """Tell cover to go down.""" await self._tydom_client.put_devices_data( self._id, self._endpoint, "positionCmd", "DOWN" ) async def up(self) -> None: - """Tell cover to go up""" + """Tell cover to go up.""" await self._tydom_client.put_devices_data( self._id, self._endpoint, "positionCmd", "UP" ) async def stop(self) -> None: - """Tell cover to stop moving""" + """Tell cover to stop moving.""" await self._tydom_client.put_devices_data( self._id, self._endpoint, "positionCmd", "STOP" ) async def set_position(self, position: int) -> None: - """Set cover to the given position. - """ + """Set cover to the given position.""" await self._tydom_client.put_devices_data( self._id, self._endpoint, "position", str(position) ) # FIXME replace command async def slope_open(self) -> None: - """Tell the cover to tilt open""" + """Tell the cover to tilt open.""" await self._tydom_client.put_devices_data( self._id, self._endpoint, "positionCmd", "DOWN" ) # FIXME replace command async def slope_close(self) -> None: - """Tell the cover to tilt closed""" + """Tell the cover to tilt closed.""" await self._tydom_client.put_devices_data( self._id, self._endpoint, "positionCmd", "UP" ) # FIXME replace command async def slope_stop(self) -> None: - """Tell the cover to stop tilt""" + """Tell the cover to stop tilt.""" await self._tydom_client.put_devices_data( self._id, self._endpoint, "positionCmd", "STOP" ) @@ -132,20 +132,21 @@ async def set_slope_position(self, position: int) -> None: class TydomEnergy(TydomDevice): - """Represents an energy sensor (for example TYWATT)""" + """Represents an energy sensor (for example TYWATT).""" class TydomSmoke(TydomDevice): - """Represents an smoke detector sensor""" + """Represents an smoke detector sensor.""" class TydomBoiler(TydomDevice): + """Represents a Boiler.""" _ha_device = None """represents a boiler""" async def set_hvac_mode(self, mode): - """Set hvac mode (ANTI_FROST/NORMAL/STOP)""" + """Set hvac mode (ANTI_FROST/NORMAL/STOP).""" LOGGER.debug("setting hvac mode to %s", mode) if mode == "ANTI_FROST": #await self._tydom_client.put_devices_data( @@ -215,36 +216,27 @@ async def set_hvac_mode(self, mode): ) else: LOGGER.error("Unknown hvac mode: %s", mode) - #await self._tydom_client.put_devices_data( - # self._id, self._endpoint, "thermicLevel", "STOP" - #) - #await self._tydom_client.put_devices_data( - # self._id, self._endpoint, "antifrostOn", True - #) - #await self._tydom_client.put_devices_data( - # self._id, self._endpoint, "authorization", mode - #) async def set_temperature(self, temperature): - """Set target temperature""" + """Set target temperature.""" await self._tydom_client.put_devices_data( self._id, self._endpoint, "setpoint", temperature ) class TydomWindow(TydomDevice): - """represents a window""" + """represents a window.""" class TydomDoor(TydomDevice): - """represents a door""" + """represents a door.""" class TydomGate(TydomDevice): - """represents a gate""" + """represents a gate.""" class TydomGarage(TydomDevice): - """represents a garage door""" + """represents a garage door.""" class TydomLight(TydomDevice): - """represents a light""" + """represents a light.""" class TydomAlarm(TydomDevice): - """represents an alarm""" + """represents an alarm.""" From 4bdace3fa2267af5dcd765bd845083dc8e296c75 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:05:34 +0100 Subject: [PATCH 60/74] =?UTF-8?q?=C2=A0fix=20ruff=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deltadore-tydom/ha_entities.py | 23 +++++++++++--- .../deltadore-tydom/tydom/MessageHandler.py | 31 +++++++++++++++---- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 8937336..d971aac 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -1,4 +1,4 @@ -"""Home assistant entites""" +"""Home assistant entites.""" from typing import Any from homeassistant.helpers import device_registry as dr @@ -40,7 +40,19 @@ from homeassistant.components.light import LightEntity from homeassistant.components.lock import LockEntity -from .tydom.tydom_devices import * +from .tydom.tydom_devices import ( + Tydom, + TydomDevice, + TydomEnergy, + TydomShutter, + TydomSmoke, + TydomBoiler, + TydomWindow, + TydomDoor, + TydomGate, + TydomGarage, + TydomLight, +) from .const import DOMAIN, LOGGER @@ -269,6 +281,7 @@ class HATydom(Entity, HAEntity): ] def __init__(self, device: Tydom, hass) -> None: + """Initialize HATydom.""" self._device = device self._attr_unique_id = f"{self._device.device_id}" self._attr_name = self._device.device_name @@ -297,7 +310,7 @@ def device_info(self): } class HAEnergy(Entity, HAEntity): - """Representation of an Energy sensor""" + """Representation of an Energy sensor.""" _attr_has_entity_name = False _attr_entity_category = None @@ -372,6 +385,7 @@ class HAEnergy(Entity, HAEntity): } def __init__(self, device: TydomEnergy, hass) -> None: + """Initialize HAEnergy.""" self._device = device self._attr_unique_id = f"{self._device.device_id}_energy" self._attr_name = self._device.device_name @@ -514,7 +528,7 @@ async def async_stop_cover_tilt(self, **kwargs): class HASmoke(BinarySensorEntity, HAEntity): - """Representation of an Smoke sensor""" + """Representation of an Smoke sensor.""" should_poll = False supported_features = None @@ -522,6 +536,7 @@ class HASmoke(BinarySensorEntity, HAEntity): sensor_classes = {"batt_defect": BinarySensorDeviceClass.PROBLEM} def __init__(self, device: TydomSmoke) -> None: + """Initialize TydomSmoke.""" self._device = device self._attr_unique_id = f"{self._device.device_id}_smoke_defect" self._attr_name = self._device.device_name diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 6579b61..9e01052 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -7,7 +7,20 @@ import urllib3 import re -from .tydom_devices import * +from .tydom.tydom_devices import ( + Tydom, + TydomDevice, + TydomEnergy, + TydomShutter, + TydomSmoke, + TydomBoiler, + TydomWindow, + TydomDoor, + TydomGate, + TydomGarage, + TydomLight, + TydomAlarm, +) from ..const import LOGGER @@ -109,9 +122,11 @@ async def incoming_triage(self, bytes_str): ) from ex return None - # Basic response parsing. Typically GET responses + instanciate covers and - # alarm class for updating data async def parse_response(self, incoming, uri_origin, http_request_line): + """Parse basic response. + + Typically GET responses + instanciate covers and alarm class for updating data. + """ data = incoming msg_type = None first = str(data[:40]) @@ -184,10 +199,10 @@ async def parse_devices_metadata(self, parsed): for endpoint in device["endpoints"]: id_endpoint = endpoint["id"] device_unique_id = str(id_endpoint) + "_" + str(id) - device_metadata[device_unique_id] = dict() + device_metadata[device_unique_id] = {} for metadata in endpoint["metadata"]: metadata_name = metadata["name"] - device_metadata[device_unique_id][metadata_name] = dict() + device_metadata[device_unique_id][metadata_name] = {} for meta in metadata: if meta == "name": continue @@ -611,16 +626,20 @@ def get_name_from_id(self, id): class BytesIOSocket: + """BytesIOSocket.""" + def __init__(self, content): """Initialize a BytesIOSocket.""" self.handle = BytesIO(content) def makefile(self, mode): - """get handle.""" + """Get handle.""" return self.handle class HTTPRequest(BaseHTTPRequestHandler): + """HTTPRequest.""" + def __init__(self, request_text): """Initialize a HTTPRequest.""" self.raw_requestline = request_text From cac989cc338c61e00e7fb2599905e565a0e3ef09 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:19:20 +0100 Subject: [PATCH 61/74] =?UTF-8?q?=C2=A0fix=20ruff=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/__init__.py | 14 +++++++---- custom_components/deltadore-tydom/hub.py | 19 ++++++++++++++- custom_components/deltadore-tydom/lock.py | 3 --- .../deltadore-tydom/tydom/MessageHandler.py | 2 +- .../deltadore-tydom/update Tydom.json | 23 +++++++++++++++++++ 5 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 custom_components/deltadore-tydom/update Tydom.json diff --git a/custom_components/deltadore-tydom/__init__.py b/custom_components/deltadore-tydom/__init__.py index 48ccdde..40ce920 100644 --- a/custom_components/deltadore-tydom/__init__.py +++ b/custom_components/deltadore-tydom/__init__.py @@ -1,7 +1,7 @@ -"""The Detailed Hello World Push integration.""" +"""The Detailed Delta Dore Tydom Push integration.""" from __future__ import annotations -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PIN, Platform +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN, Platform from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -44,9 +44,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_create_background_task( target=tydom_hub.setup(connection), hass=hass, name="Tydom" ) - # entry.async_create_background_task( - # target=tydom_hub.ping(connection), hass=hass, name="Tydom ping" - # ) + entry.async_create_background_task( + target=tydom_hub.ping(), hass=hass, name="Tydom ping" + ) + entry.async_create_background_task( + target=tydom_hub.refresh_all(), hass=hass, name="Tydom refresh metadata and data" + ) + except Exception as err: raise ConfigEntryNotReady from err diff --git a/custom_components/deltadore-tydom/hub.py b/custom_components/deltadore-tydom/hub.py index 5dd2e23..c5d8cb6 100644 --- a/custom_components/deltadore-tydom/hub.py +++ b/custom_components/deltadore-tydom/hub.py @@ -236,7 +236,24 @@ async def ping(self) -> None: """Periodically send pings.""" while True: await self._tydom_client.ping() - await asyncio.sleep(10) + await asyncio.sleep(60) + + async def refresh_all(self) -> None: + """Periodically refresh all metadata and data. + + It allows new devices to be discovered. + """ + while True: + await self._tydom_client.get_info() + await self._tydom_client.put_api_mode() + await self._tydom_client.get_groups() + await self._tydom_client.post_refresh() + await self._tydom_client.get_configs_file() + await self._tydom_client.get_devices_meta() + await self._tydom_client.get_devices_cmeta() + await self._tydom_client.get_devices_data() + await self._tydom_client.get_scenarii() + await asyncio.sleep(300) async def async_trigger_firmware_update(self) -> None: """Trigger firmware update.""" diff --git a/custom_components/deltadore-tydom/lock.py b/custom_components/deltadore-tydom/lock.py index d586718..b429461 100644 --- a/custom_components/deltadore-tydom/lock.py +++ b/custom_components/deltadore-tydom/lock.py @@ -1,15 +1,12 @@ """Platform for sensor integration.""" from __future__ import annotations -from typing import Any - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 9e01052..1a56d58 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -7,7 +7,7 @@ import urllib3 import re -from .tydom.tydom_devices import ( +from .tydom_devices import ( Tydom, TydomDevice, TydomEnergy, diff --git a/custom_components/deltadore-tydom/update Tydom.json b/custom_components/deltadore-tydom/update Tydom.json new file mode 100644 index 0000000..11e74b4 --- /dev/null +++ b/custom_components/deltadore-tydom/update Tydom.json @@ -0,0 +1,23 @@ + update Tydom +2023-09-08 18:57:13.404 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /configs/gateway/api_mode\r\nContent-Type: application/json\r\nContent-Length: 0\r\nTransac-Id: 0\r\n\r\n' +2023-09-08 18:57:13.488 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /groups/file\r\nContent-Type: application/json\r\nContent-Length: 1736\r\nTransac-Id: 0\r\nFile-Version: 57\r\n\r\n{"groups":[{"devices":[],"areas":[],"id":859468405},{"devices":[{"endpoints":[{"id":0}],"id":0},{"endpoints":[{"id":0}],"id":1},{"endpoints":[{"id":0}],"id":2},{"endpoints":[{"id":0}],"id":3},{"endpoints":[{"id":0}],"id":4},{"endpoints":[{"id":0}],"id":5},{"endpoints":[{"id":0}],"id":6},{"endpoints":[{"id":0}],"id":7}],"areas":[],"id":1725345078},{"devices":[],"areas":[],"id":562847101},{"devices":[{"endpoints":[{"id":0}],"id":0},{"endpoints":[{"id":0}],"id":1},{"endpoints":[{"id":0}],"id":3},{"endpoints":[{"id":0}],"id":4},{"endpoints":[{"id":0}],"id":6},{"endpoints":[{"id":0}],"id":7}],"areas":[],"id":1259860423},{"devices":[{"endpoints":[{"id":0}],"id":2},{"endpoints":[{"id":0}],"id":5}],"areas":[],"id":732974460},{"devices":[],"areas":[],"id":1117398060},{"devices":[{"endpoints":[{"id":1599338298}],"id":1599338298}],"areas":[],"id":2032409200},{"devices":[{"endpoints":[{"id":1599338370}],"id":1599338370}],"areas":[],"id":317891801},{"devices":[{"endpoints":[{"id":1599338418}],"id":1599338418}],"areas":[],"id":1455243199},{"devices":[{"endpoints":[{"id":1599387771}],"id":1599387771}],"areas":[],"id":1840849049},{"devices":[{"endpoints":[{"id":1604476226}],"id":1604476226},{"endpoints":[{"id":1604476251}],"id":1604476251}],"areas":[],"id":1704806706},{"devices":[{"endpoints":[{"id":1599387831}],"id":1599387831}],"areas":[],"id":724393265},{"devices":[{"endpoints":[{"id":1604476324}],"id":1604476324},{"endpoints":[{"id":1604476347}],"id":1604476347}],"areas":[],"id":2138691766},{"devices":[{"endpoints":[{"id":1604477407}],"id":1604477407}],"areas":[],"id":851256178},{"devices":[{"endpoints":[{"id":1611399103}],"id":1611399103},{"endpoints":[{"id":1611399070}],"id":1611399070}],"areas":[],"id":1213177750}]}' +2023-09-08 18:57:13.489 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Message received detected as (msg_groups) +2023-09-08 18:57:13.489 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Incoming data parsed with success +2023-09-08 18:57:13.489 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /refresh/all\r\nContent-Type: application/json\r\nContent-Length: 0\r\nTransac-Id: 0\r\n\r\n' +2023-09-08 18:57:13.599 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /configs/file\r\nContent-Type: application/json\r\nContent-Length: 8161\r\nTransac-Id: 0\r\nFile-Version: 562\r\n\r\n{"date":1693507332,"version_application":"4.9.2-3-dd","endpoints":[{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":0,"name":"Baie Salon","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":1,"name":"Bureau","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":2,"name":"Chambre 2","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":3,"name":"Baie S\xc3\xa9jour","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":4,"name":"Fixe Salon","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":5,"name":"Chambre 1","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":6,"name":"Chambre Rdc","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":7,"name":"Cuisine","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"conso","skill":"TYDOM_X3D","id_device":8,"name":"\xc3\x89quipement conso 1","anticipation_start":false,"space_id":"","picto":"picto_conso","last_usage":"conso"},{"id_endpoint":0,"first_usage":"hvac","skill":"TYDOM_X3D","id_device":9,"name":"Radiateur","anticipation_start":false,"space_id":"","picto":"picto_thermometer","last_usage":"boiler"},{"id_endpoint":0,"first_usage":"hvac","skill":"TYDOM_X3D","id_device":10,"name":"Plancher Chauffant","anticipation_start":false,"space_id":"","picto":"picto_thermometer","last_usage":"boiler"},{"id_endpoint":1599338298,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599338298,"name":"Bureau","anticipation_start":false,"space_id":"","picto":"picto_window","last_usage":"windowFrench"},{"id_endpoint":1599338370,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599338370,"name":"Chambre","anticipation_start":false,"space_id":"","picto":"picto_window","last_usage":"windowFrench"},{"id_endpoint":1599338418,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599338418,"name":"Salle de bain","anticipation_start":false,"space_id":"","picto":"picto_window","last_usage":"windowFrench"},{"id_endpoint":1599387771,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599387771,"name":"Mezzanine","anticipation_start":false,"space_id":"","picto":"picto_window","last_usage":"windowFrench"},{"id_endpoint":1599387831,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599387831,"name":"Salle de bain \xc3\xa9tage","anticipation_start":false,"space_id":"","picto":"picto_window","last_usage":"windowFrench"},{"id_endpoint":1599388036,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599388036,"name":"Battant droit","anticipation_start":false,"space_id":"","picto":"picto_window","last_usage":"windowFrench"},{"id_endpoint":1599388243,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599388243,"name":"Battant gauche","anticipation_start":false,"space_id":"","picto":"picto_window_lock","last_usage":"window"},{"id_endpoint":1604473848,"first_usage":"belmDoor","skill":"TYDOM_X3D","id_device":1604473848,"name":"Porte entr\xc3\xa9e","anticipation_start":false,"space_id":"","picto":"picto_belmdoor","last_usage":"belmDoor"},{"id_endpoint":1604476226,"first_usage":"window","skill":"TYDOM_X3D","id_device":1604476226,"name":"Cuisine droit","anticipation_start":false,"space_id":"","picto":"default_device","last_usage":"window"},{"id_endpoint":1604476251,"first_usage":"window","skill":"TYDOM_X3D","id_device":1604476251,"name":"Cuisine gauche","anticipation_start":false,"space_id":"","picto":"default_device","last_usage":"window"},{"id_endpoint":1604476324,"first_usage":"window","skill":"TYDOM_X3D","id_device":1604476324,"name":"S\xc3\xa9jour droit","anticipation_start":false,"space_id":"","picto":"default_device","last_usage":"window"},{"id_endpoint":1604476347,"first_usage":"window","skill":"TYDOM_X3D","id_device":1604476347,"name":"S\xc3\xa9jour gauche","anticipation_start":false,"space_id":"","picto":"default_device","last_usage":"window"},{"id_endpoint":1604477407,"first_usage":"window","skill":"TYDOM_X3D","id_device":1604477407,"name":"Salon gauche","anticipation_start":false,"space_id":"","picto":"picto_window_lock","last_usage":"window"},{"id_endpoint":1611399070,"first_usage":"window","skill":"TYDOM_X3D","id_device":1611399070,"name":"Chambre L\xc3\xa9a gauche","anticipation_start":false,"space_id":"","picto":"picto_window_lock","last_usage":"window"},{"id_endpoint":1611399103,"first_usage":"window","skill":"TYDOM_X3D","id_device":1611399103,"name":"Chambre L\xc3\xa9a droit","anticipation_start":false,"space_id":"","picto":"picto_window_lock","last_usage":"window"},{"id_endpoint":1664906374,"first_usage":"conso","skill":"TYDOM_X3D","id_device":1664906374,"name":"PAC","anticipation_start":false,"space_id":"","picto":"picto_conso","last_usage":"conso","widget_behavior":{"tutorial_id":"6_RecepteurRF_serie6000_4"}},{"id_endpoint":1679399651,"first_usage":"sensor","skill":"TYDOM_X3D","id_device":1679399651,"name":"D\xc3\xa9tecteur Salon","anticipation_start":false,"picto":"picto_sensor_dfr","last_usage":"sensorDFR","widget_behavior":{"tutorial_id":"sensor_dfr"}},{"id_endpoint":1679399947,"first_usage":"sensor","skill":"TYDOM_X3D","id_device":1679399947,"name":"D\xc3\xa9tecteur \xc3\x89tage","anticipation_start":false,"picto":"picto_sensor_dfr","last_usage":"sensorDFR","widget_behavior":{"tutorial_id":"sensor_dfr"}},{"id_endpoint":1679401885,"first_usage":"sensor","skill":"TYDOM_X3D","id_device":1679401885,"name":"D\xc3\xa9tecteur Cellier","anticipation_start":false,"picto":"picto_sensor_dfr","last_usage":"sensorDFR","widget_behavior":{"tutorial_id":"sensor_dfr"}}],"old_tycam":false,"os":"android","groups":[{"group_all":true,"usage":"light","name":"TOTAL","id":859468405,"picto":"picto_lamp"},{"group_all":true,"usage":"shutter","name":"TOTAL","id":1725345078,"picto":"picto_shutter"},{"group_all":true,"usage":"awning","name":"TOTAL","id":562847101,"picto":"picto_awning_awning"},{"group_all":false,"usage":"shutter","name":"Rez de chauss\xc3\xa9e","id":1259860423,"picto":"picto_shutter"},{"group_all":false,"usage":"shutter","name":"\xc3\x89tage","id":732974460,"picto":"picto_shutter"},{"group_all":true,"usage":"plug","name":"Total","id":1117398060,"picto":"default_device"},{"group_all":false,"usage":"window","name":"Bureau","id":2032409200,"picto":"picto_window"},{"group_all":false,"usage":"window","name":"Chambre","id":317891801,"picto":"picto_window"},{"group_all":false,"usage":"window","name":"Salle de bain","id":1455243199,"picto":"picto_window"},{"group_all":false,"usage":"window","name":"Mezzanine","id":1840849049,"picto":"picto_window"},{"group_all":false,"usage":"window","name":"Cuisine","id":1704806706,"picto":"picto_window_lock"},{"group_all":false,"usage":"window","name":"Salle de bain \xc3\xa9tage ","id":724393265,"picto":"picto_window"},{"group_all":false,"usage":"window","name":"Baie s\xc3\xa9jour","id":2138691766,"picto":"picto_window_lock"},{"group_all":false,"usage":"window","name":"Baie salon ","id":851256178,"picto":"picto_window_lock"},{"group_all":false,"usage":"window","name":"Chambre L\xc3\xa9a","id":1213177750,"picto":"picto_window_lock"}],"areas":[],"scenarios":[{"rule_id":"","name":"Tout ouvrir ","id":1855347001,"type":"NORMAL","picto":"picto_scenario_leaving"}],"version":"1.0.2","new_tycam":false,"moments":[{"rule_id":"","color":5132710,"name":"Nuit","id":1131613363},{"rule_id":"","color":9813268,"name":"fermer volets","id":2055498241},{"rule_id":"","color":9813268,"name":"Ouvrir volets ","id":1227929543}],"id_catalog":"F2BD90F93B888DA02C54980F11AE4796DFCC98F447CD3FE326F5A3A964C939BF","zigbee_networks":[]}' +2023-09-08 18:57:13.600 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Message received detected as (msg_config) +2023-09-08 18:57:13.600 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] parse_config_data : {'date': 1693507332, 'version_application': '4.9.2-3-dd', 'endpoints': [{'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 0, 'name': 'Baie Salon', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 1, 'name': 'Bureau', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 2, 'name': 'Chambre 2', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 3, 'name': 'Baie Séjour', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 4, 'name': 'Fixe Salon', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 5, 'name': 'Chambre 1', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 6, 'name': 'Chambre Rdc', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 7, 'name': 'Cuisine', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'conso', 'skill': 'TYDOM_X3D', 'id_device': 8, 'name': 'Équipement conso 1', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_conso', 'last_usage': 'conso'}, {'id_endpoint': 0, 'first_usage': 'hvac', 'skill': 'TYDOM_X3D', 'id_device': 9, 'name': 'Radiateur', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_thermometer', 'last_usage': 'boiler'}, {'id_endpoint': 0, 'first_usage': 'hvac', 'skill': 'TYDOM_X3D', 'id_device': 10, 'name': 'Plancher Chauffant', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_thermometer', 'last_usage': 'boiler'}, {'id_endpoint': 1599338298, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599338298, 'name': 'Bureau', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window', 'last_usage': 'windowFrench'}, {'id_endpoint': 1599338370, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599338370, 'name': 'Chambre', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window', 'last_usage': 'windowFrench'}, {'id_endpoint': 1599338418, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599338418, 'name': 'Salle de bain', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window', 'last_usage': 'windowFrench'}, {'id_endpoint': 1599387771, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599387771, 'name': 'Mezzanine', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window', 'last_usage': 'windowFrench'}, {'id_endpoint': 1599387831, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599387831, 'name': 'Salle de bain étage', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window', 'last_usage': 'windowFrench'}, {'id_endpoint': 1599388036, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599388036, 'name': 'Battant droit', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window', 'last_usage': 'windowFrench'}, {'id_endpoint': 1599388243, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599388243, 'name': 'Battant gauche', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window_lock', 'last_usage': 'window'}, {'id_endpoint': 1604473848, 'first_usage': 'belmDoor', 'skill': 'TYDOM_X3D', 'id_device': 1604473848, 'name': 'Porte entrée', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_belmdoor', 'last_usage': 'belmDoor'}, {'id_endpoint': 1604476226, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1604476226, 'name': 'Cuisine droit', 'anticipation_start': False, 'space_id': '', 'picto': 'default_device', 'last_usage': 'window'}, {'id_endpoint': 1604476251, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1604476251, 'name': 'Cuisine gauche', 'anticipation_start': False, 'space_id': '', 'picto': 'default_device', 'last_usage': 'window'}, {'id_endpoint': 1604476324, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1604476324, 'name': 'Séjour droit', 'anticipation_start': False, 'space_id': '', 'picto': 'default_device', 'last_usage': 'window'}, {'id_endpoint': 1604476347, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1604476347, 'name': 'Séjour gauche', 'anticipation_start': False, 'space_id': '', 'picto': 'default_device', 'last_usage': 'window'}, {'id_endpoint': 1604477407, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1604477407, 'name': 'Salon gauche', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window_lock', 'last_usage': 'window'}, {'id_endpoint': 1611399070, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1611399070, 'name': 'Chambre Léa gauche', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window_lock', 'last_usage': 'window'}, {'id_endpoint': 1611399103, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1611399103, 'name': 'Chambre Léa droit', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window_lock', 'last_usage': 'window'}, {'id_endpoint': 1664906374, 'first_usage': 'conso', 'skill': 'TYDOM_X3D', 'id_device': 1664906374, 'name': 'PAC', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_conso', 'last_usage': 'conso', 'widget_behavior': {'tutorial_id': '6_RecepteurRF_serie6000_4'}}, {'id_endpoint': 1679399651, 'first_usage': 'sensor', 'skill': 'TYDOM_X3D', 'id_device': 1679399651, 'name': 'Détecteur Salon', 'anticipation_start': False, 'picto': 'picto_sensor_dfr', 'last_usage': 'sensorDFR', 'widget_behavior': {'tutorial_id': 'sensor_dfr'}}, {'id_endpoint': 1679399947, 'first_usage': 'sensor', 'skill': 'TYDOM_X3D', 'id_device': 1679399947, 'name': 'Détecteur Étage', 'anticipation_start': False, 'picto': 'picto_sensor_dfr', 'last_usage': 'sensorDFR', 'widget_behavior': {'tutorial_id': 'sensor_dfr'}}, {'id_endpoint': 1679401885, 'first_usage': 'sensor', 'skill': 'TYDOM_X3D', 'id_device': 1679401885, 'name': 'Détecteur Cellier', 'anticipation_start': False, 'picto': 'picto_sensor_dfr', 'last_usage': 'sensorDFR', 'widget_behavior': {'tutorial_id': 'sensor_dfr'}}], 'old_tycam': False, 'os': 'android', 'groups': [{'group_all': True, 'usage': 'light', 'name': 'TOTAL', 'id': 859468405, 'picto': 'picto_lamp'}, {'group_all': True, 'usage': 'shutter', 'name': 'TOTAL', 'id': 1725345078, 'picto': 'picto_shutter'}, {'group_all': True, 'usage': 'awning', 'name': 'TOTAL', 'id': 562847101, 'picto': 'picto_awning_awning'}, {'group_all': False, 'usage': 'shutter', 'name': 'Rez de chaussée', 'id': 1259860423, 'picto': 'picto_shutter'}, {'group_all': False, 'usage': 'shutter', 'name': 'Étage', 'id': 732974460, 'picto': 'picto_shutter'}, {'group_all': True, 'usage': 'plug', 'name': 'Total', 'id': 1117398060, 'picto': 'default_device'}, {'group_all': False, 'usage': 'window', 'name': 'Bureau', 'id': 2032409200, 'picto': 'picto_window'}, {'group_all': False, 'usage': 'window', 'name': 'Chambre', 'id': 317891801, 'picto': 'picto_window'}, {'group_all': False, 'usage': 'window', 'name': 'Salle de bain', 'id': 1455243199, 'picto': 'picto_window'}, {'group_all': False, 'usage': 'window', 'name': 'Mezzanine', 'id': 1840849049, 'picto': 'picto_window'}, {'group_all': False, 'usage': 'window', 'name': 'Cuisine', 'id': 1704806706, 'picto': 'picto_window_lock'}, {'group_all': False, 'usage': 'window', 'name': 'Salle de bain étage ', 'id': 724393265, 'picto': 'picto_window'}, {'group_all': False, 'usage': 'window', 'name': 'Baie séjour', 'id': 2138691766, 'picto': 'picto_window_lock'}, {'group_all': False, 'usage': 'window', 'name': 'Baie salon ', 'id': 851256178, 'picto': 'picto_window_lock'}, {'group_all': False, 'usage': 'window', 'name': 'Chambre Léa', 'id': 1213177750, 'picto': 'picto_window_lock'}], 'areas': [], 'scenarios': [{'rule_id': '', 'name': 'Tout ouvrir ', 'id': 1855347001, 'type': 'NORMAL', 'picto': 'picto_scenario_leaving'}], 'version': '1.0.2', 'new_tycam': False, 'moments': [{'rule_id': '', 'color': 5132710, 'name': 'Nuit', 'id': 1131613363}, {'rule_id': '', 'color': 9813268, 'name': 'fermer volets', 'id': 2055498241}, {'rule_id': '', 'color': 9813268, 'name': 'Ouvrir volets ', 'id': 1227929543}], 'id_catalog': 'F2BD90F93B888DA02C54980F11AE4796DFCC98F447CD3FE326F5A3A964C939BF', 'zigbee_networks': []} +2023-09-08 18:57:13.600 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Configuration updated +2023-09-08 18:57:13.600 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] devices : [] +2023-09-08 18:57:13.659 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02PUT /devices/data HTTP/1.1\r\nServer: Tydom-001A25029FB7\r\ncontent-type: application/json\r\nTransfer-Encoding: chunked\r\n\r\n66\r\n[{"id":9,"endpoints":[{"id":0,"error":0,"data":[{"name":"setpoint","validity":"upToDate","value":null}\r\n3D\r\n,{"name":"thermicLevel","validity":"upToDate","value":"STOP"}\r\n3B\r\n,{"name":"hvacMode","validity":"upToDate","value":"NORMAL"}\r\n37\r\n,{"name":"tempoOn","validity":"upToDate","value":false}\r\n3B\r\n,{"name":"antifrostOn","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"loadSheddingOn","validity":"upToDate","value":false}\r\n3F\r\n,{"name":"openingDetected","validity":"upToDate","value":false}\r\n40\r\n,{"name":"presenceDetected","validity":"upToDate","value":false}\r\n37\r\n,{"name":"absence","validity":"upToDate","value":false}\r\n40\r\n,{"name":"productionDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"batteryCmdDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"tempSensorDefect","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorShortCut","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorOpenCirc","validity":"upToDate","value":false}\r\n37\r\n,{"name":"boostOn","validity":"upToDate","value":false}\r\n6B\r\n]}]},{"id":10,"endpoints":[{"id":0,"error":0,"data":[{"name":"setpoint","validity":"upToDate","value":null}\r\n3D\r\n,{"name":"thermicLevel","validity":"upToDate","value":"STOP"}\r\n3B\r\n,{"name":"hvacMode","validity":"upToDate","value":"NORMAL"}\r\n37\r\n,{"name":"tempoOn","validity":"upToDate","value":false}\r\n3B\r\n,{"name":"antifrostOn","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"loadSheddingOn","validity":"upToDate","value":false}\r\n3F\r\n,{"name":"openingDetected","validity":"upToDate","value":false}\r\n40\r\n,{"name":"presenceDetected","validity":"upToDate","value":false}\r\n37\r\n,{"name":"absence","validity":"upToDate","value":false}\r\n40\r\n,{"name":"productionDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"batteryCmdDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"tempSensorDefect","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorShortCut","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorOpenCirc","validity":"upToDate","value":false}\r\n74\r\n]}]},{"id":8,"endpoints":[{"id":0,"error":0,"data":[{"name":"energyInstantTi1I","validity":"upToDate","value":0.060}\r\n45\r\n,{"name":"energyInstantTi1I_Min","validity":"upToDate","value":0.000}\r\n45\r\n,{"name":"energyInstantTi1I_Max","validity":"upToDate","value":6.670}\r\n43\r\n,{"name":"energyScaleTi1I_Min","validity":"upToDate","value":0.000}\r\n44\r\n,{"name":"energyScaleTi1I_Max","validity":"upToDate","value":13.340}\r\n72\r\n]}]},{"id":10,"endpoints":[{"id":0,"error":0,"data":[{"name":"boostOn","validity":"upToDate","value":false}]}]}]\r\n\r\n0\r\n\r\n' +2023-09-08 18:57:13.659 WARNING (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Unknown tydom message type received (b'\x02PUT /devices/data HTTP/1.1\r\nServer: Tydom-001A25029FB7\r\ncontent-type: application/json\r\nTransfer-Encoding: chunked\r\n\r\n66\r\n[{"id":9,"endpoints":[{"id":0,"error":0,"data":[{"name":"setpoint","validity":"upToDate","value":null}\r\n3D\r\n,{"name":"thermicLevel","validity":"upToDate","value":"STOP"}\r\n3B\r\n,{"name":"hvacMode","validity":"upToDate","value":"NORMAL"}\r\n37\r\n,{"name":"tempoOn","validity":"upToDate","value":false}\r\n3B\r\n,{"name":"antifrostOn","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"loadSheddingOn","validity":"upToDate","value":false}\r\n3F\r\n,{"name":"openingDetected","validity":"upToDate","value":false}\r\n40\r\n,{"name":"presenceDetected","validity":"upToDate","value":false}\r\n37\r\n,{"name":"absence","validity":"upToDate","value":false}\r\n40\r\n,{"name":"productionDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"batteryCmdDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"tempSensorDefect","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorShortCut","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorOpenCirc","validity":"upToDate","value":false}\r\n37\r\n,{"name":"boostOn","validity":"upToDate","value":false}\r\n6B\r\n]}]},{"id":10,"endpoints":[{"id":0,"error":0,"data":[{"name":"setpoint","validity":"upToDate","value":null}\r\n3D\r\n,{"name":"thermicLevel","validity":"upToDate","value":"STOP"}\r\n3B\r\n,{"name":"hvacMode","validity":"upToDate","value":"NORMAL"}\r\n37\r\n,{"name":"tempoOn","validity":"upToDate","value":false}\r\n3B\r\n,{"name":"antifrostOn","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"loadSheddingOn","validity":"upToDate","value":false}\r\n3F\r\n,{"name":"openingDetected","validity":"upToDate","value":false}\r\n40\r\n,{"name":"presenceDetected","validity":"upToDate","value":false}\r\n37\r\n,{"name":"absence","validity":"upToDate","value":false}\r\n40\r\n,{"name":"productionDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"batteryCmdDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"tempSensorDefect","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorShortCut","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorOpenCirc","validity":"upToDate","value":false}\r\n74\r\n]}]},{"id":8,"endpoints":[{"id":0,"error":0,"data":[{"name":"energyInstantTi1I","validity":"upToDate","value":0.060}\r\n45\r\n,{"name":"energyInstantTi1I_Min","validity":"upToDate","value":0.000}\r\n45\r\n,{"name":"energyInstantTi1I_Max","validity":"upToDate","value":6.670}\r\n43\r\n,{"name":"energyScaleTi1I_Min","validity":"upToDate","value":0.000}\r\n44\r\n,{"name":"energyScaleTi1I_Max","validity":"upToDate","value":13.340}\r\n72\r\n]}]},{"id":10,"endpoints":[{"id":0,"error":0,"data":[{"name":"boostOn","validity":"upToDate","value":false}]}]}]\r\n\r\n0\r\n\r\n') +2023-09-08 18:57:13.915 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /devices/meta\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nTransac-Id: 0\r\n\r\nD0\r\n[{"id":0,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":1,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":2,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":3,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":4,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":5,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":6,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":7,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nAF\r\n]}]},{"id":8,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"jobsMP","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n69\r\n,{"name":"softVersion","type":"string","permission":"r","validity":"INFINITE","enum_values":["XX.YY.ZZ"]}\r\n69\r\n,{"name":"softPlan","type":"string","permission":"r","validity":"INFINITE","enum_values":["WW.XX.YY.ZZ"]}\r\n89\r\n,{"name":"energyTotIndexWatt","type":"numeric","permission":"r","validity":"METER_POLLING","min":0,"max":4294967294,"step":1,"unit":"Wh"}\r\n8F\r\n,{"name":"energyInstantTotElec","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n93\r\n,{"name":"energyInstantTotElec_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n93\r\n,{"name":"energyInstantTotElec_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n91\r\n,{"name":"energyScaleTotElec_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n91\r\n,{"name":"energyScaleTotElec_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n86\r\n,{"name":"energyInstantTotElecP","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n8B\r\n,{"name":"energyInstantTotElec_P_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n8B\r\n,{"name":"energyInstantTotElec_P_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n89\r\n,{"name":"energyScaleTotElec_P_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n89\r\n,{"name":"energyScaleTotElec_P_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n85\r\n,{"name":"energyIndexTi1","type":"numeric","permission":"r","validity":"METER_POLLING","min":0,"max":4294967294,"step":1,"unit":"Wh"}\r\n8C\r\n,{"name":"energyInstantTi1I","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n90\r\n,{"name":"energyInstantTi1I_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n90\r\n,{"name":"energyInstantTi1I_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n8E\r\n,{"name":"energyScaleTi1I_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n8E\r\n,{"name":"energyScaleTi1I_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n82\r\n,{"name":"energyInstantTi1P","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n86\r\n,{"name":"energyInstantTi1P_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n86\r\n,{"name":"energyInstantTi1P_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n84\r\n,{"name":"energyScaleTi1P_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n84\r\n,{"name":"energyScaleTi1P_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\nAF\r\n]}]},{"id":9,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"authorization","type":"string","permission":"rw","validity":"STATUS_POLLING","enum_values":["STOP","HEATING"]}\r\n86\r\n,{"name":"setpoint","type":"numeric","permission":"rw","validity":"DATA_POLLING","min":10.000,"max":30.000,"step":0.500,"unit":"degC"}\r\n6B\r\n,{"name":"thermicLevel","type":"string","permission":"rw","validity":"DATA_POLLING","enum_values":["STOP"]}\r\n86\r\n,{"name":"delaySetpoint","type":"numeric","permission":"w","validity":"INFINITE","min":10.000,"max":30.000,"step":0.500,"unit":"degC"}\r\n6B\r\n,{"name":"delayThermicLevel","type":"string","permission":"w","validity":"INFINITE","enum_values":["STOP"]}\r\n7D\r\n,{"name":"hvacMode","type":"string","permission":"rw","validity":"DATA_POLLING","enum_values":["NORMAL","STOP","ANTI_FROST"]}\r\n80\r\n,{"name":"timeDelay","type":"numeric","permission":"rw","validity":"TIMER_POLLING","min":0,"max":65535,"step":1,"unit":"minute"}\r\n8B\r\n,{"name":"temperature","type":"numeric","permission":"r","validity":"SENSOR_POLLING","min":-99.900,"max":99.900,"step":0.010,"unit":"degC"}\r\n62\r\n,{"name":"tempoOn","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n66\r\n,{"name":"antifrostOn","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n69\r\n,{"name":"loadSheddingOn","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6A\r\n,{"name":"openingDetected","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"presenceDetected","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n62\r\n,{"name":"absence","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"productionDefect","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"batteryCmdDefect","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"tempSensorDefect","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6D\r\n,{"name":"tempSensorShortCut","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6D\r\n,{"name":"tempSensorOpenCirc","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n63\r\n,{"name":"boostOn","type":"boolean","permission":"rw","validity":"STATUS_POLLING","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n7E\r\n,{"name":"anticipCoeff","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":65534,"step":1,"unit":"min/deg"}\r\nB0\r\n]}]},{"id":10,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"authorization","type":"string","permission":"rw","validity":"STATUS_POLLING","enum_values":["STOP","HEATING"]}\r\n86\r\n,{"name":"setpoint","type":"numeric","permission":"rw","validity":"DATA_POLLING","min":10.000,"max":30.000,"step":0.500,"unit":"degC"}\r\n6B\r\n,{"name":"thermicLevel","type":"string","permission":"rw","validity":"DATA_POLLING","enum_values":["STOP"]}\r\n86\r\n,{"name":"delaySetpoint","type":"numeric","permission":"w","validity":"INFINITE","min":10.000,"max":30.000,"step":0.500,"unit":"degC"}\r\n6B\r\n,{"name":"delayThermicLevel","type":"string","permission":"w","validity":"INFINITE","enum_values":["STOP"]}\r\n7D\r\n,{"name":"hvacMode","type":"string","permission":"rw","validity":"DATA_POLLING","enum_values":["NORMAL","STOP","ANTI_FROST"]}\r\n80\r\n,{"name":"timeDelay","type":"numeric","permission":"rw","validity":"TIMER_POLLING","min":0,"max":65535,"step":1,"unit":"minute"}\r\n8B\r\n,{"name":"temperature","type":"numeric","permission":"r","validity":"SENSOR_POLLING","min":-99.900,"max":99.900,"step":0.010,"unit":"degC"}\r\n62\r\n,{"name":"tempoOn","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n66\r\n,{"name":"antifrostOn","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n69\r\n,{"name":"loadSheddingOn","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6A\r\n,{"name":"openingDetected","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"presenceDetected","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n62\r\n,{"name":"absence","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"productionDefect","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"batteryCmdDefect","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"tempSensorDefect","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6D\r\n,{"name":"tempSensorShortCut","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6D\r\n,{"name":"tempSensorOpenCirc","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n63\r\n,{"name":"boostOn","type":"boolean","permission":"rw","validity":"STATUS_POLLING","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n7E\r\n,{"name":"anticipCoeff","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":65534,"step":1,"unit":"min/deg"}\r\nC1\r\n]}]},{"id":1599338298,"endpoints":[{"id":1599338298,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n8D\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","OPEN_FRENCH","OPEN_HOPPER"]}\r\nC1\r\n]}]},{"id":1599338370,"endpoints":[{"id":1599338370,"error":2,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n8D\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","OPEN_FRENCH","OPEN_HOPPER"]}\r\nC1\r\n]}]},{"id":1599338418,"endpoints":[{"id":1599338418,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n8D\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","OPEN_FRENCH","OPEN_HOPPER"]}\r\nC1\r\n]}]},{"id":1599387771,"endpoints":[{"id":1599387771,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n8D\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","OPEN_FRENCH","OPEN_HOPPER"]}\r\nC1\r\n]}]},{"id":1599387831,"endpoints":[{"id":1599387831,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n8D\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","OPEN_FRENCH","OPEN_HOPPER"]}\r\nC1\r\n]}]},{"id":1599388036,"endpoints":[{"id":1599388036,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n8D\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","OPEN_FRENCH","OPEN_HOPPER"]}\r\nC1\r\n]}]},{"id":1599388243,"endpoints":[{"id":1599388243,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC1\r\n]}]},{"id":1604473848,"endpoints":[{"id":1604473848,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\n66\r\n,{"name":"calibrationDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\nC1\r\n]}]},{"id":1604476226,"endpoints":[{"id":1604476226,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC1\r\n]}]},{"id":1604476251,"endpoints":[{"id":1604476251,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC2\r\n]}]},{"id":1604476324,"endpoints":[{"id":1604476324,"error":15,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC2\r\n]}]},{"id":1604476347,"endpoints":[{"id":1604476347,"error":15,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC1\r\n]}]},{"id":1604477407,"endpoints":[{"id":1604477407,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC1\r\n]}]},{"id":1611399070,"endpoints":[{"id":1611399070,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC1\r\n]}]},{"id":1611399103,"endpoints":[{"id":1611399103,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nD7\r\n]}]},{"id":1664906374,"endpoints":[{"id":1664906374,"error":0,"metadata":[{"name":"energyIndexHeatWatt","type":"numeric","permission":"r","validity":"METER_SUPERVISION","min":0,"max":4294967294,"step":1,"unit":"Wh"}\r\n8D\r\n,{"name":"energyIndexECSWatt","type":"numeric","permission":"r","validity":"METER_SUPERVISION","min":0,"max":4294967294,"step":1,"unit":"Wh"}\r\n91\r\n,{"name":"outTemperature","type":"numeric","permission":"r","validity":"METER_SUPERVISION","min":-99.900,"max":99.900,"step":0.010,"unit":"degC"}\r\nC1\r\n]}]},{"id":1679399651,"endpoints":[{"id":1679399651,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"techSmokeDefect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\nC1\r\n]}]},{"id":1679399947,"endpoints":[{"id":1679399947,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"techSmokeDefect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\nC1\r\n]}]},{"id":1679401885,"endpoints":[{"id":1679401885,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n77\r\n,{"name":"techSmokeDefect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}]}]}]\r\n\r\n0\r\n\r\n' +2023-09-08 18:57:13.920 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Message received detected as (msg_metadata) +2023-09-08 18:57:13.920 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Incoming data parsed with success +2023-09-08 18:57:13.937 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /devices/cmeta\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nTransac-Id: 0\r\n\r\n34\r\n[{"id":0,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":1,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":2,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":3,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":4,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":5,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":6,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":7,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n115\r\n[]}]},{"id":8,"endpoints":[{"id":0,"error":0,"cmetadata":[{"name":"reset","permission":"w","parameters":[{"name":"station","type":"string","enum_values":["TOTAL_ELEC","TI1","TI2","TI3","DHW","HEAT_ELEC"]},{"name":"measure","type":"string","enum_values":["INTENSITY","POWER"]}]}\r\n38\r\n]}]},{"id":9,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n3A\r\n[]}]},{"id":10,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1599338298,"endpoints":[{"id":1599338298,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1599338370,"endpoints":[{"id":1599338370,"error":2,"cmetadata":\r\n4B\r\n[]}]},{"id":1599338418,"endpoints":[{"id":1599338418,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1599387771,"endpoints":[{"id":1599387771,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1599387831,"endpoints":[{"id":1599387831,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1599388036,"endpoints":[{"id":1599388036,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1599388243,"endpoints":[{"id":1599388243,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1604473848,"endpoints":[{"id":1604473848,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1604476226,"endpoints":[{"id":1604476226,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1604476251,"endpoints":[{"id":1604476251,"error":0,"cmetadata":\r\n4C\r\n[]}]},{"id":1604476324,"endpoints":[{"id":1604476324,"error":15,"cmetadata":\r\n4C\r\n[]}]},{"id":1604476347,"endpoints":[{"id":1604476347,"error":15,"cmetadata":\r\n4B\r\n[]}]},{"id":1604477407,"endpoints":[{"id":1604477407,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1611399070,"endpoints":[{"id":1611399070,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1611399103,"endpoints":[{"id":1611399103,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1664906374,"endpoints":[{"id":1664906374,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1679399651,"endpoints":[{"id":1679399651,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1679399947,"endpoints":[{"id":1679399947,"error":0,"cmetadata":\r\n53\r\n[]}]},{"id":1679401885,"endpoints":[{"id":1679401885,"error":0,"cmetadata":[]}]}]\r\n\r\n0\r\n\r\n' +2023-09-08 18:57:13.938 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Message received detected as (msg_cmetadata) +2023-09-08 18:57:13.938 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] parse_cmeta_data : [{'id': 0, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 1, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 2, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 3, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 4, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 5, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 6, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 7, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 8, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': [{'name': 'reset', 'permission': 'w', 'parameters': [{'name': 'station', 'type': 'string', 'enum_values': ['TOTAL_ELEC', 'TI1', 'TI2', 'TI3', 'DHW', 'HEAT_ELEC']}, {'name': 'measure', 'type': 'string', 'enum_values': ['INTENSITY', 'POWER']}]}]}]}, {'id': 9, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 10, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 1599338298, 'endpoints': [{'id': 1599338298, 'error': 0, 'cmetadata': []}]}, {'id': 1599338370, 'endpoints': [{'id': 1599338370, 'error': 2, 'cmetadata': []}]}, {'id': 1599338418, 'endpoints': [{'id': 1599338418, 'error': 0, 'cmetadata': []}]}, {'id': 1599387771, 'endpoints': [{'id': 1599387771, 'error': 0, 'cmetadata': []}]}, {'id': 1599387831, 'endpoints': [{'id': 1599387831, 'error': 0, 'cmetadata': []}]}, {'id': 1599388036, 'endpoints': [{'id': 1599388036, 'error': 0, 'cmetadata': []}]}, {'id': 1599388243, 'endpoints': [{'id': 1599388243, 'error': 0, 'cmetadata': []}]}, {'id': 1604473848, 'endpoints': [{'id': 1604473848, 'error': 0, 'cmetadata': []}]}, {'id': 1604476226, 'endpoints': [{'id': 1604476226, 'error': 0, 'cmetadata': []}]}, {'id': 1604476251, 'endpoints': [{'id': 1604476251, 'error': 0, 'cmetadata': []}]}, {'id': 1604476324, 'endpoints': [{'id': 1604476324, 'error': 15, 'cmetadata': []}]}, {'id': 1604476347, 'endpoints': [{'id': 1604476347, 'error': 15, 'cmetadata': []}]}, {'id': 1604477407, 'endpoints': [{'id': 1604477407, 'error': 0, 'cmetadata': []}]}, {'id': 1611399070, 'endpoints': [{'id': 1611399070, 'error': 0, 'cmetadata': []}]}, {'id': 1611399103, 'endpoints': [{'id': 1611399103, 'error': 0, 'cmetadata': []}]}, {'id': 1664906374, 'endpoints': [{'id': 1664906374, 'error': 0, 'cmetadata': []}]}, {'id': 1679399651, 'endpoints': [{'id': 1679399651, 'error': 0, 'cmetadata': []}]}, {'id': 1679399947, 'endpoints': [{'id': 1679399947, 'error': 0, 'cmetadata': []}]}, {'id': 1679401885, 'endpoints': [{'id': 1679401885, 'error': 0, 'cmetadata': []}]}] +2023-09-08 18:57:13.939 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Metadata configuration updated +2023-09-08 18:57:14.081 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /devices/data\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nTransac-Id: 0\r\n\r\n2F\r\n[{"id":0,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":1,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":2,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":3,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":4,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":5,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":6,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":7,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n67\r\n]}]},{"id":8,"endpoints":[{"id":0,"error":0,"data":[{"name":"jobsMP","validity":"upToDate","value":144}\r\n40\r\n,{"name":"softVersion","validity":"upToDate","value":"01.02.02"}\r\n40\r\n,{"name":"softPlan","validity":"upToDate","value":"23.63.00.14"}\r\n45\r\n,{"name":"energyTotIndexWatt","validity":"upToDate","value":17047619}\r\n44\r\n,{"name":"energyInstantTotElec","validity":"upToDate","value":2.000}\r\n48\r\n,{"name":"energyInstantTotElec_Min","validity":"upToDate","value":0.000}\r\n49\r\n,{"name":"energyInstantTotElec_Max","validity":"upToDate","value":46.000}\r\n46\r\n,{"name":"energyScaleTotElec_Min","validity":"upToDate","value":0.000}\r\n47\r\n,{"name":"energyScaleTotElec_Max","validity":"upToDate","value":45.000}\r\n43\r\n,{"name":"energyInstantTotElecP","validity":"upToDate","value":482}\r\n46\r\n,{"name":"energyInstantTotElec_P_Min","validity":"upToDate","value":0}\r\n4A\r\n,{"name":"energyInstantTotElec_P_Max","validity":"upToDate","value":10212}\r\n44\r\n,{"name":"energyScaleTotElec_P_Min","validity":"upToDate","value":0}\r\n47\r\n,{"name":"energyScaleTotElec_P_Max","validity":"upToDate","value":9000}\r\n3E\r\n,{"name":"energyIndexTi1","validity":"upToDate","value":76871}\r\n41\r\n,{"name":"energyInstantTi1I","validity":"upToDate","value":0.060}\r\n45\r\n,{"name":"energyInstantTi1I_Min","validity":"upToDate","value":0.000}\r\n45\r\n,{"name":"energyInstantTi1I_Max","validity":"upToDate","value":6.670}\r\n43\r\n,{"name":"energyScaleTi1I_Min","validity":"upToDate","value":0.000}\r\n44\r\n,{"name":"energyScaleTi1I_Max","validity":"upToDate","value":13.340}\r\n3C\r\n,{"name":"energyInstantTi1P","validity":"expired","value":0}\r\n40\r\n,{"name":"energyInstantTi1P_Min","validity":"expired","value":0}\r\n43\r\n,{"name":"energyInstantTi1P_Max","validity":"expired","value":1639}\r\n3E\r\n,{"name":"energyScaleTi1P_Min","validity":"expired","value":0}\r\n41\r\n,{"name":"energyScaleTi1P_Max","validity":"expired","value":3418}\r\n71\r\n]}]},{"id":9,"endpoints":[{"id":0,"error":0,"data":[{"name":"authorization","validity":"upToDate","value":"STOP"}\r\n37\r\n,{"name":"setpoint","validity":"upToDate","value":null}\r\n3D\r\n,{"name":"thermicLevel","validity":"upToDate","value":"STOP"}\r\n3B\r\n,{"name":"hvacMode","validity":"upToDate","value":"NORMAL"}\r\n35\r\n,{"name":"timeDelay","validity":"upToDate","value":0}\r\n3B\r\n,{"name":"temperature","validity":"expired","value":25.690}\r\n37\r\n,{"name":"tempoOn","validity":"upToDate","value":false}\r\n3B\r\n,{"name":"antifrostOn","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"loadSheddingOn","validity":"upToDate","value":false}\r\n3F\r\n,{"name":"openingDetected","validity":"upToDate","value":false}\r\n40\r\n,{"name":"presenceDetected","validity":"upToDate","value":false}\r\n37\r\n,{"name":"absence","validity":"upToDate","value":false}\r\n40\r\n,{"name":"productionDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"batteryCmdDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"tempSensorDefect","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorShortCut","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorOpenCirc","validity":"upToDate","value":false}\r\n37\r\n,{"name":"boostOn","validity":"upToDate","value":false}\r\n39\r\n,{"name":"anticipCoeff","validity":"upToDate","value":30}\r\n72\r\n]}]},{"id":10,"endpoints":[{"id":0,"error":0,"data":[{"name":"authorization","validity":"upToDate","value":"STOP"}\r\n37\r\n,{"name":"setpoint","validity":"upToDate","value":null}\r\n3D\r\n,{"name":"thermicLevel","validity":"upToDate","value":"STOP"}\r\n3B\r\n,{"name":"hvacMode","validity":"upToDate","value":"NORMAL"}\r\n35\r\n,{"name":"timeDelay","validity":"upToDate","value":0}\r\n3C\r\n,{"name":"temperature","validity":"upToDate","value":24.300}\r\n37\r\n,{"name":"tempoOn","validity":"upToDate","value":false}\r\n3B\r\n,{"name":"antifrostOn","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"loadSheddingOn","validity":"upToDate","value":false}\r\n3F\r\n,{"name":"openingDetected","validity":"upToDate","value":false}\r\n40\r\n,{"name":"presenceDetected","validity":"upToDate","value":false}\r\n37\r\n,{"name":"absence","validity":"upToDate","value":false}\r\n40\r\n,{"name":"productionDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"batteryCmdDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"tempSensorDefect","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorShortCut","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorOpenCirc","validity":"upToDate","value":false}\r\n37\r\n,{"name":"boostOn","validity":"upToDate","value":false}\r\n39\r\n,{"name":"anticipCoeff","validity":"upToDate","value":30}\r\n7F\r\n]}]},{"id":1599338298,"endpoints":[{"id":1599338298,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1599338370,"endpoints":[{"id":1599338370,"error":2,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3E\r\n,{"name":"intrusionDetect","validity":"expired","value":false}\r\n3B\r\n,{"name":"openState","validity":"expired","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1599338418,"endpoints":[{"id":1599338418,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1599387771,"endpoints":[{"id":1599387771,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3E\r\n,{"name":"intrusionDetect","validity":"upToDate","value":true}\r\n41\r\n,{"name":"openState","validity":"upToDate","value":"OPEN_FRENCH"}\r\n7F\r\n]}]},{"id":1599387831,"endpoints":[{"id":1599387831,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1599388036,"endpoints":[{"id":1599388036,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1599388243,"endpoints":[{"id":1599388243,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1604473848,"endpoints":[{"id":1604473848,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3E\r\n,{"name":"intrusionDetect","validity":"upToDate","value":true}\r\n3E\r\n,{"name":"openState","validity":"upToDate","value":"UNLOCKED"}\r\n41\r\n,{"name":"calibrationDefect","validity":"upToDate","value":false}\r\n7F\r\n]}]},{"id":1604476226,"endpoints":[{"id":1604476226,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1604476251,"endpoints":[{"id":1604476251,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n80\r\n]}]},{"id":1604476324,"endpoints":[{"id":1604476324,"error":15,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n39\r\n,{"name":"battDefect","validity":"expired","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3E\r\n,{"name":"intrusionDetect","validity":"expired","value":false}\r\n37\r\n,{"name":"openState","validity":"expired","value":null}\r\n80\r\n]}]},{"id":1604476347,"endpoints":[{"id":1604476347,"error":15,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n39\r\n,{"name":"battDefect","validity":"expired","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3E\r\n,{"name":"intrusionDetect","validity":"expired","value":false}\r\n37\r\n,{"name":"openState","validity":"expired","value":null}\r\n7F\r\n]}]},{"id":1604477407,"endpoints":[{"id":1604477407,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1611399070,"endpoints":[{"id":1611399070,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3E\r\n,{"name":"intrusionDetect","validity":"upToDate","value":true}\r\n3E\r\n,{"name":"openState","validity":"upToDate","value":"UNLOCKED"}\r\n7F\r\n]}]},{"id":1611399103,"endpoints":[{"id":1611399103,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n8A\r\n]}]},{"id":1664906374,"endpoints":[{"id":1664906374,"error":0,"data":[{"name":"energyIndexHeatWatt","validity":"upToDate","value":3210000}\r\n44\r\n,{"name":"energyIndexECSWatt","validity":"upToDate","value":2249000}\r\n3F\r\n,{"name":"outTemperature","validity":"upToDate","value":22.300}\r\n7F\r\n]}]},{"id":1679399651,"endpoints":[{"id":1679399651,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"techSmokeDefect","validity":"upToDate","value":false}\r\n7F\r\n]}]},{"id":1679399947,"endpoints":[{"id":1679399947,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"techSmokeDefect","validity":"upToDate","value":false}\r\n7F\r\n]}]},{"id":1679401885,"endpoints":[{"id":1679401885,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n46\r\n,{"name":"techSmokeDefect","validity":"upToDate","value":false}]}]}]\r\n\r\n0\r\n\r\n' +2023-09-08 18:57:14.084 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Message received detected as (msg_data) +2023-09-08 18:57:14.085 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] parse_devices_data : [{'id': 0, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 1, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 2, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 3, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 4, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 5, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 6, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 7, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 8, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'jobsMP', 'validity': 'upToDate', 'value': 144}, {'name': 'softVersion', 'validity': 'upToDate', 'value': '01.02.02'}, {'name': 'softPlan', 'validity': 'upToDate', 'value': '23.63.00.14'}, {'name': 'energyTotIndexWatt', 'validity': 'upToDate', 'value': 17047619}, {'name': 'energyInstantTotElec', 'validity': 'upToDate', 'value': 2.0}, {'name': 'energyInstantTotElec_Min', 'validity': 'upToDate', 'value': 0.0}, {'name': 'energyInstantTotElec_Max', 'validity': 'upToDate', 'value': 46.0}, {'name': 'energyScaleTotElec_Min', 'validity': 'upToDate', 'value': 0.0}, {'name': 'energyScaleTotElec_Max', 'validity': 'upToDate', 'value': 45.0}, {'name': 'energyInstantTotElecP', 'validity': 'upToDate', 'value': 482}, {'name': 'energyInstantTotElec_P_Min', 'validity': 'upToDate', 'value': 0}, {'name': 'energyInstantTotElec_P_Max', 'validity': 'upToDate', 'value': 10212}, {'name': 'energyScaleTotElec_P_Min', 'validity': 'upToDate', 'value': 0}, {'name': 'energyScaleTotElec_P_Max', 'validity': 'upToDate', 'value': 9000}, {'name': 'energyIndexTi1', 'validity': 'upToDate', 'value': 76871}, {'name': 'energyInstantTi1I', 'validity': 'upToDate', 'value': 0.06}, {'name': 'energyInstantTi1I_Min', 'validity': 'upToDate', 'value': 0.0}, {'name': 'energyInstantTi1I_Max', 'validity': 'upToDate', 'value': 6.67}, {'name': 'energyScaleTi1I_Min', 'validity': 'upToDate', 'value': 0.0}, {'name': 'energyScaleTi1I_Max', 'validity': 'upToDate', 'value': 13.34}, {'name': 'energyInstantTi1P', 'validity': 'expired', 'value': 0}, {'name': 'energyInstantTi1P_Min', 'validity': 'expired', 'value': 0}, {'name': 'energyInstantTi1P_Max', 'validity': 'expired', 'value': 1639}, {'name': 'energyScaleTi1P_Min', 'validity': 'expired', 'value': 0}, {'name': 'energyScaleTi1P_Max', 'validity': 'expired', 'value': 3418}]}]}, {'id': 9, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'authorization', 'validity': 'upToDate', 'value': 'STOP'}, {'name': 'setpoint', 'validity': 'upToDate', 'value': None}, {'name': 'thermicLevel', 'validity': 'upToDate', 'value': 'STOP'}, {'name': 'hvacMode', 'validity': 'upToDate', 'value': 'NORMAL'}, {'name': 'timeDelay', 'validity': 'upToDate', 'value': 0}, {'name': 'temperature', 'validity': 'expired', 'value': 25.69}, {'name': 'tempoOn', 'validity': 'upToDate', 'value': False}, {'name': 'antifrostOn', 'validity': 'upToDate', 'value': False}, {'name': 'loadSheddingOn', 'validity': 'upToDate', 'value': False}, {'name': 'openingDetected', 'validity': 'upToDate', 'value': False}, {'name': 'presenceDetected', 'validity': 'upToDate', 'value': False}, {'name': 'absence', 'validity': 'upToDate', 'value': False}, {'name': 'productionDefect', 'validity': 'upToDate', 'value': False}, {'name': 'batteryCmdDefect', 'validity': 'upToDate', 'value': False}, {'name': 'tempSensorDefect', 'validity': 'upToDate', 'value': False}, {'name': 'tempSensorShortCut', 'validity': 'upToDate', 'value': False}, {'name': 'tempSensorOpenCirc', 'validity': 'upToDate', 'value': False}, {'name': 'boostOn', 'validity': 'upToDate', 'value': False}, {'name': 'anticipCoeff', 'validity': 'upToDate', 'value': 30}]}]}, {'id': 10, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'authorization', 'validity': 'upToDate', 'value': 'STOP'}, {'name': 'setpoint', 'validity': 'upToDate', 'value': None}, {'name': 'thermicLevel', 'validity': 'upToDate', 'value': 'STOP'}, {'name': 'hvacMode', 'validity': 'upToDate', 'value': 'NORMAL'}, {'name': 'timeDelay', 'validity': 'upToDate', 'value': 0}, {'name': 'temperature', 'validity': 'upToDate', 'value': 24.3}, {'name': 'tempoOn', 'validity': 'upToDate', 'value': False}, {'name': 'antifrostOn', 'validity': 'upToDate', 'value': False}, {'name': 'loadSheddingOn', 'validity': 'upToDate', 'value': False}, {'name': 'openingDetected', 'validity': 'upToDate', 'value': False}, {'name': 'presenceDetected', 'validity': 'upToDate', 'value': False}, {'name': 'absence', 'validity': 'upToDate', 'value': False}, {'name': 'productionDefect', 'validity': 'upToDate', 'value': False}, {'name': 'batteryCmdDefect', 'validity': 'upToDate', 'value': False}, {'name': 'tempSensorDefect', 'validity': 'upToDate', 'value': False}, {'name': 'tempSensorShortCut', 'validity': 'upToDate', 'value': False}, {'name': 'tempSensorOpenCirc', 'validity': 'upToDate', 'value': False}, {'name': 'boostOn', 'validity': 'upToDate', 'value': False}, {'name': 'anticipCoeff', 'validity': 'upToDate', 'value': 30}]}]}, {'id': 1599338298, 'endpoints': [{'id': 1599338298, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1599338370, 'endpoints': [{'id': 1599338370, 'error': 2, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'expired', 'value': False}, {'name': 'openState', 'validity': 'expired', 'value': 'LOCKED'}]}]}, {'id': 1599338418, 'endpoints': [{'id': 1599338418, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1599387771, 'endpoints': [{'id': 1599387771, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': True}, {'name': 'openState', 'validity': 'upToDate', 'value': 'OPEN_FRENCH'}]}]}, {'id': 1599387831, 'endpoints': [{'id': 1599387831, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1599388036, 'endpoints': [{'id': 1599388036, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1599388243, 'endpoints': [{'id': 1599388243, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1604473848, 'endpoints': [{'id': 1604473848, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': True}, {'name': 'openState', 'validity': 'upToDate', 'value': 'UNLOCKED'}, {'name': 'calibrationDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 1604476226, 'endpoints': [{'id': 1604476226, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1604476251, 'endpoints': [{'id': 1604476251, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1604476324, 'endpoints': [{'id': 1604476324, 'error': 15, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'expired', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'expired', 'value': False}, {'name': 'openState', 'validity': 'expired', 'value': None}]}]}, {'id': 1604476347, 'endpoints': [{'id': 1604476347, 'error': 15, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'expired', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'expired', 'value': False}, {'name': 'openState', 'validity': 'expired', 'value': None}]}]}, {'id': 1604477407, 'endpoints': [{'id': 1604477407, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1611399070, 'endpoints': [{'id': 1611399070, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': True}, {'name': 'openState', 'validity': 'upToDate', 'value': 'UNLOCKED'}]}]}, {'id': 1611399103, 'endpoints': [{'id': 1611399103, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1664906374, 'endpoints': [{'id': 1664906374, 'error': 0, 'data': [{'name': 'energyIndexHeatWatt', 'validity': 'upToDate', 'value': 3210000}, {'name': 'energyIndexECSWatt', 'validity': 'upToDate', 'value': 2249000}, {'name': 'outTemperature', 'validity': 'upToDate', 'value': 22.3}]}]}, {'id': 1679399651, 'endpoints': [{'id': 1679399651, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'techSmokeDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 1679399947, 'endpoints': [{'id': 1679399947, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'techSmokeDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 1679401885, 'endpoints': [{'id': 1679401885, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'techSmokeDefect', 'validity': 'upToDate', 'value': False}]}]}] From 37d9ec96b683956eb3b657053e4aaca8884be77b Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:24:01 +0100 Subject: [PATCH 62/74] =?UTF-8?q?=C2=A0fix=20ruff=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/cover.py | 2 +- custom_components/deltadore-tydom/ha_entities.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/deltadore-tydom/cover.py b/custom_components/deltadore-tydom/cover.py index 1ec3352..149b8ba 100644 --- a/custom_components/deltadore-tydom/cover.py +++ b/custom_components/deltadore-tydom/cover.py @@ -15,4 +15,4 @@ async def async_setup_entry( ) -> None: """Add cover for passed config_entry in HA.""" hub = hass.data[DOMAIN][config_entry.entry_id] - hub.add_cover_callback = async_add_entities \ No newline at end of file + hub.add_cover_callback = async_add_entities diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index d971aac..48f6aa4 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -57,7 +57,7 @@ from .const import DOMAIN, LOGGER class HAEntity: - + """Generic abstract HA entity.""" sensor_classes = {} state_classes = {} units = {} From f55bc22260e6fa99713744afe4e17db2a39fc9f0 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:28:29 +0100 Subject: [PATCH 63/74] =?UTF-8?q?=C2=A0fix=20ruff=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/ha_entities.py | 1 + custom_components/deltadore-tydom/tydom/MessageHandler.py | 1 + custom_components/deltadore-tydom/tydom/const.py | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/deltadore-tydom/ha_entities.py b/custom_components/deltadore-tydom/ha_entities.py index 48f6aa4..908957b 100644 --- a/custom_components/deltadore-tydom/ha_entities.py +++ b/custom_components/deltadore-tydom/ha_entities.py @@ -58,6 +58,7 @@ class HAEntity: """Generic abstract HA entity.""" + sensor_classes = {} state_classes = {} units = {} diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index 1a56d58..e55d201 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -34,6 +34,7 @@ class MessageHandler: """Handle incomming Tydom messages.""" def __init__(self, tydom_client, cmd_prefix): + """Initialize MessageHandler.""" self.tydom_client = tydom_client self.cmd_prefix = cmd_prefix diff --git a/custom_components/deltadore-tydom/tydom/const.py b/custom_components/deltadore-tydom/tydom/const.py index 5646014..e93fb67 100644 --- a/custom_components/deltadore-tydom/tydom/const.py +++ b/custom_components/deltadore-tydom/tydom/const.py @@ -1,6 +1,7 @@ +"""Instegration constants.""" MEDIATION_URL = "mediation.tydom.com" DELTADORE_AUTH_URL = "https://deltadoreadb2ciot.b2clogin.com/deltadoreadb2ciot.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_AccountProviderROPC_SignIn" DELTADORE_AUTH_GRANT_TYPE = "password" DELTADORE_AUTH_CLIENTID = "8782839f-3264-472a-ab87-4d4e23524da4" DELTADORE_AUTH_SCOPE = "openid profile offline_access https://deltadoreadb2ciot.onmicrosoft.com/iotapi/video_config https://deltadoreadb2ciot.onmicrosoft.com/iotapi/video_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_gateway_credentials https://deltadoreadb2ciot.onmicrosoft.com/iotapi/sites_management_camera_credentials https://deltadoreadb2ciot.onmicrosoft.com/iotapi/comptage_europe_collect_reader https://deltadoreadb2ciot.onmicrosoft.com/iotapi/comptage_europe_site_config_contributor https://deltadoreadb2ciot.onmicrosoft.com/iotapi/pilotage_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/consent_mgt_contributor https://deltadoreadb2ciot.onmicrosoft.com/iotapi/b2caccountprovider_manage_account https://deltadoreadb2ciot.onmicrosoft.com/iotapi/b2caccountprovider_allow_view_account https://deltadoreadb2ciot.onmicrosoft.com/iotapi/tydom_backend_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/websocket_remote_access https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_device https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_view https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_space https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_connector https://deltadoreadb2ciot.onmicrosoft.com/iotapi/orkestrator_endpoint https://deltadoreadb2ciot.onmicrosoft.com/iotapi/rule_management_allowed https://deltadoreadb2ciot.onmicrosoft.com/iotapi/collect_read_datas" -DELTADORE_API_SITES = "https://prod.iotdeltadore.com/sitesmanagement/api/v1/sites?gateway_mac=" \ No newline at end of file +DELTADORE_API_SITES = "https://prod.iotdeltadore.com/sitesmanagement/api/v1/sites?gateway_mac=" From 210a25938b83e925a8fe6edb2b38acddd3ebdca7 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:32:47 +0100 Subject: [PATCH 64/74] =?UTF-8?q?=C2=A0fix=20ruff=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deltadore-tydom/manifest.json | 2 +- .../deltadore-tydom/update Tydom.json | 23 ------------------- 2 files changed, 1 insertion(+), 24 deletions(-) delete mode 100644 custom_components/deltadore-tydom/update Tydom.json diff --git a/custom_components/deltadore-tydom/manifest.json b/custom_components/deltadore-tydom/manifest.json index 8ef2cd8..86cb07c 100644 --- a/custom_components/deltadore-tydom/manifest.json +++ b/custom_components/deltadore-tydom/manifest.json @@ -5,7 +5,7 @@ "@CyrilP" ], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/tydom", + "documentation": "https://github.com/CyrilP/hass-deltadore-tydom-component/blob/main/README.md", "iot_class": "local_push", "requirements": [ "websockets>=9.1" diff --git a/custom_components/deltadore-tydom/update Tydom.json b/custom_components/deltadore-tydom/update Tydom.json deleted file mode 100644 index 11e74b4..0000000 --- a/custom_components/deltadore-tydom/update Tydom.json +++ /dev/null @@ -1,23 +0,0 @@ - update Tydom -2023-09-08 18:57:13.404 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /configs/gateway/api_mode\r\nContent-Type: application/json\r\nContent-Length: 0\r\nTransac-Id: 0\r\n\r\n' -2023-09-08 18:57:13.488 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /groups/file\r\nContent-Type: application/json\r\nContent-Length: 1736\r\nTransac-Id: 0\r\nFile-Version: 57\r\n\r\n{"groups":[{"devices":[],"areas":[],"id":859468405},{"devices":[{"endpoints":[{"id":0}],"id":0},{"endpoints":[{"id":0}],"id":1},{"endpoints":[{"id":0}],"id":2},{"endpoints":[{"id":0}],"id":3},{"endpoints":[{"id":0}],"id":4},{"endpoints":[{"id":0}],"id":5},{"endpoints":[{"id":0}],"id":6},{"endpoints":[{"id":0}],"id":7}],"areas":[],"id":1725345078},{"devices":[],"areas":[],"id":562847101},{"devices":[{"endpoints":[{"id":0}],"id":0},{"endpoints":[{"id":0}],"id":1},{"endpoints":[{"id":0}],"id":3},{"endpoints":[{"id":0}],"id":4},{"endpoints":[{"id":0}],"id":6},{"endpoints":[{"id":0}],"id":7}],"areas":[],"id":1259860423},{"devices":[{"endpoints":[{"id":0}],"id":2},{"endpoints":[{"id":0}],"id":5}],"areas":[],"id":732974460},{"devices":[],"areas":[],"id":1117398060},{"devices":[{"endpoints":[{"id":1599338298}],"id":1599338298}],"areas":[],"id":2032409200},{"devices":[{"endpoints":[{"id":1599338370}],"id":1599338370}],"areas":[],"id":317891801},{"devices":[{"endpoints":[{"id":1599338418}],"id":1599338418}],"areas":[],"id":1455243199},{"devices":[{"endpoints":[{"id":1599387771}],"id":1599387771}],"areas":[],"id":1840849049},{"devices":[{"endpoints":[{"id":1604476226}],"id":1604476226},{"endpoints":[{"id":1604476251}],"id":1604476251}],"areas":[],"id":1704806706},{"devices":[{"endpoints":[{"id":1599387831}],"id":1599387831}],"areas":[],"id":724393265},{"devices":[{"endpoints":[{"id":1604476324}],"id":1604476324},{"endpoints":[{"id":1604476347}],"id":1604476347}],"areas":[],"id":2138691766},{"devices":[{"endpoints":[{"id":1604477407}],"id":1604477407}],"areas":[],"id":851256178},{"devices":[{"endpoints":[{"id":1611399103}],"id":1611399103},{"endpoints":[{"id":1611399070}],"id":1611399070}],"areas":[],"id":1213177750}]}' -2023-09-08 18:57:13.489 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Message received detected as (msg_groups) -2023-09-08 18:57:13.489 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Incoming data parsed with success -2023-09-08 18:57:13.489 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /refresh/all\r\nContent-Type: application/json\r\nContent-Length: 0\r\nTransac-Id: 0\r\n\r\n' -2023-09-08 18:57:13.599 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /configs/file\r\nContent-Type: application/json\r\nContent-Length: 8161\r\nTransac-Id: 0\r\nFile-Version: 562\r\n\r\n{"date":1693507332,"version_application":"4.9.2-3-dd","endpoints":[{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":0,"name":"Baie Salon","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":1,"name":"Bureau","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":2,"name":"Chambre 2","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":3,"name":"Baie S\xc3\xa9jour","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":4,"name":"Fixe Salon","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":5,"name":"Chambre 1","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":6,"name":"Chambre Rdc","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"shutter","skill":"TYDOM_X3D","id_device":7,"name":"Cuisine","anticipation_start":false,"space_id":"","picto":"picto_shutter","last_usage":"shutter"},{"id_endpoint":0,"first_usage":"conso","skill":"TYDOM_X3D","id_device":8,"name":"\xc3\x89quipement conso 1","anticipation_start":false,"space_id":"","picto":"picto_conso","last_usage":"conso"},{"id_endpoint":0,"first_usage":"hvac","skill":"TYDOM_X3D","id_device":9,"name":"Radiateur","anticipation_start":false,"space_id":"","picto":"picto_thermometer","last_usage":"boiler"},{"id_endpoint":0,"first_usage":"hvac","skill":"TYDOM_X3D","id_device":10,"name":"Plancher Chauffant","anticipation_start":false,"space_id":"","picto":"picto_thermometer","last_usage":"boiler"},{"id_endpoint":1599338298,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599338298,"name":"Bureau","anticipation_start":false,"space_id":"","picto":"picto_window","last_usage":"windowFrench"},{"id_endpoint":1599338370,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599338370,"name":"Chambre","anticipation_start":false,"space_id":"","picto":"picto_window","last_usage":"windowFrench"},{"id_endpoint":1599338418,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599338418,"name":"Salle de bain","anticipation_start":false,"space_id":"","picto":"picto_window","last_usage":"windowFrench"},{"id_endpoint":1599387771,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599387771,"name":"Mezzanine","anticipation_start":false,"space_id":"","picto":"picto_window","last_usage":"windowFrench"},{"id_endpoint":1599387831,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599387831,"name":"Salle de bain \xc3\xa9tage","anticipation_start":false,"space_id":"","picto":"picto_window","last_usage":"windowFrench"},{"id_endpoint":1599388036,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599388036,"name":"Battant droit","anticipation_start":false,"space_id":"","picto":"picto_window","last_usage":"windowFrench"},{"id_endpoint":1599388243,"first_usage":"window","skill":"TYDOM_X3D","id_device":1599388243,"name":"Battant gauche","anticipation_start":false,"space_id":"","picto":"picto_window_lock","last_usage":"window"},{"id_endpoint":1604473848,"first_usage":"belmDoor","skill":"TYDOM_X3D","id_device":1604473848,"name":"Porte entr\xc3\xa9e","anticipation_start":false,"space_id":"","picto":"picto_belmdoor","last_usage":"belmDoor"},{"id_endpoint":1604476226,"first_usage":"window","skill":"TYDOM_X3D","id_device":1604476226,"name":"Cuisine droit","anticipation_start":false,"space_id":"","picto":"default_device","last_usage":"window"},{"id_endpoint":1604476251,"first_usage":"window","skill":"TYDOM_X3D","id_device":1604476251,"name":"Cuisine gauche","anticipation_start":false,"space_id":"","picto":"default_device","last_usage":"window"},{"id_endpoint":1604476324,"first_usage":"window","skill":"TYDOM_X3D","id_device":1604476324,"name":"S\xc3\xa9jour droit","anticipation_start":false,"space_id":"","picto":"default_device","last_usage":"window"},{"id_endpoint":1604476347,"first_usage":"window","skill":"TYDOM_X3D","id_device":1604476347,"name":"S\xc3\xa9jour gauche","anticipation_start":false,"space_id":"","picto":"default_device","last_usage":"window"},{"id_endpoint":1604477407,"first_usage":"window","skill":"TYDOM_X3D","id_device":1604477407,"name":"Salon gauche","anticipation_start":false,"space_id":"","picto":"picto_window_lock","last_usage":"window"},{"id_endpoint":1611399070,"first_usage":"window","skill":"TYDOM_X3D","id_device":1611399070,"name":"Chambre L\xc3\xa9a gauche","anticipation_start":false,"space_id":"","picto":"picto_window_lock","last_usage":"window"},{"id_endpoint":1611399103,"first_usage":"window","skill":"TYDOM_X3D","id_device":1611399103,"name":"Chambre L\xc3\xa9a droit","anticipation_start":false,"space_id":"","picto":"picto_window_lock","last_usage":"window"},{"id_endpoint":1664906374,"first_usage":"conso","skill":"TYDOM_X3D","id_device":1664906374,"name":"PAC","anticipation_start":false,"space_id":"","picto":"picto_conso","last_usage":"conso","widget_behavior":{"tutorial_id":"6_RecepteurRF_serie6000_4"}},{"id_endpoint":1679399651,"first_usage":"sensor","skill":"TYDOM_X3D","id_device":1679399651,"name":"D\xc3\xa9tecteur Salon","anticipation_start":false,"picto":"picto_sensor_dfr","last_usage":"sensorDFR","widget_behavior":{"tutorial_id":"sensor_dfr"}},{"id_endpoint":1679399947,"first_usage":"sensor","skill":"TYDOM_X3D","id_device":1679399947,"name":"D\xc3\xa9tecteur \xc3\x89tage","anticipation_start":false,"picto":"picto_sensor_dfr","last_usage":"sensorDFR","widget_behavior":{"tutorial_id":"sensor_dfr"}},{"id_endpoint":1679401885,"first_usage":"sensor","skill":"TYDOM_X3D","id_device":1679401885,"name":"D\xc3\xa9tecteur Cellier","anticipation_start":false,"picto":"picto_sensor_dfr","last_usage":"sensorDFR","widget_behavior":{"tutorial_id":"sensor_dfr"}}],"old_tycam":false,"os":"android","groups":[{"group_all":true,"usage":"light","name":"TOTAL","id":859468405,"picto":"picto_lamp"},{"group_all":true,"usage":"shutter","name":"TOTAL","id":1725345078,"picto":"picto_shutter"},{"group_all":true,"usage":"awning","name":"TOTAL","id":562847101,"picto":"picto_awning_awning"},{"group_all":false,"usage":"shutter","name":"Rez de chauss\xc3\xa9e","id":1259860423,"picto":"picto_shutter"},{"group_all":false,"usage":"shutter","name":"\xc3\x89tage","id":732974460,"picto":"picto_shutter"},{"group_all":true,"usage":"plug","name":"Total","id":1117398060,"picto":"default_device"},{"group_all":false,"usage":"window","name":"Bureau","id":2032409200,"picto":"picto_window"},{"group_all":false,"usage":"window","name":"Chambre","id":317891801,"picto":"picto_window"},{"group_all":false,"usage":"window","name":"Salle de bain","id":1455243199,"picto":"picto_window"},{"group_all":false,"usage":"window","name":"Mezzanine","id":1840849049,"picto":"picto_window"},{"group_all":false,"usage":"window","name":"Cuisine","id":1704806706,"picto":"picto_window_lock"},{"group_all":false,"usage":"window","name":"Salle de bain \xc3\xa9tage ","id":724393265,"picto":"picto_window"},{"group_all":false,"usage":"window","name":"Baie s\xc3\xa9jour","id":2138691766,"picto":"picto_window_lock"},{"group_all":false,"usage":"window","name":"Baie salon ","id":851256178,"picto":"picto_window_lock"},{"group_all":false,"usage":"window","name":"Chambre L\xc3\xa9a","id":1213177750,"picto":"picto_window_lock"}],"areas":[],"scenarios":[{"rule_id":"","name":"Tout ouvrir ","id":1855347001,"type":"NORMAL","picto":"picto_scenario_leaving"}],"version":"1.0.2","new_tycam":false,"moments":[{"rule_id":"","color":5132710,"name":"Nuit","id":1131613363},{"rule_id":"","color":9813268,"name":"fermer volets","id":2055498241},{"rule_id":"","color":9813268,"name":"Ouvrir volets ","id":1227929543}],"id_catalog":"F2BD90F93B888DA02C54980F11AE4796DFCC98F447CD3FE326F5A3A964C939BF","zigbee_networks":[]}' -2023-09-08 18:57:13.600 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Message received detected as (msg_config) -2023-09-08 18:57:13.600 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] parse_config_data : {'date': 1693507332, 'version_application': '4.9.2-3-dd', 'endpoints': [{'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 0, 'name': 'Baie Salon', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 1, 'name': 'Bureau', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 2, 'name': 'Chambre 2', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 3, 'name': 'Baie Séjour', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 4, 'name': 'Fixe Salon', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 5, 'name': 'Chambre 1', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 6, 'name': 'Chambre Rdc', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'shutter', 'skill': 'TYDOM_X3D', 'id_device': 7, 'name': 'Cuisine', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_shutter', 'last_usage': 'shutter'}, {'id_endpoint': 0, 'first_usage': 'conso', 'skill': 'TYDOM_X3D', 'id_device': 8, 'name': 'Équipement conso 1', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_conso', 'last_usage': 'conso'}, {'id_endpoint': 0, 'first_usage': 'hvac', 'skill': 'TYDOM_X3D', 'id_device': 9, 'name': 'Radiateur', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_thermometer', 'last_usage': 'boiler'}, {'id_endpoint': 0, 'first_usage': 'hvac', 'skill': 'TYDOM_X3D', 'id_device': 10, 'name': 'Plancher Chauffant', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_thermometer', 'last_usage': 'boiler'}, {'id_endpoint': 1599338298, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599338298, 'name': 'Bureau', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window', 'last_usage': 'windowFrench'}, {'id_endpoint': 1599338370, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599338370, 'name': 'Chambre', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window', 'last_usage': 'windowFrench'}, {'id_endpoint': 1599338418, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599338418, 'name': 'Salle de bain', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window', 'last_usage': 'windowFrench'}, {'id_endpoint': 1599387771, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599387771, 'name': 'Mezzanine', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window', 'last_usage': 'windowFrench'}, {'id_endpoint': 1599387831, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599387831, 'name': 'Salle de bain étage', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window', 'last_usage': 'windowFrench'}, {'id_endpoint': 1599388036, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599388036, 'name': 'Battant droit', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window', 'last_usage': 'windowFrench'}, {'id_endpoint': 1599388243, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1599388243, 'name': 'Battant gauche', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window_lock', 'last_usage': 'window'}, {'id_endpoint': 1604473848, 'first_usage': 'belmDoor', 'skill': 'TYDOM_X3D', 'id_device': 1604473848, 'name': 'Porte entrée', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_belmdoor', 'last_usage': 'belmDoor'}, {'id_endpoint': 1604476226, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1604476226, 'name': 'Cuisine droit', 'anticipation_start': False, 'space_id': '', 'picto': 'default_device', 'last_usage': 'window'}, {'id_endpoint': 1604476251, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1604476251, 'name': 'Cuisine gauche', 'anticipation_start': False, 'space_id': '', 'picto': 'default_device', 'last_usage': 'window'}, {'id_endpoint': 1604476324, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1604476324, 'name': 'Séjour droit', 'anticipation_start': False, 'space_id': '', 'picto': 'default_device', 'last_usage': 'window'}, {'id_endpoint': 1604476347, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1604476347, 'name': 'Séjour gauche', 'anticipation_start': False, 'space_id': '', 'picto': 'default_device', 'last_usage': 'window'}, {'id_endpoint': 1604477407, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1604477407, 'name': 'Salon gauche', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window_lock', 'last_usage': 'window'}, {'id_endpoint': 1611399070, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1611399070, 'name': 'Chambre Léa gauche', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window_lock', 'last_usage': 'window'}, {'id_endpoint': 1611399103, 'first_usage': 'window', 'skill': 'TYDOM_X3D', 'id_device': 1611399103, 'name': 'Chambre Léa droit', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_window_lock', 'last_usage': 'window'}, {'id_endpoint': 1664906374, 'first_usage': 'conso', 'skill': 'TYDOM_X3D', 'id_device': 1664906374, 'name': 'PAC', 'anticipation_start': False, 'space_id': '', 'picto': 'picto_conso', 'last_usage': 'conso', 'widget_behavior': {'tutorial_id': '6_RecepteurRF_serie6000_4'}}, {'id_endpoint': 1679399651, 'first_usage': 'sensor', 'skill': 'TYDOM_X3D', 'id_device': 1679399651, 'name': 'Détecteur Salon', 'anticipation_start': False, 'picto': 'picto_sensor_dfr', 'last_usage': 'sensorDFR', 'widget_behavior': {'tutorial_id': 'sensor_dfr'}}, {'id_endpoint': 1679399947, 'first_usage': 'sensor', 'skill': 'TYDOM_X3D', 'id_device': 1679399947, 'name': 'Détecteur Étage', 'anticipation_start': False, 'picto': 'picto_sensor_dfr', 'last_usage': 'sensorDFR', 'widget_behavior': {'tutorial_id': 'sensor_dfr'}}, {'id_endpoint': 1679401885, 'first_usage': 'sensor', 'skill': 'TYDOM_X3D', 'id_device': 1679401885, 'name': 'Détecteur Cellier', 'anticipation_start': False, 'picto': 'picto_sensor_dfr', 'last_usage': 'sensorDFR', 'widget_behavior': {'tutorial_id': 'sensor_dfr'}}], 'old_tycam': False, 'os': 'android', 'groups': [{'group_all': True, 'usage': 'light', 'name': 'TOTAL', 'id': 859468405, 'picto': 'picto_lamp'}, {'group_all': True, 'usage': 'shutter', 'name': 'TOTAL', 'id': 1725345078, 'picto': 'picto_shutter'}, {'group_all': True, 'usage': 'awning', 'name': 'TOTAL', 'id': 562847101, 'picto': 'picto_awning_awning'}, {'group_all': False, 'usage': 'shutter', 'name': 'Rez de chaussée', 'id': 1259860423, 'picto': 'picto_shutter'}, {'group_all': False, 'usage': 'shutter', 'name': 'Étage', 'id': 732974460, 'picto': 'picto_shutter'}, {'group_all': True, 'usage': 'plug', 'name': 'Total', 'id': 1117398060, 'picto': 'default_device'}, {'group_all': False, 'usage': 'window', 'name': 'Bureau', 'id': 2032409200, 'picto': 'picto_window'}, {'group_all': False, 'usage': 'window', 'name': 'Chambre', 'id': 317891801, 'picto': 'picto_window'}, {'group_all': False, 'usage': 'window', 'name': 'Salle de bain', 'id': 1455243199, 'picto': 'picto_window'}, {'group_all': False, 'usage': 'window', 'name': 'Mezzanine', 'id': 1840849049, 'picto': 'picto_window'}, {'group_all': False, 'usage': 'window', 'name': 'Cuisine', 'id': 1704806706, 'picto': 'picto_window_lock'}, {'group_all': False, 'usage': 'window', 'name': 'Salle de bain étage ', 'id': 724393265, 'picto': 'picto_window'}, {'group_all': False, 'usage': 'window', 'name': 'Baie séjour', 'id': 2138691766, 'picto': 'picto_window_lock'}, {'group_all': False, 'usage': 'window', 'name': 'Baie salon ', 'id': 851256178, 'picto': 'picto_window_lock'}, {'group_all': False, 'usage': 'window', 'name': 'Chambre Léa', 'id': 1213177750, 'picto': 'picto_window_lock'}], 'areas': [], 'scenarios': [{'rule_id': '', 'name': 'Tout ouvrir ', 'id': 1855347001, 'type': 'NORMAL', 'picto': 'picto_scenario_leaving'}], 'version': '1.0.2', 'new_tycam': False, 'moments': [{'rule_id': '', 'color': 5132710, 'name': 'Nuit', 'id': 1131613363}, {'rule_id': '', 'color': 9813268, 'name': 'fermer volets', 'id': 2055498241}, {'rule_id': '', 'color': 9813268, 'name': 'Ouvrir volets ', 'id': 1227929543}], 'id_catalog': 'F2BD90F93B888DA02C54980F11AE4796DFCC98F447CD3FE326F5A3A964C939BF', 'zigbee_networks': []} -2023-09-08 18:57:13.600 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Configuration updated -2023-09-08 18:57:13.600 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] devices : [] -2023-09-08 18:57:13.659 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02PUT /devices/data HTTP/1.1\r\nServer: Tydom-001A25029FB7\r\ncontent-type: application/json\r\nTransfer-Encoding: chunked\r\n\r\n66\r\n[{"id":9,"endpoints":[{"id":0,"error":0,"data":[{"name":"setpoint","validity":"upToDate","value":null}\r\n3D\r\n,{"name":"thermicLevel","validity":"upToDate","value":"STOP"}\r\n3B\r\n,{"name":"hvacMode","validity":"upToDate","value":"NORMAL"}\r\n37\r\n,{"name":"tempoOn","validity":"upToDate","value":false}\r\n3B\r\n,{"name":"antifrostOn","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"loadSheddingOn","validity":"upToDate","value":false}\r\n3F\r\n,{"name":"openingDetected","validity":"upToDate","value":false}\r\n40\r\n,{"name":"presenceDetected","validity":"upToDate","value":false}\r\n37\r\n,{"name":"absence","validity":"upToDate","value":false}\r\n40\r\n,{"name":"productionDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"batteryCmdDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"tempSensorDefect","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorShortCut","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorOpenCirc","validity":"upToDate","value":false}\r\n37\r\n,{"name":"boostOn","validity":"upToDate","value":false}\r\n6B\r\n]}]},{"id":10,"endpoints":[{"id":0,"error":0,"data":[{"name":"setpoint","validity":"upToDate","value":null}\r\n3D\r\n,{"name":"thermicLevel","validity":"upToDate","value":"STOP"}\r\n3B\r\n,{"name":"hvacMode","validity":"upToDate","value":"NORMAL"}\r\n37\r\n,{"name":"tempoOn","validity":"upToDate","value":false}\r\n3B\r\n,{"name":"antifrostOn","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"loadSheddingOn","validity":"upToDate","value":false}\r\n3F\r\n,{"name":"openingDetected","validity":"upToDate","value":false}\r\n40\r\n,{"name":"presenceDetected","validity":"upToDate","value":false}\r\n37\r\n,{"name":"absence","validity":"upToDate","value":false}\r\n40\r\n,{"name":"productionDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"batteryCmdDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"tempSensorDefect","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorShortCut","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorOpenCirc","validity":"upToDate","value":false}\r\n74\r\n]}]},{"id":8,"endpoints":[{"id":0,"error":0,"data":[{"name":"energyInstantTi1I","validity":"upToDate","value":0.060}\r\n45\r\n,{"name":"energyInstantTi1I_Min","validity":"upToDate","value":0.000}\r\n45\r\n,{"name":"energyInstantTi1I_Max","validity":"upToDate","value":6.670}\r\n43\r\n,{"name":"energyScaleTi1I_Min","validity":"upToDate","value":0.000}\r\n44\r\n,{"name":"energyScaleTi1I_Max","validity":"upToDate","value":13.340}\r\n72\r\n]}]},{"id":10,"endpoints":[{"id":0,"error":0,"data":[{"name":"boostOn","validity":"upToDate","value":false}]}]}]\r\n\r\n0\r\n\r\n' -2023-09-08 18:57:13.659 WARNING (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Unknown tydom message type received (b'\x02PUT /devices/data HTTP/1.1\r\nServer: Tydom-001A25029FB7\r\ncontent-type: application/json\r\nTransfer-Encoding: chunked\r\n\r\n66\r\n[{"id":9,"endpoints":[{"id":0,"error":0,"data":[{"name":"setpoint","validity":"upToDate","value":null}\r\n3D\r\n,{"name":"thermicLevel","validity":"upToDate","value":"STOP"}\r\n3B\r\n,{"name":"hvacMode","validity":"upToDate","value":"NORMAL"}\r\n37\r\n,{"name":"tempoOn","validity":"upToDate","value":false}\r\n3B\r\n,{"name":"antifrostOn","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"loadSheddingOn","validity":"upToDate","value":false}\r\n3F\r\n,{"name":"openingDetected","validity":"upToDate","value":false}\r\n40\r\n,{"name":"presenceDetected","validity":"upToDate","value":false}\r\n37\r\n,{"name":"absence","validity":"upToDate","value":false}\r\n40\r\n,{"name":"productionDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"batteryCmdDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"tempSensorDefect","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorShortCut","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorOpenCirc","validity":"upToDate","value":false}\r\n37\r\n,{"name":"boostOn","validity":"upToDate","value":false}\r\n6B\r\n]}]},{"id":10,"endpoints":[{"id":0,"error":0,"data":[{"name":"setpoint","validity":"upToDate","value":null}\r\n3D\r\n,{"name":"thermicLevel","validity":"upToDate","value":"STOP"}\r\n3B\r\n,{"name":"hvacMode","validity":"upToDate","value":"NORMAL"}\r\n37\r\n,{"name":"tempoOn","validity":"upToDate","value":false}\r\n3B\r\n,{"name":"antifrostOn","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"loadSheddingOn","validity":"upToDate","value":false}\r\n3F\r\n,{"name":"openingDetected","validity":"upToDate","value":false}\r\n40\r\n,{"name":"presenceDetected","validity":"upToDate","value":false}\r\n37\r\n,{"name":"absence","validity":"upToDate","value":false}\r\n40\r\n,{"name":"productionDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"batteryCmdDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"tempSensorDefect","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorShortCut","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorOpenCirc","validity":"upToDate","value":false}\r\n74\r\n]}]},{"id":8,"endpoints":[{"id":0,"error":0,"data":[{"name":"energyInstantTi1I","validity":"upToDate","value":0.060}\r\n45\r\n,{"name":"energyInstantTi1I_Min","validity":"upToDate","value":0.000}\r\n45\r\n,{"name":"energyInstantTi1I_Max","validity":"upToDate","value":6.670}\r\n43\r\n,{"name":"energyScaleTi1I_Min","validity":"upToDate","value":0.000}\r\n44\r\n,{"name":"energyScaleTi1I_Max","validity":"upToDate","value":13.340}\r\n72\r\n]}]},{"id":10,"endpoints":[{"id":0,"error":0,"data":[{"name":"boostOn","validity":"upToDate","value":false}]}]}]\r\n\r\n0\r\n\r\n') -2023-09-08 18:57:13.915 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /devices/meta\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nTransac-Id: 0\r\n\r\nD0\r\n[{"id":0,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":1,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":2,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":3,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":4,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":5,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":6,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nD4\r\n]}]},{"id":7,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"positionCmd","type":"string","permission":"w","validity":"INFINITE","enum_values":["DOWN","UP","STOP","FAVORIT1","FAVORIT2","UP_SLOW","DOWN_SLOW"]}\r\n68\r\n,{"name":"thermicDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n79\r\n,{"name":"position","type":"numeric","permission":"rw","validity":"ES_SUPERVISION","min":0,"max":100,"step":1,"unit":"%"}\r\n6F\r\n,{"name":"recFav","type":"string","permission":"w","validity":"INFINITE","enum_values":["FAVORIT1","FAVORIT2"]}\r\n63\r\n,{"name":"onFavPos","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n63\r\n,{"name":"upDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"downDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n69\r\n,{"name":"obstacleDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n64\r\n,{"name":"intrusion","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n65\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"ES_SUPERVISION","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\nAF\r\n]}]},{"id":8,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"jobsMP","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n69\r\n,{"name":"softVersion","type":"string","permission":"r","validity":"INFINITE","enum_values":["XX.YY.ZZ"]}\r\n69\r\n,{"name":"softPlan","type":"string","permission":"r","validity":"INFINITE","enum_values":["WW.XX.YY.ZZ"]}\r\n89\r\n,{"name":"energyTotIndexWatt","type":"numeric","permission":"r","validity":"METER_POLLING","min":0,"max":4294967294,"step":1,"unit":"Wh"}\r\n8F\r\n,{"name":"energyInstantTotElec","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n93\r\n,{"name":"energyInstantTotElec_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n93\r\n,{"name":"energyInstantTotElec_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n91\r\n,{"name":"energyScaleTotElec_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n91\r\n,{"name":"energyScaleTotElec_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n86\r\n,{"name":"energyInstantTotElecP","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n8B\r\n,{"name":"energyInstantTotElec_P_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n8B\r\n,{"name":"energyInstantTotElec_P_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n89\r\n,{"name":"energyScaleTotElec_P_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n89\r\n,{"name":"energyScaleTotElec_P_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n85\r\n,{"name":"energyIndexTi1","type":"numeric","permission":"r","validity":"METER_POLLING","min":0,"max":4294967294,"step":1,"unit":"Wh"}\r\n8C\r\n,{"name":"energyInstantTi1I","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n90\r\n,{"name":"energyInstantTi1I_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n90\r\n,{"name":"energyInstantTi1I_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n8E\r\n,{"name":"energyScaleTi1I_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n8E\r\n,{"name":"energyScaleTi1I_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0.000,"max":327.660,"step":0.010,"unit":"A"}\r\n82\r\n,{"name":"energyInstantTi1P","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n86\r\n,{"name":"energyInstantTi1P_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n86\r\n,{"name":"energyInstantTi1P_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n84\r\n,{"name":"energyScaleTi1P_Min","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\n84\r\n,{"name":"energyScaleTi1P_Max","type":"numeric","permission":"r","validity":"METER_INSTANT","min":0,"max":65534,"step":1,"unit":"W"}\r\nAF\r\n]}]},{"id":9,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"authorization","type":"string","permission":"rw","validity":"STATUS_POLLING","enum_values":["STOP","HEATING"]}\r\n86\r\n,{"name":"setpoint","type":"numeric","permission":"rw","validity":"DATA_POLLING","min":10.000,"max":30.000,"step":0.500,"unit":"degC"}\r\n6B\r\n,{"name":"thermicLevel","type":"string","permission":"rw","validity":"DATA_POLLING","enum_values":["STOP"]}\r\n86\r\n,{"name":"delaySetpoint","type":"numeric","permission":"w","validity":"INFINITE","min":10.000,"max":30.000,"step":0.500,"unit":"degC"}\r\n6B\r\n,{"name":"delayThermicLevel","type":"string","permission":"w","validity":"INFINITE","enum_values":["STOP"]}\r\n7D\r\n,{"name":"hvacMode","type":"string","permission":"rw","validity":"DATA_POLLING","enum_values":["NORMAL","STOP","ANTI_FROST"]}\r\n80\r\n,{"name":"timeDelay","type":"numeric","permission":"rw","validity":"TIMER_POLLING","min":0,"max":65535,"step":1,"unit":"minute"}\r\n8B\r\n,{"name":"temperature","type":"numeric","permission":"r","validity":"SENSOR_POLLING","min":-99.900,"max":99.900,"step":0.010,"unit":"degC"}\r\n62\r\n,{"name":"tempoOn","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n66\r\n,{"name":"antifrostOn","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n69\r\n,{"name":"loadSheddingOn","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6A\r\n,{"name":"openingDetected","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"presenceDetected","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n62\r\n,{"name":"absence","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"productionDefect","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"batteryCmdDefect","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"tempSensorDefect","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6D\r\n,{"name":"tempSensorShortCut","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6D\r\n,{"name":"tempSensorOpenCirc","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n63\r\n,{"name":"boostOn","type":"boolean","permission":"rw","validity":"STATUS_POLLING","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n7E\r\n,{"name":"anticipCoeff","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":65534,"step":1,"unit":"min/deg"}\r\nB0\r\n]}]},{"id":10,"endpoints":[{"id":0,"error":0,"metadata":[{"name":"authorization","type":"string","permission":"rw","validity":"STATUS_POLLING","enum_values":["STOP","HEATING"]}\r\n86\r\n,{"name":"setpoint","type":"numeric","permission":"rw","validity":"DATA_POLLING","min":10.000,"max":30.000,"step":0.500,"unit":"degC"}\r\n6B\r\n,{"name":"thermicLevel","type":"string","permission":"rw","validity":"DATA_POLLING","enum_values":["STOP"]}\r\n86\r\n,{"name":"delaySetpoint","type":"numeric","permission":"w","validity":"INFINITE","min":10.000,"max":30.000,"step":0.500,"unit":"degC"}\r\n6B\r\n,{"name":"delayThermicLevel","type":"string","permission":"w","validity":"INFINITE","enum_values":["STOP"]}\r\n7D\r\n,{"name":"hvacMode","type":"string","permission":"rw","validity":"DATA_POLLING","enum_values":["NORMAL","STOP","ANTI_FROST"]}\r\n80\r\n,{"name":"timeDelay","type":"numeric","permission":"rw","validity":"TIMER_POLLING","min":0,"max":65535,"step":1,"unit":"minute"}\r\n8B\r\n,{"name":"temperature","type":"numeric","permission":"r","validity":"SENSOR_POLLING","min":-99.900,"max":99.900,"step":0.010,"unit":"degC"}\r\n62\r\n,{"name":"tempoOn","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n66\r\n,{"name":"antifrostOn","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n69\r\n,{"name":"loadSheddingOn","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6A\r\n,{"name":"openingDetected","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"presenceDetected","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n62\r\n,{"name":"absence","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"productionDefect","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"batteryCmdDefect","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6B\r\n,{"name":"tempSensorDefect","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6D\r\n,{"name":"tempSensorShortCut","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n6D\r\n,{"name":"tempSensorOpenCirc","type":"boolean","permission":"r","validity":"STATUS_POLLING","unit":"boolean"}\r\n63\r\n,{"name":"boostOn","type":"boolean","permission":"rw","validity":"STATUS_POLLING","unit":"boolean"}\r\n67\r\n,{"name":"localisation","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n63\r\n,{"name":"modeAsso","type":"string","permission":"w","validity":"INFINITE","enum_values":["START"]}\r\n7E\r\n,{"name":"anticipCoeff","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":65534,"step":1,"unit":"min/deg"}\r\nC1\r\n]}]},{"id":1599338298,"endpoints":[{"id":1599338298,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n8D\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","OPEN_FRENCH","OPEN_HOPPER"]}\r\nC1\r\n]}]},{"id":1599338370,"endpoints":[{"id":1599338370,"error":2,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n8D\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","OPEN_FRENCH","OPEN_HOPPER"]}\r\nC1\r\n]}]},{"id":1599338418,"endpoints":[{"id":1599338418,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n8D\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","OPEN_FRENCH","OPEN_HOPPER"]}\r\nC1\r\n]}]},{"id":1599387771,"endpoints":[{"id":1599387771,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n8D\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","OPEN_FRENCH","OPEN_HOPPER"]}\r\nC1\r\n]}]},{"id":1599387831,"endpoints":[{"id":1599387831,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n8D\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","OPEN_FRENCH","OPEN_HOPPER"]}\r\nC1\r\n]}]},{"id":1599388036,"endpoints":[{"id":1599388036,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n8D\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","OPEN_FRENCH","OPEN_HOPPER"]}\r\nC1\r\n]}]},{"id":1599388243,"endpoints":[{"id":1599388243,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC1\r\n]}]},{"id":1604473848,"endpoints":[{"id":1604473848,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\n66\r\n,{"name":"calibrationDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\nC1\r\n]}]},{"id":1604476226,"endpoints":[{"id":1604476226,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC1\r\n]}]},{"id":1604476251,"endpoints":[{"id":1604476251,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC2\r\n]}]},{"id":1604476324,"endpoints":[{"id":1604476324,"error":15,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC2\r\n]}]},{"id":1604476347,"endpoints":[{"id":1604476347,"error":15,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC1\r\n]}]},{"id":1604477407,"endpoints":[{"id":1604477407,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC1\r\n]}]},{"id":1611399070,"endpoints":[{"id":1611399070,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nC1\r\n]}]},{"id":1611399103,"endpoints":[{"id":1611399103,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"intrusionDetect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\n7C\r\n,{"name":"openState","type":"string","permission":"r","validity":"DETECTOR_SUPERVISION","enum_values":["LOCKED","UNLOCKED"]}\r\nD7\r\n]}]},{"id":1664906374,"endpoints":[{"id":1664906374,"error":0,"metadata":[{"name":"energyIndexHeatWatt","type":"numeric","permission":"r","validity":"METER_SUPERVISION","min":0,"max":4294967294,"step":1,"unit":"Wh"}\r\n8D\r\n,{"name":"energyIndexECSWatt","type":"numeric","permission":"r","validity":"METER_SUPERVISION","min":0,"max":4294967294,"step":1,"unit":"Wh"}\r\n91\r\n,{"name":"outTemperature","type":"numeric","permission":"r","validity":"METER_SUPERVISION","min":-99.900,"max":99.900,"step":0.010,"unit":"degC"}\r\nC1\r\n]}]},{"id":1679399651,"endpoints":[{"id":1679399651,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"techSmokeDefect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\nC1\r\n]}]},{"id":1679399947,"endpoints":[{"id":1679399947,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n70\r\n,{"name":"techSmokeDefect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}\r\nC1\r\n]}]},{"id":1679401885,"endpoints":[{"id":1679401885,"error":0,"metadata":[{"name":"config","type":"numeric","permission":"r","validity":"INFINITE","min":0,"max":4294967294,"step":1,"unit":"NA"}\r\n5F\r\n,{"name":"battDefect","type":"boolean","permission":"r","validity":"INFINITE","unit":"boolean"}\r\n78\r\n,{"name":"supervisionMode","type":"string","permission":"r","validity":"INFINITE","enum_values":["SHORT","LONG","NONE"]}\r\n77\r\n,{"name":"techSmokeDefect","type":"boolean","permission":"r","validity":"DETECTOR_SUPERVISION","unit":"boolean"}]}]}]\r\n\r\n0\r\n\r\n' -2023-09-08 18:57:13.920 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Message received detected as (msg_metadata) -2023-09-08 18:57:13.920 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Incoming data parsed with success -2023-09-08 18:57:13.937 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /devices/cmeta\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nTransac-Id: 0\r\n\r\n34\r\n[{"id":0,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":1,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":2,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":3,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":4,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":5,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":6,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n39\r\n[]}]},{"id":7,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n115\r\n[]}]},{"id":8,"endpoints":[{"id":0,"error":0,"cmetadata":[{"name":"reset","permission":"w","parameters":[{"name":"station","type":"string","enum_values":["TOTAL_ELEC","TI1","TI2","TI3","DHW","HEAT_ELEC"]},{"name":"measure","type":"string","enum_values":["INTENSITY","POWER"]}]}\r\n38\r\n]}]},{"id":9,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n3A\r\n[]}]},{"id":10,"endpoints":[{"id":0,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1599338298,"endpoints":[{"id":1599338298,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1599338370,"endpoints":[{"id":1599338370,"error":2,"cmetadata":\r\n4B\r\n[]}]},{"id":1599338418,"endpoints":[{"id":1599338418,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1599387771,"endpoints":[{"id":1599387771,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1599387831,"endpoints":[{"id":1599387831,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1599388036,"endpoints":[{"id":1599388036,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1599388243,"endpoints":[{"id":1599388243,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1604473848,"endpoints":[{"id":1604473848,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1604476226,"endpoints":[{"id":1604476226,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1604476251,"endpoints":[{"id":1604476251,"error":0,"cmetadata":\r\n4C\r\n[]}]},{"id":1604476324,"endpoints":[{"id":1604476324,"error":15,"cmetadata":\r\n4C\r\n[]}]},{"id":1604476347,"endpoints":[{"id":1604476347,"error":15,"cmetadata":\r\n4B\r\n[]}]},{"id":1604477407,"endpoints":[{"id":1604477407,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1611399070,"endpoints":[{"id":1611399070,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1611399103,"endpoints":[{"id":1611399103,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1664906374,"endpoints":[{"id":1664906374,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1679399651,"endpoints":[{"id":1679399651,"error":0,"cmetadata":\r\n4B\r\n[]}]},{"id":1679399947,"endpoints":[{"id":1679399947,"error":0,"cmetadata":\r\n53\r\n[]}]},{"id":1679401885,"endpoints":[{"id":1679401885,"error":0,"cmetadata":[]}]}]\r\n\r\n0\r\n\r\n' -2023-09-08 18:57:13.938 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Message received detected as (msg_cmetadata) -2023-09-08 18:57:13.938 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] parse_cmeta_data : [{'id': 0, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 1, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 2, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 3, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 4, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 5, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 6, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 7, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 8, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': [{'name': 'reset', 'permission': 'w', 'parameters': [{'name': 'station', 'type': 'string', 'enum_values': ['TOTAL_ELEC', 'TI1', 'TI2', 'TI3', 'DHW', 'HEAT_ELEC']}, {'name': 'measure', 'type': 'string', 'enum_values': ['INTENSITY', 'POWER']}]}]}]}, {'id': 9, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 10, 'endpoints': [{'id': 0, 'error': 0, 'cmetadata': []}]}, {'id': 1599338298, 'endpoints': [{'id': 1599338298, 'error': 0, 'cmetadata': []}]}, {'id': 1599338370, 'endpoints': [{'id': 1599338370, 'error': 2, 'cmetadata': []}]}, {'id': 1599338418, 'endpoints': [{'id': 1599338418, 'error': 0, 'cmetadata': []}]}, {'id': 1599387771, 'endpoints': [{'id': 1599387771, 'error': 0, 'cmetadata': []}]}, {'id': 1599387831, 'endpoints': [{'id': 1599387831, 'error': 0, 'cmetadata': []}]}, {'id': 1599388036, 'endpoints': [{'id': 1599388036, 'error': 0, 'cmetadata': []}]}, {'id': 1599388243, 'endpoints': [{'id': 1599388243, 'error': 0, 'cmetadata': []}]}, {'id': 1604473848, 'endpoints': [{'id': 1604473848, 'error': 0, 'cmetadata': []}]}, {'id': 1604476226, 'endpoints': [{'id': 1604476226, 'error': 0, 'cmetadata': []}]}, {'id': 1604476251, 'endpoints': [{'id': 1604476251, 'error': 0, 'cmetadata': []}]}, {'id': 1604476324, 'endpoints': [{'id': 1604476324, 'error': 15, 'cmetadata': []}]}, {'id': 1604476347, 'endpoints': [{'id': 1604476347, 'error': 15, 'cmetadata': []}]}, {'id': 1604477407, 'endpoints': [{'id': 1604477407, 'error': 0, 'cmetadata': []}]}, {'id': 1611399070, 'endpoints': [{'id': 1611399070, 'error': 0, 'cmetadata': []}]}, {'id': 1611399103, 'endpoints': [{'id': 1611399103, 'error': 0, 'cmetadata': []}]}, {'id': 1664906374, 'endpoints': [{'id': 1664906374, 'error': 0, 'cmetadata': []}]}, {'id': 1679399651, 'endpoints': [{'id': 1679399651, 'error': 0, 'cmetadata': []}]}, {'id': 1679399947, 'endpoints': [{'id': 1679399947, 'error': 0, 'cmetadata': []}]}, {'id': 1679401885, 'endpoints': [{'id': 1679401885, 'error': 0, 'cmetadata': []}]}] -2023-09-08 18:57:13.939 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Metadata configuration updated -2023-09-08 18:57:14.081 INFO (MainThread) [custom_components.deltadore-tydom.tydom.tydom_client] Incomming message - type : 2 - message : b'\x02HTTP/1.1 200 OK\r\nServer: Tydom-001A25029FB7\r\nUri-Origin: /devices/data\r\nContent-Type: application/json\r\nTransfer-Encoding: chunked\r\nTransac-Id: 0\r\n\r\n2F\r\n[{"id":0,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":1,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":2,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":3,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":4,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":5,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":6,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n33\r\n]}]},{"id":7,"endpoints":[{"id":0,"error":0,"data":\r\n3D\r\n[{"name":"thermicDefect","validity":"upToDate","value":false}\r\n36\r\n,{"name":"position","validity":"upToDate","value":100}\r\n38\r\n,{"name":"onFavPos","validity":"upToDate","value":false}\r\n38\r\n,{"name":"upDefect","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"downDefect","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"obstacleDefect","validity":"upToDate","value":false}\r\n39\r\n,{"name":"intrusion","validity":"upToDate","value":false}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n67\r\n]}]},{"id":8,"endpoints":[{"id":0,"error":0,"data":[{"name":"jobsMP","validity":"upToDate","value":144}\r\n40\r\n,{"name":"softVersion","validity":"upToDate","value":"01.02.02"}\r\n40\r\n,{"name":"softPlan","validity":"upToDate","value":"23.63.00.14"}\r\n45\r\n,{"name":"energyTotIndexWatt","validity":"upToDate","value":17047619}\r\n44\r\n,{"name":"energyInstantTotElec","validity":"upToDate","value":2.000}\r\n48\r\n,{"name":"energyInstantTotElec_Min","validity":"upToDate","value":0.000}\r\n49\r\n,{"name":"energyInstantTotElec_Max","validity":"upToDate","value":46.000}\r\n46\r\n,{"name":"energyScaleTotElec_Min","validity":"upToDate","value":0.000}\r\n47\r\n,{"name":"energyScaleTotElec_Max","validity":"upToDate","value":45.000}\r\n43\r\n,{"name":"energyInstantTotElecP","validity":"upToDate","value":482}\r\n46\r\n,{"name":"energyInstantTotElec_P_Min","validity":"upToDate","value":0}\r\n4A\r\n,{"name":"energyInstantTotElec_P_Max","validity":"upToDate","value":10212}\r\n44\r\n,{"name":"energyScaleTotElec_P_Min","validity":"upToDate","value":0}\r\n47\r\n,{"name":"energyScaleTotElec_P_Max","validity":"upToDate","value":9000}\r\n3E\r\n,{"name":"energyIndexTi1","validity":"upToDate","value":76871}\r\n41\r\n,{"name":"energyInstantTi1I","validity":"upToDate","value":0.060}\r\n45\r\n,{"name":"energyInstantTi1I_Min","validity":"upToDate","value":0.000}\r\n45\r\n,{"name":"energyInstantTi1I_Max","validity":"upToDate","value":6.670}\r\n43\r\n,{"name":"energyScaleTi1I_Min","validity":"upToDate","value":0.000}\r\n44\r\n,{"name":"energyScaleTi1I_Max","validity":"upToDate","value":13.340}\r\n3C\r\n,{"name":"energyInstantTi1P","validity":"expired","value":0}\r\n40\r\n,{"name":"energyInstantTi1P_Min","validity":"expired","value":0}\r\n43\r\n,{"name":"energyInstantTi1P_Max","validity":"expired","value":1639}\r\n3E\r\n,{"name":"energyScaleTi1P_Min","validity":"expired","value":0}\r\n41\r\n,{"name":"energyScaleTi1P_Max","validity":"expired","value":3418}\r\n71\r\n]}]},{"id":9,"endpoints":[{"id":0,"error":0,"data":[{"name":"authorization","validity":"upToDate","value":"STOP"}\r\n37\r\n,{"name":"setpoint","validity":"upToDate","value":null}\r\n3D\r\n,{"name":"thermicLevel","validity":"upToDate","value":"STOP"}\r\n3B\r\n,{"name":"hvacMode","validity":"upToDate","value":"NORMAL"}\r\n35\r\n,{"name":"timeDelay","validity":"upToDate","value":0}\r\n3B\r\n,{"name":"temperature","validity":"expired","value":25.690}\r\n37\r\n,{"name":"tempoOn","validity":"upToDate","value":false}\r\n3B\r\n,{"name":"antifrostOn","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"loadSheddingOn","validity":"upToDate","value":false}\r\n3F\r\n,{"name":"openingDetected","validity":"upToDate","value":false}\r\n40\r\n,{"name":"presenceDetected","validity":"upToDate","value":false}\r\n37\r\n,{"name":"absence","validity":"upToDate","value":false}\r\n40\r\n,{"name":"productionDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"batteryCmdDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"tempSensorDefect","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorShortCut","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorOpenCirc","validity":"upToDate","value":false}\r\n37\r\n,{"name":"boostOn","validity":"upToDate","value":false}\r\n39\r\n,{"name":"anticipCoeff","validity":"upToDate","value":30}\r\n72\r\n]}]},{"id":10,"endpoints":[{"id":0,"error":0,"data":[{"name":"authorization","validity":"upToDate","value":"STOP"}\r\n37\r\n,{"name":"setpoint","validity":"upToDate","value":null}\r\n3D\r\n,{"name":"thermicLevel","validity":"upToDate","value":"STOP"}\r\n3B\r\n,{"name":"hvacMode","validity":"upToDate","value":"NORMAL"}\r\n35\r\n,{"name":"timeDelay","validity":"upToDate","value":0}\r\n3C\r\n,{"name":"temperature","validity":"upToDate","value":24.300}\r\n37\r\n,{"name":"tempoOn","validity":"upToDate","value":false}\r\n3B\r\n,{"name":"antifrostOn","validity":"upToDate","value":false}\r\n3E\r\n,{"name":"loadSheddingOn","validity":"upToDate","value":false}\r\n3F\r\n,{"name":"openingDetected","validity":"upToDate","value":false}\r\n40\r\n,{"name":"presenceDetected","validity":"upToDate","value":false}\r\n37\r\n,{"name":"absence","validity":"upToDate","value":false}\r\n40\r\n,{"name":"productionDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"batteryCmdDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"tempSensorDefect","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorShortCut","validity":"upToDate","value":false}\r\n42\r\n,{"name":"tempSensorOpenCirc","validity":"upToDate","value":false}\r\n37\r\n,{"name":"boostOn","validity":"upToDate","value":false}\r\n39\r\n,{"name":"anticipCoeff","validity":"upToDate","value":30}\r\n7F\r\n]}]},{"id":1599338298,"endpoints":[{"id":1599338298,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1599338370,"endpoints":[{"id":1599338370,"error":2,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3E\r\n,{"name":"intrusionDetect","validity":"expired","value":false}\r\n3B\r\n,{"name":"openState","validity":"expired","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1599338418,"endpoints":[{"id":1599338418,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1599387771,"endpoints":[{"id":1599387771,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3E\r\n,{"name":"intrusionDetect","validity":"upToDate","value":true}\r\n41\r\n,{"name":"openState","validity":"upToDate","value":"OPEN_FRENCH"}\r\n7F\r\n]}]},{"id":1599387831,"endpoints":[{"id":1599387831,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1599388036,"endpoints":[{"id":1599388036,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1599388243,"endpoints":[{"id":1599388243,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1604473848,"endpoints":[{"id":1604473848,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3E\r\n,{"name":"intrusionDetect","validity":"upToDate","value":true}\r\n3E\r\n,{"name":"openState","validity":"upToDate","value":"UNLOCKED"}\r\n41\r\n,{"name":"calibrationDefect","validity":"upToDate","value":false}\r\n7F\r\n]}]},{"id":1604476226,"endpoints":[{"id":1604476226,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1604476251,"endpoints":[{"id":1604476251,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n80\r\n]}]},{"id":1604476324,"endpoints":[{"id":1604476324,"error":15,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n39\r\n,{"name":"battDefect","validity":"expired","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3E\r\n,{"name":"intrusionDetect","validity":"expired","value":false}\r\n37\r\n,{"name":"openState","validity":"expired","value":null}\r\n80\r\n]}]},{"id":1604476347,"endpoints":[{"id":1604476347,"error":15,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n39\r\n,{"name":"battDefect","validity":"expired","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3E\r\n,{"name":"intrusionDetect","validity":"expired","value":false}\r\n37\r\n,{"name":"openState","validity":"expired","value":null}\r\n7F\r\n]}]},{"id":1604477407,"endpoints":[{"id":1604477407,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n7F\r\n]}]},{"id":1611399070,"endpoints":[{"id":1611399070,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3E\r\n,{"name":"intrusionDetect","validity":"upToDate","value":true}\r\n3E\r\n,{"name":"openState","validity":"upToDate","value":"UNLOCKED"}\r\n7F\r\n]}]},{"id":1611399103,"endpoints":[{"id":1611399103,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"intrusionDetect","validity":"upToDate","value":false}\r\n3C\r\n,{"name":"openState","validity":"upToDate","value":"LOCKED"}\r\n8A\r\n]}]},{"id":1664906374,"endpoints":[{"id":1664906374,"error":0,"data":[{"name":"energyIndexHeatWatt","validity":"upToDate","value":3210000}\r\n44\r\n,{"name":"energyIndexECSWatt","validity":"upToDate","value":2249000}\r\n3F\r\n,{"name":"outTemperature","validity":"upToDate","value":22.300}\r\n7F\r\n]}]},{"id":1679399651,"endpoints":[{"id":1679399651,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"techSmokeDefect","validity":"upToDate","value":false}\r\n7F\r\n]}]},{"id":1679399947,"endpoints":[{"id":1679399947,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n3F\r\n,{"name":"techSmokeDefect","validity":"upToDate","value":false}\r\n7F\r\n]}]},{"id":1679401885,"endpoints":[{"id":1679401885,"error":0,"data":[{"name":"config","validity":"upToDate","value":134630146}\r\n3A\r\n,{"name":"battDefect","validity":"upToDate","value":false}\r\n40\r\n,{"name":"supervisionMode","validity":"upToDate","value":"LONG"}\r\n46\r\n,{"name":"techSmokeDefect","validity":"upToDate","value":false}]}]}]\r\n\r\n0\r\n\r\n' -2023-09-08 18:57:14.084 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] Message received detected as (msg_data) -2023-09-08 18:57:14.085 DEBUG (MainThread) [custom_components.deltadore-tydom.tydom.MessageHandler] parse_devices_data : [{'id': 0, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 1, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 2, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 3, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 4, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 5, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 6, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 7, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'thermicDefect', 'validity': 'upToDate', 'value': False}, {'name': 'position', 'validity': 'upToDate', 'value': 100}, {'name': 'onFavPos', 'validity': 'upToDate', 'value': False}, {'name': 'upDefect', 'validity': 'upToDate', 'value': False}, {'name': 'downDefect', 'validity': 'upToDate', 'value': False}, {'name': 'obstacleDefect', 'validity': 'upToDate', 'value': False}, {'name': 'intrusion', 'validity': 'upToDate', 'value': False}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 8, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'jobsMP', 'validity': 'upToDate', 'value': 144}, {'name': 'softVersion', 'validity': 'upToDate', 'value': '01.02.02'}, {'name': 'softPlan', 'validity': 'upToDate', 'value': '23.63.00.14'}, {'name': 'energyTotIndexWatt', 'validity': 'upToDate', 'value': 17047619}, {'name': 'energyInstantTotElec', 'validity': 'upToDate', 'value': 2.0}, {'name': 'energyInstantTotElec_Min', 'validity': 'upToDate', 'value': 0.0}, {'name': 'energyInstantTotElec_Max', 'validity': 'upToDate', 'value': 46.0}, {'name': 'energyScaleTotElec_Min', 'validity': 'upToDate', 'value': 0.0}, {'name': 'energyScaleTotElec_Max', 'validity': 'upToDate', 'value': 45.0}, {'name': 'energyInstantTotElecP', 'validity': 'upToDate', 'value': 482}, {'name': 'energyInstantTotElec_P_Min', 'validity': 'upToDate', 'value': 0}, {'name': 'energyInstantTotElec_P_Max', 'validity': 'upToDate', 'value': 10212}, {'name': 'energyScaleTotElec_P_Min', 'validity': 'upToDate', 'value': 0}, {'name': 'energyScaleTotElec_P_Max', 'validity': 'upToDate', 'value': 9000}, {'name': 'energyIndexTi1', 'validity': 'upToDate', 'value': 76871}, {'name': 'energyInstantTi1I', 'validity': 'upToDate', 'value': 0.06}, {'name': 'energyInstantTi1I_Min', 'validity': 'upToDate', 'value': 0.0}, {'name': 'energyInstantTi1I_Max', 'validity': 'upToDate', 'value': 6.67}, {'name': 'energyScaleTi1I_Min', 'validity': 'upToDate', 'value': 0.0}, {'name': 'energyScaleTi1I_Max', 'validity': 'upToDate', 'value': 13.34}, {'name': 'energyInstantTi1P', 'validity': 'expired', 'value': 0}, {'name': 'energyInstantTi1P_Min', 'validity': 'expired', 'value': 0}, {'name': 'energyInstantTi1P_Max', 'validity': 'expired', 'value': 1639}, {'name': 'energyScaleTi1P_Min', 'validity': 'expired', 'value': 0}, {'name': 'energyScaleTi1P_Max', 'validity': 'expired', 'value': 3418}]}]}, {'id': 9, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'authorization', 'validity': 'upToDate', 'value': 'STOP'}, {'name': 'setpoint', 'validity': 'upToDate', 'value': None}, {'name': 'thermicLevel', 'validity': 'upToDate', 'value': 'STOP'}, {'name': 'hvacMode', 'validity': 'upToDate', 'value': 'NORMAL'}, {'name': 'timeDelay', 'validity': 'upToDate', 'value': 0}, {'name': 'temperature', 'validity': 'expired', 'value': 25.69}, {'name': 'tempoOn', 'validity': 'upToDate', 'value': False}, {'name': 'antifrostOn', 'validity': 'upToDate', 'value': False}, {'name': 'loadSheddingOn', 'validity': 'upToDate', 'value': False}, {'name': 'openingDetected', 'validity': 'upToDate', 'value': False}, {'name': 'presenceDetected', 'validity': 'upToDate', 'value': False}, {'name': 'absence', 'validity': 'upToDate', 'value': False}, {'name': 'productionDefect', 'validity': 'upToDate', 'value': False}, {'name': 'batteryCmdDefect', 'validity': 'upToDate', 'value': False}, {'name': 'tempSensorDefect', 'validity': 'upToDate', 'value': False}, {'name': 'tempSensorShortCut', 'validity': 'upToDate', 'value': False}, {'name': 'tempSensorOpenCirc', 'validity': 'upToDate', 'value': False}, {'name': 'boostOn', 'validity': 'upToDate', 'value': False}, {'name': 'anticipCoeff', 'validity': 'upToDate', 'value': 30}]}]}, {'id': 10, 'endpoints': [{'id': 0, 'error': 0, 'data': [{'name': 'authorization', 'validity': 'upToDate', 'value': 'STOP'}, {'name': 'setpoint', 'validity': 'upToDate', 'value': None}, {'name': 'thermicLevel', 'validity': 'upToDate', 'value': 'STOP'}, {'name': 'hvacMode', 'validity': 'upToDate', 'value': 'NORMAL'}, {'name': 'timeDelay', 'validity': 'upToDate', 'value': 0}, {'name': 'temperature', 'validity': 'upToDate', 'value': 24.3}, {'name': 'tempoOn', 'validity': 'upToDate', 'value': False}, {'name': 'antifrostOn', 'validity': 'upToDate', 'value': False}, {'name': 'loadSheddingOn', 'validity': 'upToDate', 'value': False}, {'name': 'openingDetected', 'validity': 'upToDate', 'value': False}, {'name': 'presenceDetected', 'validity': 'upToDate', 'value': False}, {'name': 'absence', 'validity': 'upToDate', 'value': False}, {'name': 'productionDefect', 'validity': 'upToDate', 'value': False}, {'name': 'batteryCmdDefect', 'validity': 'upToDate', 'value': False}, {'name': 'tempSensorDefect', 'validity': 'upToDate', 'value': False}, {'name': 'tempSensorShortCut', 'validity': 'upToDate', 'value': False}, {'name': 'tempSensorOpenCirc', 'validity': 'upToDate', 'value': False}, {'name': 'boostOn', 'validity': 'upToDate', 'value': False}, {'name': 'anticipCoeff', 'validity': 'upToDate', 'value': 30}]}]}, {'id': 1599338298, 'endpoints': [{'id': 1599338298, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1599338370, 'endpoints': [{'id': 1599338370, 'error': 2, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'expired', 'value': False}, {'name': 'openState', 'validity': 'expired', 'value': 'LOCKED'}]}]}, {'id': 1599338418, 'endpoints': [{'id': 1599338418, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1599387771, 'endpoints': [{'id': 1599387771, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': True}, {'name': 'openState', 'validity': 'upToDate', 'value': 'OPEN_FRENCH'}]}]}, {'id': 1599387831, 'endpoints': [{'id': 1599387831, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1599388036, 'endpoints': [{'id': 1599388036, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1599388243, 'endpoints': [{'id': 1599388243, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1604473848, 'endpoints': [{'id': 1604473848, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': True}, {'name': 'openState', 'validity': 'upToDate', 'value': 'UNLOCKED'}, {'name': 'calibrationDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 1604476226, 'endpoints': [{'id': 1604476226, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1604476251, 'endpoints': [{'id': 1604476251, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1604476324, 'endpoints': [{'id': 1604476324, 'error': 15, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'expired', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'expired', 'value': False}, {'name': 'openState', 'validity': 'expired', 'value': None}]}]}, {'id': 1604476347, 'endpoints': [{'id': 1604476347, 'error': 15, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'expired', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'expired', 'value': False}, {'name': 'openState', 'validity': 'expired', 'value': None}]}]}, {'id': 1604477407, 'endpoints': [{'id': 1604477407, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1611399070, 'endpoints': [{'id': 1611399070, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': True}, {'name': 'openState', 'validity': 'upToDate', 'value': 'UNLOCKED'}]}]}, {'id': 1611399103, 'endpoints': [{'id': 1611399103, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'intrusionDetect', 'validity': 'upToDate', 'value': False}, {'name': 'openState', 'validity': 'upToDate', 'value': 'LOCKED'}]}]}, {'id': 1664906374, 'endpoints': [{'id': 1664906374, 'error': 0, 'data': [{'name': 'energyIndexHeatWatt', 'validity': 'upToDate', 'value': 3210000}, {'name': 'energyIndexECSWatt', 'validity': 'upToDate', 'value': 2249000}, {'name': 'outTemperature', 'validity': 'upToDate', 'value': 22.3}]}]}, {'id': 1679399651, 'endpoints': [{'id': 1679399651, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'techSmokeDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 1679399947, 'endpoints': [{'id': 1679399947, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'techSmokeDefect', 'validity': 'upToDate', 'value': False}]}]}, {'id': 1679401885, 'endpoints': [{'id': 1679401885, 'error': 0, 'data': [{'name': 'config', 'validity': 'upToDate', 'value': 134630146}, {'name': 'battDefect', 'validity': 'upToDate', 'value': False}, {'name': 'supervisionMode', 'validity': 'upToDate', 'value': 'LONG'}, {'name': 'techSmokeDefect', 'validity': 'upToDate', 'value': False}]}]}] From 80129c06da37656bb83a9c1847b21e2029ce8a74 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:22:03 +0100 Subject: [PATCH 65/74] =?UTF-8?q?=C2=A0cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deltadore-tydom/tydom/MessageHandler.py | 99 ------------------- 1 file changed, 99 deletions(-) diff --git a/custom_components/deltadore-tydom/tydom/MessageHandler.py b/custom_components/deltadore-tydom/tydom/MessageHandler.py index e55d201..28c63d1 100644 --- a/custom_components/deltadore-tydom/tydom/MessageHandler.py +++ b/custom_components/deltadore-tydom/tydom/MessageHandler.py @@ -470,105 +470,6 @@ async def parse_devices_data(self, parsed): async def parse_devices_cdata(self, parsed): """Parse devices cdata.""" LOGGER.debug("parse_devices_data : %s", parsed) - for i in parsed: - for endpoint in i["endpoints"]: - if endpoint["error"] == 0 and len(endpoint["cdata"]) > 0: - try: - device_id = i["id"] - endpoint_id = endpoint["id"] - unique_id = str(endpoint_id) + "_" + str(device_id) - name_of_id = self.get_name_from_id(unique_id) - type_of_id = self.get_type_from_id(unique_id) - LOGGER.info( - "Device configured (id=%s, endpoint=%s, name=%s, type=%s)", - device_id, - endpoint_id, - name_of_id, - type_of_id, - ) - - for elem in endpoint["cdata"]: - if type_of_id == "conso": - if elem["name"] == "energyIndex": - device_class_of_id = "energy" - state_class_of_id = "total_increasing" - unit_of_measurement_of_id = "Wh" - element_name = elem["parameters"]["dest"] - element_index = "counter" - - attr_conso = { - "device_id": device_id, - "endpoint_id": endpoint_id, - "id": unique_id, - "name": name_of_id, - "device_type": "sensor", - "device_class": device_class_of_id, - "state_class": state_class_of_id, - "unit_of_measurement": unit_of_measurement_of_id, - element_name: elem["values"][element_index], - } - - # new_conso = Sensor( - # elem_name=element_name, - # tydom_attributes_payload=attr_conso, - # mqtt=self.mqtt_client, - # ) - # await new_conso.update() - - elif elem["name"] == "energyInstant": - device_class_of_id = "current" - state_class_of_id = "measurement" - unit_of_measurement_of_id = "VA" - element_name = elem["parameters"]["unit"] - element_index = "measure" - - attr_conso = { - "device_id": device_id, - "endpoint_id": endpoint_id, - "id": unique_id, - "name": name_of_id, - "device_type": "sensor", - "device_class": device_class_of_id, - "state_class": state_class_of_id, - "unit_of_measurement": unit_of_measurement_of_id, - element_name: elem["values"][element_index], - } - - # new_conso = Sensor( - # elem_name=element_name, - # tydom_attributes_payload=attr_conso, - # mqtt=self.mqtt_client, - # ) - # await new_conso.update() - - elif elem["name"] == "energyDistrib": - for elName in elem["values"]: - if elName != "date": - element_name = elName - element_index = elName - attr_conso = { - "device_id": device_id, - "endpoint_id": endpoint_id, - "id": unique_id, - "name": name_of_id, - "device_type": "sensor", - "device_class": "energy", - "state_class": "total_increasing", - "unit_of_measurement": "Wh", - element_name: elem["values"][ - element_index - ], - } - - # new_conso = Sensor( - # elem_name=element_name, - # tydom_attributes_payload=attr_conso, - # mqtt=self.mqtt_client, - # ) - # await new_conso.update() - - except Exception as e: - LOGGER.error("Error when parsing msg_cdata (%s)", e) # PUT response DIRTY parsing def parse_put_response(self, bytes_str, start=6): From 00b64e33c2b1a74e48d662dc176a4a035182f7eb Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:24:40 +0100 Subject: [PATCH 66/74] =?UTF-8?q?=C2=A0fix=20ruff=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/tydom/tydom_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index c0ecc9b..30ac648 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -492,7 +492,7 @@ async def put_data(self, path, name, value): body: str if value is None: body = '{"' + name + '":"null}' - elif type(value)==bool or type(value)==int: + elif isinstance(value, bool) or isinstance(value, int): body = '{"' + name + '":"' + str(value).lower() + '}' else: body = '{"' + name + '":"' + value + '"}' @@ -516,7 +516,7 @@ async def put_devices_data(self, device_id, endpoint_id, name, value): body: str if value is None: body = '[{"name":"' + name + '","value":null}]' - elif type(value)==bool: + elif isinstance(value, bool): body = '[{"name":"' + name + '","value":' + str(value).lower() + '}]' else: body = '[{"name":"' + name + '","value":"' + value + '"}]' From 1147887c9c9d77374fd0500da3c1768f474d758b Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:40:34 +0100 Subject: [PATCH 67/74] =?UTF-8?q?=C2=A0update=20manifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/deltadore-tydom/manifest.json b/custom_components/deltadore-tydom/manifest.json index 86cb07c..bbdf26b 100644 --- a/custom_components/deltadore-tydom/manifest.json +++ b/custom_components/deltadore-tydom/manifest.json @@ -5,8 +5,9 @@ "@CyrilP" ], "config_flow": true, - "documentation": "https://github.com/CyrilP/hass-deltadore-tydom-component/blob/main/README.md", + "documentation": "https://github.com/CyrilP/hass-deltadore-tydom-component", "iot_class": "local_push", + "issue_tracker": "https://github.com/CyrilP/hass-deltadore-tydom-component/issues", "requirements": [ "websockets>=9.1" ], From 63a036cf272db7cd3a905df9de8bca2e0671df4c Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:46:14 +0100 Subject: [PATCH 68/74] =?UTF-8?q?=C2=A0fix=20hassfest=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/translations/en.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/deltadore-tydom/translations/en.json b/custom_components/deltadore-tydom/translations/en.json index 2c1304b..b0c4d79 100644 --- a/custom_components/deltadore-tydom/translations/en.json +++ b/custom_components/deltadore-tydom/translations/en.json @@ -39,6 +39,5 @@ "cannot_connect": "Unable to connect to the device", "already_configured": "Device is already configured" } - }, - "flow_title": "{name}" + } } \ No newline at end of file From 47b0a223e79473d70d03eeb93b559338be8b765a Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 7 Nov 2023 19:11:12 +0100 Subject: [PATCH 69/74] =?UTF-8?q?=C2=A0add=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/deltadore-tydom/manifest.json b/custom_components/deltadore-tydom/manifest.json index bbdf26b..c7781ec 100644 --- a/custom_components/deltadore-tydom/manifest.json +++ b/custom_components/deltadore-tydom/manifest.json @@ -5,6 +5,7 @@ "@CyrilP" ], "config_flow": true, + "dependencies": ["manual"], "documentation": "https://github.com/CyrilP/hass-deltadore-tydom-component", "iot_class": "local_push", "issue_tracker": "https://github.com/CyrilP/hass-deltadore-tydom-component/issues", From ca06f4f1bb44af8942602cdfd393da0cffc89f18 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 7 Nov 2023 19:14:17 +0100 Subject: [PATCH 70/74] =?UTF-8?q?=C2=A0fix=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/manifest.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/deltadore-tydom/manifest.json b/custom_components/deltadore-tydom/manifest.json index c7781ec..081ec4c 100644 --- a/custom_components/deltadore-tydom/manifest.json +++ b/custom_components/deltadore-tydom/manifest.json @@ -7,19 +7,19 @@ "config_flow": true, "dependencies": ["manual"], "documentation": "https://github.com/CyrilP/hass-deltadore-tydom-component", - "iot_class": "local_push", - "issue_tracker": "https://github.com/CyrilP/hass-deltadore-tydom-component/issues", - "requirements": [ - "websockets>=9.1" - ], "dhcp": [ { "hostname": "tydom-*", "macaddress": "001A25*" } ], + "iot_class": "local_push", + "issue_tracker": "https://github.com/CyrilP/hass-deltadore-tydom-component/issues", "loggers": [ "tydom" ], + "requirements": [ + "websockets>=9.1" + ], "version": "0.0.1" } \ No newline at end of file From f8ec344530a088d5e9e30da0b25e1ef7a47917d5 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Tue, 7 Nov 2023 19:16:51 +0100 Subject: [PATCH 71/74] =?UTF-8?q?=C2=A0fix=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/deltadore-tydom/manifest.json b/custom_components/deltadore-tydom/manifest.json index 081ec4c..4324450 100644 --- a/custom_components/deltadore-tydom/manifest.json +++ b/custom_components/deltadore-tydom/manifest.json @@ -6,13 +6,13 @@ ], "config_flow": true, "dependencies": ["manual"], - "documentation": "https://github.com/CyrilP/hass-deltadore-tydom-component", "dhcp": [ { "hostname": "tydom-*", "macaddress": "001A25*" } ], + "documentation": "https://github.com/CyrilP/hass-deltadore-tydom-component", "iot_class": "local_push", "issue_tracker": "https://github.com/CyrilP/hass-deltadore-tydom-component/issues", "loggers": [ From c4a9953bb7476fcc563480fff245b6edb67558e5 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Thu, 9 Nov 2023 20:28:35 +0100 Subject: [PATCH 72/74] =?UTF-8?q?=C2=A0fix=20local=20mode=20connection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/tydom/tydom_client.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/custom_components/deltadore-tydom/tydom/tydom_client.py b/custom_components/deltadore-tydom/tydom/tydom_client.py index 30ac648..897cec7 100644 --- a/custom_components/deltadore-tydom/tydom/tydom_client.py +++ b/custom_components/deltadore-tydom/tydom/tydom_client.py @@ -6,6 +6,7 @@ import re import async_timeout import aiohttp +import ssl import traceback from typing import cast @@ -184,6 +185,12 @@ async def async_connect(self) -> ClientWebSocketResponse: "Sec-WebSocket-Version": "13", } + # configuration needed for local mode + sslcontext = ssl.create_default_context() + sslcontext.options |= 0x4 # OP_LEGACY_SERVER_CONNECT + sslcontext.check_hostname = False + sslcontext.verify_mode = ssl.CERT_NONE + session = async_create_clientsession(self._hass, False) try: @@ -194,6 +201,7 @@ async def async_connect(self) -> ClientWebSocketResponse: headers=http_headers, json=None, proxy=proxy, + ssl_context=sslcontext, ) LOGGER.debug( "response status : %s\nheaders : %s\ncontent : %s", @@ -225,6 +233,7 @@ async def async_connect(self) -> ClientWebSocketResponse: autoping=True, heartbeat=2, proxy=proxy, + ssl_context=sslcontext, ) return connection From e86dbf8fb430903e718a10cd2f7956e8a78ad937 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Thu, 9 Nov 2023 20:28:58 +0100 Subject: [PATCH 73/74] =?UTF-8?q?=C2=A0fix=20config=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/deltadore-tydom/config_flow.py b/custom_components/deltadore-tydom/config_flow.py index 3bf84c5..077ae5d 100644 --- a/custom_components/deltadore-tydom/config_flow.py +++ b/custom_components/deltadore-tydom/config_flow.py @@ -83,6 +83,7 @@ async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: return { CONF_HOST: data[CONF_HOST], CONF_MAC: data[CONF_MAC], + CONF_EMAIL: data[CONF_EMAIL], CONF_PASSWORD: data[CONF_PASSWORD], CONF_TYDOM_PASSWORD: password, CONF_PIN: pin, From 74ca428b355fdc6fd01a318533c03ef2bd34c032 Mon Sep 17 00:00:00 2001 From: cyrilp <5814027+CyrilP@users.noreply.github.com> Date: Thu, 9 Nov 2023 20:29:10 +0100 Subject: [PATCH 74/74] =?UTF-8?q?=C2=A0fix=20manifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/deltadore-tydom/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/deltadore-tydom/manifest.json b/custom_components/deltadore-tydom/manifest.json index 4324450..01e648d 100644 --- a/custom_components/deltadore-tydom/manifest.json +++ b/custom_components/deltadore-tydom/manifest.json @@ -5,7 +5,7 @@ "@CyrilP" ], "config_flow": true, - "dependencies": ["manual"], + "dependencies": ["dhcp"], "dhcp": [ { "hostname": "tydom-*",