From e00e04159a6689d7042fa8fe3d9e6f0cf3888dde Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Sun, 5 Jan 2025 19:31:26 +0100 Subject: [PATCH] support setting tilt --- .../hunterdouglas_powerview_ble/api.py | 19 ++++--- .../hunterdouglas_powerview_ble/cover.py | 51 ++++++++++++++++++- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/custom_components/hunterdouglas_powerview_ble/api.py b/custom_components/hunterdouglas_powerview_ble/api.py index df2149c..b2daf23 100644 --- a/custom_components/hunterdouglas_powerview_ble/api.py +++ b/custom_components/hunterdouglas_powerview_ble/api.py @@ -71,6 +71,7 @@ class ShadeCmd(Enum): SET_POSITION = 0x01F7 STOP = 0xB8F7 ACTIVATE_SCENE = 0xBAF7 + IDENTIFY = 0x11F7 @dataclass @@ -154,11 +155,12 @@ async def _cmd(self, cmd: tuple[ShadeCmd, bytes], disconnect: bool = True) -> No + bytes([self._seqcnt, len(cmd_run[1])]) + cmd_run[1] ) + LOGGER.debug("sending cmd: %s", tx_data.hex(" ")) if self._cipher is not None and self._is_encrypted: enc: AEADEncryptionContext = self._cipher.encryptor() tx_data = enc.update(tx_data) + enc.finalize() + LOGGER.debug(" encrypted: %s", tx_data.hex(" ")) self._data_event.clear() - LOGGER.debug("sending cmd: %s", tx_data) await self._client.write_gatt_char(UUID_TX, tx_data, False) self._seqcnt += 1 LOGGER.debug("waiting for response") @@ -180,8 +182,8 @@ def dec_manufacturer_data(data: bytearray) -> list[tuple[str, float]]: if len(data) != 9: LOGGER.debug("not a V2 record!") return [] - pos: int = int.from_bytes(data[3:5], byteorder="little") - pos2: int = (int(data[5]) << 4) + (int(data[4]) >> 4) + pos: Final[int] = int.from_bytes(data[3:5], byteorder="little") + pos2: Final[int] = (int(data[5]) << 4) + (int(data[4]) >> 4) return [ (ATTR_CURRENT_POSITION, ((pos >> 2) / 10)), ("position2", pos2 >> 2), @@ -270,7 +272,7 @@ def _verify_response(self, din: bytearray, seq_nr: int, cmd: ShadeCmd) -> bool: LOGGER.error("Wrong response data length") return False if int(data[4] != 0): - LOGGER.error("Command %d returned error #%d", cmd.value, int(data[4])) + LOGGER.error("Command %X returned error #%d", cmd.value, int(data[4])) return False return True @@ -308,8 +310,13 @@ def _on_disconnect(self, client: BleakClient) -> None: LOGGER.debug("Disconnected from %s", client.address) def _notification_handler(self, _sender, data: bytearray) -> None: - LOGGER.debug("%s received BLE data: %s", self.name, data) + LOGGER.debug("%s received BLE data: %s", self.name, data.hex(" ")) self._data = data + if self._cipher is not None and self._is_encrypted: + dec: AEADDecryptionContext = self._cipher.decryptor() + self._data = bytearray(dec.update(data) + dec.finalize()) + LOGGER.debug("%s %s", "decoded data: ".rjust(19+len(self.name)), self._data.hex(" ")) + self._data_event.set() async def _connect(self) -> None: @@ -321,7 +328,7 @@ async def _connect(self) -> None: LOGGER.debug("%s already connected", self.name) return - start = time.time() + start: float = time.time() self._client = await establish_connection( BleakClient, self._ble_device, diff --git a/custom_components/hunterdouglas_powerview_ble/cover.py b/custom_components/hunterdouglas_powerview_ble/cover.py index 7134592..d5d9327 100644 --- a/custom_components/hunterdouglas_powerview_ble/cover.py +++ b/custom_components/hunterdouglas_powerview_ble/cover.py @@ -8,7 +8,9 @@ ) from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, ATTR_POSITION, + ATTR_TILT_POSITION, CoverDeviceClass, CoverEntity, CoverEntityFeature, @@ -35,7 +37,7 @@ async def async_setup_entry( class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEntity): # type: ignore[reportIncompatibleVariableOverride] - """Representation of a powerview shade.""" + """Representation of a PowerView shade with Up/Down functionality only.""" _attr_has_entity_name = True _attr_device_class = CoverDeviceClass.SHADE @@ -170,3 +172,50 @@ async def async_stop_cover(self, **kwargs: Any) -> None: self.async_write_ha_state() except BleakError as err: LOGGER.error("Failed to stop cover '%s': %s", self.name, err) + + +class PowerViewCoverTilt(PowerViewCover): + """Representation of a PowerView shade with additional tilt functionality.""" + + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.STOP + # | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.SET_TILT_POSITION + # | CoverEntityFeature.OPEN_TILT + ) + + @property + def current_cover_tilt_position(self) -> int | None: # type: ignore[reportIncompatibleVariableOverride] + """Return current tilt of cover. + + None is unknown + """ + pos: Final = self._coord.data.get(ATTR_CURRENT_TILT_POSITION) + return round(pos) if pos is not None else None + + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: + """Move the tilt to a specific position.""" + + if isinstance(target_position := kwargs.get(ATTR_TILT_POSITION), int): + LOGGER.debug("set cover tilt to position %i", target_position) + if ( + self.current_cover_tilt_position == round(target_position) + or self.current_cover_position is None + ): + return + + try: + await self._coord.api.set_position( + self.current_cover_position, tilt=target_position + ) + self.async_write_ha_state() + except BleakError as err: + LOGGER.error( + "Failed to tilt cover '%s' to %f%%: %s", + self.name, + target_position, + err, + )