From 30b6ed39e9723e47f5e33991defa60b85baa0cc6 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 1 Sep 2020 12:48:31 +0200 Subject: [PATCH 1/2] Initial commit of new service Signed-off-by: Mick Vleeshouwer --- custom_components/tahoma/__init__.py | 21 ++++++++++++++++++++- custom_components/tahoma/manifest.json | 2 +- custom_components/tahoma/services.yaml | 5 ++++- requirements.test.txt | 2 +- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/custom_components/tahoma/__init__.py b/custom_components/tahoma/__init__.py index f79786446..c7aa7560f 100644 --- a/custom_components/tahoma/__init__.py +++ b/custom_components/tahoma/__init__.py @@ -12,7 +12,12 @@ from homeassistant import config_entries from homeassistant.components.scene import DOMAIN as SCENE from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_EXCLUDE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_EXCLUDE, + CONF_PASSWORD, + CONF_USERNAME, + EVENT_HOMEASSISTANT_START, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, config_validation as cv @@ -45,6 +50,8 @@ extra=vol.ALLOW_EXTRA, ) +SERVICE_REFRESH_STATES = "refresh_states" + async def async_setup(hass: HomeAssistant, config: dict): """Set up the TaHoma component.""" @@ -137,6 +144,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, platform) ) + async def handle_service_refresh_states(call): + """Handle the service call.""" + await client.refresh_states() + + def _register_services(event): + """Register the domain services.""" + hass.services.register( + DOMAIN, SERVICE_REFRESH_STATES, handle_service_refresh_states + ) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _register_services) + return True diff --git a/custom_components/tahoma/manifest.json b/custom_components/tahoma/manifest.json index 533a34ff9..7c1bc51f7 100644 --- a/custom_components/tahoma/manifest.json +++ b/custom_components/tahoma/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tahoma", "requirements": [ - "pyhoma==0.4.0" + "pyhoma==0.4.1" ], "codeowners": ["@philklei", "@imicknl", "@vlebourl", "@tetienne"], "issue_tracker": "https://github.com/imicknl/ha-tahoma/issues" diff --git a/custom_components/tahoma/services.yaml b/custom_components/tahoma/services.yaml index 363264ccd..6099ab4f0 100644 --- a/custom_components/tahoma/services.yaml +++ b/custom_components/tahoma/services.yaml @@ -20,4 +20,7 @@ set_cover_position_low_speed: example: "cover.living_room" position: description: Position of the cover (0 to 100). - example: 30 \ No newline at end of file + example: 30 + +refresh_states: + description: Ask the box to refresh all devices states for protocols supporting that operation. diff --git a/requirements.test.txt b/requirements.test.txt index 5009a7d0f..90b8146b0 100644 --- a/requirements.test.txt +++ b/requirements.test.txt @@ -7,4 +7,4 @@ pytest-cov<3.0.0 pytest-homeassistant # from our manifest.json for our Custom Component -pyhoma==0.4.0 \ No newline at end of file +pyhoma==0.4.1 \ No newline at end of file From cb569b6105ba2074999ef833c2fe49ac72b3358f Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 14 Sep 2020 15:58:14 +0200 Subject: [PATCH 2/2] Squashed commit of the following: commit 39baced09af3246ec48bb7ed40e410d4d81f1cbf Author: Mick Vleeshouwer Date: Mon Sep 14 10:00:18 2020 +0200 Improve `stop` and `stop tilt` command for covers (#251) commit af03275e25baaa3c333780a4d426b986d953ed0e Author: Vincent Le Bourlot Date: Thu Sep 10 12:35:42 2020 +0200 Rename UNIT_PERCENTAGE to PERCENTAGE (#257) commit 34eeb61eb5ed084c0e18e898e32804bf376a43a7 Author: Vincent Le Bourlot Date: Fri Sep 4 11:38:53 2020 +0200 Add support for StatelessExteriorHeating device. (#244) * Add support for StatelessExteriorHeating device. * Add support for StatelessExteriorHeating device. * Changed unknown property to None. Added SUPPORT_PRESET_MODE. * Added services to services.yaml, removed up and down services. #211 * Fix temperature unit. #211 * Fix types. #211 * limit service my to supported devices. #211 * Added some debug logs. * Merge master into branch. * fixed wrong import #211 * Add last ha action as preset and hvac mode state. #211 * Renamed service. #211 * Revert "Add last ha action as preset and hvac mode state." This reverts commit bdfb6f3b #211 * improved debug logging. #211 * improved debug logging. #211 * fix possible circular import. #211 * Removed service * Changed preset to my only * Removed commented lines. * Cleaned up code. * Cleaned up code. * Removed unwanted logging message. #211 * Switch mode and preset back to None. #211 * cleanup code. #211 commit a95fb730f27f2fad3b7b6ec37d4bcaf8f58d59e9 Author: Vincent Le Bourlot Date: Fri Sep 4 09:57:46 2020 +0200 Add service to send any command to tahoma. (#252) * Add service to send any command to tahoma. * Fixed jsons. * Update strings.json * Applied comments. * Add service to send any command to tahoma. * Rebased onto master. * Cleanup leftovers. * Cleanup some more leftovers. commit 7a19a7cbd534bc22f6138657a41bc27e6e8583f1 Author: Mick Vleeshouwer Date: Thu Sep 3 11:39:59 2020 +0200 Add support for DomesticHotWaterTank (#247) Signed-off-by: Mick Vleeshouwer --- custom_components/tahoma/__init__.py | 42 +++++++-- .../tahoma/alarm_control_panel.py | 4 +- custom_components/tahoma/binary_sensor.py | 4 +- custom_components/tahoma/climate.py | 8 +- .../stateless_exterior_heating.py | 71 ++++++++++++++ custom_components/tahoma/const.py | 4 +- custom_components/tahoma/cover.py | 93 ++++++++++++------- custom_components/tahoma/light.py | 4 +- custom_components/tahoma/lock.py | 4 +- custom_components/tahoma/manifest.json | 2 +- custom_components/tahoma/scene.py | 5 +- custom_components/tahoma/sensor.py | 13 ++- custom_components/tahoma/services.yaml | 13 +++ custom_components/tahoma/switch.py | 17 +++- custom_components/tahoma/tahoma_device.py | 4 + requirements.test.txt | 2 +- 16 files changed, 225 insertions(+), 65 deletions(-) create mode 100644 custom_components/tahoma/climate_devices/stateless_exterior_heating.py diff --git a/custom_components/tahoma/__init__.py b/custom_components/tahoma/__init__.py index c7aa7560f..c4883326d 100644 --- a/custom_components/tahoma/__init__.py +++ b/custom_components/tahoma/__init__.py @@ -7,6 +7,7 @@ from aiohttp import CookieJar from pyhoma.client import TahomaClient from pyhoma.exceptions import BadCredentialsException, TooManyRequestsException +from pyhoma.models import Command import voluptuous as vol from homeassistant import config_entries @@ -32,6 +33,8 @@ _LOGGER = logging.getLogger(__name__) +SERVICE_EXECUTE_COMMAND = "execute_command" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -144,18 +147,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, platform) ) - async def handle_service_refresh_states(call): - """Handle the service call.""" - await client.refresh_states() - def _register_services(event): """Register the domain services.""" - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_REFRESH_STATES, handle_service_refresh_states ) + hass.services.async_register( + DOMAIN, + SERVICE_EXECUTE_COMMAND, + handle_execute_command, + vol.Schema( + { + vol.Required("entity_id"): cv.string, + vol.Required("command"): cv.string, + vol.Optional("args", default=[]): vol.All( + cv.ensure_list, [vol.Any(str, int)] + ), + } + ), + ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _register_services) + async def handle_execute_command(call): + """Handle execute command service.""" + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity = entity_registry.entities.get(call.data.get("entity_id")) + await tahoma_coordinator.client.execute_command( + entity.unique_id, + Command(call.data.get("command"), call.data.get("args")), + "Home Assistant Service", + ) + + async def handle_service_refresh_states(call): + """Handle the service call.""" + await client.refresh_states() + return True @@ -180,10 +208,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def update_listener(hass, entry): +async def update_listener(hass: HomeAssistant, entry: ConfigEntry): """Update when config_entry options update.""" if entry.options[CONF_UPDATE_INTERVAL]: - coordinator = hass.data[DOMAIN][entry.entry_id].get("coordinator") + coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"] new_update_interval = timedelta(seconds=entry.options[CONF_UPDATE_INTERVAL]) coordinator.update_interval = new_update_interval coordinator.original_update_interval = new_update_interval diff --git a/custom_components/tahoma/alarm_control_panel.py b/custom_components/tahoma/alarm_control_panel.py index 4fcc31db4..fdf08a8b2 100644 --- a/custom_components/tahoma/alarm_control_panel.py +++ b/custom_components/tahoma/alarm_control_panel.py @@ -80,11 +80,11 @@ 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] - coordinator = data.get("coordinator") + coordinator = data["coordinator"] entities = [ TahomaAlarmControlPanel(device.deviceurl, coordinator) - for device in data.get("entities").get(ALARM_CONTROL_PANEL) + for device in data["entities"].get(ALARM_CONTROL_PANEL) ] async_add_entities(entities) diff --git a/custom_components/tahoma/binary_sensor.py b/custom_components/tahoma/binary_sensor.py index eb9a6a9d8..daca89735 100644 --- a/custom_components/tahoma/binary_sensor.py +++ b/custom_components/tahoma/binary_sensor.py @@ -62,11 +62,11 @@ 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] - coordinator = data.get("coordinator") + coordinator = data["coordinator"] entities = [ TahomaBinarySensor(device.deviceurl, coordinator) - for device in data.get("entities").get(BINARY_SENSOR) + for device in data["entities"].get(BINARY_SENSOR) ] async_add_entities(entities) diff --git a/custom_components/tahoma/climate.py b/custom_components/tahoma/climate.py index 2c1c3f53f..ca83ecace 100644 --- a/custom_components/tahoma/climate.py +++ b/custom_components/tahoma/climate.py @@ -5,22 +5,26 @@ from .climate_devices.atlantic_electrical_heater import AtlanticElectricalHeater from .climate_devices.dimmer_exterior_heating import DimmerExteriorHeating from .climate_devices.somfy_thermostat import SomfyThermostat +from .climate_devices.stateless_exterior_heating import StatelessExteriorHeating from .const import DOMAIN TYPE = { "AtlanticElectricalHeater": AtlanticElectricalHeater, "SomfyThermostat": SomfyThermostat, "DimmerExteriorHeating": DimmerExteriorHeating, + "StatelessExteriorHeating": StatelessExteriorHeating, } +SERVICE_CLIMATE_MY_POSITION = "set_climate_my_position" + async def async_setup_entry(hass, entry, async_add_entities): """Set up the TaHoma climate from a config entry.""" data = hass.data[DOMAIN][entry.entry_id] - coordinator = data.get("coordinator") + coordinator = data["coordinator"] - climate_devices = [device for device in data.get("entities").get(CLIMATE)] + climate_devices = [device for device in data["entities"].get(CLIMATE)] entities = [ TYPE[device.widget](device.deviceurl, coordinator) diff --git a/custom_components/tahoma/climate_devices/stateless_exterior_heating.py b/custom_components/tahoma/climate_devices/stateless_exterior_heating.py new file mode 100644 index 000000000..d62386d8e --- /dev/null +++ b/custom_components/tahoma/climate_devices/stateless_exterior_heating.py @@ -0,0 +1,71 @@ +"""Support for Stateless Exterior Heating device.""" +import logging +from typing import List, Optional + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_PRESET_MODE, +) +from homeassistant.const import TEMP_CELSIUS + +from ..tahoma_device import TahomaDevice + +_LOGGER = logging.getLogger(__name__) + +COMMAND_MY = "my" +COMMAND_OFF = "off" +COMMAND_ON = "on" + +PRESET_MY = "My" + + +class StatelessExteriorHeating(TahomaDevice, ClimateEntity): + """Representation of TaHoma Stateless Exterior Heating device.""" + + @property + def temperature_unit(self) -> Optional[str]: + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS # Not used but climate devices need a recognized temperature unit... + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_PRESET_MODE + + @property + def preset_mode(self) -> Optional[str]: + """Return the current preset mode, e.g., home, away, temp.""" + return None + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return a list of available preset modes.""" + return [PRESET_MY] + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if preset_mode in PRESET_MY: + await self.async_execute_command(COMMAND_MY) + else: + _LOGGER.error( + "Invalid preset mode %s for device %s", preset_mode, self.name + ) + + @property + def hvac_mode(self) -> Optional[str]: + """Return hvac operation ie. heat, cool mode.""" + return None + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes.""" + return [HVAC_MODE_OFF, HVAC_MODE_HEAT] + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + if hvac_mode == HVAC_MODE_HEAT: + await self.async_execute_command(COMMAND_ON) + else: + await self.async_execute_command(COMMAND_OFF) diff --git a/custom_components/tahoma/const.py b/custom_components/tahoma/const.py index 4d04ba49b..a42b512da 100644 --- a/custom_components/tahoma/const.py +++ b/custom_components/tahoma/const.py @@ -21,9 +21,9 @@ # Used to map the Somfy widget and ui_class to the Home Assistant platform TAHOMA_TYPES = { "AdjustableSlatsRollerShutter": COVER, - "Alarm": ALARM_CONTROL_PANEL, "AirFlowSensor": BINARY_SENSOR, # widgetName, uiClass is AirSensor (sensor) "AirSensor": SENSOR, + "Alarm": ALARM_CONTROL_PANEL, "AtlanticElectricalHeater": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) "Awning": COVER, "CarButtonSensor": BINARY_SENSOR, @@ -31,6 +31,7 @@ "ContactSensor": BINARY_SENSOR, "Curtain": COVER, "DimmerExteriorHeating": CLIMATE, # widgetName, uiClass is ExteriorHeatingSystem (not supported) + "DomesticHotWaterTank": SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) "DoorLock": LOCK, "ElectricitySensor": SENSOR, "ExteriorScreen": COVER, @@ -56,6 +57,7 @@ "SirenStatus": BINARY_SENSOR, # widgetName, uiClass is Siren (switch) "SmokeSensor": BINARY_SENSOR, "SomfyThermostat": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) + "StatelessExteriorHeating": CLIMATE, # widgetName, uiClass is ExteriorHeatingSystem. "SunIntensitySensor": SENSOR, "SunSensor": SENSOR, "SwimmingPool": SWITCH, diff --git a/custom_components/tahoma/cover.py b/custom_components/tahoma/cover.py index 06ab5d3c5..539779476 100644 --- a/custom_components/tahoma/cover.py +++ b/custom_components/tahoma/cover.py @@ -47,6 +47,19 @@ COMMAND_STOP_IDENTIFY = "stopIdentify" COMMAND_UP = "up" +COMMANDS_STOP = [COMMAND_STOP_IDENTIFY, COMMAND_STOP, COMMAND_MY] +COMMANDS_STOP_TILT = [COMMAND_STOP_IDENTIFY, COMMAND_STOP, COMMAND_MY] +COMMANDS_OPEN = [COMMAND_OPEN, COMMAND_UP, COMMAND_CYCLE] +COMMANDS_OPEN_TILT = [COMMAND_OPEN_SLATS] +COMMANDS_CLOSE = [COMMAND_CLOSE, COMMAND_DOWN, COMMAND_CYCLE] +COMMANDS_CLOSE_TILT = [COMMAND_CLOSE_SLATS] +COMMANDS_SET_POSITION = [ + COMMAND_SET_POSITION, + COMMAND_SET_CLOSURE, + COMMAND_SET_PEDESTRIAN_POSITION, +] +COMMANDS_SET_TILT_POSITION = [COMMAND_SET_ORIENTATION] + CORE_CLOSURE_STATE = "core:ClosureState" CORE_CLOSURE_OR_ROCKER_POSITION_STATE = "core:ClosureOrRockerPositionState" CORE_DEPLOYMENT_STATE = "core:DeploymentState" @@ -95,13 +108,12 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the TaHoma covers from a config entry.""" - data = hass.data[DOMAIN][entry.entry_id] - coordinator = data.get("coordinator") + coordinator = data["coordinator"] entities = [ TahomaCover(device.deviceurl, coordinator) - for device in data.get("entities").get(COVER) + for device in data["entities"].get(COVER) ] async_add_entities(entities) @@ -167,10 +179,9 @@ async def async_set_cover_position(self, **kwargs): if "Horizontal" in self.device.widget: position = kwargs.get(ATTR_POSITION, 0) - command = self.select_command( - COMMAND_SET_POSITION, COMMAND_SET_CLOSURE, COMMAND_SET_PEDESTRIAN_POSITION + await self.async_execute_command( + self.select_command(*COMMANDS_SET_POSITION), position ) - await self.async_execute_command(command, position) async def async_set_cover_position_low_speed(self, **kwargs): """Move the cover to a specific position with a low speed.""" @@ -187,7 +198,8 @@ async def async_set_cover_position_low_speed(self, **kwargs): async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" await self.async_execute_command( - COMMAND_SET_ORIENTATION, 100 - kwargs.get(ATTR_TILT_POSITION, 0) + self.select_command(*COMMANDS_SET_TILT_POSITION), + 100 - kwargs.get(ATTR_TILT_POSITION, 0), ) @property @@ -238,36 +250,53 @@ def icon(self): async def async_open_cover(self, **_): """Open the cover.""" - await self.async_execute_command( - self.select_command(COMMAND_OPEN, COMMAND_UP, COMMAND_CYCLE) - ) + await self.async_execute_command(self.select_command(*COMMANDS_OPEN)) async def async_open_cover_tilt(self, **_): """Open the cover tilt.""" - await self.async_execute_command(self.select_command(COMMAND_OPEN_SLATS)) + await self.async_execute_command(self.select_command(*COMMANDS_OPEN_TILT)) async def async_close_cover(self, **_): """Close the cover.""" - await self.async_execute_command( - self.select_command(COMMAND_CLOSE, COMMAND_DOWN, COMMAND_CYCLE) - ) + await self.async_execute_command(self.select_command(*COMMANDS_CLOSE)) async def async_close_cover_tilt(self, **_): """Close the cover tilt.""" - await self.async_execute_command(self.select_command(COMMAND_CLOSE_SLATS)) + await self.async_execute_command(self.select_command(*COMMANDS_CLOSE_TILT)) async def async_stop_cover(self, **_): """Stop the cover.""" - await self.async_execute_command( - self.select_command(COMMAND_STOP, COMMAND_STOP_IDENTIFY, COMMAND_MY) + await self.async_cancel_or_stop_cover( + COMMANDS_OPEN + COMMANDS_SET_POSITION + COMMANDS_CLOSE, COMMANDS_STOP, ) async def async_stop_cover_tilt(self, **_): - """Stop the cover.""" - await self.async_execute_command( - self.select_command(COMMAND_STOP_IDENTIFY, COMMAND_STOP, COMMAND_MY) + """Stop the cover tilt.""" + await self.async_cancel_or_stop_cover( + COMMANDS_OPEN_TILT + COMMANDS_SET_TILT_POSITION + COMMANDS_CLOSE_TILT, + COMMANDS_STOP_TILT, ) + async def async_cancel_or_stop_cover(self, cancel_commands, stop_commands) -> None: + """Cancel running execution or send stop command.""" + exec_id = next( + ( + exec_id + # Reverse dictionary to cancel the last added execution + for exec_id, execution in reversed(self.coordinator.executions.items()) + if execution.get("deviceurl") == self.device.deviceurl + and execution.get("command_name") in cancel_commands + ), + None, + ) + + # Cancelling a running execution will stop the cover movement + if exec_id: + await self.async_cancel_command(exec_id) + # Fallback to available stop commands when execution was initiated outside Home Assistant + else: + await self.async_execute_command(self.select_command(*stop_commands)) + async def async_my(self, **_): """Set cover to preset position.""" await self.async_execute_command(COMMAND_MY) @@ -277,8 +306,7 @@ def is_opening(self): """Return if the cover is opening or not.""" return any( execution.get("deviceurl") == self.device.deviceurl - and execution.get("command_name") - in [COMMAND_OPEN, COMMAND_UP, COMMAND_OPEN_SLATS] + and execution.get("command_name") in COMMANDS_OPEN + COMMANDS_OPEN_TILT for execution in self.coordinator.executions.values() ) @@ -287,8 +315,7 @@ def is_closing(self): """Return if the cover is closing or not.""" return any( execution.get("deviceurl") == self.device.deviceurl - and execution.get("command_name") - in [COMMAND_CLOSE, COMMAND_DOWN, COMMAND_CLOSE_SLATS] + and execution.get("command_name") in COMMANDS_CLOSE + COMMANDS_CLOSE_TILT for execution in self.coordinator.executions.values() ) @@ -297,30 +324,28 @@ def supported_features(self): """Flag supported features.""" supported_features = 0 - if self.has_command(COMMAND_OPEN_SLATS): + if self.has_command(*COMMANDS_OPEN_TILT): supported_features |= SUPPORT_OPEN_TILT - if self.has_command(COMMAND_STOP_IDENTIFY, COMMAND_STOP, COMMAND_MY): + if self.has_command(*COMMANDS_STOP_TILT): supported_features |= SUPPORT_STOP_TILT - if self.has_command(COMMAND_CLOSE_SLATS): + if self.has_command(*COMMANDS_CLOSE_TILT): supported_features |= SUPPORT_CLOSE_TILT - if self.has_command(COMMAND_SET_ORIENTATION): + if self.has_command(*COMMANDS_SET_TILT_POSITION): supported_features |= SUPPORT_SET_TILT_POSITION - if self.has_command( - COMMAND_SET_POSITION, COMMAND_SET_CLOSURE, COMMAND_SET_PEDESTRIAN_POSITION - ): + if self.has_command(*COMMANDS_SET_POSITION): supported_features |= SUPPORT_SET_POSITION - if self.has_command(COMMAND_OPEN, COMMAND_UP, COMMAND_CYCLE): + if self.has_command(*COMMANDS_OPEN): supported_features |= SUPPORT_OPEN - if self.has_command(COMMAND_STOP_IDENTIFY, COMMAND_STOP, COMMAND_MY): + if self.has_command(*COMMANDS_STOP): supported_features |= SUPPORT_STOP - if self.has_command(COMMAND_CLOSE, COMMAND_DOWN, COMMAND_CYCLE): + if self.has_command(*COMMANDS_CLOSE): supported_features |= SUPPORT_CLOSE if self.has_command(COMMAND_SET_POSITION_AND_LINEAR_SPEED): diff --git a/custom_components/tahoma/light.py b/custom_components/tahoma/light.py index 393d2bac9..495563822 100644 --- a/custom_components/tahoma/light.py +++ b/custom_components/tahoma/light.py @@ -39,11 +39,11 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the TaHoma lights from a config entry.""" data = hass.data[DOMAIN][entry.entry_id] - coordinator = data.get("coordinator") + coordinator = data["coordinator"] entities = [ TahomaLight(device.deviceurl, coordinator) - for device in data.get("entities").get(LIGHT) + for device in data["entities"].get(LIGHT) ] async_add_entities(entities) diff --git a/custom_components/tahoma/lock.py b/custom_components/tahoma/lock.py index 4190876f5..8cf6dbe41 100644 --- a/custom_components/tahoma/lock.py +++ b/custom_components/tahoma/lock.py @@ -20,11 +20,11 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the TaHoma locks from a config entry.""" data = hass.data[DOMAIN][entry.entry_id] - coordinator = data.get("coordinator") + coordinator = data["coordinator"] entities = [ TahomaLock(device.deviceurl, coordinator) - for device in data.get("entities").get(LOCK) + for device in data["entities"].get(LOCK) ] async_add_entities(entities) diff --git a/custom_components/tahoma/manifest.json b/custom_components/tahoma/manifest.json index 7c1bc51f7..6c2ec013a 100644 --- a/custom_components/tahoma/manifest.json +++ b/custom_components/tahoma/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tahoma", "requirements": [ - "pyhoma==0.4.1" + "pyhoma==0.4.2" ], "codeowners": ["@philklei", "@imicknl", "@vlebourl", "@tetienne"], "issue_tracker": "https://github.com/imicknl/ha-tahoma/issues" diff --git a/custom_components/tahoma/scene.py b/custom_components/tahoma/scene.py index a72ad420a..3c8d985b0 100644 --- a/custom_components/tahoma/scene.py +++ b/custom_components/tahoma/scene.py @@ -15,11 +15,10 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the TaHoma scenes from a config entry.""" data = hass.data[DOMAIN][entry.entry_id] - coordinator = data.get("coordinator") + coordinator = data["coordinator"] entities = [ - TahomaScene(scene, coordinator.client) - for scene in data.get("entities").get(SCENE) + TahomaScene(scene, coordinator.client) for scene in data["entities"].get(SCENE) ] async_add_entities(entities) diff --git a/custom_components/tahoma/sensor.py b/custom_components/tahoma/sensor.py index 7f92b8898..51dd01291 100644 --- a/custom_components/tahoma/sensor.py +++ b/custom_components/tahoma/sensor.py @@ -18,7 +18,6 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, - UNIT_PERCENTAGE, VOLT, VOLUME_CUBIC_METERS, VOLUME_LITERS, @@ -28,6 +27,12 @@ from .const import DOMAIN from .tahoma_device import TahomaDevice +try: # TODO: Remove for core PR. This ensures compatibility with <0.115 + from homeassistant.const import PERCENTAGE +except Exception: + from homeassistant.const import UNIT_PERCENTAGE as PERCENTAGE + + _LOGGER = logging.getLogger(__name__) CORE_CO2_CONCENTRATION_STATE = "core:CO2ConcentrationState" @@ -85,7 +90,7 @@ "core:ElectricalPowerInMW": f"M{POWER_WATT}", "core:FlowInMeterCubePerHour": VOLUME_CUBIC_METERS, "core:LinearSpeedInMeterPerSecond": SPEED_METERS_PER_SECOND, - "core:RelativeValueInPercentage": UNIT_PERCENTAGE, + "core:RelativeValueInPercentage": PERCENTAGE, "core:VolumeInCubicMeter": VOLUME_CUBIC_METERS, "core:VolumeInLiter": VOLUME_LITERS, "core:FossilEnergyInWh": ENERGY_WATT_HOUR, @@ -98,11 +103,11 @@ 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] - coordinator = data.get("coordinator") + coordinator = data["coordinator"] entities = [ TahomaSensor(device.deviceurl, coordinator) - for device in data.get("entities").get(SENSOR) + for device in data["entities"].get(SENSOR) if device.states ] diff --git a/custom_components/tahoma/services.yaml b/custom_components/tahoma/services.yaml index 6099ab4f0..b40ec461f 100644 --- a/custom_components/tahoma/services.yaml +++ b/custom_components/tahoma/services.yaml @@ -24,3 +24,16 @@ set_cover_position_low_speed: refresh_states: description: Ask the box to refresh all devices states for protocols supporting that operation. + +execute_command: + description: Send a command to tahomalink endpoint through the API. + fields: + entity_id: + description: Name of entity that will be set to preset position. + example: "light.living_room" + command: + description: Command to send + example: "setIntensity" + args: + description: List of arguments to pass to the command if necessary + example: 100 diff --git a/custom_components/tahoma/switch.py b/custom_components/tahoma/switch.py index 603283620..109391248 100644 --- a/custom_components/tahoma/switch.py +++ b/custom_components/tahoma/switch.py @@ -7,7 +7,7 @@ DOMAIN as SWITCH, SwitchEntity, ) -from homeassistant.const import STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON from .const import COMMAND_OFF, COMMAND_ON, CORE_ON_OFF_STATE, DOMAIN from .tahoma_device import TahomaDevice @@ -17,8 +17,11 @@ COMMAND_CYCLE = "cycle" COMMAND_MEMORIZED_VOLUME = "memorizedVolume" COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE = "ringWithSingleSimpleSequence" +COMMAND_SET_FORCE_HEATING = "setForceHeating" COMMAND_STANDARD = "standard" +IO_FORCE_HEATING_STATE = "io:ForceHeatingState" + DEVICE_CLASS_SIREN = "siren" ICON_BELL_RING = "mdi:bell-ring" @@ -28,11 +31,11 @@ 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] - coordinator = data.get("coordinator") + coordinator = data["coordinator"] entities = [ TahomaSwitch(device.deviceurl, coordinator) - for device in data.get("entities").get(SWITCH) + for device in data["entities"].get(SWITCH) ] async_add_entities(entities) @@ -65,6 +68,9 @@ async def async_turn_on(self, **_): if self.has_command(COMMAND_ON): await self.async_execute_command(COMMAND_ON) + elif self.has_command(COMMAND_SET_FORCE_HEATING): + await self.async_execute_command(COMMAND_SET_FORCE_HEATING, STATE_ON) + elif self.has_command(COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE): await self.async_execute_command( COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE, # https://www.tahomalink.com/enduser-mobile-web/steer-html5-client/vendor/somfy/io/siren/const.js @@ -85,6 +91,9 @@ async def async_turn_off(self, **_): COMMAND_STANDARD, ) + elif self.has_command(COMMAND_SET_FORCE_HEATING): + await self.async_execute_command(COMMAND_SET_FORCE_HEATING, STATE_OFF) + elif self.has_command(COMMAND_OFF): await self.async_execute_command(COMMAND_OFF) @@ -96,4 +105,4 @@ async def async_toggle(self, **_): @property def is_on(self): """Get whether the switch is in on state.""" - return self.select_state(CORE_ON_OFF_STATE) == STATE_ON + return self.select_state(CORE_ON_OFF_STATE, IO_FORCE_HEATING_STATE) == STATE_ON diff --git a/custom_components/tahoma/tahoma_device.py b/custom_components/tahoma/tahoma_device.py index d9b8984e3..bf209cdd8 100644 --- a/custom_components/tahoma/tahoma_device.py +++ b/custom_components/tahoma/tahoma_device.py @@ -163,3 +163,7 @@ async def async_execute_command(self, command_name: str, *args: Any): } await self.coordinator.async_refresh() + + async def async_cancel_command(self, exec_id: str): + """Cancel device command in async context.""" + await self.coordinator.client.cancel_command(exec_id) diff --git a/requirements.test.txt b/requirements.test.txt index 90b8146b0..114fa43d5 100644 --- a/requirements.test.txt +++ b/requirements.test.txt @@ -7,4 +7,4 @@ pytest-cov<3.0.0 pytest-homeassistant # from our manifest.json for our Custom Component -pyhoma==0.4.1 \ No newline at end of file +pyhoma==0.4.2