From f59b068e14a25aec96a468ef95cc6f3a52489cef Mon Sep 17 00:00:00 2001 From: vlebourl Date: Thu, 4 Jun 2020 14:57:37 +0200 Subject: [PATCH 01/90] First commit into adding climate entity. --- custom_components/tahoma/__init__.py | 1 + custom_components/tahoma/climate.py | 38 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 custom_components/tahoma/climate.py diff --git a/custom_components/tahoma/__init__.py b/custom_components/tahoma/__init__.py index 9a6cc5507..4a871db9d 100644 --- a/custom_components/tahoma/__init__.py +++ b/custom_components/tahoma/__init__.py @@ -38,6 +38,7 @@ ) PLATFORMS = [ + "climate", "cover", "light", "lock", diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py new file mode 100644 index 000000000..a44677d8b --- /dev/null +++ b/custom_components/tahoma/climate.py @@ -0,0 +1,38 @@ +"""Support for Tahoma climate.""" +from datetime import timedelta +import logging + +from homeassistant.components.climate import ClimateEntity + +from .const import DOMAIN, TAHOMA_TYPES +from .tahoma_device import TahomaDevice + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=120) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Tahoma sensors from a config entry.""" + + data = hass.data[DOMAIN][entry.entry_id] + + entities = [] + controller = data.get("controller") + + for device in data.get("devices"): + if TAHOMA_TYPES[device.uiclass] == "climate": + entities.append(TahomaClimate(device, controller)) + + async_add_entities(entities) + +class TahomaClimate(TahomaDevice, ClimateEntity): + """Representation of a Tahoma thermostat.""" + + def __init__(self, tahoma_device, controller): + """Initialize the sensor.""" + super().__init__(tahoma_device, controller) + + def update(self): + """Update the state.""" + self.controller.get_states([self.tahoma_device]) \ No newline at end of file From e9123fbae8d927063d4de7a9f3c074a32c99b648 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Thu, 4 Jun 2020 20:59:20 +0200 Subject: [PATCH 02/90] implemented abstract methods --- custom_components/tahoma/climate.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index a44677d8b..940655d31 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -1,8 +1,13 @@ """Support for Tahoma climate.""" from datetime import timedelta import logging +from typing import List from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + HVAC_MODE_OFF, + HVAC_MODE_AUTO, +) from .const import DOMAIN, TAHOMA_TYPES from .tahoma_device import TahomaDevice @@ -32,7 +37,26 @@ class TahomaClimate(TahomaDevice, ClimateEntity): def __init__(self, tahoma_device, controller): """Initialize the sensor.""" super().__init__(tahoma_device, controller) + self._hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_AUTO] + self._hvac_mode = None def update(self): """Update the state.""" - self.controller.get_states([self.tahoma_device]) \ No newline at end of file + self.controller.get_states([self.tahoma_device]) + #TODO implement update method + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode. + + Need to be one of HVAC_MODE_*. + """ + return self._hvac_mode + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes. + + Need to be a subset of HVAC_MODES. + """ + return self._hvac_modes \ No newline at end of file From 7f0ec23e92f004d4c7ed60b6063802084e0330fa Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Thu, 4 Jun 2020 21:43:36 +0200 Subject: [PATCH 03/90] limit the climate integration to SomfyThermostat widget, implemented required properties and methods. --- custom_components/tahoma/climate.py | 41 ++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 940655d31..11324d658 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -1,12 +1,18 @@ """Support for Tahoma climate.""" from datetime import timedelta import logging -from typing import List +from typing import List, Optional from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_OFF, HVAC_MODE_AUTO, + PRESET_AWAY, + PRESET_HOME, + PRESET_NONE, + PRESET_SLEEP, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from .const import DOMAIN, TAHOMA_TYPES @@ -16,6 +22,8 @@ SCAN_INTERVAL = timedelta(seconds=120) +PRESET_FP = "Frost Protection" + async def async_setup_entry(hass, entry, async_add_entities): """Set up the Tahoma sensors from a config entry.""" @@ -27,23 +35,27 @@ async def async_setup_entry(hass, entry, async_add_entities): for device in data.get("devices"): if TAHOMA_TYPES[device.uiclass] == "climate": - entities.append(TahomaClimate(device, controller)) + if device.widget == "SomfyThermostat": + entities.append(TahomaClimate(device, controller)) async_add_entities(entities) + class TahomaClimate(TahomaDevice, ClimateEntity): """Representation of a Tahoma thermostat.""" def __init__(self, tahoma_device, controller): """Initialize the sensor.""" super().__init__(tahoma_device, controller) + self._preset_modes = [PRESET_AWAY, PRESET_FP, PRESET_HOME, PRESET_NONE, PRESET_SLEEP] + self._preset_mode = None self._hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_AUTO] self._hvac_mode = None def update(self): """Update the state.""" self.controller.get_states([self.tahoma_device]) - #TODO implement update method + # TODO implement update method @property def hvac_mode(self) -> str: @@ -59,4 +71,25 @@ def hvac_modes(self) -> List[str]: Need to be a subset of HVAC_MODES. """ - return self._hvac_modes \ No newline at end of file + return self._hvac_modes + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE + + @property + def preset_mode(self) -> Optional[str]: + """Return the current preset mode, e.g., home, away, temp. + + Requires SUPPORT_PRESET_MODE. + """ + return self._preset_mode + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return a list of available preset modes. + + Requires SUPPORT_PRESET_MODE. + """ + return self._preset_modes From 089d790a8427400907b0afec0f02d33d5ead5b49 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Thu, 4 Jun 2020 21:47:16 +0200 Subject: [PATCH 04/90] Added temperature unit. --- custom_components/tahoma/climate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 11324d658..8d6dbcfa2 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -3,6 +3,7 @@ import logging from typing import List, Optional +from homeassistant.const import TEMP_CELSIUS from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_OFF, @@ -57,6 +58,11 @@ def update(self): self.controller.get_states([self.tahoma_device]) # TODO implement update method + @property + def temperature_unit(self) -> str: + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS + @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode. From 8e4f9e5485739331180e8c688697ed3ebfb71793 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Thu, 4 Jun 2020 22:05:16 +0200 Subject: [PATCH 05/90] Added a skeleton for essential methods. --- custom_components/tahoma/climate.py | 32 ++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 8d6dbcfa2..745781f89 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -3,10 +3,10 @@ import logging from typing import List, Optional -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( - HVAC_MODE_OFF, + HVAC_MODE_HEAT, HVAC_MODE_AUTO, PRESET_AWAY, PRESET_HOME, @@ -48,10 +48,11 @@ class TahomaClimate(TahomaDevice, ClimateEntity): def __init__(self, tahoma_device, controller): """Initialize the sensor.""" super().__init__(tahoma_device, controller) - self._preset_modes = [PRESET_AWAY, PRESET_FP, PRESET_HOME, PRESET_NONE, PRESET_SLEEP] - self._preset_mode = None - self._hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_AUTO] + self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] self._hvac_mode = None + self._preset_mode = None + self._preset_modes = [PRESET_NONE, PRESET_FP, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] + self._target_temp = None def update(self): """Update the state.""" @@ -79,6 +80,10 @@ def hvac_modes(self) -> List[str]: """ return self._hvac_modes + def set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + raise NotImplementedError() # TODO implement + @property def supported_features(self) -> int: """Return the list of supported features.""" @@ -99,3 +104,20 @@ def preset_modes(self) -> Optional[List[str]]: Requires SUPPORT_PRESET_MODE. """ return self._preset_modes + + def set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + raise NotImplementedError() # TODO implement + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temp + + def set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + self._target_temp = temperature + self.schedule_update_ha_state() From b2a222b9abfd4c0b8ad96f643795078f70614e17 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 15:29:00 +0200 Subject: [PATCH 06/90] implement set_hvac_mode --- custom_components/tahoma/climate.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 745781f89..a56e095e6 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -23,8 +23,7 @@ SCAN_INTERVAL = timedelta(seconds=120) -PRESET_FP = "Frost Protection" - +PRESET_FROST_GUARD = "Frost Guard" async def async_setup_entry(hass, entry, async_add_entities): """Set up the Tahoma sensors from a config entry.""" @@ -48,15 +47,22 @@ class TahomaClimate(TahomaDevice, ClimateEntity): def __init__(self, tahoma_device, controller): """Initialize the sensor.""" super().__init__(tahoma_device, controller) + self._current_temp = None + self._target_temp = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] self._hvac_mode = None self._preset_mode = None - self._preset_modes = [PRESET_NONE, PRESET_FP, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] - self._target_temp = None + self._preset_modes = [ + PRESET_NONE, PRESET_FROST_GUARD, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] def update(self): """Update the state.""" + self.apply_action("refreshState") self.controller.get_states([self.tahoma_device]) + if self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState']: + self._hvac_mode = HVAC_MODE_HEAT + else: + self._hvac_mode = HVAC_MODE_AUTO # TODO implement update method @property @@ -82,6 +88,11 @@ def hvac_modes(self) -> List[str]: def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" + if hvac_mode == HVAC_MODE_AUTO: + self.apply_action("exitDerogation") + elif hvac_mode == HVAC_MODE_HEAT: + self.apply_action("setDerogation", self.current_temperature, "further_notice") + self.apply_action("refreshState") raise NotImplementedError() # TODO implement @property @@ -109,6 +120,11 @@ def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" raise NotImplementedError() # TODO implement + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature""" + return self._current_temp + @property def target_temperature(self): """Return the temperature we try to reach.""" From e416ee69257012c53b41f61a4b18aa58117d0972 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 15:32:39 +0200 Subject: [PATCH 07/90] implement set_hvac_mode --- custom_components/tahoma/climate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index a56e095e6..eb2cc7893 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -93,7 +93,6 @@ def set_hvac_mode(self, hvac_mode: str) -> None: elif hvac_mode == HVAC_MODE_HEAT: self.apply_action("setDerogation", self.current_temperature, "further_notice") self.apply_action("refreshState") - raise NotImplementedError() # TODO implement @property def supported_features(self) -> int: From 9f31f2cbc4e1c14b0def593f3677b406cccdcc10 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 15:40:51 +0200 Subject: [PATCH 08/90] implement set_hvac_mode --- custom_components/tahoma/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index eb2cc7893..f63c60635 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -57,8 +57,13 @@ def __init__(self, tahoma_device, controller): def update(self): """Update the state.""" - self.apply_action("refreshState") self.controller.get_states([self.tahoma_device]) + _LOGGER.debug("modes: \n%s: %s\n,%s:%s", + 'somfythermostat:DerogationHeatingModeState', + self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState'], + 'somfythermostat:HeatingModeState', + self.tahoma_device.active_states['somfythermostat:HeatingModeState']) + if self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState']: self._hvac_mode = HVAC_MODE_HEAT else: From 77ec0491bc74a04dfe0fba96d5afa3eb8f328b00 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 15:47:31 +0200 Subject: [PATCH 09/90] implement set_hvac_mode --- custom_components/tahoma/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index f63c60635..e1c5d7b13 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -58,13 +58,13 @@ def __init__(self, tahoma_device, controller): def update(self): """Update the state.""" self.controller.get_states([self.tahoma_device]) - _LOGGER.debug("modes: \n%s: %s\n,%s:%s", + _LOGGER.debug("\nmodes: \n\t%s: %s\n\t%s:%s", 'somfythermostat:DerogationHeatingModeState', self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState'], 'somfythermostat:HeatingModeState', self.tahoma_device.active_states['somfythermostat:HeatingModeState']) - if self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState']: + if self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState'] == "manualMode": self._hvac_mode = HVAC_MODE_HEAT else: self._hvac_mode = HVAC_MODE_AUTO From d9d0a29c2577f1e05e8b335c5df2f5b321fa1376 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 15:59:49 +0200 Subject: [PATCH 10/90] implement set_hvac_mode --- custom_components/tahoma/climate.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index e1c5d7b13..6d161697d 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -58,17 +58,18 @@ def __init__(self, tahoma_device, controller): def update(self): """Update the state.""" self.controller.get_states([self.tahoma_device]) - _LOGGER.debug("\nmodes: \n\t%s: %s\n\t%s:%s", - 'somfythermostat:DerogationHeatingModeState', - self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState'], - 'somfythermostat:HeatingModeState', - self.tahoma_device.active_states['somfythermostat:HeatingModeState']) - if self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState'] == "manualMode": self._hvac_mode = HVAC_MODE_HEAT else: self._hvac_mode = HVAC_MODE_AUTO - # TODO implement update method + _LOGGER.debug("\nmodes: \n\t%s: %s\n\t%s: %s\n\t%s: %s", + 'somfythermostat:DerogationHeatingModeState', + self.tahoma_device.active_states[ + 'somfythermostat:DerogationHeatingModeState'], + 'somfythermostat:HeatingModeState', + self.tahoma_device.active_states['somfythermostat:HeatingModeState'], + 'hvac_mode', self.hvac_mode) + @property def temperature_unit(self) -> str: From abe779371f20be8292f585d6a87515c9db58580d Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 16:16:17 +0200 Subject: [PATCH 11/90] implement set_hvac_mode --- custom_components/tahoma/climate.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 6d161697d..e34a17c33 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -57,6 +57,7 @@ def __init__(self, tahoma_device, controller): def update(self): """Update the state.""" + self.apply_action("refreshState") self.controller.get_states([self.tahoma_device]) if self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState'] == "manualMode": self._hvac_mode = HVAC_MODE_HEAT @@ -78,18 +79,12 @@ def temperature_unit(self) -> str: @property def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ + """Return hvac operation ie. heat, cool mode.""" return self._hvac_mode @property def hvac_modes(self) -> List[str]: - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ + """Return the list of available hvac operation modes.""" return self._hvac_modes def set_hvac_mode(self, hvac_mode: str) -> None: From dc9bed95570117e4fe3ec3c90c55245cc463ca5a Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 16:36:28 +0200 Subject: [PATCH 12/90] add temperature sensor. --- custom_components/tahoma/climate.py | 52 ++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index e34a17c33..53d835407 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -1,9 +1,13 @@ """Support for Tahoma climate.""" from datetime import timedelta +from time import sleep import logging from typing import List, Optional -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE +from homeassistant.core import callback +from homeassistant.helpers.event import async_track_state_change +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, EVENT_HOMEASSISTANT_START, \ + STATE_UNKNOWN from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, @@ -47,6 +51,9 @@ class TahomaClimate(TahomaDevice, ClimateEntity): def __init__(self, tahoma_device, controller): """Initialize the sensor.""" super().__init__(tahoma_device, controller) + self._temp_sensor_entity_id = "sensor."+( + self.controller.get_device(self.tahoma_device.url.replace("#1", "#2")) + ).replace("°", "deg").replace(" ", "_") self._current_temp = None self._target_temp = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] @@ -55,6 +62,38 @@ def __init__(self, tahoma_device, controller): self._preset_modes = [ PRESET_NONE, PRESET_FROST_GUARD, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] + async def async_added_to_hass(self): + await super().async_added_to_hass() + + async_track_state_change( + self.hass, self._temp_sensor_entity_id, self._async_sensor_changed + ) + + @callback + def _async_startup(event): + """Init on startup.""" + sensor_state = self.hass.states.get(self._temp_sensor_entity_id) + if sensor_state and sensor_state.state != STATE_UNKNOWN: + self.update_temp(sensor_state) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) + + async def _async_sensor_changed(self, entity_id, old_state, new_state): + """Handle temperature changes.""" + if new_state is None: + return + + self.update_temp(new_state) + self.schedule_update_ha_state() + + @callback + def update_temp(self, state): + """Update thermostat with latest state from sensor.""" + try: + self._current_temp = float(state.state) + except ValueError as ex: + _LOGGER.error("Unable to update from sensor: %s", ex) + def update(self): """Update the state.""" self.apply_action("refreshState") @@ -71,6 +110,10 @@ def update(self): self.tahoma_device.active_states['somfythermostat:HeatingModeState'], 'hvac_mode', self.hvac_mode) + @property + def temperature_sensor(self) -> str: + """Return the id of the temperature sensor""" + return self._temp_sensor_entity_id @property def temperature_unit(self) -> str: @@ -89,11 +132,12 @@ def hvac_modes(self) -> List[str]: def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - if hvac_mode == HVAC_MODE_AUTO: + if hvac_mode == HVAC_MODE_AUTO and self._hvac_mode != HVAC_MODE_AUTO: self.apply_action("exitDerogation") - elif hvac_mode == HVAC_MODE_HEAT: + elif hvac_mode == HVAC_MODE_HEAT and self._hvac_mode != HVAC_MODE_HEAT: self.apply_action("setDerogation", self.current_temperature, "further_notice") - self.apply_action("refreshState") + sleep(10) + self.schedule_update_ha_state() @property def supported_features(self) -> int: From 02cb978bac541f1b73dd2fdcff0e3e7983faa4bf Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 16:38:59 +0200 Subject: [PATCH 13/90] add temperature sensor. --- custom_components/tahoma/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 53d835407..5195b2ec2 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -52,8 +52,8 @@ def __init__(self, tahoma_device, controller): """Initialize the sensor.""" super().__init__(tahoma_device, controller) self._temp_sensor_entity_id = "sensor."+( - self.controller.get_device(self.tahoma_device.url.replace("#1", "#2")) - ).replace("°", "deg").replace(" ", "_") + self.controller.get_device(self.tahoma_device.url.replace("#1", "#2")).label + ).replace("°", "deg").replace(" ", "_").lower() self._current_temp = None self._target_temp = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] From 2ff986fda130b785555ae4cc6d7812d8622d8e7c Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 16:42:57 +0200 Subject: [PATCH 14/90] add temperature sensor. --- custom_components/tahoma/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 5195b2ec2..18ab70ab4 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -51,9 +51,9 @@ class TahomaClimate(TahomaDevice, ClimateEntity): def __init__(self, tahoma_device, controller): """Initialize the sensor.""" super().__init__(tahoma_device, controller) - self._temp_sensor_entity_id = "sensor."+( - self.controller.get_device(self.tahoma_device.url.replace("#1", "#2")).label - ).replace("°", "deg").replace(" ", "_").lower() + device = self.controller.get_device(self.tahoma_device.url.replace("#1", "#2")) + _LOGGER.debug("device: %s", device.label) + self._temp_sensor_entity_id = "sensor." + device.label.replace("°", "deg").replace(" ", "_").lower() self._current_temp = None self._target_temp = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] From 017ce74c14d16fd14f1f96e7362311260145a1ad Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 16:53:00 +0200 Subject: [PATCH 15/90] add temperature sensor. --- custom_components/tahoma/climate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 18ab70ab4..72bd756a4 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -51,9 +51,10 @@ class TahomaClimate(TahomaDevice, ClimateEntity): def __init__(self, tahoma_device, controller): """Initialize the sensor.""" super().__init__(tahoma_device, controller) - device = self.controller.get_device(self.tahoma_device.url.replace("#1", "#2")) - _LOGGER.debug("device: %s", device.label) - self._temp_sensor_entity_id = "sensor." + device.label.replace("°", "deg").replace(" ", "_").lower() + device = self.controller.get_device( + self.tahoma_device.url.replace("#1", "#2")).label.replace("°", "deg").replace(" ", "_").lower() + _LOGGER.debug("device: %s", device) + self._temp_sensor_entity_id = device self._current_temp = None self._target_temp = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] From b4c790cb1d2a82ec95fbe94ae29e200b492aae22 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 16:54:41 +0200 Subject: [PATCH 16/90] add temperature sensor. --- custom_components/tahoma/climate.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 72bd756a4..c7fbaf9f4 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -29,6 +29,7 @@ PRESET_FROST_GUARD = "Frost Guard" + async def async_setup_entry(hass, entry, async_add_entities): """Set up the Tahoma sensors from a config entry.""" @@ -51,8 +52,10 @@ class TahomaClimate(TahomaDevice, ClimateEntity): def __init__(self, tahoma_device, controller): """Initialize the sensor.""" super().__init__(tahoma_device, controller) - device = self.controller.get_device( - self.tahoma_device.url.replace("#1", "#2")).label.replace("°", "deg").replace(" ", "_").lower() + device = "sensor." + \ + self.controller.get_device( + self.tahoma_device.url.replace("#1", "#2") + ).label.replace("°", "deg").replace(" ", "_").lower() _LOGGER.debug("device: %s", device) self._temp_sensor_entity_id = device self._current_temp = None @@ -99,7 +102,8 @@ def update(self): """Update the state.""" self.apply_action("refreshState") self.controller.get_states([self.tahoma_device]) - if self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState'] == "manualMode": + if self.tahoma_device.active_states[ + 'somfythermostat:DerogationHeatingModeState'] == "manualMode": self._hvac_mode = HVAC_MODE_HEAT else: self._hvac_mode = HVAC_MODE_AUTO From 211625f239d7603b1873568a89a057482d9bcf2e Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 17:00:47 +0200 Subject: [PATCH 17/90] add temperature sensor. --- custom_components/tahoma/climate.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index c7fbaf9f4..d9bc3e57c 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -65,6 +65,7 @@ def __init__(self, tahoma_device, controller): self._preset_mode = None self._preset_modes = [ PRESET_NONE, PRESET_FROST_GUARD, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] + self.update_temp(None) async def async_added_to_hass(self): await super().async_added_to_hass() @@ -93,6 +94,9 @@ async def _async_sensor_changed(self, entity_id, old_state, new_state): @callback def update_temp(self, state): """Update thermostat with latest state from sensor.""" + if state is None: + state = self.hass.states.get(self._temp_sensor_entity_id) + try: self._current_temp = float(state.state) except ValueError as ex: @@ -107,13 +111,7 @@ def update(self): self._hvac_mode = HVAC_MODE_HEAT else: self._hvac_mode = HVAC_MODE_AUTO - _LOGGER.debug("\nmodes: \n\t%s: %s\n\t%s: %s\n\t%s: %s", - 'somfythermostat:DerogationHeatingModeState', - self.tahoma_device.active_states[ - 'somfythermostat:DerogationHeatingModeState'], - 'somfythermostat:HeatingModeState', - self.tahoma_device.active_states['somfythermostat:HeatingModeState'], - 'hvac_mode', self.hvac_mode) + self.update_temp(None) @property def temperature_sensor(self) -> str: From 93b265cb2b0a0cd8e38cae0e0f9bda4c7d9aa3ec Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 17:02:11 +0200 Subject: [PATCH 18/90] add temperature sensor. --- custom_components/tahoma/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index d9bc3e57c..a22d559bf 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -96,6 +96,7 @@ def update_temp(self, state): """Update thermostat with latest state from sensor.""" if state is None: state = self.hass.states.get(self._temp_sensor_entity_id) + _LOGGER.debug("state: %s", str(state)) try: self._current_temp = float(state.state) From 2a9e7c60cc9bb7d4c1c2bc9cb179a43d691ddd01 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 17:09:52 +0200 Subject: [PATCH 19/90] add temperature sensor. --- custom_components/tahoma/climate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index a22d559bf..c91928232 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -65,7 +65,7 @@ def __init__(self, tahoma_device, controller): self._preset_mode = None self._preset_modes = [ PRESET_NONE, PRESET_FROST_GUARD, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] - self.update_temp(None) + self.schedule_update_ha_state() async def async_added_to_hass(self): await super().async_added_to_hass() @@ -96,7 +96,6 @@ def update_temp(self, state): """Update thermostat with latest state from sensor.""" if state is None: state = self.hass.states.get(self._temp_sensor_entity_id) - _LOGGER.debug("state: %s", str(state)) try: self._current_temp = float(state.state) From 583963de7fe1de7661040c9c8e7c6c48a5fa0fd4 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 17:28:53 +0200 Subject: [PATCH 20/90] add humidity sensor. --- custom_components/tahoma/climate.py | 64 ++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index c91928232..eec7f7f86 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -56,22 +56,29 @@ def __init__(self, tahoma_device, controller): self.controller.get_device( self.tahoma_device.url.replace("#1", "#2") ).label.replace("°", "deg").replace(" ", "_").lower() - _LOGGER.debug("device: %s", device) self._temp_sensor_entity_id = device self._current_temp = None self._target_temp = None + device = "sensor." + \ + self.controller.get_device( + self.tahoma_device.url.replace("#1", "#3") + ).label.replace(" ", "_").lower() + self._humidity_sensor_entity_id = device + self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] self._hvac_mode = None self._preset_mode = None self._preset_modes = [ PRESET_NONE, PRESET_FROST_GUARD, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] - self.schedule_update_ha_state() async def async_added_to_hass(self): await super().async_added_to_hass() async_track_state_change( - self.hass, self._temp_sensor_entity_id, self._async_sensor_changed + self.hass, self._temp_sensor_entity_id, self._async_temp_sensor_changed + ) + async_track_state_change( + self.hass, self._temp_sensor_entity_id, self._async_humidity_sensor_changed ) @callback @@ -83,7 +90,7 @@ def _async_startup(event): self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) - async def _async_sensor_changed(self, entity_id, old_state, new_state): + async def _async_temp_sensor_changed(self, entity_id, old_state, new_state): """Handle temperature changes.""" if new_state is None: return @@ -102,6 +109,25 @@ def update_temp(self, state): except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) + async def _async_humidity_sensor_changed(self, entity_id, old_state, new_state): + """Handle temperature changes.""" + if new_state is None: + return + + self.update_humidity(new_state) + self.schedule_update_ha_state() + + @callback + def update_humidity(self, state): + """Update thermostat with latest state from sensor.""" + if state is None: + state = self.hass.states.get(self._humidity_sensor_entity_id) + + try: + self._current_humidity = float(state.state) + except ValueError as ex: + _LOGGER.error("Unable to update from sensor: %s", ex) + def update(self): """Update the state.""" self.apply_action("refreshState") @@ -113,16 +139,6 @@ def update(self): self._hvac_mode = HVAC_MODE_AUTO self.update_temp(None) - @property - def temperature_sensor(self) -> str: - """Return the id of the temperature sensor""" - return self._temp_sensor_entity_id - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" @@ -167,6 +183,26 @@ def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" raise NotImplementedError() # TODO implement + @property + def humidity_sensor(self) -> str: + """Return the id of the temperature sensor""" + return self._humidity_sensor_entity_id + + @property + def current_humidity(self) -> Optional[int]: + """Return the current humidity""" + return self._current_humidity + + @property + def temperature_sensor(self) -> str: + """Return the id of the temperature sensor""" + return self._temp_sensor_entity_id + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS + @property def current_temperature(self) -> Optional[float]: """Return the current temperature""" From 6046575e23a1053caed25129bfbf947ad68ed23b Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 17:30:05 +0200 Subject: [PATCH 21/90] fix humidity sensor. --- custom_components/tahoma/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index eec7f7f86..9ce7883b2 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -124,7 +124,7 @@ def update_humidity(self, state): state = self.hass.states.get(self._humidity_sensor_entity_id) try: - self._current_humidity = float(state.state) + self._current_humidity = int(state.state) except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) From a965ddf5f4b2b563d8735a7c07e06611f73fccc1 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 17:31:43 +0200 Subject: [PATCH 22/90] removed sleep. --- custom_components/tahoma/climate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 9ce7883b2..73fc91599 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -1,6 +1,5 @@ """Support for Tahoma climate.""" from datetime import timedelta -from time import sleep import logging from typing import List, Optional @@ -138,6 +137,7 @@ def update(self): else: self._hvac_mode = HVAC_MODE_AUTO self.update_temp(None) + self.update_humidity(None) @property def hvac_mode(self) -> str: @@ -155,7 +155,6 @@ def set_hvac_mode(self, hvac_mode: str) -> None: self.apply_action("exitDerogation") elif hvac_mode == HVAC_MODE_HEAT and self._hvac_mode != HVAC_MODE_HEAT: self.apply_action("setDerogation", self.current_temperature, "further_notice") - sleep(10) self.schedule_update_ha_state() @property From e5ce10eec4afc2c0010be5c518158eeb8ad7e625 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 17:36:13 +0200 Subject: [PATCH 23/90] fix humidity sensor. --- custom_components/tahoma/climate.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 73fc91599..5b668217d 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -63,6 +63,7 @@ def __init__(self, tahoma_device, controller): self.tahoma_device.url.replace("#1", "#3") ).label.replace(" ", "_").lower() self._humidity_sensor_entity_id = device + _LOGGER.debug("humidity sensor: %s", self._humidity_sensor_entity_id) self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] self._hvac_mode = None @@ -83,9 +84,13 @@ async def async_added_to_hass(self): @callback def _async_startup(event): """Init on startup.""" - sensor_state = self.hass.states.get(self._temp_sensor_entity_id) - if sensor_state and sensor_state.state != STATE_UNKNOWN: - self.update_temp(sensor_state) + temp_sensor_state = self.hass.states.get(self._temp_sensor_entity_id) + if temp_sensor_state and temp_sensor_state.state != STATE_UNKNOWN: + self.update_temp(temp_sensor_state) + + humidity_sensor_state = self.hass.states.get(self._humidity_sensor_entity_id) + if humidity_sensor_state and humidity_sensor_state.state != STATE_UNKNOWN: + self.update_humidity(humidity_sensor_state) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) From 98ef7b617137da13c1a754cf148b84112a8d3fb2 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 17:43:18 +0200 Subject: [PATCH 24/90] retrieve old state --- custom_components/tahoma/climate.py | 32 +++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 5b668217d..8d5cf98fb 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -5,6 +5,7 @@ from homeassistant.core import callback from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, EVENT_HOMEASSISTANT_START, \ STATE_UNKNOWN from homeassistant.components.climate import ClimateEntity @@ -16,7 +17,7 @@ PRESET_NONE, PRESET_SLEEP, SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE, ATTR_PRESET_MODE, ) from .const import DOMAIN, TAHOMA_TYPES @@ -45,7 +46,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class TahomaClimate(TahomaDevice, ClimateEntity): +class TahomaClimate(TahomaDevice, ClimateEntity, RestoreEntity): """Representation of a Tahoma thermostat.""" def __init__(self, tahoma_device, controller): @@ -70,6 +71,7 @@ def __init__(self, tahoma_device, controller): self._preset_mode = None self._preset_modes = [ PRESET_NONE, PRESET_FROST_GUARD, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] + self._is_away = None async def async_added_to_hass(self): await super().async_added_to_hass() @@ -94,6 +96,32 @@ def _async_startup(event): self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) + old_state = await self.async_get_last_state() + if old_state is not None: + if self._target_temp is None: + if old_state.attributes.get(ATTR_TEMPERATURE) is None: + self._target_temp = self.min_temp + _LOGGER.warning( + "Undefined target temperature, falling back to %s", + self._target_temp, + ) + else: + self._target_temp = float(old_state.attributes[ATTR_TEMPERATURE]) + if old_state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY: + self._is_away = True + if not self._hvac_mode and old_state.state: + self._hvac_mode = old_state.state + + else: + if self._target_temp is None: + self._target_temp = self.min_temp + _LOGGER.warning( + "No previously saved temperature, setting to %s", self._target_temp + ) + + if not self._hvac_mode: + self._hvac_mode = HVAC_MODE_HEAT + async def _async_temp_sensor_changed(self, entity_id, old_state, new_state): """Handle temperature changes.""" if new_state is None: From d44e9f24f61e73d6890e37757175bd0c188ae634 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 17:44:35 +0200 Subject: [PATCH 25/90] retrieve old state --- custom_components/tahoma/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 8d5cf98fb..bfaf129bb 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -154,7 +154,7 @@ def update_humidity(self, state): """Update thermostat with latest state from sensor.""" if state is None: state = self.hass.states.get(self._humidity_sensor_entity_id) - + _LOGGER.debug("retrieved humidity: %s", str(state)) try: self._current_humidity = int(state.state) except ValueError as ex: From 566901fe13605246df71110db2f0941b6cecbee5 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 17:55:13 +0200 Subject: [PATCH 26/90] remove accents. --- custom_components/tahoma/climate.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index bfaf129bb..d01b360ec 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging from typing import List, Optional +import unicodedata from homeassistant.core import callback from homeassistant.helpers.event import async_track_state_change @@ -29,6 +30,9 @@ PRESET_FROST_GUARD = "Frost Guard" +def remove_accents(input_str): + nfkd_form = unicodedata.normalize('NFKD', input_str) + return u"".join([c for c in nfkd_form if not unicodedata.combining(c)]) async def async_setup_entry(hass, entry, async_add_entities): """Set up the Tahoma sensors from a config entry.""" @@ -53,17 +57,17 @@ def __init__(self, tahoma_device, controller): """Initialize the sensor.""" super().__init__(tahoma_device, controller) device = "sensor." + \ - self.controller.get_device( - self.tahoma_device.url.replace("#1", "#2") - ).label.replace("°", "deg").replace(" ", "_").lower() - self._temp_sensor_entity_id = device + self.controller.get_device( + self.tahoma_device.url.replace("#1", "#2") + ).label.replace("°", "deg").replace(" ", "_").lower() + self._temp_sensor_entity_id = remove_accents(device) self._current_temp = None self._target_temp = None device = "sensor." + \ self.controller.get_device( self.tahoma_device.url.replace("#1", "#3") ).label.replace(" ", "_").lower() - self._humidity_sensor_entity_id = device + self._humidity_sensor_entity_id = remove_accents(device) _LOGGER.debug("humidity sensor: %s", self._humidity_sensor_entity_id) self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] @@ -114,7 +118,7 @@ def _async_startup(event): else: if self._target_temp is None: - self._target_temp = self.min_temp + self._target_temp = self.min_temp _LOGGER.warning( "No previously saved temperature, setting to %s", self._target_temp ) From b9b15cb62747ca29e1f87d08dd96f5e71df85127 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 5 Jun 2020 18:03:50 +0200 Subject: [PATCH 27/90] fix humidity --- custom_components/tahoma/climate.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index d01b360ec..2c047e113 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -30,10 +30,12 @@ PRESET_FROST_GUARD = "Frost Guard" + def remove_accents(input_str): nfkd_form = unicodedata.normalize('NFKD', input_str) return u"".join([c for c in nfkd_form if not unicodedata.combining(c)]) + async def async_setup_entry(hass, entry, async_add_entities): """Set up the Tahoma sensors from a config entry.""" @@ -56,18 +58,18 @@ class TahomaClimate(TahomaDevice, ClimateEntity, RestoreEntity): def __init__(self, tahoma_device, controller): """Initialize the sensor.""" super().__init__(tahoma_device, controller) - device = "sensor." + \ - self.controller.get_device( - self.tahoma_device.url.replace("#1", "#2") - ).label.replace("°", "deg").replace(" ", "_").lower() - self._temp_sensor_entity_id = remove_accents(device) + device1 = "sensor." + \ + self.controller.get_device( + self.tahoma_device.url.replace("#1", "#2") + ).label.replace("°", "deg").replace(" ", "_").lower() + self._temp_sensor_entity_id = remove_accents(device1) self._current_temp = None self._target_temp = None - device = "sensor." + \ - self.controller.get_device( - self.tahoma_device.url.replace("#1", "#3") - ).label.replace(" ", "_").lower() - self._humidity_sensor_entity_id = remove_accents(device) + device2 = "sensor." + \ + self.controller.get_device( + self.tahoma_device.url.replace("#1", "#3") + ).label.replace(" ", "_").lower() + self._humidity_sensor_entity_id = remove_accents(device2) _LOGGER.debug("humidity sensor: %s", self._humidity_sensor_entity_id) self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] @@ -160,7 +162,7 @@ def update_humidity(self, state): state = self.hass.states.get(self._humidity_sensor_entity_id) _LOGGER.debug("retrieved humidity: %s", str(state)) try: - self._current_humidity = int(state.state) + self._current_humidity = float(state.state) except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) @@ -225,7 +227,7 @@ def humidity_sensor(self) -> str: return self._humidity_sensor_entity_id @property - def current_humidity(self) -> Optional[int]: + def current_humidity(self) -> Optional[float]: """Return the current humidity""" return self._current_humidity From 37b9f5d3fff75c1207551a69d9f50d5c84b92c61 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Sat, 6 Jun 2020 09:48:13 +0200 Subject: [PATCH 28/90] quick fix --- custom_components/tahoma/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 2c047e113..d8b7a60d5 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -86,7 +86,7 @@ async def async_added_to_hass(self): self.hass, self._temp_sensor_entity_id, self._async_temp_sensor_changed ) async_track_state_change( - self.hass, self._temp_sensor_entity_id, self._async_humidity_sensor_changed + self.hass, self._humidity_sensor_entity_id, self._async_humidity_sensor_changed ) @callback From 0dcd8644786843848a448abe8f992f7e1a036d3f Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Sun, 7 Jun 2020 10:11:29 +0200 Subject: [PATCH 29/90] Added initial hvac state and update target temperature. --- custom_components/tahoma/climate.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index d8b7a60d5..7066016e0 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -73,7 +73,10 @@ def __init__(self, tahoma_device, controller): _LOGGER.debug("humidity sensor: %s", self._humidity_sensor_entity_id) self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] - self._hvac_mode = None + if self.tahoma_device.active_states['somfythermostat:DerogationTypeState'] == 'date': + self._hvac_mode = HVAC_MODE_AUTO + else: + self._hvac_mode = HVAC_MODE_HEAT self._preset_mode = None self._preset_modes = [ PRESET_NONE, PRESET_FROST_GUARD, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] @@ -127,6 +130,7 @@ def _async_startup(event): if not self._hvac_mode: self._hvac_mode = HVAC_MODE_HEAT + self.schedule_update_ha_state() async def _async_temp_sensor_changed(self, entity_id, old_state, new_state): """Handle temperature changes.""" @@ -173,6 +177,8 @@ def update(self): if self.tahoma_device.active_states[ 'somfythermostat:DerogationHeatingModeState'] == "manualMode": self._hvac_mode = HVAC_MODE_HEAT + self._target_temp = self.tahoma_device.active_states[ + 'core:DerogatedTargetTemperatureState'] else: self._hvac_mode = HVAC_MODE_AUTO self.update_temp(None) From 08616e296605511f3b8e9e9cd51a766c7deb6b89 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Sun, 7 Jun 2020 10:28:42 +0200 Subject: [PATCH 30/90] Added initial and update for hvac state, preset mode and target temperature. --- custom_components/tahoma/climate.py | 36 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 7066016e0..bc4d4b6e1 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -28,7 +28,19 @@ SCAN_INTERVAL = timedelta(seconds=120) -PRESET_FROST_GUARD = "Frost Guard" +PRESET_FREEZE = "freeze" +MAP_MODE = { + "date": HVAC_MODE_AUTO, + "next_mode": HVAC_MODE_HEAT, + "further_notice": HVAC_MODE_HEAT +} +MAP_PRESET = { + "sleepingMode": PRESET_SLEEP, + "atHomeMode": PRESET_HOME, + "awayMode": PRESET_AWAY, + "freezeMode": PRESET_FREEZE, + "manualMode": PRESET_NONE +} def remove_accents(input_str): @@ -64,7 +76,8 @@ def __init__(self, tahoma_device, controller): ).label.replace("°", "deg").replace(" ", "_").lower() self._temp_sensor_entity_id = remove_accents(device1) self._current_temp = None - self._target_temp = None + self._target_temp = self.tahoma_device.active_states[ + 'core:DerogatedTargetTemperatureState'] device2 = "sensor." + \ self.controller.get_device( self.tahoma_device.url.replace("#1", "#3") @@ -73,13 +86,10 @@ def __init__(self, tahoma_device, controller): _LOGGER.debug("humidity sensor: %s", self._humidity_sensor_entity_id) self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] - if self.tahoma_device.active_states['somfythermostat:DerogationTypeState'] == 'date': - self._hvac_mode = HVAC_MODE_AUTO - else: - self._hvac_mode = HVAC_MODE_HEAT - self._preset_mode = None + self._hvac_mode = MAP_MODE[self.tahoma_device.active_states['somfythermostat:DerogationTypeState']] + self._preset_mode = MAP_PRESET[self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState']] self._preset_modes = [ - PRESET_NONE, PRESET_FROST_GUARD, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] + PRESET_NONE, PRESET_FREEZE, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] self._is_away = None async def async_added_to_hass(self): @@ -174,13 +184,9 @@ def update(self): """Update the state.""" self.apply_action("refreshState") self.controller.get_states([self.tahoma_device]) - if self.tahoma_device.active_states[ - 'somfythermostat:DerogationHeatingModeState'] == "manualMode": - self._hvac_mode = HVAC_MODE_HEAT - self._target_temp = self.tahoma_device.active_states[ - 'core:DerogatedTargetTemperatureState'] - else: - self._hvac_mode = HVAC_MODE_AUTO + self._hvac_mode = MAP_MODE[self.tahoma_device.active_states['somfythermostat:DerogationTypeState']] + self._preset_mode = MAP_PRESET[self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState']] + self._target_temp = self.tahoma_device.active_states['core:DerogatedTargetTemperatureState'] self.update_temp(None) self.update_humidity(None) From 2b31184e13b14edba4069ba41dc18520e7d2ad80 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Sun, 7 Jun 2020 10:32:42 +0200 Subject: [PATCH 31/90] Added initial and update for hvac state, preset mode and target temperature. --- custom_components/tahoma/climate.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index bc4d4b6e1..f35315755 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -87,7 +87,12 @@ def __init__(self, tahoma_device, controller): self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] self._hvac_mode = MAP_MODE[self.tahoma_device.active_states['somfythermostat:DerogationTypeState']] - self._preset_mode = MAP_PRESET[self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState']] + if self._hvac_mode == HVAC_MODE_AUTO: + self._preset_mode = MAP_PRESET[ + self.tahoma_device.active_states['somfythermostat:HeatingModeState']] + else: + self._preset_mode = MAP_PRESET[ + self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState']] self._preset_modes = [ PRESET_NONE, PRESET_FREEZE, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] self._is_away = None @@ -185,7 +190,12 @@ def update(self): self.apply_action("refreshState") self.controller.get_states([self.tahoma_device]) self._hvac_mode = MAP_MODE[self.tahoma_device.active_states['somfythermostat:DerogationTypeState']] - self._preset_mode = MAP_PRESET[self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState']] + if self._hvac_mode == HVAC_MODE_AUTO: + self._preset_mode = MAP_PRESET[ + self.tahoma_device.active_states['somfythermostat:HeatingModeState']] + else: + self._preset_mode = MAP_PRESET[ + self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState']] self._target_temp = self.tahoma_device.active_states['core:DerogatedTargetTemperatureState'] self.update_temp(None) self.update_humidity(None) From c6a6074debd09f053b781d6084cda8b311197466 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Sun, 7 Jun 2020 17:12:47 +0200 Subject: [PATCH 32/90] add mapping to keys and commands to remove strings from the implementation. --- custom_components/tahoma/climate.py | 53 +++++++++++++++-------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index f35315755..a9d49faa7 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -4,7 +4,7 @@ from typing import List, Optional import unicodedata -from homeassistant.core import callback +from homeassistant.core import callback, State from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, EVENT_HOMEASSISTANT_START, \ @@ -28,12 +28,17 @@ SCAN_INTERVAL = timedelta(seconds=120) -PRESET_FREEZE = "freeze" -MAP_MODE = { +HVAC_MODE_KEY = 'somfythermostat:DerogationTypeState' +MAP_HVAC_MODE = { "date": HVAC_MODE_AUTO, "next_mode": HVAC_MODE_HEAT, "further_notice": HVAC_MODE_HEAT } +PRESET_FREEZE = "freeze" +MAP_PRESET_KEY = { + HVAC_MODE_AUTO: 'somfythermostat:HeatingModeState', + HVAC_MODE_HEAT: 'somfythermostat:DerogationHeatingModeState' +} MAP_PRESET = { "sleepingMode": PRESET_SLEEP, "atHomeMode": PRESET_HOME, @@ -41,6 +46,14 @@ "freezeMode": PRESET_FREEZE, "manualMode": PRESET_NONE } +MAP_TARGET_TEMP_KEY = { + HVAC_MODE_AUTO: 'core:TargetTemperatureState', + HVAC_MODE_HEAT: 'core:DerogatedTargetTemperatureState' +} +COMMAND_REFRESH = "refreshState" +COMMAND_EXIT_DEROGATION = "exitDerogation" +COMMAND_SET_DEROGATION = "setDerogation" +COMMAND_OPTION_FURTHER_NOTICE = "further_notice" def remove_accents(input_str): @@ -76,8 +89,7 @@ def __init__(self, tahoma_device, controller): ).label.replace("°", "deg").replace(" ", "_").lower() self._temp_sensor_entity_id = remove_accents(device1) self._current_temp = None - self._target_temp = self.tahoma_device.active_states[ - 'core:DerogatedTargetTemperatureState'] + self._target_temp = self.tahoma_device.active_states[MAP_TARGET_TEMP_KEY[self._hvac_mode]] device2 = "sensor." + \ self.controller.get_device( self.tahoma_device.url.replace("#1", "#3") @@ -86,13 +98,8 @@ def __init__(self, tahoma_device, controller): _LOGGER.debug("humidity sensor: %s", self._humidity_sensor_entity_id) self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] - self._hvac_mode = MAP_MODE[self.tahoma_device.active_states['somfythermostat:DerogationTypeState']] - if self._hvac_mode == HVAC_MODE_AUTO: - self._preset_mode = MAP_PRESET[ - self.tahoma_device.active_states['somfythermostat:HeatingModeState']] - else: - self._preset_mode = MAP_PRESET[ - self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState']] + self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[HVAC_MODE_KEY]] + self._preset_mode = MAP_PRESET[self.tahoma_device.active_states[MAP_PRESET_KEY[self._hvac_mode]]] self._preset_modes = [ PRESET_NONE, PRESET_FREEZE, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] self._is_away = None @@ -147,7 +154,7 @@ def _async_startup(event): self._hvac_mode = HVAC_MODE_HEAT self.schedule_update_ha_state() - async def _async_temp_sensor_changed(self, entity_id, old_state, new_state): + async def _async_temp_sensor_changed(self, entity_id: str, old_state: State, new_state: State) -> None: """Handle temperature changes.""" if new_state is None: return @@ -166,7 +173,7 @@ def update_temp(self, state): except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) - async def _async_humidity_sensor_changed(self, entity_id, old_state, new_state): + async def _async_humidity_sensor_changed(self, entity_id: str, old_state: State, new_state: State) -> None: """Handle temperature changes.""" if new_state is None: return @@ -179,7 +186,6 @@ def update_humidity(self, state): """Update thermostat with latest state from sensor.""" if state is None: state = self.hass.states.get(self._humidity_sensor_entity_id) - _LOGGER.debug("retrieved humidity: %s", str(state)) try: self._current_humidity = float(state.state) except ValueError as ex: @@ -187,16 +193,11 @@ def update_humidity(self, state): def update(self): """Update the state.""" - self.apply_action("refreshState") + self.apply_action(COMMAND_REFRESH) self.controller.get_states([self.tahoma_device]) - self._hvac_mode = MAP_MODE[self.tahoma_device.active_states['somfythermostat:DerogationTypeState']] - if self._hvac_mode == HVAC_MODE_AUTO: - self._preset_mode = MAP_PRESET[ - self.tahoma_device.active_states['somfythermostat:HeatingModeState']] - else: - self._preset_mode = MAP_PRESET[ - self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState']] - self._target_temp = self.tahoma_device.active_states['core:DerogatedTargetTemperatureState'] + self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[HVAC_MODE_KEY]] + self._preset_mode = MAP_PRESET[self.tahoma_device.active_states[MAP_PRESET_KEY[self._hvac_mode]]] + self._target_temp = self.tahoma_device.active_states[MAP_TARGET_TEMP_KEY[self._hvac_mode]] self.update_temp(None) self.update_humidity(None) @@ -213,9 +214,9 @@ def hvac_modes(self) -> List[str]: def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_AUTO and self._hvac_mode != HVAC_MODE_AUTO: - self.apply_action("exitDerogation") + self.apply_action(COMMAND_EXIT_DEROGATION) elif hvac_mode == HVAC_MODE_HEAT and self._hvac_mode != HVAC_MODE_HEAT: - self.apply_action("setDerogation", self.current_temperature, "further_notice") + self.apply_action(COMMAND_SET_DEROGATION, self.current_temperature, COMMAND_OPTION_FURTHER_NOTICE) self.schedule_update_ha_state() @property From c82a9d8fa4316e02caa2f5b37ef9429d7812dfea Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Sun, 7 Jun 2020 18:26:40 +0200 Subject: [PATCH 33/90] cleanup mapping --- custom_components/tahoma/climate.py | 67 ++++++++++++++++++----------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index a9d49faa7..3f0ada795 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -28,32 +28,51 @@ SCAN_INTERVAL = timedelta(seconds=120) -HVAC_MODE_KEY = 'somfythermostat:DerogationTypeState' +SUPPORTED_CLIMATE_DEVICES = [ + "SomfyThermostat" +] + +COMMAND_REFRESH = "refreshState" +COMMAND_EXIT_DEROGATION = "exitDerogation" +COMMAND_SET_DEROGATION = "setDerogation" + +KEY_HVAC_MODE = 'somfythermostat:DerogationTypeState' +KEY_HEATING_MODE = 'somfythermostat:HeatingModeState' +KEY_DEROGATION_HEATING_MODE = 'somfythermostat:DerogationHeatingModeState' +KEY_TARGET_TEMPERATURE = 'core:TargetTemperatureState' +KEY_DEROGATION_TARGET_TEMPERATURE = 'core:DerogatedTargetTemperatureState' + +PRESET_FREEZE = "freeze" + +STATE_DEROGATION_FURTHER_NOTICE = "further_notice" +STATE_DEROGATION_NEXT_MODE = "next_mode" +STATE_DEROGATION_DATE = "date" +STATE_PRESET_AT_HOME = "atHomeMode" +STATE_PRESET_AWAY = "awayMode" +STATE_PRESET_FREEZE = "freezeMode" +STATE_PRESET_MANUAL = "manualMode" +STATE_PRESET_SLEEPING_MODE = "sleepingMode" + MAP_HVAC_MODE = { - "date": HVAC_MODE_AUTO, - "next_mode": HVAC_MODE_HEAT, - "further_notice": HVAC_MODE_HEAT + STATE_DEROGATION_DATE: HVAC_MODE_AUTO, + STATE_DEROGATION_NEXT_MODE: HVAC_MODE_HEAT, + STATE_DEROGATION_FURTHER_NOTICE: HVAC_MODE_HEAT } -PRESET_FREEZE = "freeze" MAP_PRESET_KEY = { - HVAC_MODE_AUTO: 'somfythermostat:HeatingModeState', - HVAC_MODE_HEAT: 'somfythermostat:DerogationHeatingModeState' + HVAC_MODE_AUTO: KEY_HEATING_MODE, + HVAC_MODE_HEAT: KEY_DEROGATION_HEATING_MODE } MAP_PRESET = { - "sleepingMode": PRESET_SLEEP, - "atHomeMode": PRESET_HOME, - "awayMode": PRESET_AWAY, - "freezeMode": PRESET_FREEZE, - "manualMode": PRESET_NONE + STATE_PRESET_AT_HOME: PRESET_HOME, + STATE_PRESET_AWAY: PRESET_AWAY, + STATE_PRESET_FREEZE: PRESET_FREEZE, + STATE_PRESET_MANUAL: PRESET_NONE, + STATE_PRESET_SLEEPING_MODE: PRESET_SLEEP, } MAP_TARGET_TEMP_KEY = { - HVAC_MODE_AUTO: 'core:TargetTemperatureState', - HVAC_MODE_HEAT: 'core:DerogatedTargetTemperatureState' + HVAC_MODE_AUTO: KEY_TARGET_TEMPERATURE, + HVAC_MODE_HEAT: KEY_DEROGATION_TARGET_TEMPERATURE } -COMMAND_REFRESH = "refreshState" -COMMAND_EXIT_DEROGATION = "exitDerogation" -COMMAND_SET_DEROGATION = "setDerogation" -COMMAND_OPTION_FURTHER_NOTICE = "further_notice" def remove_accents(input_str): @@ -71,7 +90,7 @@ async def async_setup_entry(hass, entry, async_add_entities): for device in data.get("devices"): if TAHOMA_TYPES[device.uiclass] == "climate": - if device.widget == "SomfyThermostat": + if device.widget in SUPPORTED_CLIMATE_DEVICES: entities.append(TahomaClimate(device, controller)) async_add_entities(entities) @@ -98,7 +117,7 @@ def __init__(self, tahoma_device, controller): _LOGGER.debug("humidity sensor: %s", self._humidity_sensor_entity_id) self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] - self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[HVAC_MODE_KEY]] + self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[KEY_HVAC_MODE]] self._preset_mode = MAP_PRESET[self.tahoma_device.active_states[MAP_PRESET_KEY[self._hvac_mode]]] self._preset_modes = [ PRESET_NONE, PRESET_FREEZE, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] @@ -132,10 +151,6 @@ def _async_startup(event): if self._target_temp is None: if old_state.attributes.get(ATTR_TEMPERATURE) is None: self._target_temp = self.min_temp - _LOGGER.warning( - "Undefined target temperature, falling back to %s", - self._target_temp, - ) else: self._target_temp = float(old_state.attributes[ATTR_TEMPERATURE]) if old_state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY: @@ -195,7 +210,7 @@ def update(self): """Update the state.""" self.apply_action(COMMAND_REFRESH) self.controller.get_states([self.tahoma_device]) - self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[HVAC_MODE_KEY]] + self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[KEY_HVAC_MODE]] self._preset_mode = MAP_PRESET[self.tahoma_device.active_states[MAP_PRESET_KEY[self._hvac_mode]]] self._target_temp = self.tahoma_device.active_states[MAP_TARGET_TEMP_KEY[self._hvac_mode]] self.update_temp(None) @@ -216,7 +231,7 @@ def set_hvac_mode(self, hvac_mode: str) -> None: if hvac_mode == HVAC_MODE_AUTO and self._hvac_mode != HVAC_MODE_AUTO: self.apply_action(COMMAND_EXIT_DEROGATION) elif hvac_mode == HVAC_MODE_HEAT and self._hvac_mode != HVAC_MODE_HEAT: - self.apply_action(COMMAND_SET_DEROGATION, self.current_temperature, COMMAND_OPTION_FURTHER_NOTICE) + self.apply_action(COMMAND_SET_DEROGATION, self.current_temperature, STATE_DEROGATION_FURTHER_NOTICE) self.schedule_update_ha_state() @property From 22b767cb0959fbf6b11054681fb7d21c89014cc6 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 8 Jun 2020 08:25:13 +0200 Subject: [PATCH 34/90] Added /Config to ignore list. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 30961fe6e..cb5b41f50 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,6 @@ dmypy.json # Pyre type checker .pyre/ + +# HA Config directory for local testing +/Config/ From 7c7196bbfd329b5332ca8c9a68cb07916117a288 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 8 Jun 2020 10:49:01 +0200 Subject: [PATCH 35/90] Allow None for sensor id. --- custom_components/tahoma/climate.py | 92 +++++++++++++---------------- 1 file changed, 40 insertions(+), 52 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 3f0ada795..79cba279c 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -90,8 +90,18 @@ async def async_setup_entry(hass, entry, async_add_entities): for device in data.get("devices"): if TAHOMA_TYPES[device.uiclass] == "climate": - if device.widget in SUPPORTED_CLIMATE_DEVICES: - entities.append(TahomaClimate(device, controller)) + if device.widget == "SomfyThermostat": + device1 = "sensor." + \ + controller.get_device( + device.url.replace("#1", "#2") + ).label.replace("°", "deg").replace(" ", "_").lower() + device2 = remove_accents("sensor." + \ + controller.get_device( + device.url.replace("#1", "#3") + ).label.replace(" ", "_").lower()) + entities.append(TahomaClimate(device, controller, device1, device2)) + else: + entities.append(TahomaClimate(device, controller) async_add_entities(entities) @@ -99,77 +109,52 @@ async def async_setup_entry(hass, entry, async_add_entities): class TahomaClimate(TahomaDevice, ClimateEntity, RestoreEntity): """Representation of a Tahoma thermostat.""" - def __init__(self, tahoma_device, controller): + def __init__(self, tahoma_device, controller, device1=None, device2=None): """Initialize the sensor.""" super().__init__(tahoma_device, controller) - device1 = "sensor." + \ - self.controller.get_device( - self.tahoma_device.url.replace("#1", "#2") - ).label.replace("°", "deg").replace(" ", "_").lower() self._temp_sensor_entity_id = remove_accents(device1) self._current_temp = None self._target_temp = self.tahoma_device.active_states[MAP_TARGET_TEMP_KEY[self._hvac_mode]] - device2 = "sensor." + \ - self.controller.get_device( - self.tahoma_device.url.replace("#1", "#3") - ).label.replace(" ", "_").lower() - self._humidity_sensor_entity_id = remove_accents(device2) + self._humidity_sensor_entity_id = device2 _LOGGER.debug("humidity sensor: %s", self._humidity_sensor_entity_id) self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[KEY_HVAC_MODE]] - self._preset_mode = MAP_PRESET[self.tahoma_device.active_states[MAP_PRESET_KEY[self._hvac_mode]]] + self._preset_mode = MAP_PRESET[ + self.tahoma_device.active_states[MAP_PRESET_KEY[self._hvac_mode]]] self._preset_modes = [ PRESET_NONE, PRESET_FREEZE, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] self._is_away = None async def async_added_to_hass(self): await super().async_added_to_hass() - - async_track_state_change( - self.hass, self._temp_sensor_entity_id, self._async_temp_sensor_changed - ) - async_track_state_change( - self.hass, self._humidity_sensor_entity_id, self._async_humidity_sensor_changed - ) + if self._temp_sensor_entity_id is not None: + async_track_state_change( + self.hass, self._temp_sensor_entity_id, self._async_temp_sensor_changed + ) + if self._humidity_sensor_entity_id is not None: + async_track_state_change( + self.hass, self._humidity_sensor_entity_id, self._async_humidity_sensor_changed + ) @callback def _async_startup(event): """Init on startup.""" - temp_sensor_state = self.hass.states.get(self._temp_sensor_entity_id) - if temp_sensor_state and temp_sensor_state.state != STATE_UNKNOWN: - self.update_temp(temp_sensor_state) - - humidity_sensor_state = self.hass.states.get(self._humidity_sensor_entity_id) - if humidity_sensor_state and humidity_sensor_state.state != STATE_UNKNOWN: - self.update_humidity(humidity_sensor_state) + if self._temp_sensor_entity_id is not None: + temp_sensor_state = self.hass.states.get(self._temp_sensor_entity_id) + if temp_sensor_state and temp_sensor_state.state != STATE_UNKNOWN: + self.update_temp(temp_sensor_state) + if self._humidity_sensor_entity_id is not None: + humidity_sensor_state = self.hass.states.get(self._humidity_sensor_entity_id) + if humidity_sensor_state and humidity_sensor_state.state != STATE_UNKNOWN: + self.update_humidity(humidity_sensor_state) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) - old_state = await self.async_get_last_state() - if old_state is not None: - if self._target_temp is None: - if old_state.attributes.get(ATTR_TEMPERATURE) is None: - self._target_temp = self.min_temp - else: - self._target_temp = float(old_state.attributes[ATTR_TEMPERATURE]) - if old_state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY: - self._is_away = True - if not self._hvac_mode and old_state.state: - self._hvac_mode = old_state.state - - else: - if self._target_temp is None: - self._target_temp = self.min_temp - _LOGGER.warning( - "No previously saved temperature, setting to %s", self._target_temp - ) - - if not self._hvac_mode: - self._hvac_mode = HVAC_MODE_HEAT self.schedule_update_ha_state() - async def _async_temp_sensor_changed(self, entity_id: str, old_state: State, new_state: State) -> None: + async def _async_temp_sensor_changed(self, entity_id: str, old_state: State, + new_state: State) -> None: """Handle temperature changes.""" if new_state is None: return @@ -188,7 +173,8 @@ def update_temp(self, state): except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) - async def _async_humidity_sensor_changed(self, entity_id: str, old_state: State, new_state: State) -> None: + async def _async_humidity_sensor_changed(self, entity_id: str, old_state: State, + new_state: State) -> None: """Handle temperature changes.""" if new_state is None: return @@ -211,7 +197,8 @@ def update(self): self.apply_action(COMMAND_REFRESH) self.controller.get_states([self.tahoma_device]) self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[KEY_HVAC_MODE]] - self._preset_mode = MAP_PRESET[self.tahoma_device.active_states[MAP_PRESET_KEY[self._hvac_mode]]] + self._preset_mode = MAP_PRESET[ + self.tahoma_device.active_states[MAP_PRESET_KEY[self._hvac_mode]]] self._target_temp = self.tahoma_device.active_states[MAP_TARGET_TEMP_KEY[self._hvac_mode]] self.update_temp(None) self.update_humidity(None) @@ -231,7 +218,8 @@ def set_hvac_mode(self, hvac_mode: str) -> None: if hvac_mode == HVAC_MODE_AUTO and self._hvac_mode != HVAC_MODE_AUTO: self.apply_action(COMMAND_EXIT_DEROGATION) elif hvac_mode == HVAC_MODE_HEAT and self._hvac_mode != HVAC_MODE_HEAT: - self.apply_action(COMMAND_SET_DEROGATION, self.current_temperature, STATE_DEROGATION_FURTHER_NOTICE) + self.apply_action(COMMAND_SET_DEROGATION, self.current_temperature, + STATE_DEROGATION_FURTHER_NOTICE) self.schedule_update_ha_state() @property From 0823ed4f8379259fa120c50045e5dae3581687c0 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 8 Jun 2020 11:01:03 +0200 Subject: [PATCH 36/90] Allow None for sensor id. --- custom_components/tahoma/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 79cba279c..2f30254ef 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -100,8 +100,8 @@ async def async_setup_entry(hass, entry, async_add_entities): device.url.replace("#1", "#3") ).label.replace(" ", "_").lower()) entities.append(TahomaClimate(device, controller, device1, device2)) - else: - entities.append(TahomaClimate(device, controller) + elif device.widget in SUPPORTED_CLIMATE_DEVICES: + entities.append(TahomaClimate(device, controller)) async_add_entities(entities) @@ -114,7 +114,6 @@ def __init__(self, tahoma_device, controller, device1=None, device2=None): super().__init__(tahoma_device, controller) self._temp_sensor_entity_id = remove_accents(device1) self._current_temp = None - self._target_temp = self.tahoma_device.active_states[MAP_TARGET_TEMP_KEY[self._hvac_mode]] self._humidity_sensor_entity_id = device2 _LOGGER.debug("humidity sensor: %s", self._humidity_sensor_entity_id) self._current_humidity = None @@ -124,6 +123,7 @@ def __init__(self, tahoma_device, controller, device1=None, device2=None): self.tahoma_device.active_states[MAP_PRESET_KEY[self._hvac_mode]]] self._preset_modes = [ PRESET_NONE, PRESET_FREEZE, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] + self._target_temp = self.tahoma_device.active_states[MAP_TARGET_TEMP_KEY[self._hvac_mode]] self._is_away = None async def async_added_to_hass(self): From 9e7fdb0918dba6fbf7c7ea385f7e1aef8f403666 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Tue, 9 Jun 2020 15:13:11 +0200 Subject: [PATCH 37/90] only refresh if command exists. --- custom_components/tahoma/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 2f30254ef..c8b4036cb 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -194,7 +194,8 @@ def update_humidity(self, state): def update(self): """Update the state.""" - self.apply_action(COMMAND_REFRESH) + if COMMAND_REFRESH in self.tahoma_device.command_definitions: + self.apply_action(COMMAND_REFRESH) self.controller.get_states([self.tahoma_device]) self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[KEY_HVAC_MODE]] self._preset_mode = MAP_PRESET[ From fa35e2ad8bd5909b5b4aa6a378676c30d6017c98 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Thu, 11 Jun 2020 16:04:27 +0200 Subject: [PATCH 38/90] Fiest step to adding an option flow to select the temperature sensor. --- custom_components/tahoma/climate.py | 57 +-------- custom_components/tahoma/config_flow.py | 111 +++++++++++++++++- custom_components/tahoma/strings.json | 10 ++ custom_components/tahoma/translations/en.json | 10 ++ 4 files changed, 132 insertions(+), 56 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index c8b4036cb..816aaef00 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -75,11 +75,6 @@ } -def remove_accents(input_str): - nfkd_form = unicodedata.normalize('NFKD', input_str) - return u"".join([c for c in nfkd_form if not unicodedata.combining(c)]) - - async def async_setup_entry(hass, entry, async_add_entities): """Set up the Tahoma sensors from a config entry.""" @@ -90,16 +85,9 @@ async def async_setup_entry(hass, entry, async_add_entities): for device in data.get("devices"): if TAHOMA_TYPES[device.uiclass] == "climate": - if device.widget == "SomfyThermostat": - device1 = "sensor." + \ - controller.get_device( - device.url.replace("#1", "#2") - ).label.replace("°", "deg").replace(" ", "_").lower() - device2 = remove_accents("sensor." + \ - controller.get_device( - device.url.replace("#1", "#3") - ).label.replace(" ", "_").lower()) - entities.append(TahomaClimate(device, controller, device1, device2)) + if device.url in entry.data and device.widget == "SomfyThermostat": + sensor_id = entry.data[device.url] + entities.append(TahomaClimate(device, controller, sensor_id)) elif device.widget in SUPPORTED_CLIMATE_DEVICES: entities.append(TahomaClimate(device, controller)) @@ -109,13 +97,11 @@ async def async_setup_entry(hass, entry, async_add_entities): class TahomaClimate(TahomaDevice, ClimateEntity, RestoreEntity): """Representation of a Tahoma thermostat.""" - def __init__(self, tahoma_device, controller, device1=None, device2=None): + def __init__(self, tahoma_device, controller, sensor_id=None): """Initialize the sensor.""" super().__init__(tahoma_device, controller) - self._temp_sensor_entity_id = remove_accents(device1) + self._temp_sensor_entity_id = sensor_id self._current_temp = None - self._humidity_sensor_entity_id = device2 - _LOGGER.debug("humidity sensor: %s", self._humidity_sensor_entity_id) self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[KEY_HVAC_MODE]] @@ -132,10 +118,6 @@ async def async_added_to_hass(self): async_track_state_change( self.hass, self._temp_sensor_entity_id, self._async_temp_sensor_changed ) - if self._humidity_sensor_entity_id is not None: - async_track_state_change( - self.hass, self._humidity_sensor_entity_id, self._async_humidity_sensor_changed - ) @callback def _async_startup(event): @@ -144,10 +126,6 @@ def _async_startup(event): temp_sensor_state = self.hass.states.get(self._temp_sensor_entity_id) if temp_sensor_state and temp_sensor_state.state != STATE_UNKNOWN: self.update_temp(temp_sensor_state) - if self._humidity_sensor_entity_id is not None: - humidity_sensor_state = self.hass.states.get(self._humidity_sensor_entity_id) - if humidity_sensor_state and humidity_sensor_state.state != STATE_UNKNOWN: - self.update_humidity(humidity_sensor_state) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) @@ -173,25 +151,6 @@ def update_temp(self, state): except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) - async def _async_humidity_sensor_changed(self, entity_id: str, old_state: State, - new_state: State) -> None: - """Handle temperature changes.""" - if new_state is None: - return - - self.update_humidity(new_state) - self.schedule_update_ha_state() - - @callback - def update_humidity(self, state): - """Update thermostat with latest state from sensor.""" - if state is None: - state = self.hass.states.get(self._humidity_sensor_entity_id) - try: - self._current_humidity = float(state.state) - except ValueError as ex: - _LOGGER.error("Unable to update from sensor: %s", ex) - def update(self): """Update the state.""" if COMMAND_REFRESH in self.tahoma_device.command_definitions: @@ -202,7 +161,6 @@ def update(self): self.tahoma_device.active_states[MAP_PRESET_KEY[self._hvac_mode]]] self._target_temp = self.tahoma_device.active_states[MAP_TARGET_TEMP_KEY[self._hvac_mode]] self.update_temp(None) - self.update_humidity(None) @property def hvac_mode(self) -> str: @@ -248,11 +206,6 @@ def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" raise NotImplementedError() # TODO implement - @property - def humidity_sensor(self) -> str: - """Return the id of the temperature sensor""" - return self._humidity_sensor_entity_id - @property def current_humidity(self) -> Optional[float]: """Return the current humidity""" diff --git a/custom_components/tahoma/config_flow.py b/custom_components/tahoma/config_flow.py index 83e23884f..31672b5b2 100644 --- a/custom_components/tahoma/config_flow.py +++ b/custom_components/tahoma/config_flow.py @@ -1,12 +1,19 @@ """Config flow for TaHoma integration.""" +import copy import logging import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME - -from .const import DOMAIN # pylint:disable=unused-import +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + CONF_ENTITY_ID, + DEVICE_CLASS_TEMPERATURE +) +from homeassistant.core import callback + +from .const import DOMAIN, TAHOMA_TYPES, TAHOMA_TYPE_HEATING_SYSTEM # pylint:disable=unused-import from .tahoma_api import TahomaApi from requests.exceptions import RequestException @@ -34,13 +41,22 @@ async def validate_input(hass: core.HomeAssistant, data): _LOGGER.exception("Error when trying to log in to the TaHoma API") raise CannotConnect + return_dict = {"title": username} + controller.get_setup() + devices = controller.get_devices() + for key, device in devices.items(): + if device.uiclass == TAHOMA_TYPE_HEATING_SYSTEM: + if TAHOMA_TYPE_HEATING_SYSTEM not in return_dict: + return_dict[TAHOMA_TYPE_HEATING_SYSTEM] = {} + return_dict[TAHOMA_TYPE_HEATING_SYSTEM][device.url] = device.label + # If you cannot connect: # throw CannotConnect # If the authentication is wrong: # InvalidAuth # Return info that you want to store in the config entry. - return {"title": username} + return return_dict class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -49,6 +65,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + @staticmethod + @callback + def async_get_options_flow(config_entry): + return ThermoOptionsFlowHandler(config_entry) + + def __init__(self): + self._user_input = {} + self._thermos = {} + async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} @@ -60,6 +85,11 @@ async def async_step_user(self, user_input=None): try: info = await validate_input(self.hass, user_input) + if TAHOMA_TYPE_HEATING_SYSTEM in info: + user_input[TAHOMA_TYPE_HEATING_SYSTEM] = info[TAHOMA_TYPE_HEATING_SYSTEM] + # self._user_input = user_input + # self._thermos = info[TAHOMA_TYPE_HEATING_SYSTEM] + # return await self.async_step_thermo() return self.async_create_entry(title=info["title"], data=user_input) except CannotConnect: @@ -74,6 +104,79 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + # async def async_step_thermo(self, user_input=None): + # """Handle thermo step""" + # errors = {} + # device_url = None + # device_name = None + # data = self._user_input + # thermos = self._thermos + # + # device_url = next(iter(thermos.keys())) + # device_name = next(iter(thermos.values())) + # + # if user_input is not None: + # data[device_url] = next(iter(user_input.values())) + # thermos.pop(device_url) + # if len(thermos) > 0: + # return await self.async_step_thermo() + # return self.async_create_entry(title=data[CONF_USERNAME], data=data) + # + # return self.async_show_form( + # step_id="thermo", + # data_schema=vol.Schema({ + # vol.Required(CONF_ENTITY_ID, description={"suggested_value": device_name}): str + # }), + # errors=errors + # ) + + +class ThermoOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Tahoma options for thermostat.""" + + def __init__(self, config_entry): + """Initialize Tahoma options flow.""" + self.options = copy.deepcopy(dict(config_entry.options)) + self.data = copy.deepcopy(dict(config_entry.data)) + + async def async_step_init(self, user_input=None): + """Manage the options.""" + errors = {} + + if user_input is not None: + try: + for k, v in user_input.items(): + for u, l in self.data[TAHOMA_TYPE_HEATING_SYSTEM].items(): + if v == l: + raise Exception("Invalid sensor", "Please select a sensor from the list") + self.data[DEVICE_CLASS_TEMPERATURE] = user_input + return self.async_create_entry(title="", data=self.data) + + except Exception as e: # pylint: disable=broad-except + _LOGGER.exception(str(e)) + errors["base"] = str(e) + + available_sensors = [] + for k, v in self.hass.data['entity_registry'].entities.items(): + if str.startswith(k, "sensor") and v.device_class == DEVICE_CLASS_TEMPERATURE: + available_sensors.append(k) + + schema = {} + if TAHOMA_TYPE_HEATING_SYSTEM in self.data: + for k, v in self.data[TAHOMA_TYPE_HEATING_SYSTEM].items(): + key = vol.Required( + k, + default=v, + msg="temperature sensor for "+v) + value = vol.In([v]+available_sensors) + schema[key] = value + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema(schema), + ) + + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/custom_components/tahoma/strings.json b/custom_components/tahoma/strings.json index 2820a44e9..0c51d12bb 100644 --- a/custom_components/tahoma/strings.json +++ b/custom_components/tahoma/strings.json @@ -17,5 +17,15 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "options": { + "step": { + "init": { + "description": "Select temperature sensors for thermostats", + "data": { + "_": "entity" + } + } + } } } \ No newline at end of file diff --git a/custom_components/tahoma/translations/en.json b/custom_components/tahoma/translations/en.json index e063eec96..20d23c51b 100644 --- a/custom_components/tahoma/translations/en.json +++ b/custom_components/tahoma/translations/en.json @@ -17,5 +17,15 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "options": { + "step": { + "init": { + "description": "Select temperature sensors for thermostats", + "data": { + "_": "entity" + } + } + } } } \ No newline at end of file From 645b1f9078358d21204cfb390796006dfea62aea Mon Sep 17 00:00:00 2001 From: vlebourl Date: Thu, 11 Jun 2020 16:18:32 +0200 Subject: [PATCH 39/90] Added a better error message. --- custom_components/tahoma/config_flow.py | 29 +++++++++++++------ custom_components/tahoma/strings.json | 3 ++ custom_components/tahoma/translations/en.json | 7 +++-- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/custom_components/tahoma/config_flow.py b/custom_components/tahoma/config_flow.py index 31672b5b2..80a7281a4 100644 --- a/custom_components/tahoma/config_flow.py +++ b/custom_components/tahoma/config_flow.py @@ -131,6 +131,13 @@ async def async_step_user(self, user_input=None): # ) +async def validate_options_input(hass: core.HomeAssistant, data): + for k, v in data.items(): + if not str.startswith(v, "sensor"): + _LOGGER.exception("Please select a valid sensor from the list") + raise InvalidSensor + + class ThermoOptionsFlowHandler(config_entries.OptionsFlow): """Handle Tahoma options for thermostat.""" @@ -145,16 +152,15 @@ async def async_step_init(self, user_input=None): if user_input is not None: try: - for k, v in user_input.items(): - for u, l in self.data[TAHOMA_TYPE_HEATING_SYSTEM].items(): - if v == l: - raise Exception("Invalid sensor", "Please select a sensor from the list") + await validate_options_input(self.hass, user_input) self.data[DEVICE_CLASS_TEMPERATURE] = user_input return self.async_create_entry(title="", data=self.data) - except Exception as e: # pylint: disable=broad-except - _LOGGER.exception(str(e)) - errors["base"] = str(e) + except InvalidSensor: + errors["base"] = "invalid_sensor" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" available_sensors = [] for k, v in self.hass.data['entity_registry'].entities.items(): @@ -167,13 +173,14 @@ async def async_step_init(self, user_input=None): key = vol.Required( k, default=v, - msg="temperature sensor for "+v) - value = vol.In([v]+available_sensors) + msg="temperature sensor for " + v) + value = vol.In([v] + available_sensors) schema[key] = value return self.async_show_form( step_id="init", data_schema=vol.Schema(schema), + errors=errors ) @@ -183,3 +190,7 @@ class CannotConnect(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class InvalidSensor(exceptions.HomeAssistantError): + """Error to indicate the selection is not a sensor.""" diff --git a/custom_components/tahoma/strings.json b/custom_components/tahoma/strings.json index 0c51d12bb..ae22949d5 100644 --- a/custom_components/tahoma/strings.json +++ b/custom_components/tahoma/strings.json @@ -26,6 +26,9 @@ "_": "entity" } } + }, + "error": { + "invalid_sensor": "Please select a valid sensor from the list" } } } \ No newline at end of file diff --git a/custom_components/tahoma/translations/en.json b/custom_components/tahoma/translations/en.json index 20d23c51b..ae22949d5 100644 --- a/custom_components/tahoma/translations/en.json +++ b/custom_components/tahoma/translations/en.json @@ -4,8 +4,8 @@ "user": { "description": "Enter your TaHoma credentials for the TaHoma App or the tahomalink.com platform.", "data": { - "username": "Email address", - "password": "Password" + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } } }, @@ -26,6 +26,9 @@ "_": "entity" } } + }, + "error": { + "invalid_sensor": "Please select a valid sensor from the list" } } } \ No newline at end of file From 21be394c3bc4338ef3ca5df2679563ff4047f2b5 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Thu, 11 Jun 2020 16:43:10 +0200 Subject: [PATCH 40/90] save chosen option. --- custom_components/tahoma/config_flow.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/custom_components/tahoma/config_flow.py b/custom_components/tahoma/config_flow.py index 80a7281a4..f3c00c7e8 100644 --- a/custom_components/tahoma/config_flow.py +++ b/custom_components/tahoma/config_flow.py @@ -143,8 +143,8 @@ class ThermoOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry): """Initialize Tahoma options flow.""" + self.config_entry = config_entry self.options = copy.deepcopy(dict(config_entry.options)) - self.data = copy.deepcopy(dict(config_entry.data)) async def async_step_init(self, user_input=None): """Manage the options.""" @@ -153,8 +153,8 @@ async def async_step_init(self, user_input=None): if user_input is not None: try: await validate_options_input(self.hass, user_input) - self.data[DEVICE_CLASS_TEMPERATURE] = user_input - return self.async_create_entry(title="", data=self.data) + self.options[DEVICE_CLASS_TEMPERATURE] = user_input + return self.async_create_entry(title="", data=self.options) except InvalidSensor: errors["base"] = "invalid_sensor" @@ -168,11 +168,14 @@ async def async_step_init(self, user_input=None): available_sensors.append(k) schema = {} - if TAHOMA_TYPE_HEATING_SYSTEM in self.data: - for k, v in self.data[TAHOMA_TYPE_HEATING_SYSTEM].items(): + if TAHOMA_TYPE_HEATING_SYSTEM in self.config_entry.data: + for k, v in self.config_entry.data[TAHOMA_TYPE_HEATING_SYSTEM].items(): + default = self.config_entry.options.get(DEVICE_CLASS_TEMPERATURE).get(k) + if default is None: + default = v key = vol.Required( k, - default=v, + default=default, msg="temperature sensor for " + v) value = vol.In([v] + available_sensors) schema[key] = value From 8fbb7da15deec03d098f7c747569c873e765341e Mon Sep 17 00:00:00 2001 From: vlebourl Date: Thu, 11 Jun 2020 17:27:30 +0200 Subject: [PATCH 41/90] Update temperature sensor id according to the option. --- custom_components/tahoma/climate.py | 44 +++++++++++++++++++------ custom_components/tahoma/config_flow.py | 3 +- custom_components/tahoma/const.py | 2 -- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 816aaef00..f48bff4df 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -2,13 +2,16 @@ from datetime import timedelta import logging from typing import List, Optional -import unicodedata from homeassistant.core import callback, State from homeassistant.helpers.event import async_track_state_change -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, EVENT_HOMEASSISTANT_START, \ - STATE_UNKNOWN +from homeassistant.const import ( + ATTR_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + EVENT_HOMEASSISTANT_START, + STATE_UNKNOWN, + TEMP_CELSIUS, +) from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, @@ -18,10 +21,14 @@ PRESET_NONE, PRESET_SLEEP, SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE, ATTR_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + ATTR_PRESET_MODE, ) -from .const import DOMAIN, TAHOMA_TYPES +from .const import ( + DOMAIN, + TAHOMA_TYPES, +) from .tahoma_device import TahomaDevice _LOGGER = logging.getLogger(__name__) @@ -73,6 +80,7 @@ HVAC_MODE_AUTO: KEY_TARGET_TEMPERATURE, HVAC_MODE_HEAT: KEY_DEROGATION_TARGET_TEMPERATURE } +TAHOMA_TYPE_HEATING_SYSTEM = "HeatingSystem" async def async_setup_entry(hass, entry, async_add_entities): @@ -80,21 +88,32 @@ async def async_setup_entry(hass, entry, async_add_entities): data = hass.data[DOMAIN][entry.entry_id] + entry.add_update_listener(update_listener) + entities = [] controller = data.get("controller") for device in data.get("devices"): if TAHOMA_TYPES[device.uiclass] == "climate": - if device.url in entry.data and device.widget == "SomfyThermostat": - sensor_id = entry.data[device.url] + options = dict(entry.options) + if device.url in options[TAHOMA_TYPE_HEATING_SYSTEM] and device.widget == "SomfyThermostat": + sensor_id = options[DEVICE_CLASS_TEMPERATURE][device.url] entities.append(TahomaClimate(device, controller, sensor_id)) elif device.widget in SUPPORTED_CLIMATE_DEVICES: entities.append(TahomaClimate(device, controller)) async_add_entities(entities) +async def update_listener(hass, entry): + """Handle options update.""" + options = dict(entry.options) + for entity in hass.data["climate"].entities: + if entity.unique_id in options[TAHOMA_TYPE_HEATING_SYSTEM]: + entity.set_temperature_sensor(options[DEVICE_CLASS_TEMPERATURE][entity.unique_id]) + entity.update_temp() + -class TahomaClimate(TahomaDevice, ClimateEntity, RestoreEntity): +class TahomaClimate(TahomaDevice, ClimateEntity): """Representation of a Tahoma thermostat.""" def __init__(self, tahoma_device, controller, sensor_id=None): @@ -102,6 +121,8 @@ def __init__(self, tahoma_device, controller, sensor_id=None): super().__init__(tahoma_device, controller) self._temp_sensor_entity_id = sensor_id self._current_temp = None + if self._temp_sensor_entity_id is not None: + self.update_temp() self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[KEY_HVAC_MODE]] @@ -141,7 +162,7 @@ async def _async_temp_sensor_changed(self, entity_id: str, old_state: State, self.schedule_update_ha_state() @callback - def update_temp(self, state): + def update_temp(self, state=None): """Update thermostat with latest state from sensor.""" if state is None: state = self.hass.states.get(self._temp_sensor_entity_id) @@ -216,6 +237,9 @@ def temperature_sensor(self) -> str: """Return the id of the temperature sensor""" return self._temp_sensor_entity_id + def set_temperature_sensor(self, sensor_id: str): + self._temp_sensor_entity_id = sensor_id + @property def temperature_unit(self) -> str: """Return the unit of measurement used by the platform.""" diff --git a/custom_components/tahoma/config_flow.py b/custom_components/tahoma/config_flow.py index f3c00c7e8..152b50e06 100644 --- a/custom_components/tahoma/config_flow.py +++ b/custom_components/tahoma/config_flow.py @@ -13,7 +13,7 @@ ) from homeassistant.core import callback -from .const import DOMAIN, TAHOMA_TYPES, TAHOMA_TYPE_HEATING_SYSTEM # pylint:disable=unused-import +from .const import DOMAIN, TAHOMA_TYPES # pylint:disable=unused-import from .tahoma_api import TahomaApi from requests.exceptions import RequestException @@ -137,6 +137,7 @@ async def validate_options_input(hass: core.HomeAssistant, data): _LOGGER.exception("Please select a valid sensor from the list") raise InvalidSensor +TAHOMA_TYPE_HEATING_SYSTEM = "HeatingSystem" class ThermoOptionsFlowHandler(config_entries.OptionsFlow): """Handle Tahoma options for thermostat.""" diff --git a/custom_components/tahoma/const.py b/custom_components/tahoma/const.py index ebdfd3348..6d2034de5 100644 --- a/custom_components/tahoma/const.py +++ b/custom_components/tahoma/const.py @@ -43,8 +43,6 @@ "Gate": "cover" } -TAHOMA_TYPE_HEATING_SYSTEM = "HeatingSystem" - # Used to map the Somfy widget or uiClass to the Home Assistant device classes TAHOMA_COVER_DEVICE_CLASSES = { "Awning": DEVICE_CLASS_AWNING, From 279c944ded5c676fef821ae1523e567a3a10a807 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 12 Jun 2020 09:04:34 +0200 Subject: [PATCH 42/90] update climate.py --- custom_components/tahoma/climate.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index f48bff4df..aebcba9bf 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -110,7 +110,7 @@ async def update_listener(hass, entry): for entity in hass.data["climate"].entities: if entity.unique_id in options[TAHOMA_TYPE_HEATING_SYSTEM]: entity.set_temperature_sensor(options[DEVICE_CLASS_TEMPERATURE][entity.unique_id]) - entity.update_temp() + entity.schedule_update_ha_state() class TahomaClimate(TahomaDevice, ClimateEntity): @@ -121,9 +121,6 @@ def __init__(self, tahoma_device, controller, sensor_id=None): super().__init__(tahoma_device, controller) self._temp_sensor_entity_id = sensor_id self._current_temp = None - if self._temp_sensor_entity_id is not None: - self.update_temp() - self._current_humidity = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[KEY_HVAC_MODE]] self._preset_mode = MAP_PRESET[ @@ -227,11 +224,6 @@ def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" raise NotImplementedError() # TODO implement - @property - def current_humidity(self) -> Optional[float]: - """Return the current humidity""" - return self._current_humidity - @property def temperature_sensor(self) -> str: """Return the id of the temperature sensor""" From 7209598b4244293b67f7083092afc8298516160e Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 12 Jun 2020 09:09:43 +0200 Subject: [PATCH 43/90] Tahoma seems to not send all possible keys, depending on the devices states, we should add the unknown key instead of raising an error. --- custom_components/tahoma/tahoma_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/tahoma/tahoma_api.py b/custom_components/tahoma/tahoma_api.py index 183d9207c..955b520aa 100644 --- a/custom_components/tahoma/tahoma_api.py +++ b/custom_components/tahoma/tahoma_api.py @@ -650,7 +650,7 @@ def active_states(self): def set_active_state(self, name, value): """Set active state.""" if name not in self.__active_states.keys(): - raise ValueError("Can not set unknown state '" + name + "'") + self.__active_states[name] = value if (isinstance(self.__active_states[name], int) and isinstance(value, str)): From d69c2664f20dd89b42569faec08615b742f04207 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 12 Jun 2020 09:41:46 +0200 Subject: [PATCH 44/90] added some device info. --- custom_components/tahoma/climate.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index aebcba9bf..332afec77 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -119,6 +119,9 @@ class TahomaClimate(TahomaDevice, ClimateEntity): def __init__(self, tahoma_device, controller, sensor_id=None): """Initialize the sensor.""" super().__init__(tahoma_device, controller) + self._uiclass = tahoma_device.uiclass + self._unique_id = tahoma_device.url + self._widget = tahoma_device.widget self._temp_sensor_entity_id = sensor_id self._current_temp = None self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] @@ -180,6 +183,20 @@ def update(self): self._target_temp = self.tahoma_device.active_states[MAP_TARGET_TEMP_KEY[self._hvac_mode]] self.update_temp(None) + @property + def device_info(self): + """Return the device info for the thermostat/valve.""" + return { + "url": self._unique_id, + "uiClass": self._uiclass, + "widget": self._widget, + } + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" From 0d1d25dc709eee625939e41866a5a2006b60071e Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 12 Jun 2020 11:50:22 +0200 Subject: [PATCH 45/90] implemented set_temperature. --- custom_components/tahoma/climate.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 332afec77..11f83cc93 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -167,6 +167,9 @@ def update_temp(self, state=None): if state is None: state = self.hass.states.get(self._temp_sensor_entity_id) + if state.state == "unknown": + return float("nan") + try: self._current_temp = float(state.state) except ValueError as ex: @@ -269,5 +272,11 @@ def set_temperature(self, **kwargs) -> None: temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return + if temperature < 15: + self.apply_action("setDerogation", "freezeMode", "further_notice") + if temperature > 26: + temperature = 26 self._target_temp = temperature + self.apply_action("setDerogation", temperature, "further_notice") + self.apply_action("setModeTemperature", "manualMode", temperature) self.schedule_update_ha_state() From b11305b18e2014a36dd56e6f938340c4e7220a22 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 12 Jun 2020 15:57:11 +0200 Subject: [PATCH 46/90] implemented climate for somfy's smart thermostat. --- custom_components/tahoma/climate.py | 211 ++++++++++++++++++---------- 1 file changed, 135 insertions(+), 76 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 11f83cc93..390132dc8 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -1,6 +1,7 @@ """Support for Tahoma climate.""" from datetime import timedelta import logging +from math import nan from typing import List, Optional from homeassistant.core import callback, State @@ -9,11 +10,16 @@ ATTR_TEMPERATURE, DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS, ) from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_HEAT, HVAC_MODE_AUTO, PRESET_AWAY, @@ -23,6 +29,7 @@ SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ATTR_PRESET_MODE, + HVAC_MODE_OFF, ) from .const import ( @@ -36,7 +43,8 @@ SCAN_INTERVAL = timedelta(seconds=120) SUPPORTED_CLIMATE_DEVICES = [ - "SomfyThermostat" + "SomfyThermostat", + # "AtlanticElectricalHeater" ] COMMAND_REFRESH = "refreshState" @@ -63,11 +71,9 @@ MAP_HVAC_MODE = { STATE_DEROGATION_DATE: HVAC_MODE_AUTO, STATE_DEROGATION_NEXT_MODE: HVAC_MODE_HEAT, - STATE_DEROGATION_FURTHER_NOTICE: HVAC_MODE_HEAT -} -MAP_PRESET_KEY = { - HVAC_MODE_AUTO: KEY_HEATING_MODE, - HVAC_MODE_HEAT: KEY_DEROGATION_HEATING_MODE + STATE_DEROGATION_FURTHER_NOTICE: HVAC_MODE_HEAT, + STATE_ON: HVAC_MODE_HEAT, + STATE_OFF: HVAC_MODE_OFF, } MAP_PRESET = { STATE_PRESET_AT_HOME: PRESET_HOME, @@ -76,9 +82,12 @@ STATE_PRESET_MANUAL: PRESET_NONE, STATE_PRESET_SLEEPING_MODE: PRESET_SLEEP, } -MAP_TARGET_TEMP_KEY = { - HVAC_MODE_AUTO: KEY_TARGET_TEMPERATURE, - HVAC_MODE_HEAT: KEY_DEROGATION_TARGET_TEMPERATURE +MAP_PRESET_REVERSE = { + PRESET_HOME: STATE_PRESET_AT_HOME, + PRESET_AWAY: STATE_PRESET_AWAY, + PRESET_FREEZE: STATE_PRESET_FREEZE, + PRESET_NONE: STATE_PRESET_MANUAL, + PRESET_SLEEP: STATE_PRESET_SLEEPING_MODE, } TAHOMA_TYPE_HEATING_SYSTEM = "HeatingSystem" @@ -94,16 +103,17 @@ async def async_setup_entry(hass, entry, async_add_entities): controller = data.get("controller") for device in data.get("devices"): - if TAHOMA_TYPES[device.uiclass] == "climate": + if TAHOMA_TYPES[device.uiclass] == "climate" and device.widget in SUPPORTED_CLIMATE_DEVICES: options = dict(entry.options) - if device.url in options[TAHOMA_TYPE_HEATING_SYSTEM] and device.widget == "SomfyThermostat": + if device.url in options[TAHOMA_TYPE_HEATING_SYSTEM]: sensor_id = options[DEVICE_CLASS_TEMPERATURE][device.url] entities.append(TahomaClimate(device, controller, sensor_id)) - elif device.widget in SUPPORTED_CLIMATE_DEVICES: + else: entities.append(TahomaClimate(device, controller)) async_add_entities(entities) + async def update_listener(hass, entry): """Handle options update.""" options = dict(entry.options) @@ -119,18 +129,34 @@ class TahomaClimate(TahomaDevice, ClimateEntity): def __init__(self, tahoma_device, controller, sensor_id=None): """Initialize the sensor.""" super().__init__(tahoma_device, controller) + if COMMAND_REFRESH in self.tahoma_device.command_definitions: + self.apply_action(COMMAND_REFRESH) + self.controller.get_states([self.tahoma_device]) self._uiclass = tahoma_device.uiclass self._unique_id = tahoma_device.url self._widget = tahoma_device.widget self._temp_sensor_entity_id = sensor_id - self._current_temp = None - self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] - self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[KEY_HVAC_MODE]] - self._preset_mode = MAP_PRESET[ - self.tahoma_device.active_states[MAP_PRESET_KEY[self._hvac_mode]]] - self._preset_modes = [ - PRESET_NONE, PRESET_FREEZE, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME] - self._target_temp = self.tahoma_device.active_states[MAP_TARGET_TEMP_KEY[self._hvac_mode]] + self._current_temperature = 0 + if self._widget == "SomfyThermostat": + self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] + self._hvac_mode = MAP_HVAC_MODE[ + self.tahoma_device.active_states['somfythermostat:DerogationTypeState'] + ] + self._current_hvac_modes = CURRENT_HVAC_IDLE + self._preset_mode = MAP_PRESET[ + self.tahoma_device.active_states['somfythermostat:HeatingModeState'] + if self._hvac_mode == HVAC_MODE_AUTO + else self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState'] + ] + self._preset_modes = [ + PRESET_NONE, PRESET_FREEZE, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME + ] + self._target_temp = ( + self.tahoma_device.active_states['core:TargetTemperatureState'] + if self._hvac_mode == HVAC_MODE_AUTO + else self.tahoma_device.active_states['core:DerogatedTargetTemperatureState'] + ) + self._stored_target_temp = self._target_temp self._is_away = None async def async_added_to_hass(self): @@ -150,7 +176,7 @@ def _async_startup(event): self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) - self.schedule_update_ha_state() + self.schedule_update_ha_state(True) async def _async_temp_sensor_changed(self, entity_id: str, old_state: State, new_state: State) -> None: @@ -167,11 +193,11 @@ def update_temp(self, state=None): if state is None: state = self.hass.states.get(self._temp_sensor_entity_id) - if state.state == "unknown": - return float("nan") - try: - self._current_temp = float(state.state) + self._current_temperature = ( + None if state.state == STATE_UNKNOWN + else float(state.state) + ) except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) @@ -180,25 +206,31 @@ def update(self): if COMMAND_REFRESH in self.tahoma_device.command_definitions: self.apply_action(COMMAND_REFRESH) self.controller.get_states([self.tahoma_device]) - self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[KEY_HVAC_MODE]] - self._preset_mode = MAP_PRESET[ - self.tahoma_device.active_states[MAP_PRESET_KEY[self._hvac_mode]]] - self._target_temp = self.tahoma_device.active_states[MAP_TARGET_TEMP_KEY[self._hvac_mode]] self.update_temp(None) + if self._widget == "SomfyThermostat": + self._hvac_mode = MAP_HVAC_MODE[ + self.tahoma_device.active_states['somfythermostat:DerogationTypeState'] + ] + self._target_temp = ( + self.tahoma_device.active_states['core:TargetTemperatureState'] + if self._hvac_mode == HVAC_MODE_AUTO + else self.tahoma_device.active_states['core:DerogatedTargetTemperatureState'] + ) + self._preset_mode = MAP_PRESET[ + self.tahoma_device.active_states['somfythermostat:HeatingModeState'] + if self._hvac_mode == HVAC_MODE_AUTO + else self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState'] + ] + self._current_hvac_modes = ( + CURRENT_HVAC_IDLE + if self._current_temperature is None or self._current_temperature > self._target_temp + else CURRENT_HVAC_HEAT + ) @property - def device_info(self): - """Return the device info for the thermostat/valve.""" - return { - "url": self._unique_id, - "uiClass": self._uiclass, - "widget": self._widget, - } - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id + def available(self) -> bool: + """If the device hasn't been able to connect, mark as unavailable.""" + return bool(self._current_temperature == 0) @property def hvac_mode(self) -> str: @@ -210,40 +242,28 @@ def hvac_modes(self) -> List[str]: """Return the list of available hvac operation modes.""" return self._hvac_modes + @property + def hvac_action(self) -> Optional[str]: + """Return the current running hvac operation if supported.""" + return self._current_hvac_modes + def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - if hvac_mode == HVAC_MODE_AUTO and self._hvac_mode != HVAC_MODE_AUTO: - self.apply_action(COMMAND_EXIT_DEROGATION) - elif hvac_mode == HVAC_MODE_HEAT and self._hvac_mode != HVAC_MODE_HEAT: - self.apply_action(COMMAND_SET_DEROGATION, self.current_temperature, - STATE_DEROGATION_FURTHER_NOTICE) - self.schedule_update_ha_state() + if self._widget == "SomfyThermostat": + if hvac_mode == HVAC_MODE_AUTO and self._hvac_mode != HVAC_MODE_AUTO: + self._stored_target_temp = self._target_temp + self.apply_action(COMMAND_EXIT_DEROGATION) + elif hvac_mode == HVAC_MODE_HEAT and self._hvac_mode != HVAC_MODE_HEAT: + self._target_temp = self._stored_target_temp + self._preset_mode = PRESET_NONE + self.apply_action(COMMAND_SET_DEROGATION, self._target_temp, + STATE_DEROGATION_FURTHER_NOTICE) @property def supported_features(self) -> int: """Return the list of supported features.""" return SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE - @property - def preset_mode(self) -> Optional[str]: - """Return the current preset mode, e.g., home, away, temp. - - Requires SUPPORT_PRESET_MODE. - """ - return self._preset_mode - - @property - def preset_modes(self) -> Optional[List[str]]: - """Return a list of available preset modes. - - Requires SUPPORT_PRESET_MODE. - """ - return self._preset_modes - - def set_preset_mode(self, preset_mode: str) -> None: - """Set new preset mode.""" - raise NotImplementedError() # TODO implement - @property def temperature_sensor(self) -> str: """Return the id of the temperature sensor""" @@ -251,6 +271,7 @@ def temperature_sensor(self) -> str: def set_temperature_sensor(self, sensor_id: str): self._temp_sensor_entity_id = sensor_id + self.schedule_update_ha_state() @property def temperature_unit(self) -> str: @@ -260,7 +281,7 @@ def temperature_unit(self) -> str: @property def current_temperature(self) -> Optional[float]: """Return the current temperature""" - return self._current_temp + return self._current_temperature @property def target_temperature(self): @@ -272,11 +293,49 @@ def set_temperature(self, **kwargs) -> None: temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - if temperature < 15: - self.apply_action("setDerogation", "freezeMode", "further_notice") - if temperature > 26: - temperature = 26 - self._target_temp = temperature - self.apply_action("setDerogation", temperature, "further_notice") - self.apply_action("setModeTemperature", "manualMode", temperature) - self.schedule_update_ha_state() + if self._widget == "SomfyThermostat": + if temperature < 15: + self.apply_action("setDerogation", "freezeMode", "further_notice") + if temperature > 26: + temperature = 26 + self._target_temp = temperature + self.apply_action("setDerogation", temperature, "further_notice") + self.apply_action("setModeTemperature", "manualMode", temperature) + + @property + def preset_mode(self) -> Optional[str]: + """Return the current preset mode, e.g., home, away, temp. + + Requires SUPPORT_PRESET_MODE. + """ + return self._preset_mode + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return a list of available preset modes. + + Requires SUPPORT_PRESET_MODE. + """ + return self._preset_modes + + def set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if preset_mode not in self.preset_modes: + _LOGGER.error( + "Preset " + preset_mode + " is not available for " + self._name + ) + return + if self._widget == "SomfyThermostat": + if preset_mode in [PRESET_FREEZE, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME]: + if self._preset_mode == preset_mode: + return + self._preset_mode = preset_mode + self._stored_target_temp = self._target_temp + self.apply_action("setDerogation", MAP_PRESET_REVERSE[preset_mode], + "further_notice") + elif preset_mode == PRESET_NONE and not self._preset_mode == PRESET_NONE: + self._preset_mode = PRESET_NONE + self._target_temp = self._stored_target_temp + self.apply_action("setDerogation", self._target_temp, "further_notice") + self.apply_action("setModeTemperature", "manualMode", self._target_temp) + From 9edf1d33b48059417022d0a61b3b075c1a9886e8 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 12 Jun 2020 16:06:20 +0200 Subject: [PATCH 47/90] revert some changes not belonging to the PR --- custom_components/tahoma/tahoma_api.py | 2 +- custom_components/tahoma/translations/en.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/tahoma/tahoma_api.py b/custom_components/tahoma/tahoma_api.py index 955b520aa..183d9207c 100644 --- a/custom_components/tahoma/tahoma_api.py +++ b/custom_components/tahoma/tahoma_api.py @@ -650,7 +650,7 @@ def active_states(self): def set_active_state(self, name, value): """Set active state.""" if name not in self.__active_states.keys(): - self.__active_states[name] = value + raise ValueError("Can not set unknown state '" + name + "'") if (isinstance(self.__active_states[name], int) and isinstance(value, str)): diff --git a/custom_components/tahoma/translations/en.json b/custom_components/tahoma/translations/en.json index ae22949d5..1be9533f8 100644 --- a/custom_components/tahoma/translations/en.json +++ b/custom_components/tahoma/translations/en.json @@ -4,8 +4,8 @@ "user": { "description": "Enter your TaHoma credentials for the TaHoma App or the tahomalink.com platform.", "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "username": "Email address", + "password": "Password" } } }, From 42b807176059831c938445dd44acd7e216cb7692 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 12 Jun 2020 16:07:35 +0200 Subject: [PATCH 48/90] removed some comments left out from previous trials. --- custom_components/tahoma/config_flow.py | 29 ------------------------- 1 file changed, 29 deletions(-) diff --git a/custom_components/tahoma/config_flow.py b/custom_components/tahoma/config_flow.py index 152b50e06..781dc1e55 100644 --- a/custom_components/tahoma/config_flow.py +++ b/custom_components/tahoma/config_flow.py @@ -87,9 +87,6 @@ async def async_step_user(self, user_input=None): info = await validate_input(self.hass, user_input) if TAHOMA_TYPE_HEATING_SYSTEM in info: user_input[TAHOMA_TYPE_HEATING_SYSTEM] = info[TAHOMA_TYPE_HEATING_SYSTEM] - # self._user_input = user_input - # self._thermos = info[TAHOMA_TYPE_HEATING_SYSTEM] - # return await self.async_step_thermo() return self.async_create_entry(title=info["title"], data=user_input) except CannotConnect: @@ -104,32 +101,6 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - # async def async_step_thermo(self, user_input=None): - # """Handle thermo step""" - # errors = {} - # device_url = None - # device_name = None - # data = self._user_input - # thermos = self._thermos - # - # device_url = next(iter(thermos.keys())) - # device_name = next(iter(thermos.values())) - # - # if user_input is not None: - # data[device_url] = next(iter(user_input.values())) - # thermos.pop(device_url) - # if len(thermos) > 0: - # return await self.async_step_thermo() - # return self.async_create_entry(title=data[CONF_USERNAME], data=data) - # - # return self.async_show_form( - # step_id="thermo", - # data_schema=vol.Schema({ - # vol.Required(CONF_ENTITY_ID, description={"suggested_value": device_name}): str - # }), - # errors=errors - # ) - async def validate_options_input(hass: core.HomeAssistant, data): for k, v in data.items(): From 9c4c99e92d8fcf753156de37df25b5c95e8e0b30 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 12 Jun 2020 16:18:47 +0200 Subject: [PATCH 49/90] Change current temperature from None to 0 for unavailable to prevent Error Unable to serialize to JSON: Out of range float values are not JSON compliant --- custom_components/tahoma/climate.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 390132dc8..1ec092a9f 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -51,11 +51,11 @@ COMMAND_EXIT_DEROGATION = "exitDerogation" COMMAND_SET_DEROGATION = "setDerogation" -KEY_HVAC_MODE = 'somfythermostat:DerogationTypeState' -KEY_HEATING_MODE = 'somfythermostat:HeatingModeState' -KEY_DEROGATION_HEATING_MODE = 'somfythermostat:DerogationHeatingModeState' -KEY_TARGET_TEMPERATURE = 'core:TargetTemperatureState' -KEY_DEROGATION_TARGET_TEMPERATURE = 'core:DerogatedTargetTemperatureState' +ST_DEROGATION_TYPE_STATE = 'somfythermostat:DerogationTypeState' +ST_HEATING_MODE_STATE = 'somfythermostat:HeatingModeState' +ST_DEROGATION_HEATING_MODE_STATE = 'somfythermostat:DerogationHeatingModeState' +CORE_TARGET_TEMPERATURE_STATE = 'core:TargetTemperatureState' +CORE_DEROGATED_TARGET_TEMPERATURE_STATE = 'core:DerogatedTargetTemperatureState' PRESET_FREEZE = "freeze" @@ -195,7 +195,7 @@ def update_temp(self, state=None): try: self._current_temperature = ( - None if state.state == STATE_UNKNOWN + 0 if state.state == STATE_UNKNOWN else float(state.state) ) except ValueError as ex: From b996b7206744939eb9233dc264e08206f61344c9 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 12 Jun 2020 16:21:09 +0200 Subject: [PATCH 50/90] Fixed availability detection. --- custom_components/tahoma/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 1ec092a9f..910ca5362 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -230,7 +230,7 @@ def update(self): @property def available(self) -> bool: """If the device hasn't been able to connect, mark as unavailable.""" - return bool(self._current_temperature == 0) + return bool(self._current_temperature != 0) @property def hvac_mode(self) -> str: From 2355a46edda26275f947572a390475109d9245d8 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Fri, 12 Jun 2020 16:25:37 +0200 Subject: [PATCH 51/90] optimize _async_temp_sensor_changed --- custom_components/tahoma/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 910ca5362..1f544daa5 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -1,7 +1,6 @@ """Support for Tahoma climate.""" from datetime import timedelta import logging -from math import nan from typing import List, Optional from homeassistant.core import callback, State @@ -183,6 +182,8 @@ async def _async_temp_sensor_changed(self, entity_id: str, old_state: State, """Handle temperature changes.""" if new_state is None: return + if old_state == new_state: + return self.update_temp(new_state) self.schedule_update_ha_state() From 22d29f27e239df68f978094cc206bf0bda4c3169 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Mon, 15 Jun 2020 15:10:27 +0200 Subject: [PATCH 52/90] wait on action to be completed Wait for an applied action to be completed before returning. Not sure whether we should keep this like this as it is a blocking call. This was done because some actions in the climate entity can take several seconds to complete before the new state should be retrieved for an update. This way avoids adding a sleep. --- custom_components/tahoma/tahoma_device.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index 74b514532..2ba84ea12 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -1,3 +1,5 @@ +import logging + from homeassistant.helpers.entity import Entity from homeassistant.const import ATTR_BATTERY_LEVEL @@ -11,6 +13,7 @@ CORE_SENSOR_DEFECT_STATE, ) +_LOGGER = logging.getLogger(__name__) class TahomaDevice(Entity): """Representation of a TaHoma device entity.""" @@ -21,6 +24,10 @@ def __init__(self, tahoma_device, controller): self._name = self.tahoma_device.label self.controller = controller + async def async_added_to_hass(self): + await super().async_added_to_hass() + self.schedule_update_ha_state(True) + @property def name(self): """Return the name of the device.""" @@ -97,4 +104,6 @@ def apply_action(self, cmd_name, *args): action = Action(self.tahoma_device.url) action.add_command(cmd_name, *args) - self.controller.apply_actions("HomeAssistant", [action]) + exec_id = self.controller.apply_actions("HomeAssistant", [action]) + while exec_id in self.controller.get_current_executions(): + _LOGGER.info("Waiting for action to execute") From d6b66c468138bc6bd88ce990749a566e265dec57 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Mon, 15 Jun 2020 15:11:45 +0200 Subject: [PATCH 53/90] Update tahoma_device.py --- custom_components/tahoma/tahoma_device.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index 2ba84ea12..ca6dc4a04 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -24,10 +24,6 @@ def __init__(self, tahoma_device, controller): self._name = self.tahoma_device.label self.controller = controller - async def async_added_to_hass(self): - await super().async_added_to_hass() - self.schedule_update_ha_state(True) - @property def name(self): """Return the name of the device.""" From e9e25b8f71a1643f8f7ffeb9d729169cb3d1d21c Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Mon, 15 Jun 2020 15:11:45 +0200 Subject: [PATCH 54/90] Update tahoma_device.py --- custom_components/tahoma/tahoma_device.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index 90f3159fc..74b514532 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -21,10 +21,6 @@ def __init__(self, tahoma_device, controller): self._name = self.tahoma_device.label self.controller = controller - async def async_added_to_hass(self): - await super().async_added_to_hass() - self.schedule_update_ha_state(True) - @property def name(self): """Return the name of the device.""" From 7350b8a66a2903a0eca19bade94d324ea4984232 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 09:39:51 +0200 Subject: [PATCH 55/90] moved async_added_to_hacs to TahomaDevice. --- custom_components/tahoma/tahoma_device.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index 74b514532..90f3159fc 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -21,6 +21,10 @@ def __init__(self, tahoma_device, controller): self._name = self.tahoma_device.label self.controller = controller + async def async_added_to_hass(self): + await super().async_added_to_hass() + self.schedule_update_ha_state(True) + @property def name(self): """Return the name of the device.""" From a422f401815dfaca30c8f5d8d248ea34eb327961 Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Mon, 15 Jun 2020 15:11:45 +0200 Subject: [PATCH 56/90] Update tahoma_device.py --- custom_components/tahoma/tahoma_device.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index 90f3159fc..74b514532 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -21,10 +21,6 @@ def __init__(self, tahoma_device, controller): self._name = self.tahoma_device.label self.controller = controller - async def async_added_to_hass(self): - await super().async_added_to_hass() - self.schedule_update_ha_state(True) - @property def name(self): """Return the name of the device.""" From 8630637df9c84114ef74214b8a888d34870c93db Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 09:39:51 +0200 Subject: [PATCH 57/90] moved async_added_to_hacs to TahomaDevice. --- custom_components/tahoma/tahoma_device.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index ca6dc4a04..2ba84ea12 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -24,6 +24,10 @@ def __init__(self, tahoma_device, controller): self._name = self.tahoma_device.label self.controller = controller + async def async_added_to_hass(self): + await super().async_added_to_hass() + self.schedule_update_ha_state(True) + @property def name(self): """Return the name of the device.""" From 51573311ece5570b28ce65a90595da8c66928a5c Mon Sep 17 00:00:00 2001 From: Vincent Le Bourlot Date: Mon, 15 Jun 2020 15:11:45 +0200 Subject: [PATCH 58/90] Update tahoma_device.py --- custom_components/tahoma/tahoma_device.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index 2ba84ea12..ca6dc4a04 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -24,10 +24,6 @@ def __init__(self, tahoma_device, controller): self._name = self.tahoma_device.label self.controller = controller - async def async_added_to_hass(self): - await super().async_added_to_hass() - self.schedule_update_ha_state(True) - @property def name(self): """Return the name of the device.""" From f4da323f1c055527917ea28dd221c78538d1f399 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 09:39:51 +0200 Subject: [PATCH 59/90] moved async_added_to_hacs to TahomaDevice. --- custom_components/tahoma/tahoma_device.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index ca6dc4a04..2ba84ea12 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -24,6 +24,10 @@ def __init__(self, tahoma_device, controller): self._name = self.tahoma_device.label self.controller = controller + async def async_added_to_hass(self): + await super().async_added_to_hass() + self.schedule_update_ha_state(True) + @property def name(self): """Return the name of the device.""" From 08e7d8c3b0b22e3f083047e8f66e12192f833312 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 15:16:41 +0200 Subject: [PATCH 60/90] Added AtlanticElectricalHeater to climate devices --- custom_components/tahoma/climate.py | 276 +++++++++++++++++++--------- custom_components/tahoma/const.py | 42 +++-- 2 files changed, 220 insertions(+), 98 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 1f544daa5..88cf9f44e 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -1,62 +1,55 @@ """Support for Tahoma climate.""" -from datetime import timedelta import logging +from datetime import timedelta +from time import sleep from typing import List, Optional -from homeassistant.core import callback, State -from homeassistant.helpers.event import async_track_state_change -from homeassistant.const import ( - ATTR_TEMPERATURE, - DEVICE_CLASS_TEMPERATURE, - EVENT_HOMEASSISTANT_START, - STATE_OFF, - STATE_ON, - STATE_UNKNOWN, - TEMP_CELSIUS, -) from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( + ATTR_PRESET_MODE, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, - HVAC_MODE_HEAT, HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, PRESET_HOME, PRESET_NONE, PRESET_SLEEP, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - ATTR_PRESET_MODE, - HVAC_MODE_OFF, ) - -from .const import ( - DOMAIN, - TAHOMA_TYPES, +from homeassistant.const import ( + ATTR_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, + TEMP_CELSIUS, ) +from homeassistant.core import State, callback +from homeassistant.helpers.event import async_track_state_change + +from .const import * from .tahoma_device import TahomaDevice _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=120) - -SUPPORTED_CLIMATE_DEVICES = [ - "SomfyThermostat", - # "AtlanticElectricalHeater" -] +SCAN_INTERVAL = timedelta(seconds=60) -COMMAND_REFRESH = "refreshState" -COMMAND_EXIT_DEROGATION = "exitDerogation" -COMMAND_SET_DEROGATION = "setDerogation" +W_ST = "SomfyThermostat" +W_AEH = "AtlanticElectricalHeater" -ST_DEROGATION_TYPE_STATE = 'somfythermostat:DerogationTypeState' -ST_HEATING_MODE_STATE = 'somfythermostat:HeatingModeState' -ST_DEROGATION_HEATING_MODE_STATE = 'somfythermostat:DerogationHeatingModeState' -CORE_TARGET_TEMPERATURE_STATE = 'core:TargetTemperatureState' -CORE_DEROGATED_TARGET_TEMPERATURE_STATE = 'core:DerogatedTargetTemperatureState' +SUPPORTED_CLIMATE_DEVICES = [W_ST, W_AEH] PRESET_FREEZE = "freeze" +PRESET_FROST_PROTECTION = "frostprotection" +PRESET_OFF = "off" STATE_DEROGATION_FURTHER_NOTICE = "further_notice" STATE_DEROGATION_NEXT_MODE = "next_mode" @@ -80,14 +73,24 @@ STATE_PRESET_FREEZE: PRESET_FREEZE, STATE_PRESET_MANUAL: PRESET_NONE, STATE_PRESET_SLEEPING_MODE: PRESET_SLEEP, + PRESET_OFF: PRESET_NONE, + PRESET_FROST_PROTECTION: PRESET_FREEZE, + PRESET_ECO: PRESET_ECO, + PRESET_COMFORT: PRESET_COMFORT, } -MAP_PRESET_REVERSE = { +ST_MAP_PRESET_REVERSE = { PRESET_HOME: STATE_PRESET_AT_HOME, PRESET_AWAY: STATE_PRESET_AWAY, PRESET_FREEZE: STATE_PRESET_FREEZE, PRESET_NONE: STATE_PRESET_MANUAL, PRESET_SLEEP: STATE_PRESET_SLEEPING_MODE, } +AEH_MAP_PRESET_REVERSE = { + PRESET_NONE: PRESET_OFF, + PRESET_FREEZE: PRESET_FROST_PROTECTION, + PRESET_ECO: PRESET_ECO, + PRESET_COMFORT: PRESET_COMFORT, +} TAHOMA_TYPE_HEATING_SYSTEM = "HeatingSystem" @@ -102,7 +105,10 @@ async def async_setup_entry(hass, entry, async_add_entities): controller = data.get("controller") for device in data.get("devices"): - if TAHOMA_TYPES[device.uiclass] == "climate" and device.widget in SUPPORTED_CLIMATE_DEVICES: + if ( + TAHOMA_TYPES[device.uiclass] == "climate" + and device.widget in SUPPORTED_CLIMATE_DEVICES + ): options = dict(entry.options) if device.url in options[TAHOMA_TYPE_HEATING_SYSTEM]: sensor_id = options[DEVICE_CLASS_TEMPERATURE][device.url] @@ -118,7 +124,9 @@ async def update_listener(hass, entry): options = dict(entry.options) for entity in hass.data["climate"].entities: if entity.unique_id in options[TAHOMA_TYPE_HEATING_SYSTEM]: - entity.set_temperature_sensor(options[DEVICE_CLASS_TEMPERATURE][entity.unique_id]) + entity.set_temperature_sensor( + options[DEVICE_CLASS_TEMPERATURE][entity.unique_id] + ) entity.schedule_update_ha_state() @@ -128,35 +136,57 @@ class TahomaClimate(TahomaDevice, ClimateEntity): def __init__(self, tahoma_device, controller, sensor_id=None): """Initialize the sensor.""" super().__init__(tahoma_device, controller) - if COMMAND_REFRESH in self.tahoma_device.command_definitions: - self.apply_action(COMMAND_REFRESH) + self._cold_tolerance = 0.3 + self._hot_tolerance = 0.3 + self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE + if COMMAND_REFRESH_STATE in self.tahoma_device.command_definitions: + self.apply_action(COMMAND_REFRESH_STATE) self.controller.get_states([self.tahoma_device]) self._uiclass = tahoma_device.uiclass self._unique_id = tahoma_device.url self._widget = tahoma_device.widget self._temp_sensor_entity_id = sensor_id self._current_temperature = 0 - if self._widget == "SomfyThermostat": - self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] + self._current_hvac_modes = CURRENT_HVAC_IDLE + self._target_temp = None + if self._widget == W_ST: + self._hvac_modes = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] self._hvac_mode = MAP_HVAC_MODE[ - self.tahoma_device.active_states['somfythermostat:DerogationTypeState'] + self.tahoma_device.active_states[ST_DEROGATION_TYPE_STATE] ] - self._current_hvac_modes = CURRENT_HVAC_IDLE self._preset_mode = MAP_PRESET[ - self.tahoma_device.active_states['somfythermostat:HeatingModeState'] + self.tahoma_device.active_states[ST_HEATING_MODE_STATE] if self._hvac_mode == HVAC_MODE_AUTO - else self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState'] + else self.tahoma_device.active_states[ST_DEROGATION_HEATING_MODE_STATE] ] self._preset_modes = [ - PRESET_NONE, PRESET_FREEZE, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME + PRESET_NONE, + PRESET_FREEZE, + PRESET_SLEEP, + PRESET_AWAY, + PRESET_HOME, ] self._target_temp = ( - self.tahoma_device.active_states['core:TargetTemperatureState'] + self.tahoma_device.active_states[CORE_TARGET_TEMPERATURE_STATE] if self._hvac_mode == HVAC_MODE_AUTO - else self.tahoma_device.active_states['core:DerogatedTargetTemperatureState'] + else self.tahoma_device.active_states[ + CORE_DEROGATED_TARGET_TEMPERATURE_STATE + ] ) - self._stored_target_temp = self._target_temp - self._is_away = None + elif self._widget == W_AEH: + self._hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] + self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[CORE_ON_OFF_STATE]] + self._preset_mode = MAP_PRESET[ + self.tahoma_device.active_states[IO_TARGET_HEATING_LEVEL_STATE] + ] + self._preset_modes = [ + PRESET_NONE, + PRESET_FREEZE, + PRESET_ECO, + PRESET_COMFORT, + ] + self._target_temp = 19 + self._stored_target_temp = self._target_temp async def async_added_to_hass(self): await super().async_added_to_hass() @@ -177,8 +207,9 @@ def _async_startup(event): self.schedule_update_ha_state(True) - async def _async_temp_sensor_changed(self, entity_id: str, old_state: State, - new_state: State) -> None: + async def _async_temp_sensor_changed( + self, entity_id: str, old_state: State, new_state: State + ) -> None: """Handle temperature changes.""" if new_state is None: return @@ -186,6 +217,8 @@ async def _async_temp_sensor_changed(self, entity_id: str, old_state: State, return self.update_temp(new_state) + if self._widget == W_AEH: + self._control_heating() self.schedule_update_ha_state() @callback @@ -196,37 +229,46 @@ def update_temp(self, state=None): try: self._current_temperature = ( - 0 if state.state == STATE_UNKNOWN - else float(state.state) + 0 if state.state == STATE_UNKNOWN else float(state.state) ) except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) def update(self): """Update the state.""" - if COMMAND_REFRESH in self.tahoma_device.command_definitions: - self.apply_action(COMMAND_REFRESH) + if COMMAND_REFRESH_STATE in self.tahoma_device.command_definitions: + self.apply_action(COMMAND_REFRESH_STATE) self.controller.get_states([self.tahoma_device]) self.update_temp(None) - if self._widget == "SomfyThermostat": + if self._widget == W_ST: self._hvac_mode = MAP_HVAC_MODE[ - self.tahoma_device.active_states['somfythermostat:DerogationTypeState'] + self.tahoma_device.active_states[ST_DEROGATION_TYPE_STATE] ] self._target_temp = ( - self.tahoma_device.active_states['core:TargetTemperatureState'] + self.tahoma_device.active_states[CORE_TARGET_TEMPERATURE_STATE] if self._hvac_mode == HVAC_MODE_AUTO - else self.tahoma_device.active_states['core:DerogatedTargetTemperatureState'] + else self.tahoma_device.active_states[ + CORE_DEROGATED_TARGET_TEMPERATURE_STATE + ] ) self._preset_mode = MAP_PRESET[ - self.tahoma_device.active_states['somfythermostat:HeatingModeState'] + self.tahoma_device.active_states[ST_HEATING_MODE_STATE] if self._hvac_mode == HVAC_MODE_AUTO - else self.tahoma_device.active_states['somfythermostat:DerogationHeatingModeState'] + else self.tahoma_device.active_states[ST_DEROGATION_HEATING_MODE_STATE] ] - self._current_hvac_modes = ( - CURRENT_HVAC_IDLE - if self._current_temperature is None or self._current_temperature > self._target_temp - else CURRENT_HVAC_HEAT - ) + elif self._widget == W_AEH: + self._hvac_mode = MAP_HVAC_MODE[self.tahoma_device.active_states[CORE_ON_OFF_STATE]] + self._preset_mode = MAP_PRESET[ + self.tahoma_device.active_states[IO_TARGET_HEATING_LEVEL_STATE] + ] + self._current_hvac_modes = ( + CURRENT_HVAC_OFF + if self._hvac_mode == HVAC_MODE_OFF + else CURRENT_HVAC_IDLE + if self._current_temperature == 0 + or self._current_temperature > self._target_temp + else CURRENT_HVAC_HEAT + ) @property def available(self) -> bool: @@ -250,20 +292,31 @@ def hvac_action(self) -> Optional[str]: def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - if self._widget == "SomfyThermostat": - if hvac_mode == HVAC_MODE_AUTO and self._hvac_mode != HVAC_MODE_AUTO: + if hvac_mode == self._hvac_mode: + return + if self._widget == W_ST: + if hvac_mode == HVAC_MODE_AUTO: self._stored_target_temp = self._target_temp self.apply_action(COMMAND_EXIT_DEROGATION) - elif hvac_mode == HVAC_MODE_HEAT and self._hvac_mode != HVAC_MODE_HEAT: + elif hvac_mode == HVAC_MODE_HEAT: self._target_temp = self._stored_target_temp self._preset_mode = PRESET_NONE - self.apply_action(COMMAND_SET_DEROGATION, self._target_temp, - STATE_DEROGATION_FURTHER_NOTICE) + self.apply_action( + COMMAND_SET_DEROGATION, + self._target_temp, + STATE_DEROGATION_FURTHER_NOTICE, + ) + if self._widget == W_AEH: + if hvac_mode == HVAC_MODE_OFF: + self.apply_action(COMMAND_OFF) + if hvac_mode == HVAC_MODE_HEAT: + self.apply_action(COMMAND_SET_HEATING_LEVEL,AEH_MAP_PRESET_REVERSE[self._preset_mode]) + return @property def supported_features(self) -> int: """Return the list of supported features.""" - return SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE + return self._supported_features @property def temperature_sensor(self) -> str: @@ -294,14 +347,52 @@ def set_temperature(self, **kwargs) -> None: temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - if self._widget == "SomfyThermostat": + if self._widget == W_ST: if temperature < 15: - self.apply_action("setDerogation", "freezeMode", "further_notice") + self.apply_action( + COMMAND_SET_DEROGATION, + STATE_PRESET_FREEZE, + STATE_DEROGATION_FURTHER_NOTICE, + ) if temperature > 26: temperature = 26 self._target_temp = temperature - self.apply_action("setDerogation", temperature, "further_notice") - self.apply_action("setModeTemperature", "manualMode", temperature) + self.apply_action( + COMMAND_SET_DEROGATION, temperature, STATE_DEROGATION_FURTHER_NOTICE + ) + self.apply_action( + COMMAND_SET_MODE_TEMPERATURE, STATE_PRESET_MANUAL, temperature + ) + if self._widget == W_AEH: + self._target_temp = temperature + self._control_heating() + + def _control_heating(self) -> None: + """Controls whether heater should be turned on or off""" + if self._current_temperature == 0: + return + too_cold = self._target_temp - self._current_temperature >= self._cold_tolerance + too_hot = self._current_temperature - self._target_temp >= self._hot_tolerance + if too_hot: + self.turn_off() + if too_cold: + self.turn_on() + + def turn_off(self): + """Turn the entity off.""" + if self._widget == W_AEH: + if self._preset_mode == PRESET_NONE: + return + self.apply_action(COMMAND_SET_HEATING_LEVEL, AEH_MAP_PRESET_REVERSE[PRESET_NONE]) + + def turn_on(self): + """Turn the entity on.""" + if self._widget == W_AEH: + if self._preset_mode == PRESET_NONE: + self._preset_mode = PRESET_COMFORT + self.apply_action( + COMMAND_SET_HEATING_LEVEL, AEH_MAP_PRESET_REVERSE[self._preset_mode] + ) @property def preset_mode(self) -> Optional[str]: @@ -326,17 +417,28 @@ def set_preset_mode(self, preset_mode: str) -> None: "Preset " + preset_mode + " is not available for " + self._name ) return - if self._widget == "SomfyThermostat": + if self._preset_mode == preset_mode: + return + self._preset_mode = preset_mode + if self._widget == W_ST: if preset_mode in [PRESET_FREEZE, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME]: - if self._preset_mode == preset_mode: - return - self._preset_mode = preset_mode self._stored_target_temp = self._target_temp - self.apply_action("setDerogation", MAP_PRESET_REVERSE[preset_mode], - "further_notice") - elif preset_mode == PRESET_NONE and not self._preset_mode == PRESET_NONE: - self._preset_mode = PRESET_NONE + self.apply_action( + COMMAND_SET_DEROGATION, + ST_MAP_PRESET_REVERSE[preset_mode], + STATE_DEROGATION_FURTHER_NOTICE, + ) + elif preset_mode == PRESET_NONE: self._target_temp = self._stored_target_temp - self.apply_action("setDerogation", self._target_temp, "further_notice") - self.apply_action("setModeTemperature", "manualMode", self._target_temp) - + self.apply_action( + COMMAND_SET_DEROGATION, + self._target_temp, + STATE_DEROGATION_FURTHER_NOTICE, + ) + self.apply_action( + COMMAND_SET_MODE_TEMPERATURE, STATE_PRESET_MANUAL, self._target_temp + ) + elif self._widget == W_AEH: + self.apply_action( + COMMAND_SET_HEATING_LEVEL, AEH_MAP_PRESET_REVERSE[preset_mode] + ) diff --git a/custom_components/tahoma/const.py b/custom_components/tahoma/const.py index 6d2034de5..75f8debef 100644 --- a/custom_components/tahoma/const.py +++ b/custom_components/tahoma/const.py @@ -61,7 +61,7 @@ TAHOMA_BINARY_SENSOR_DEVICE_CLASSES = { "SmokeSensor": DEVICE_CLASS_SMOKE, "OccupancySensor": DEVICE_CLASS_OCCUPANCY, - "ContactSensor": DEVICE_CLASS_OPENING + "ContactSensor": DEVICE_CLASS_OPENING } # Used to map the Somfy widget or uiClass to the Home Assistant device classes @@ -80,27 +80,47 @@ ATTR_LOCK_ORIG = "lock_originator" # TaHoma internal device states -CORE_RSSI_LEVEL_STATE = "core:RSSILevelState" -CORE_STATUS_STATE = "core:StatusState" CORE_CLOSURE_STATE = "core:ClosureState" +CORE_CONTACT_STATE = "core:ContactState" CORE_DEPLOYMENT_STATE = "core:DeploymentState" -CORE_SLATS_ORIENTATION_STATE = "core:SlatsOrientationState" +CORE_DEROGATED_TARGET_TEMPERATURE_STATE = "core:DerogatedTargetTemperatureState" +CORE_LUMINANCE_STATE = "core:LuminanceState" +CORE_MEMORIZED_1_POSITION_STATE = "core:Memorized1PositionState" +CORE_NAME_STATE = "core:NameState" +CORE_OCCUPANCY_STATE = "core:OccupancyState" +CORE_ON_OFF_STATE = "core:OnOffState" +CORE_PEDESTRIAN_POSITION_STATE = "core:PedestrianPositionState" CORE_PRIORITY_LOCK_TIMER_STATE = "core:PriorityLockTimerState" +CORE_RELATIVE_HUMIDITY_STATE = "core:RelativeHumidityState" +CORE_RSSI_LEVEL_STATE = "core:RSSILevelState" CORE_SENSOR_DEFECT_STATE = "core:SensorDefectState" -CORE_CONTACT_STATE = "core:ContactState" -CORE_OCCUPANCY_STATE = "core:OccupancyState" +CORE_SLATS_ORIENTATION_STATE = "core:SlatsOrientationState" CORE_SMOKE_STATE = "core:SmokeState" +CORE_STATUS_STATE = "core:StatusState" +CORE_TARGET_TEMPERATURE_STATE = "core:TargetTemperatureState" CORE_TEMPERATURE_STATE = "core:TemperatureState" -CORE_LUMINANCE_STATE = "core:LuminanceState" -CORE_RELATIVE_HUMIDITY_STATE = "core:RelativeHumidityState" -CORE_MEMORIZED_1_POSITION_STATE = "core:Memorized1PositionState" -CORE_PEDESTRIAN_POSITION_STATE = "core:PedestrianPositionState" +CORE_VERSION_STATE = "core:VersionState" +# IO Devices specific states +IO_MAXIMUM_HEATING_LEVEL_STATE = "io:MaximumHeatingLevelState" IO_PRIORITY_LOCK_LEVEL_STATE = "io:PriorityLockLevelState" IO_PRIORITY_LOCK_ORIGINATOR_STATE = "io:PriorityLockOriginatorState" +IO_TARGET_HEATING_LEVEL_STATE = "io:TargetHeatingLevelState" +IO_TIMER_FOR_TRANSITORY_STATE_STATE = "io:TimerForTransitoryStateState" + +# Somfy Thermostat specific states +ST_DEROGATION_TYPE_STATE = "somfythermostat:DerogationTypeState" +ST_HEATING_MODE_STATE = "somfythermostat:HeatingModeState" +ST_DEROGATION_HEATING_MODE_STATE = "somfythermostat:DerogationHeatingModeState" # Commands +COMMAND_EXIT_DEROGATION = "exitDerogation" +COMMAND_OFF = "off" +COMMAND_REFRESH_STATE = "refreshState" COMMAND_SET_CLOSURE = "setClosure" -COMMAND_SET_POSITION = "setPosition" +COMMAND_SET_DEROGATION = "setDerogation" +COMMAND_SET_HEATING_LEVEL = "setHeatingLevel" +COMMAND_SET_MODE_TEMPERATURE = "setModeTemperature" COMMAND_SET_ORIENTATION = "setOrientation" COMMAND_SET_PEDESTRIAN_POSITION = "setPedestrianPosition" +COMMAND_SET_POSITION = "setPosition" From 4ca4f772dd994fad90796ec696c13fa11aed5282 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 15:24:16 +0200 Subject: [PATCH 61/90] control heating after every set action. --- custom_components/tahoma/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 88cf9f44e..b3c747a92 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -311,6 +311,7 @@ def set_hvac_mode(self, hvac_mode: str) -> None: self.apply_action(COMMAND_OFF) if hvac_mode == HVAC_MODE_HEAT: self.apply_action(COMMAND_SET_HEATING_LEVEL,AEH_MAP_PRESET_REVERSE[self._preset_mode]) + self._control_heating() return @property @@ -442,3 +443,5 @@ def set_preset_mode(self, preset_mode: str) -> None: self.apply_action( COMMAND_SET_HEATING_LEVEL, AEH_MAP_PRESET_REVERSE[preset_mode] ) + self._control_heating() + From 1f83cb6160fad9ce5f9ebc7d6984c5b9e110050e Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 15:28:47 +0200 Subject: [PATCH 62/90] fixes update_listener --- custom_components/tahoma/climate.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index b3c747a92..7ea5f2748 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -123,11 +123,12 @@ async def update_listener(hass, entry): """Handle options update.""" options = dict(entry.options) for entity in hass.data["climate"].entities: - if entity.unique_id in options[TAHOMA_TYPE_HEATING_SYSTEM]: - entity.set_temperature_sensor( - options[DEVICE_CLASS_TEMPERATURE][entity.unique_id] - ) - entity.schedule_update_ha_state() + if TAHOMA_TYPE_HEATING_SYSTEM in options: + if entity.unique_id in options[TAHOMA_TYPE_HEATING_SYSTEM]: + entity.set_temperature_sensor( + options[DEVICE_CLASS_TEMPERATURE][entity.unique_id] + ) + entity.schedule_update_ha_state() class TahomaClimate(TahomaDevice, ClimateEntity): From 365d1fc0cd25d9b5daa57ba99f5b22500effc07d Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 15:37:50 +0200 Subject: [PATCH 63/90] fixes async_setup_entry for empty options. --- custom_components/tahoma/climate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 7ea5f2748..56480b790 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -110,9 +110,10 @@ async def async_setup_entry(hass, entry, async_add_entities): and device.widget in SUPPORTED_CLIMATE_DEVICES ): options = dict(entry.options) - if device.url in options[TAHOMA_TYPE_HEATING_SYSTEM]: - sensor_id = options[DEVICE_CLASS_TEMPERATURE][device.url] - entities.append(TahomaClimate(device, controller, sensor_id)) + if TAHOMA_TYPE_HEATING_SYSTEM in options: + if device.url in options[TAHOMA_TYPE_HEATING_SYSTEM]: + sensor_id = options[DEVICE_CLASS_TEMPERATURE][device.url] + entities.append(TahomaClimate(device, controller, sensor_id)) else: entities.append(TahomaClimate(device, controller)) From 01539133f45b1ce94d7d21a547cdc8f8170ab68b Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 16:01:09 +0200 Subject: [PATCH 64/90] fixes options flow --- custom_components/tahoma/config_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/tahoma/config_flow.py b/custom_components/tahoma/config_flow.py index 781dc1e55..6a908c94d 100644 --- a/custom_components/tahoma/config_flow.py +++ b/custom_components/tahoma/config_flow.py @@ -142,7 +142,10 @@ async def async_step_init(self, user_input=None): schema = {} if TAHOMA_TYPE_HEATING_SYSTEM in self.config_entry.data: for k, v in self.config_entry.data[TAHOMA_TYPE_HEATING_SYSTEM].items(): - default = self.config_entry.options.get(DEVICE_CLASS_TEMPERATURE).get(k) + if DEVICE_CLASS_TEMPERATURE not in self.config_entry.options: + default = None + else: + default = self.config_entry.options.get(DEVICE_CLASS_TEMPERATURE).get(k) if default is None: default = v key = vol.Required( From 56fb593cb77ae590ab3cbce20ce8bac456363019 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 16:30:50 +0200 Subject: [PATCH 65/90] fix update_temp. --- custom_components/tahoma/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 56480b790..52c84666c 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -226,6 +226,9 @@ async def _async_temp_sensor_changed( @callback def update_temp(self, state=None): """Update thermostat with latest state from sensor.""" + if self._temp_sensor_entity_id is None: + return + if state is None: state = self.hass.states.get(self._temp_sensor_entity_id) From efe89e78f12c42281b908d6a97ce8c8c749343ab Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 16:34:37 +0200 Subject: [PATCH 66/90] fix options flow --- custom_components/tahoma/climate.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 52c84666c..7aa55af8d 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -110,8 +110,7 @@ async def async_setup_entry(hass, entry, async_add_entities): and device.widget in SUPPORTED_CLIMATE_DEVICES ): options = dict(entry.options) - if TAHOMA_TYPE_HEATING_SYSTEM in options: - if device.url in options[TAHOMA_TYPE_HEATING_SYSTEM]: + if device.url in options[TAHOMA_TYPE_HEATING_SYSTEM]: sensor_id = options[DEVICE_CLASS_TEMPERATURE][device.url] entities.append(TahomaClimate(device, controller, sensor_id)) else: @@ -124,12 +123,11 @@ async def update_listener(hass, entry): """Handle options update.""" options = dict(entry.options) for entity in hass.data["climate"].entities: - if TAHOMA_TYPE_HEATING_SYSTEM in options: - if entity.unique_id in options[TAHOMA_TYPE_HEATING_SYSTEM]: - entity.set_temperature_sensor( - options[DEVICE_CLASS_TEMPERATURE][entity.unique_id] - ) - entity.schedule_update_ha_state() + if entity.unique_id in options[TAHOMA_TYPE_HEATING_SYSTEM]: + entity.set_temperature_sensor( + options[DEVICE_CLASS_TEMPERATURE][entity.unique_id] + ) + entity.schedule_update_ha_state() class TahomaClimate(TahomaDevice, ClimateEntity): From 27acb1d54bc1e2f9ba4a09b68a3b05e31b76fc75 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 16:45:36 +0200 Subject: [PATCH 67/90] fix options flow --- custom_components/tahoma/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 7aa55af8d..b8a7f0678 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass, entry, async_add_entities): and device.widget in SUPPORTED_CLIMATE_DEVICES ): options = dict(entry.options) - if device.url in options[TAHOMA_TYPE_HEATING_SYSTEM]: + if device.url in options[DEVICE_CLASS_TEMPERATURE]: sensor_id = options[DEVICE_CLASS_TEMPERATURE][device.url] entities.append(TahomaClimate(device, controller, sensor_id)) else: @@ -123,7 +123,7 @@ async def update_listener(hass, entry): """Handle options update.""" options = dict(entry.options) for entity in hass.data["climate"].entities: - if entity.unique_id in options[TAHOMA_TYPE_HEATING_SYSTEM]: + if entity.unique_id in options[DEVICE_CLASS_TEMPERATURE]: entity.set_temperature_sensor( options[DEVICE_CLASS_TEMPERATURE][entity.unique_id] ) From 98971a1a32083b57529abda8b93eccf15cfdb2eb Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 17:09:58 +0200 Subject: [PATCH 68/90] move the refresh to apply_action if the command exists. --- custom_components/tahoma/climate.py | 2 -- custom_components/tahoma/tahoma_device.py | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index b8a7f0678..5df71c7a8 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -239,8 +239,6 @@ def update_temp(self, state=None): def update(self): """Update the state.""" - if COMMAND_REFRESH_STATE in self.tahoma_device.command_definitions: - self.apply_action(COMMAND_REFRESH_STATE) self.controller.get_states([self.tahoma_device]) self.update_temp(None) if self._widget == W_ST: diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index 2ba84ea12..89e6ed7f9 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -8,6 +8,7 @@ from .const import ( DOMAIN, ATTR_RSSI_LEVEL, + COMMAND_REFRESH_STATE, CORE_RSSI_LEVEL_STATE, CORE_STATUS_STATE, CORE_SENSOR_DEFECT_STATE, @@ -107,3 +108,6 @@ def apply_action(self, cmd_name, *args): exec_id = self.controller.apply_actions("HomeAssistant", [action]) while exec_id in self.controller.get_current_executions(): _LOGGER.info("Waiting for action to execute") + if COMMAND_REFRESH_STATE in self.tahoma_device.command_definitions: + self.apply_action(COMMAND_REFRESH_STATE) + From b483b356409d1f25bac414222241438fd80d1e44 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 17:21:53 +0200 Subject: [PATCH 69/90] fixes infinite recursion. --- custom_components/tahoma/tahoma_device.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index 89e6ed7f9..720b2c22a 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -102,12 +102,10 @@ def device_info(self): def apply_action(self, cmd_name, *args): """Apply Action to Device.""" - action = Action(self.tahoma_device.url) action.add_command(cmd_name, *args) + if COMMAND_REFRESH_STATE in self.tahoma_device.command_definitions: + action.add_command(COMMAND_REFRESH_STATE) exec_id = self.controller.apply_actions("HomeAssistant", [action]) while exec_id in self.controller.get_current_executions(): _LOGGER.info("Waiting for action to execute") - if COMMAND_REFRESH_STATE in self.tahoma_device.command_definitions: - self.apply_action(COMMAND_REFRESH_STATE) - From de90c2f989f2e505717f2341a1e451a4e3a41bc2 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 17:24:36 +0200 Subject: [PATCH 70/90] revert change. --- custom_components/tahoma/climate.py | 3 +++ custom_components/tahoma/tahoma_device.py | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 5df71c7a8..d865c5519 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -307,6 +307,7 @@ def set_hvac_mode(self, hvac_mode: str) -> None: self._target_temp, STATE_DEROGATION_FURTHER_NOTICE, ) + self.apply_action(COMMAND_EXIT_DEROGATION) if self._widget == W_AEH: if hvac_mode == HVAC_MODE_OFF: self.apply_action(COMMAND_OFF) @@ -365,6 +366,7 @@ def set_temperature(self, **kwargs) -> None: self.apply_action( COMMAND_SET_MODE_TEMPERATURE, STATE_PRESET_MANUAL, temperature ) + self.apply_action(COMMAND_EXIT_DEROGATION) if self._widget == W_AEH: self._target_temp = temperature self._control_heating() @@ -440,6 +442,7 @@ def set_preset_mode(self, preset_mode: str) -> None: self.apply_action( COMMAND_SET_MODE_TEMPERATURE, STATE_PRESET_MANUAL, self._target_temp ) + self.apply_action(COMMAND_EXIT_DEROGATION) elif self._widget == W_AEH: self.apply_action( COMMAND_SET_HEATING_LEVEL, AEH_MAP_PRESET_REVERSE[preset_mode] diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index 720b2c22a..4fd0575f4 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -104,8 +104,6 @@ def apply_action(self, cmd_name, *args): """Apply Action to Device.""" action = Action(self.tahoma_device.url) action.add_command(cmd_name, *args) - if COMMAND_REFRESH_STATE in self.tahoma_device.command_definitions: - action.add_command(COMMAND_REFRESH_STATE) exec_id = self.controller.apply_actions("HomeAssistant", [action]) while exec_id in self.controller.get_current_executions(): _LOGGER.info("Waiting for action to execute") From 19824d083b173ed8a88f724bb6c9f93681ebcb89 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Mon, 15 Jun 2020 17:36:32 +0200 Subject: [PATCH 71/90] improve availability detection. --- custom_components/tahoma/climate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index d865c5519..5ea313a69 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -274,7 +274,10 @@ def update(self): @property def available(self) -> bool: """If the device hasn't been able to connect, mark as unavailable.""" - return bool(self._current_temperature != 0) + return ( + bool(self._current_temperature != 0) and + self.hass.states.get(self._temp_sensor_entity_id) is not None + ) @property def hvac_mode(self) -> str: From a882f63b9107c801b28b83d4a3ac7298ed865338 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Tue, 16 Jun 2020 11:40:32 +0200 Subject: [PATCH 72/90] Fixes refresh state. --- custom_components/tahoma/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 5ea313a69..4d9171e07 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -310,7 +310,7 @@ def set_hvac_mode(self, hvac_mode: str) -> None: self._target_temp, STATE_DEROGATION_FURTHER_NOTICE, ) - self.apply_action(COMMAND_EXIT_DEROGATION) + self.apply_action(COMMAND_REFRESH_STATE) if self._widget == W_AEH: if hvac_mode == HVAC_MODE_OFF: self.apply_action(COMMAND_OFF) @@ -369,7 +369,7 @@ def set_temperature(self, **kwargs) -> None: self.apply_action( COMMAND_SET_MODE_TEMPERATURE, STATE_PRESET_MANUAL, temperature ) - self.apply_action(COMMAND_EXIT_DEROGATION) + self.apply_action(COMMAND_REFRESH_STATE) if self._widget == W_AEH: self._target_temp = temperature self._control_heating() @@ -445,7 +445,7 @@ def set_preset_mode(self, preset_mode: str) -> None: self.apply_action( COMMAND_SET_MODE_TEMPERATURE, STATE_PRESET_MANUAL, self._target_temp ) - self.apply_action(COMMAND_EXIT_DEROGATION) + self.apply_action(COMMAND_REFRESH_STATE) elif self._widget == W_AEH: self.apply_action( COMMAND_SET_HEATING_LEVEL, AEH_MAP_PRESET_REVERSE[preset_mode] From 21804c67915c752c8c0f1faca9897692b6c7077e Mon Sep 17 00:00:00 2001 From: vlebourl Date: Tue, 16 Jun 2020 12:36:41 +0200 Subject: [PATCH 73/90] add a stack trace on I/O events. --- custom_components/tahoma/tahoma_api.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/custom_components/tahoma/tahoma_api.py b/custom_components/tahoma/tahoma_api.py index b0442476b..3300a2618 100644 --- a/custom_components/tahoma/tahoma_api.py +++ b/custom_components/tahoma/tahoma_api.py @@ -5,12 +5,17 @@ """ import json +import logging +import pprint import requests +import traceback import urllib.parse BASE_URL = 'https://tahomalink.com/enduser-mobile-web/enduserAPI/' # /doc for API doc BASE_HEADERS = {'User-Agent': 'mine'} +_LOGGER = logging.getLogger(__name__) + class TahomaApi: """Connection to TaHoma API.""" @@ -86,6 +91,11 @@ def send_request(self, method, url: str, headers, data=None, timeout: int = 10, if not self.__logged_in: self.login() + stack = pprint.pformat(traceback.extract_stack()) + if "asyncio" in stack: + _LOGGER.warning( + "I/O stack trace:\n"+stack + ) request = method(url, headers=headers, data=data, timeout=timeout) if request.status_code == 200: try: From 4cdba13165e8141c581b969fd4aa2869c98ac5f9 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Tue, 16 Jun 2020 12:40:19 +0200 Subject: [PATCH 74/90] Removed I/O calls from __init__ method. --- custom_components/tahoma/climate.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 4d9171e07..28b26f91c 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -139,9 +139,6 @@ def __init__(self, tahoma_device, controller, sensor_id=None): self._cold_tolerance = 0.3 self._hot_tolerance = 0.3 self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE - if COMMAND_REFRESH_STATE in self.tahoma_device.command_definitions: - self.apply_action(COMMAND_REFRESH_STATE) - self.controller.get_states([self.tahoma_device]) self._uiclass = tahoma_device.uiclass self._unique_id = tahoma_device.url self._widget = tahoma_device.widget From c174da8d3ec7d2fb0babd19b548390007244419f Mon Sep 17 00:00:00 2001 From: vlebourl Date: Tue, 16 Jun 2020 12:49:53 +0200 Subject: [PATCH 75/90] Moved the waiting part back into climate.py. --- custom_components/tahoma/climate.py | 38 +++++++++++++---------- custom_components/tahoma/tahoma_device.py | 4 +-- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 28b26f91c..39cf55316 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -291,6 +291,12 @@ def hvac_action(self) -> Optional[str]: """Return the current running hvac operation if supported.""" return self._current_hvac_modes + def _apply_action(self, cmd_name, *args): + """Apply action to the climate device""" + exec_id = self.apply_action(cmd_name, *args) + while exec_id in self.controller.get_current_executions(): + _LOGGER.info("Waiting for action to execute") + def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode == self._hvac_mode: @@ -298,21 +304,21 @@ def set_hvac_mode(self, hvac_mode: str) -> None: if self._widget == W_ST: if hvac_mode == HVAC_MODE_AUTO: self._stored_target_temp = self._target_temp - self.apply_action(COMMAND_EXIT_DEROGATION) + self._apply_action(COMMAND_EXIT_DEROGATION) elif hvac_mode == HVAC_MODE_HEAT: self._target_temp = self._stored_target_temp self._preset_mode = PRESET_NONE - self.apply_action( + self._apply_action( COMMAND_SET_DEROGATION, self._target_temp, STATE_DEROGATION_FURTHER_NOTICE, ) - self.apply_action(COMMAND_REFRESH_STATE) + self._apply_action(COMMAND_REFRESH_STATE) if self._widget == W_AEH: if hvac_mode == HVAC_MODE_OFF: - self.apply_action(COMMAND_OFF) + self._apply_action(COMMAND_OFF) if hvac_mode == HVAC_MODE_HEAT: - self.apply_action(COMMAND_SET_HEATING_LEVEL,AEH_MAP_PRESET_REVERSE[self._preset_mode]) + self._apply_action(COMMAND_SET_HEATING_LEVEL,AEH_MAP_PRESET_REVERSE[self._preset_mode]) self._control_heating() return @@ -352,7 +358,7 @@ def set_temperature(self, **kwargs) -> None: return if self._widget == W_ST: if temperature < 15: - self.apply_action( + self._apply_action( COMMAND_SET_DEROGATION, STATE_PRESET_FREEZE, STATE_DEROGATION_FURTHER_NOTICE, @@ -360,13 +366,13 @@ def set_temperature(self, **kwargs) -> None: if temperature > 26: temperature = 26 self._target_temp = temperature - self.apply_action( + self._apply_action( COMMAND_SET_DEROGATION, temperature, STATE_DEROGATION_FURTHER_NOTICE ) - self.apply_action( + self._apply_action( COMMAND_SET_MODE_TEMPERATURE, STATE_PRESET_MANUAL, temperature ) - self.apply_action(COMMAND_REFRESH_STATE) + self._apply_action(COMMAND_REFRESH_STATE) if self._widget == W_AEH: self._target_temp = temperature self._control_heating() @@ -387,14 +393,14 @@ def turn_off(self): if self._widget == W_AEH: if self._preset_mode == PRESET_NONE: return - self.apply_action(COMMAND_SET_HEATING_LEVEL, AEH_MAP_PRESET_REVERSE[PRESET_NONE]) + self._apply_action(COMMAND_SET_HEATING_LEVEL, AEH_MAP_PRESET_REVERSE[PRESET_NONE]) def turn_on(self): """Turn the entity on.""" if self._widget == W_AEH: if self._preset_mode == PRESET_NONE: self._preset_mode = PRESET_COMFORT - self.apply_action( + self._apply_action( COMMAND_SET_HEATING_LEVEL, AEH_MAP_PRESET_REVERSE[self._preset_mode] ) @@ -427,24 +433,24 @@ def set_preset_mode(self, preset_mode: str) -> None: if self._widget == W_ST: if preset_mode in [PRESET_FREEZE, PRESET_SLEEP, PRESET_AWAY, PRESET_HOME]: self._stored_target_temp = self._target_temp - self.apply_action( + self._apply_action( COMMAND_SET_DEROGATION, ST_MAP_PRESET_REVERSE[preset_mode], STATE_DEROGATION_FURTHER_NOTICE, ) elif preset_mode == PRESET_NONE: self._target_temp = self._stored_target_temp - self.apply_action( + self._apply_action( COMMAND_SET_DEROGATION, self._target_temp, STATE_DEROGATION_FURTHER_NOTICE, ) - self.apply_action( + self._apply_action( COMMAND_SET_MODE_TEMPERATURE, STATE_PRESET_MANUAL, self._target_temp ) - self.apply_action(COMMAND_REFRESH_STATE) + self._apply_action(COMMAND_REFRESH_STATE) elif self._widget == W_AEH: - self.apply_action( + self._apply_action( COMMAND_SET_HEATING_LEVEL, AEH_MAP_PRESET_REVERSE[preset_mode] ) self._control_heating() diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index 4fd0575f4..9bff71d54 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -104,6 +104,4 @@ def apply_action(self, cmd_name, *args): """Apply Action to Device.""" action = Action(self.tahoma_device.url) action.add_command(cmd_name, *args) - exec_id = self.controller.apply_actions("HomeAssistant", [action]) - while exec_id in self.controller.get_current_executions(): - _LOGGER.info("Waiting for action to execute") + return self.controller.apply_actions("HomeAssistant", [action]) From ab94a04376be9e8d382f01de927bc7dbfb0d186e Mon Sep 17 00:00:00 2001 From: vlebourl Date: Tue, 16 Jun 2020 12:52:11 +0200 Subject: [PATCH 76/90] Reverted add a stack trace on I/O events. --- custom_components/tahoma/tahoma_api.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/custom_components/tahoma/tahoma_api.py b/custom_components/tahoma/tahoma_api.py index 3300a2618..b0442476b 100644 --- a/custom_components/tahoma/tahoma_api.py +++ b/custom_components/tahoma/tahoma_api.py @@ -5,17 +5,12 @@ """ import json -import logging -import pprint import requests -import traceback import urllib.parse BASE_URL = 'https://tahomalink.com/enduser-mobile-web/enduserAPI/' # /doc for API doc BASE_HEADERS = {'User-Agent': 'mine'} -_LOGGER = logging.getLogger(__name__) - class TahomaApi: """Connection to TaHoma API.""" @@ -91,11 +86,6 @@ def send_request(self, method, url: str, headers, data=None, timeout: int = 10, if not self.__logged_in: self.login() - stack = pprint.pformat(traceback.extract_stack()) - if "asyncio" in stack: - _LOGGER.warning( - "I/O stack trace:\n"+stack - ) request = method(url, headers=headers, data=data, timeout=timeout) if request.status_code == 200: try: From 903d411c77566f554e9e7eab039f9622d4e57620 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Tue, 16 Jun 2020 14:23:33 +0200 Subject: [PATCH 77/90] fixes a missing test. --- custom_components/tahoma/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 39cf55316..7aa7c8d17 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -110,7 +110,8 @@ async def async_setup_entry(hass, entry, async_add_entities): and device.widget in SUPPORTED_CLIMATE_DEVICES ): options = dict(entry.options) - if device.url in options[DEVICE_CLASS_TEMPERATURE]: + if DEVICE_CLASS_TEMPERATURE in options: + if device.url in options[DEVICE_CLASS_TEMPERATURE]: sensor_id = options[DEVICE_CLASS_TEMPERATURE][device.url] entities.append(TahomaClimate(device, controller, sensor_id)) else: From c3ff829d3ab592484a5baab7ba68f81bc6d23fdc Mon Sep 17 00:00:00 2001 From: vlebourl Date: Tue, 16 Jun 2020 14:50:34 +0200 Subject: [PATCH 78/90] add a different message if no climate is found --- custom_components/tahoma/config_flow.py | 9 +++++++++ custom_components/tahoma/strings.json | 3 ++- custom_components/tahoma/translations/en.json | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/custom_components/tahoma/config_flow.py b/custom_components/tahoma/config_flow.py index 6a908c94d..27db1917d 100644 --- a/custom_components/tahoma/config_flow.py +++ b/custom_components/tahoma/config_flow.py @@ -123,6 +123,8 @@ async def async_step_init(self, user_input=None): errors = {} if user_input is not None: + if not user_input or 'no-climate' in user_input: + return self.async_create_entry(title="", data=dict(self.config_entry.data)) try: await validate_options_input(self.hass, user_input) self.options[DEVICE_CLASS_TEMPERATURE] = user_input @@ -134,6 +136,13 @@ async def async_step_init(self, user_input=None): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" + # if TAHOMA_TYPE_HEATING_SYSTEM not in dict(self.config_entry.data): + return self.async_show_form( + step_id="init", + data_schema=vol.Schema({vol.Optional("no-climate"): str}), + errors=errors + ) + available_sensors = [] for k, v in self.hass.data['entity_registry'].entities.items(): if str.startswith(k, "sensor") and v.device_class == DEVICE_CLASS_TEMPERATURE: diff --git a/custom_components/tahoma/strings.json b/custom_components/tahoma/strings.json index ae22949d5..e8ea7a9e5 100644 --- a/custom_components/tahoma/strings.json +++ b/custom_components/tahoma/strings.json @@ -23,7 +23,8 @@ "init": { "description": "Select temperature sensors for thermostats", "data": { - "_": "entity" + "_": "entity", + "no-climate": "No climate device, you can close this window" } } }, diff --git a/custom_components/tahoma/translations/en.json b/custom_components/tahoma/translations/en.json index 1be9533f8..1e43ff45a 100644 --- a/custom_components/tahoma/translations/en.json +++ b/custom_components/tahoma/translations/en.json @@ -23,7 +23,8 @@ "init": { "description": "Select temperature sensors for thermostats", "data": { - "_": "entity" + "_": "entity", + "no-climate": "No climate device, you can close this window" } } }, From 49f951817bfd282a63b3f0f8498d73108a6c6a85 Mon Sep 17 00:00:00 2001 From: vlebourl Date: Wed, 17 Jun 2020 08:23:07 +0200 Subject: [PATCH 79/90] Merge master into climate. --- .github/workflows/black.yml | 28 ++ .pre-commit-config.yaml | 31 ++ CHANGELOG.md | 5 + README.md | 22 +- custom_components/tahoma/binary_sensor.py | 8 +- custom_components/tahoma/cover.py | 34 +- custom_components/tahoma/light.py | 15 +- custom_components/tahoma/lock.py | 4 +- custom_components/tahoma/sensor.py | 56 ++- custom_components/tahoma/switch.py | 3 +- custom_components/tahoma/tahoma_api.py | 526 +++++++++++----------- requirement.txt | 1 + setup.cfg | 38 ++ 13 files changed, 459 insertions(+), 312 deletions(-) create mode 100644 .github/workflows/black.yml create mode 100644 .pre-commit-config.yaml create mode 100644 requirement.txt create mode 100644 setup.cfg diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 000000000..27aa9f351 --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,28 @@ +name: Linters (flake8, black, isort) + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + +jobs: + black-format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: ricardochaves/python-lint@v1.1.0 + with: + python-root-list: ./custom_components/tahoma + use-pylint: false + use-pycodestyle: false + use-flake8: true + use-black: true + use-mypy: false + use-isort: true + extra-pylint-options: "" + extra-pycodestyle-options: "" + extra-flake8-options: "" + extra-black-options: "" + extra-mypy-options: "" + extra-isort-options: "" \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..f9eb7666f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +repos: + - repo: https://github.com/asottile/pyupgrade + rev: v2.3.0 + hooks: + - id: pyupgrade + args: [--py37-plus] + - repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black + args: + - --safe + - --quiet + files: ^((custom_components)/.+)?[^/]+\.py$ + - repo: https://github.com/codespell-project/codespell + rev: v1.16.0 + hooks: + - id: codespell + args: + - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing + - --skip="./.*,*.csv,*.json,*.md" + - --quiet-level=2 + exclude_types: [csv, json] + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.1 + hooks: + - id: flake8 + additional_dependencies: + - flake8-docstrings==1.5.0 + - pydocstyle==5.0.2 + files: ^(custom_components)/.+\.py$ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3775381cd..d3b254505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for Gate devices +- Added support for AirSensor devices +- Added support for ElectricitySensor devices +- Added support for Curtain devices +- Added support for Generic devices (cover) +- Added support for SwingingShutter devices ### Changed diff --git a/README.md b/README.md index 37ed42505..5d17edb2f 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,16 @@ If your device is not supported, it will show the following message in the loggi | Somfy uiClass | Home Assistant platform | | ----------------- | ----------------------- | | Awning | cover | +| Curtain | cover | | ExteriorScreen | cover | | Gate | cover | | GarageDoor | cover | | Pergola | cover | | RollerShutter | cover | +| SwingingShutter | cover | | Window | cover | +| AirSensor | sensor | +| ElectricitySensor | sensor | | HumiditySensor | sensor | | LightSensor | sensor | | TemperatureSensor | sensor | @@ -45,12 +49,20 @@ If your device is not supported, it will show the following message in the loggi | ContactSensor | binary_sensor | | OccupancySensor | binary_sensor | | SmokeSensor | binary_sensor | +| WindowHandle | binary_sensor | | Light | light | ## Not supported (yet) -| Somfy uiClass | -| ---------------- | -| RemoteController | -| Alarm | -| HeatingSystem | +| Somfy uiClass | +| --------------------- | +| RemoteController | +| Alarm | +| HeatingSystem | +| EvoHome | +| HitachiHeatingSystem | +| ExteriorHeatingSystem | +| Fan | +| Siren | +| MusicPlayer | +| VentilationSystem | diff --git a/custom_components/tahoma/binary_sensor.py b/custom_components/tahoma/binary_sensor.py index d49c7aa4c..10c4454f7 100644 --- a/custom_components/tahoma/binary_sensor.py +++ b/custom_components/tahoma/binary_sensor.py @@ -6,12 +6,12 @@ from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON from .const import ( + CORE_CONTACT_STATE, + CORE_OCCUPANCY_STATE, + CORE_SMOKE_STATE, DOMAIN, - TAHOMA_TYPES, TAHOMA_BINARY_SENSOR_DEVICE_CLASSES, - CORE_SMOKE_STATE, - CORE_OCCUPANCY_STATE, - CORE_CONTACT_STATE, + TAHOMA_TYPES, ) from .tahoma_device import TahomaDevice diff --git a/custom_components/tahoma/cover.py b/custom_components/tahoma/cover.py index ea2a9f409..58a80467d 100644 --- a/custom_components/tahoma/cover.py +++ b/custom_components/tahoma/cover.py @@ -10,42 +10,41 @@ DEVICE_CLASS_GARAGE, DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, - CoverEntity, - SUPPORT_OPEN, SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, - SUPPORT_OPEN_TILT, - SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, - SUPPORT_SET_TILT_POSITION, + CoverEntity, ) from homeassistant.util.dt import utcnow -from .tahoma_device import TahomaDevice - from .const import ( - DOMAIN, - TAHOMA_TYPES, - TAHOMA_COVER_DEVICE_CLASSES, - ATTR_MEM_POS, - ATTR_LOCK_START_TS, ATTR_LOCK_END_TS, ATTR_LOCK_LEVEL, ATTR_LOCK_ORIG, + ATTR_LOCK_START_TS, + ATTR_MEM_POS, + COMMAND_SET_CLOSURE, + COMMAND_SET_ORIENTATION, + COMMAND_SET_PEDESTRIAN_POSITION, + COMMAND_SET_POSITION, CORE_CLOSURE_STATE, CORE_DEPLOYMENT_STATE, + CORE_MEMORIZED_1_POSITION_STATE, CORE_PEDESTRIAN_POSITION_STATE, CORE_PRIORITY_LOCK_TIMER_STATE, CORE_SLATS_ORIENTATION_STATE, - CORE_MEMORIZED_1_POSITION_STATE, + DOMAIN, IO_PRIORITY_LOCK_LEVEL_STATE, IO_PRIORITY_LOCK_ORIGINATOR_STATE, - COMMAND_SET_CLOSURE, - COMMAND_SET_POSITION, - COMMAND_SET_ORIENTATION, - COMMAND_SET_PEDESTRIAN_POSITION, + TAHOMA_COVER_DEVICE_CLASSES, + TAHOMA_TYPES, ) +from .tahoma_device import TahomaDevice _LOGGER = logging.getLogger(__name__) @@ -169,6 +168,7 @@ def current_cover_position(self): @property def current_cover_tilt_position(self): """Return current position of cover tilt. + None is unknown, 0 is closed, 100 is fully open. """ return getattr(self, "_tilt_position", None) diff --git a/custom_components/tahoma/light.py b/custom_components/tahoma/light.py index 0e7bead06..bc31a1f92 100644 --- a/custom_components/tahoma/light.py +++ b/custom_components/tahoma/light.py @@ -1,15 +1,14 @@ """TaHoma light platform that implements dimmable TaHoma lights.""" -import logging from datetime import timedelta +import logging from homeassistant.components.light import ( - LightEntity, ATTR_BRIGHTNESS, ATTR_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, + LightEntity, ) - from homeassistant.const import STATE_OFF, STATE_ON from .const import DOMAIN, TAHOMA_TYPES @@ -36,17 +35,16 @@ async def async_setup_entry(hass, entry, async_add_entities): class TahomaLight(TahomaDevice, LightEntity): - """Representation of a Tahome light""" + """Representation of a Tahome light.""" def __init__(self, tahoma_device, controller): + """Initialize a device.""" super().__init__(tahoma_device, controller) self._skip_update = False self._effect = None self._brightness = None self._state = None - - self.update() @property def brightness(self) -> int: @@ -72,7 +70,7 @@ def supported_features(self) -> int: return supported_features - async def async_turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs) -> None: """Turn the light on.""" self._state = True self._skip_update = True @@ -88,7 +86,7 @@ async def async_turn_on(self, **kwargs) -> None: self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs) -> None: """Turn the light off.""" self._state = False self._skip_update = True @@ -108,6 +106,7 @@ def effect(self) -> str: def update(self): """Fetch new state data for this light. + This is the only method that should fetch new data for Home Assistant. """ # Postpone the immediate state check for changes that take time. diff --git a/custom_components/tahoma/lock.py b/custom_components/tahoma/lock.py index 7307b5965..a164c712c 100644 --- a/custom_components/tahoma/lock.py +++ b/custom_components/tahoma/lock.py @@ -85,9 +85,7 @@ def is_locked(self): @property def device_state_attributes(self): """Return the lock state attributes.""" - attr = { - ATTR_BATTERY_LEVEL: self._battery_level, - } + attr = {ATTR_BATTERY_LEVEL: self._battery_level} super_attr = super().device_state_attributes if super_attr is not None: attr.update(super_attr) diff --git a/custom_components/tahoma/sensor.py b/custom_components/tahoma/sensor.py index a42ed7a4b..a6075f08a 100644 --- a/custom_components/tahoma/sensor.py +++ b/custom_components/tahoma/sensor.py @@ -3,16 +3,24 @@ import logging from typing import Optional -from homeassistant.const import ATTR_BATTERY_LEVEL, TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + CONCENTRATION_PARTS_PER_MILLION, + POWER_WATT, + TEMP_CELSIUS, + UNIT_PERCENTAGE, +) from homeassistant.helpers.entity import Entity from .const import ( + CORE_CO2_CONCENTRATION_STATE, + CORE_ELECTRIC_POWER_CONSUMPTION_STATE, + CORE_LUMINANCE_STATE, + CORE_RELATIVE_HUMIDITY_STATE, + CORE_TEMPERATURE_STATE, DOMAIN, - TAHOMA_TYPES, TAHOMA_SENSOR_DEVICE_CLASSES, - CORE_RELATIVE_HUMIDITY_STATE, - CORE_LUMINANCE_STATE, - CORE_TEMPERATURE_STATE, + TAHOMA_TYPES, ) from .tahoma_device import TahomaDevice @@ -55,7 +63,8 @@ def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" if self.tahoma_device.uiclass == "TemperatureSensor": - return TEMP_CELSIUS # TODO Retrieve core:MeasuredValueType to understand if it is Celsius or Kelvin + # TODO Retrieve core:MeasuredValueType to understand if it is Celsius or Kelvin + return TEMP_CELSIUS if self.tahoma_device.uiclass == "HumiditySensor": return UNIT_PERCENTAGE @@ -63,15 +72,30 @@ def unit_of_measurement(self): if self.tahoma_device.uiclass == "LightSensor": return "lx" + if self.tahoma_device.uiclass == "ElectricitySensor": + return POWER_WATT + + if self.tahoma_device.uiclass == "AirSensor": + return CONCENTRATION_PARTS_PER_MILLION + + return None + + @property + def icon(self) -> Optional[str]: + """Return the icon to use in the frontend, if any.""" + + if self.tahoma_device.uiclass == "AirSensor": + return "mdi:periodic-table-co2" + return None @property def device_class(self) -> Optional[str]: """Return the device class of this entity if any.""" return ( - TAHOMA_SENSOR_DEVICE_CLASSES.get(self.tahoma_device.widget) - or TAHOMA_SENSOR_DEVICE_CLASSES.get(self.tahoma_device.uiclass) - or None + TAHOMA_SENSOR_DEVICE_CLASSES.get(self.tahoma_device.widget) + or TAHOMA_SENSOR_DEVICE_CLASSES.get(self.tahoma_device.uiclass) + or None ) def update(self): @@ -97,4 +121,16 @@ def update(self): ) ) - _LOGGER.debug("Update %s, value: %d", self._name, self.current_value) + if CORE_ELECTRIC_POWER_CONSUMPTION_STATE in self.tahoma_device.active_states: + self.current_value = float( + "{:.2f}".format( + self.tahoma_device.active_states.get( + CORE_ELECTRIC_POWER_CONSUMPTION_STATE + ) + ) + ) + + if CORE_CO2_CONCENTRATION_STATE in self.tahoma_device.active_states: + self.current_value = int( + self.tahoma_device.active_states.get(CORE_CO2_CONCENTRATION_STATE) + ) diff --git a/custom_components/tahoma/switch.py b/custom_components/tahoma/switch.py index 248421307..6575cb10d 100644 --- a/custom_components/tahoma/switch.py +++ b/custom_components/tahoma/switch.py @@ -1,9 +1,8 @@ """Support for TaHoma switches.""" import logging -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.components.switch import DEVICE_CLASS_SWITCH from .const import DOMAIN, TAHOMA_TYPES from .tahoma_device import TahomaDevice diff --git a/custom_components/tahoma/tahoma_api.py b/custom_components/tahoma/tahoma_api.py index b0442476b..84f26f47f 100644 --- a/custom_components/tahoma/tahoma_api.py +++ b/custom_components/tahoma/tahoma_api.py @@ -5,18 +5,19 @@ """ import json -import requests import urllib.parse -BASE_URL = 'https://tahomalink.com/enduser-mobile-web/enduserAPI/' # /doc for API doc -BASE_HEADERS = {'User-Agent': 'mine'} +import requests + +BASE_URL = "https://tahomalink.com/enduser-mobile-web/enduserAPI/" # /doc for API doc +BASE_HEADERS = {"User-Agent": "mine"} class TahomaApi: """Connection to TaHoma API.""" def __init__(self, userName, userPassword, **kwargs): - """Initalize the TaHoma protocol. + """Initialize the TaHoma protocol. :param userName: TaHoma username :param userPassword: Password @@ -37,30 +38,38 @@ def login(self): """Login to TaHoma API.""" if self.__logged_in: return - login = {'userId': self.__username, 'userPassword': self.__password} + login = {"userId": self.__username, "userPassword": self.__password} header = BASE_HEADERS.copy() - request = requests.post(BASE_URL + 'login', - data=login, - headers=header, - timeout=10) + request = requests.post( + BASE_URL + "login", data=login, headers=header, timeout=10 + ) try: result = request.json() except ValueError as error: raise Exception( - "Not a valid result for login, " + - "protocol error: " + request.status_code + ' - ' + - request.reason + "(" + error + ")") - - if 'error' in result.keys(): - raise Exception("Could not login: " + result['error']) + "Not a valid result for login, " + + "protocol error: " + + request.status_code + + " - " + + request.reason + + "(" + + error + + ")" + ) + + if "error" in result.keys(): + raise Exception("Could not login: " + result["error"]) if request.status_code != 200: raise Exception( - "Could not login, HTTP code: " + - str(request.status_code) + ' - ' + request.reason) + "Could not login, HTTP code: " + + str(request.status_code) + + " - " + + request.reason + ) - if 'success' not in result.keys() or not result['success']: + if "success" not in result.keys() or not result["success"]: raise Exception("Could not login, no success") cookie = request.headers.get("set-cookie") @@ -71,9 +80,10 @@ def login(self): self.__logged_in = True return self.__logged_in - def send_request(self, method, url: str, headers, data=None, timeout: int = 10, - retries: int = 3): - """Wrap the http requests and retries + def send_request( + self, method, url: str, headers, data=None, timeout: int = 10, retries: int = 3 + ): + """Wrap the http requests and retries. :param method: The method to use for the request: post, get, delete. :param url: The url to send the POST to. @@ -91,47 +101,47 @@ def send_request(self, method, url: str, headers, data=None, timeout: int = 10, try: result = request.json() except ValueError as error: - raise Exception( - "Not a valid result, protocol error: " + str(error)) + raise Exception("Not a valid result, protocol error: " + str(error)) return result elif retries == 0: raise Exception( - "Maximum number of consecutive retries reached. Error is:\n" + request.text) + "Maximum number of consecutive retries reached. Error is:\n" + + request.text + ) else: self.send_request(method, url, headers, data, timeout, retries - 1) def get_user(self): - """Get the user informations from the server. + """Get the user information from the server. - :return: a dict with all the informations + :return: a dict with all the information :rtype: dict raises ValueError in case of protocol issues :Example: - >>> "creationTime":