diff --git a/custom_components/espsomfy_rts/__init__.py b/custom_components/espsomfy_rts/__init__.py index 5e46a5f..3c3e139 100644 --- a/custom_components/espsomfy_rts/__init__.py +++ b/custom_components/espsomfy_rts/__init__.py @@ -27,8 +27,8 @@ async def _async_ws_close(_: Event) -> None: entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_ws_close) ) - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # hass.config_entries.async_setup_platforms(entry, PLATFORMS) await controller.ws_connect() return True diff --git a/custom_components/espsomfy_rts/__pycache__/__init__.cpython-310.pyc b/custom_components/espsomfy_rts/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..00ca7c4 Binary files /dev/null and b/custom_components/espsomfy_rts/__pycache__/__init__.cpython-310.pyc differ diff --git a/custom_components/espsomfy_rts/__pycache__/config_flow.cpython-310.pyc b/custom_components/espsomfy_rts/__pycache__/config_flow.cpython-310.pyc new file mode 100644 index 0000000..015a097 Binary files /dev/null and b/custom_components/espsomfy_rts/__pycache__/config_flow.cpython-310.pyc differ diff --git a/custom_components/espsomfy_rts/__pycache__/const.cpython-310.pyc b/custom_components/espsomfy_rts/__pycache__/const.cpython-310.pyc new file mode 100644 index 0000000..4b92d20 Binary files /dev/null and b/custom_components/espsomfy_rts/__pycache__/const.cpython-310.pyc differ diff --git a/custom_components/espsomfy_rts/__pycache__/controller.cpython-310.pyc b/custom_components/espsomfy_rts/__pycache__/controller.cpython-310.pyc new file mode 100644 index 0000000..e241641 Binary files /dev/null and b/custom_components/espsomfy_rts/__pycache__/controller.cpython-310.pyc differ diff --git a/custom_components/espsomfy_rts/__pycache__/cover.cpython-310.pyc b/custom_components/espsomfy_rts/__pycache__/cover.cpython-310.pyc new file mode 100644 index 0000000..1305b2d Binary files /dev/null and b/custom_components/espsomfy_rts/__pycache__/cover.cpython-310.pyc differ diff --git a/custom_components/espsomfy_rts/__pycache__/entity.cpython-310.pyc b/custom_components/espsomfy_rts/__pycache__/entity.cpython-310.pyc new file mode 100644 index 0000000..b747505 Binary files /dev/null and b/custom_components/espsomfy_rts/__pycache__/entity.cpython-310.pyc differ diff --git a/custom_components/espsomfy_rts/const.py b/custom_components/espsomfy_rts/const.py index c0e0b22..39540ca 100644 --- a/custom_components/espsomfy_rts/const.py +++ b/custom_components/espsomfy_rts/const.py @@ -5,6 +5,7 @@ API_CONTROLLER = "/controller" API_SHADES = "/shades" API_SHADECOMMAND = "/shadeCommand" +API_TILTCOMMAND = "/tiltCommand" API_DISCOVERY = "/discovery" EVT_CONTROLLER = "controller" EVT_SHADESTATE = "shadeState" diff --git a/custom_components/espsomfy_rts/controller.py b/custom_components/espsomfy_rts/controller.py index 7de7a67..c8b9a17 100644 --- a/custom_components/espsomfy_rts/controller.py +++ b/custom_components/espsomfy_rts/controller.py @@ -11,6 +11,7 @@ import aiohttp import websocket + from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant @@ -23,6 +24,7 @@ from .const import ( API_DISCOVERY, API_SHADECOMMAND, + API_TILTCOMMAND, API_SHADES, DOMAIN, EVT_CONNECTED, @@ -220,21 +222,36 @@ def ensure_shade_configured(self, data): for entity in async_entries_for_config_entry(entities, self.config_entry_id): if entity.unique_id == uuid: return + dev_features = (CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.SET_POSITION) + + dev_class = CoverDeviceClass.SHADE + if "shadeType" in data: + match int(data["shadeType"]): + case 1: + dev_class = CoverDeviceClass.BLIND + if "hasTilt" in data and data["hasTilt"] is True: + dev_features |= (CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT | CoverEntityFeature.SET_TILT_POSITION) + case 2: + dev_class = CoverDeviceClass.CURTAIN + case _: + dev_class = CoverDeviceClass.SHADE + + # Reload all the shades # self.api.load_shades() # I have no idea whether this reloads the devices or not. entities.async_get_or_create( domain=DOMAIN, platform=Platform.COVER, - original_device_class=CoverDeviceClass.SHADE, + original_device_class=dev_class, unique_id=uuid, device_id=device.id, original_name=data["name"], suggested_object_id=f"{str(data['name']).lower().replace(' ', '_')}", - supported_features=CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.STOP - | CoverEntityFeature.SET_POSITION, + supported_features=dev_features, ) print(f"Shade not found {uuid} and one was added") @@ -333,6 +350,19 @@ async def load_shades(self) -> Any | None: else: _LOGGER.error(await resp.text()) + async def tilt_open(self, shade_id: int): + """Send the command to open the tilt""" + await self.tilt_command({"shadeId": shade_id, "command": "up"}) + + async def tilt_close(self, shade_id: int): + """Send the command to close the tilt""" + await self.tilt_command({"shadeId": shade_id, "command": "down"}) + + async def position_tilt(self, shade_id: int, position: int): + """Send the command to position the shade""" + print(f"Setting tilt position to {position}") + await self.tilt_command({"shadeId": shade_id, "target": position}) + async def open_shade(self, shade_id: int): """Send the command to open the shade""" await self.shade_command({"shadeId": shade_id, "command": "up"}) @@ -359,6 +389,16 @@ async def shade_command(self, data): else: _LOGGER.error(await resp.text()) + async def tilt_command(self, data): + """Send commands to ESPSomfyRTS via PUT request""" + async with self._session.put( + f"{self._api_url}{API_TILTCOMMAND}", json=data + ) as resp: + if resp.status == 200: + pass + else: + _LOGGER.error(await resp.text()) + async def get_initial(self): """Get the initial config from nodejs-PoolController.""" try: diff --git a/custom_components/espsomfy_rts/cover.py b/custom_components/espsomfy_rts/cover.py index 4070aa7..a3c3929 100644 --- a/custom_components/espsomfy_rts/cover.py +++ b/custom_components/espsomfy_rts/cover.py @@ -1,4 +1,4 @@ -"""Support for ESPSomfy RTS Shades.""" +"""Support for ESPSomfy RTS Shades and Blinds.""" from __future__ import annotations from typing import Any @@ -7,6 +7,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, + ATTR_TILT_POSITION, CoverDeviceClass, CoverEntity, CoverEntityFeature, @@ -25,6 +26,9 @@ SVC_CLOSE_SHADE = "close_shade" SVC_STOP_SHADE = "stop_shade" SVC_SET_SHADE_POS = "set_shade_position" +SVC_TILT_OPEN = "tilt_open" +SVC_TILT_CLOSE = "tilt_close" +SVC_SET_TILT_POS = "set_tilt_position" async def async_setup_entry( @@ -49,9 +53,16 @@ async def async_setup_entry( {vol.Required(ATTR_POSITION): cv.string}, "async_set_cover_position", ) + platform.async_register_entity_service( + SVC_SET_TILT_POS, + {vol.Required(ATTR_POSITION): cv.string}, + "async_set_cover_tilt_position", + ) platform.async_register_entity_service(SVC_OPEN_SHADE, {}, "async_open_cover") platform.async_register_entity_service(SVC_CLOSE_SHADE, {}, "async_close_cover") platform.async_register_entity_service(SVC_STOP_SHADE, {}, "async_stop_cover") + platform.async_register_entity_service(SVC_TILT_OPEN, {}, "async_tilt_open") + platform.async_register_entity_service(SVC_TILT_CLOSE, {}, "async_tilt_close") class ESPSomfyShade(ESPSomfyEntity, CoverEntity): @@ -62,10 +73,13 @@ def __init__(self, controller: ESPSomfyController, data): self._controller = controller self._shade_id = data["shadeId"] self._position = data["position"] + self._tilt_postition = 100 + self._tilt_direction = 0 self._attr_unique_id = f"{controller.unique_id}_{self._shade_id}" self._attr_name = data["name"] self._direction = 0 self._available = True + self._has_tilt = False self._attr_device_class = CoverDeviceClass.SHADE self._attr_supported_features = ( @@ -74,6 +88,24 @@ def __init__(self, controller: ESPSomfyController, data): | CoverEntityFeature.STOP | CoverEntityFeature.SET_POSITION ) + if "hasTilt" in data and data["hasTilt"] is True: + self._attr_supported_features |= ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.SET_TILT_POSITION + ) + self._has_tilt = True + self._tilt_postition = data["tiltPosition"] if "tiltPosition" in data else 100 + self._tilt_direction = data["tiltDirection"] if "tiltDirecion" in data else 0 + if "shadeType" in data: + match data["shadeType"]: + case 1: + self._attr_device_class = CoverDeviceClass.BLIND + case 2: + self._attr_device_class = CoverDeviceClass.CURTAIN + case _: + self._attr_device_class = CoverDeviceClass.SHADE + self._attr_is_closed: bool = False # print(f"Set up shade {self._attr_unique_id} - {self._attr_name}") @@ -87,6 +119,13 @@ def _handle_coordinator_update(self) -> None: self._position = int(self._controller.data["position"]) if "direction" in self._controller.data: self._direction = int(self._controller.data["direction"]) + if "hasTilt" in self._controller.data: + self._has_tilt = self._controller.data["hasTilt"] + if self._has_tilt is True: + if "tiltDirection" in self._controller.data: + self._tilt_direction = int(self._controller.data["tiltDirection"]) + if "tiltPosition" in self._controller.data: + self._tilt_postition = int(self._controller.data["tiltPosition"]) self._available = True elif self._controller.data["event"] == EVT_SHADEREMOVED: self._available = False @@ -109,9 +148,15 @@ def should_poll(self) -> bool: return False @property - def current_cover_position(self) -> int: + def current_cover_position(self) -> int | None: """Return the current position of the shade.""" return 100 - self._position + @property + def current_cover_tilt_position(self) -> int | None: + """Return current position of cover tilt. 0 is closed, 100 is open.""" + if not self._has_tilt: + return None + return 100 - self._tilt_postition @property def is_opening(self) -> bool: @@ -133,6 +178,19 @@ def is_open(self) -> bool: """Return true if cover is closed.""" return self._position == 0 + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: + """Set the tilt postion""" + await self._controller.api.position_tilt( + self._shade_id, 100 - kwargs[ATTR_TILT_POSITION] + ) + async def async_open_cover_tilt(self, **kwargs: Any) -> None: + """Open the tilt position""" + await self._controller.api.tilt_open(self._shade_id) + + async def async_close_cover_tilt(self, **kwargs: Any) -> None: + """Close the tilt position""" + await self._controller.api.tilt_close(self._shade_id) + async def async_set_cover_position(self, **kwargs: Any) -> None: """Set the cover position.""" await self._controller.api.position_shade( diff --git a/custom_components/espsomfy_rts/manifest.json b/custom_components/espsomfy_rts/manifest.json index b7824ce..0c4d075 100644 --- a/custom_components/espsomfy_rts/manifest.json +++ b/custom_components/espsomfy_rts/manifest.json @@ -20,5 +20,5 @@ "dependencies": [], "codeowners": ["@rstrouse"], "iot_class": "local_push", - "version": "1.0.0" + "version": "1.0.8" } diff --git a/custom_components/espsomfy_rts/services.yaml b/custom_components/espsomfy_rts/services.yaml index a047a84..feb824d 100644 --- a/custom_components/espsomfy_rts/services.yaml +++ b/custom_components/espsomfy_rts/services.yaml @@ -38,6 +38,37 @@ set_shade_postion: integration: espsomfy_rts domain: cover +tilt_open: + name: Tilt Open + description: Tilts the slats open + target: + integration: espsomfy_rts + domain: cover + +tilt_close: + name: Tilt Close + description: Tilts the slats closed + target: + integration: espsomfy_rts + domain: cover + +set_tilt_postion: + name: Set Position + description: Sets the tilt position to % of open + fields: + position: + name: position + description: Percentage of open for the shade slats + required: true + example: 50 + selector: + text: + target: + entity: + integration: espsomfy_rts + domain: cover + +