From 0bf3bf8da5de7ae3f553587d8a6d8415899f7217 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Tue, 19 Sep 2023 10:59:21 +0000 Subject: [PATCH 01/13] Fix logging regression --- aiopvapi/hub.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiopvapi/hub.py b/aiopvapi/hub.py index 7feb720..6124540 100644 --- a/aiopvapi/hub.py +++ b/aiopvapi/hub.py @@ -349,7 +349,7 @@ async def detect_api_version(self): if _main: self._main_processor_version = self._make_version(_main) self.request.api_version = self._main_processor_version.api - _LOGGER.error(self._main_processor_version.api) + _LOGGER.debug("API Version: %s", self._main_processor_version.api) if not self.api_version: - _LOGGER.error(self._raw_firmware) + _LOGGER.error("Unable to decipher firmware %s", self._raw_firmware) From 6e5dc665f25e1a59449e84e7f63467e7ca0ce4ce Mon Sep 17 00:00:00 2001 From: kingy444 Date: Tue, 19 Sep 2023 10:59:44 +0000 Subject: [PATCH 02/13] ShadeVerticalTiltAnywhere + ShadeTiltOnly --- aiopvapi/resources/shade.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/aiopvapi/resources/shade.py b/aiopvapi/resources/shade.py index 442d318..5c91625 100644 --- a/aiopvapi/resources/shade.py +++ b/aiopvapi/resources/shade.py @@ -809,6 +809,14 @@ class ShadeVerticalTiltAnywhere(ShadeBottomUpTiltAnywhere): "Vertical Tilt Anywhere", ) + def get_additional_positions(self, positions: ShadePosition) -> ShadePosition: + """Returns additonal positions not reported by the hub""" + if positions.primary is None: + positions.primary = MIN_POSITION + if positions.tilt is None: + positions.tilt = MIN_POSITION + return positions + class ShadeTiltOnly(BaseShadeTilt): """Type 5 - Tilt Only 180° @@ -833,14 +841,8 @@ def __init__( super().__init__(raw_data, shade_type, request) self._open_position = ShadePosition() self._close_position = ShadePosition() - self._open_position_tilt = ShadePosition(tilt=MAX_POSITION) + self._open_position_tilt = ShadePosition(tilt=MID_POSITION) self._close_position_tilt = ShadePosition(tilt=MIN_POSITION) - if self.api_version < 3: - self._open_position_tilt = ShadePosition(tilt=MID_POSITION) - - async def move(self, position_data=None): - _LOGGER.error("Move unsupported. Position request(%s) ignored", position_data) - return def get_additional_positions(self, positions: ShadePosition) -> ShadePosition: """Returns additonal positions not reported by the hub""" @@ -1040,6 +1042,7 @@ def __init__( if self.api_version < 3: self.shade_limits = ShadeLimits(tilt_max=MAX_POSITION) + def factory(raw_data: dict, request: AioRequest): """Class factory to create different types of shades depending on shade type.""" From 59f0e975df274f7b6d687b13b569ae090d40ed74 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Tue, 19 Sep 2023 10:59:53 +0000 Subject: [PATCH 03/13] update readme --- README.rst | 7 ++++++- readme.md | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 8480092..46052d3 100644 --- a/README.rst +++ b/README.rst @@ -60,4 +60,9 @@ Changelog - Raw hub data updates made via defined function (`request_raw_data`, `request_home_data`, `request_raw_firware`, `detect_api_version`) - Parse Gen 3 hub name based on serial + mac - Find API version based on firmware revision -- Remove async_timeout and move to asyncio \ No newline at end of file +- Remove async_timeout and move to asyncio + +**v3.0.2** + +- Bugfix logging on initial setup +- Fixes for ShadeVerticalTiltAnywhere + ShadeTiltOnly \ No newline at end of file diff --git a/readme.md b/readme.md index b769ef7..1a8dba2 100644 --- a/readme.md +++ b/readme.md @@ -71,6 +71,11 @@ Have a look at the examples folder for some guidance how to use it. - Find API version based on firmware revision - Remove async_timeout and move to asyncio +### v3.0.2 + +- Bugfix logging on initial setup +- Fixes for ShadeVerticalTiltAnywhere + ShadeTiltOnly + ## Links --- From 49af3364b528ee31c03e046135178cfec51a60b6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 20 Jan 2024 11:35:42 +0100 Subject: [PATCH 04/13] Remove asyncio --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e78ac8f..b5c9177 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ VERSION = None # What packages are required for this module to be executed? -REQUIRED = ["asyncio", "aiohttp>=3.7.4,<4"] +REQUIRED = ["aiohttp>=3.7.4,<4"] # What packages are optional? EXTRAS = {} From f78466309e60a0965fc32533b2015267df8a577c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 20 Jan 2024 11:46:19 +0100 Subject: [PATCH 05/13] Remove json --- requirements-dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 26b527b..f22e2bd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,4 +5,3 @@ nox pytest-cov python-coveralls wheel -json From e2369c33ce9dde58eea56627fbc395b8c25e9331 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Tue, 30 Jan 2024 21:14:08 +1100 Subject: [PATCH 06/13] Update readme.md --- readme.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 1a8dba2..e436cb9 100644 --- a/readme.md +++ b/readme.md @@ -5,11 +5,74 @@ # Aio PowerView API -A python async API for PowerView blinds. -Written for Home-Assistant. Adding features as I go... +A python async API for PowerView blinds written for Home-Assistant. Have a look at the examples folder for some guidance how to use it. +## Capabilities + +| Description | Capabilities | Primary | Secondary | Tilt | Tilt Position | Vertical | DualShade | +| :------------------------------------ | :----------: | :-----: | :-------: | :--: | :-----------: | :------: | :-------: | +| Bottom Up | 0 | X | | | | | | +| Bottom Up Tilt 180° | 1* | X | | 180° | Closed | | | +| Bottom Up Tilt 90° | 1 | X | | 90° | Closed | | | +| Bottom Up Tilt 180° | 2 | X | | 180° | Anywhere | | | +| Vertical | 3 | X | | | | X | | +| Vertical Tilt Anywhere | 4 | X | | 180° | Anywhere | X | | +| Tilt Only 180° | 5 | | | 180° | Anywhere | | | +| Top Down | 6 | | X | | | | | +| Top Down Bottom Up | 7 | X | X | | | | | +| Dual Shade Overlapped | 8 | X | X | | | | X | +| Dual Shade Overlapped Tilt 90° | 9 | X | X | 90° | Closed | | X | +| Dual Shade Overlapped Tilt 90° | 10 | X | X | 180° | Closed | | X | + +## Shades + +Shades that have been directly added to the API are listed below and should function correctly. In **most** cases this is identification is purely aestetic. + +Shades not listed will get their features from their **capabilities**, unfortunately the json returned from the shade can sometimes be incorrect and we need to override the features for the API (and Home-Assistant) to read them correctly. + +| Name | Type | Capability | +| :------------------------------------ | :--: | :--------: | +| AC Roller | 49 | 0 | +| Banded Shades | 52 | 0 | +| Bottom Up | 5 | 0 | +| Curtain, Left Stack | 69 | 3 | +| Curtain, Right Stack | 70 | 3 | +| Curtain, Split Stack | 71 | 3 | +| Designer Roller | 1 | 0 | +| Duette | 6 | 0 | +| Duette, Top Down Bottom Up | 8 | 7 | +| Duette Architella, Top Down Bottom Up | 33 | 7 | +| Duette DuoLite, Top Down Bottom Up | 9 | 7 | +| Duolite Lift | 79 | 9 | +| Facette | 43 | 1 | +| M25T Roller Blind | 42 | 0 | +| Palm Beach Shutters | 66 | 5 | +| Pirouette | 18 | 1 | +| Pleated, Top Down Bottom Up | 47 | 7 | +| Provenance Woven Wood | 19 | 0 | +| Roman | 4 | 0 | +| Silhouette | 23 | 1 | +| Silhouette Duolite | 38 | 9 | +| Skyline Panel, Left Stack | 26 | 3 | +| Skyline Panel, Right Stack | 27 | 3 | +| Skyline Panel, Split Stack | 28 | 3 | +| Top Down | 7 | 6 | +| Twist | 44 | 1* | +| Vignette | 31 | 0 | +| Vignette | 32 | 0 | +| Vignette | 84 | 0 | +| Vignette Duolite | 65 | 8 | +| Vertical | 3 | 0 | +| Vertical Slats, Left Stack | 54 | 4 | +| Vertical Slats, Right Stack | 55 | 4 | +| Vertical Slats, Split Stack | 56 | 4 | +| Venetian, Tilt Anywhere | 51 | 2 | +| Venetian, Tilt Anywhere | 62 | 2 | + +\* No other shade are known to have this capability and the only way to get this functionality is by hardcoding in the API + ## Development - Install dev requirements. @@ -73,8 +136,9 @@ Have a look at the examples folder for some guidance how to use it. ### v3.0.2 -- Bugfix logging on initial setup -- Fixes for ShadeVerticalTiltAnywhere + ShadeTiltOnly +- Add type 19 (Provenance Woven Wood) +- Fix Positioning for ShadeVerticalTiltAnywhere + ShadeTiltOnly (Mid only) + Logging regression fix ## Links From 92562f177895cdfb41c5a4f6eaf5c83932c5cd6d Mon Sep 17 00:00:00 2001 From: kingy444 Date: Tue, 30 Jan 2024 21:14:27 +1100 Subject: [PATCH 07/13] Add Type 19 --- aiopvapi/resources/shade.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiopvapi/resources/shade.py b/aiopvapi/resources/shade.py index 5c91625..882ddec 100644 --- a/aiopvapi/resources/shade.py +++ b/aiopvapi/resources/shade.py @@ -629,6 +629,7 @@ class ShadeBottomUp(BaseShade): ShadeType(5, "Bottom Up"), ShadeType(6, "Duette"), ShadeType(10, "Duette and Applause SkyLift"), + ShadeType(19, "Provenance Woven Wood"), ShadeType(31, "Vignette"), ShadeType(32, "Vignette"), ShadeType(42, "M25T Roller Blind"), From e8b86c0fbe9ae02c2d63455fb857a38b37b2d44e Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 15 Feb 2024 21:44:34 +1100 Subject: [PATCH 08/13] handle v2 shade positioning --- aiopvapi/helpers/constants.py | 7 +++++++ aiopvapi/resources/shade.py | 16 +++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/aiopvapi/helpers/constants.py b/aiopvapi/helpers/constants.py index fa30181..fd293d6 100644 --- a/aiopvapi/helpers/constants.py +++ b/aiopvapi/helpers/constants.py @@ -61,6 +61,13 @@ MID_POSITION = 50 MAX_POSITION = 100 CLOSED_POSITION = 0 +# there are a number of shades (duette variety) that despite +# being closed visually, actually report a position that is not 0 +# this number is generally below 491.5125, and if not a calibration +# can bring the shade within this realm +# essentially treat a v2 shade that reports a position of 491.5125 or +# less as closed +CLOSED_POSITION_V2 = 0.75 # v2 FWVERSION = "fwversion" diff --git a/aiopvapi/resources/shade.py b/aiopvapi/resources/shade.py index 882ddec..5d3dc3e 100644 --- a/aiopvapi/resources/shade.py +++ b/aiopvapi/resources/shade.py @@ -23,6 +23,8 @@ ATTR_TILT, ATTR_BATTERY_KIND, ATTR_POWER_TYPE, + CLOSED_POSITION, + CLOSED_POSITION_V2, FIRMWARE, FIRMWARE_REVISION, FIRMWARE_SUB_REVISION, @@ -252,8 +254,8 @@ def api_to_percent(self, position: float, position_type: str) -> int: if self.api_version < 3: max_position_api = MAX_POSITION_V2 * max_position_api - percent = self.position_limit(round((position / max_position_api) * 100)) - return percent + percent = self.position_limit((position / max_position_api) * 100) + return round(percent) def structured_to_raw(self, data: ShadePosition) -> dict[str, Any]: """Convert structured ShadePosition to API relevant dict""" @@ -415,6 +417,10 @@ def position_limit(self, position: int, position_type: str = ""): min_limit, max_limit = limits.get(position_type, (0, 100)) + if self.api_version < 3 and position != 0 and position < CLOSED_POSITION_V2: + _LOGGER.debug("%s: Assuming shade is closed as %s is less than %s", self.name, position, CLOSED_POSITION) + position = CLOSED_POSITION + return min(max(min_limit, position), max_limit) async def _motion(self, motion): @@ -674,7 +680,7 @@ class ShadeBottomUpTiltOnClosed180(BaseShadeTilt): tilt_onclosed=True, tilt_180=True, ), - "Bottom Up Tilt 180°", + "Bottom Up TiltOnClosed 180°", ) def __init__( @@ -708,7 +714,7 @@ class ShadeBottomUpTiltOnClosed90(BaseShadeTilt): tilt_onclosed=True, tilt_90=True, ), - "Bottom Up Tilt 90°", + "Bottom Up TiltOnClosed 90°", ) def __init__( @@ -743,7 +749,7 @@ class ShadeBottomUpTiltAnywhere(BaseShadeTilt): tilt_anywhere=True, tilt_180=True, ), - "Bottom Up Tilt 180°", + "Bottom Up TiltAnywhere 180°", ) def __init__( From 998c24ce828133f1bee7b5ede184489a30a64384 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 15 Feb 2024 22:02:07 +1100 Subject: [PATCH 09/13] string formatting --- aiopvapi/resources/shade.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/aiopvapi/resources/shade.py b/aiopvapi/resources/shade.py index 5d3dc3e..a87cb22 100644 --- a/aiopvapi/resources/shade.py +++ b/aiopvapi/resources/shade.py @@ -188,7 +188,12 @@ def firmware(self) -> str | None: if FIRMWARE not in self.raw_data: return None firmware = self.raw_data[FIRMWARE] - return f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}" + + revision = firmware[FIRMWARE_REVISION] + sub_revision = firmware[FIRMWARE_SUB_REVISION] + build = firmware[FIRMWARE_BUILD] + + return f"{revision}.{sub_revision}.{build}" @property def url(self) -> str: @@ -418,7 +423,12 @@ def position_limit(self, position: int, position_type: str = ""): min_limit, max_limit = limits.get(position_type, (0, 100)) if self.api_version < 3 and position != 0 and position < CLOSED_POSITION_V2: - _LOGGER.debug("%s: Assuming shade is closed as %s is less than %s", self.name, position, CLOSED_POSITION) + _LOGGER.debug( + "%s: Assuming shade is closed as %s is less than %s", + self.name, + position, + CLOSED_POSITION, + ) position = CLOSED_POSITION return min(max(min_limit, position), max_limit) From e30d300cc6a587e2a2ce44bb691318c7906dea99 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 15 Feb 2024 22:02:37 +1100 Subject: [PATCH 10/13] handle empty data --- aiopvapi/helpers/aiorequest.py | 4 ++++ aiopvapi/hub.py | 37 ++++++++++++++++++++-------------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/aiopvapi/helpers/aiorequest.py b/aiopvapi/helpers/aiorequest.py index de6bf61..9be33cd 100644 --- a/aiopvapi/helpers/aiorequest.py +++ b/aiopvapi/helpers/aiorequest.py @@ -25,6 +25,10 @@ class PvApiConnectionError(PvApiError): """Problem connecting to PowerView hub.""" +class PvApiEmptyData(PvApiError): + """PowerView hub returned empty data.""" + + class AioRequest: """Request class managing hub connection.""" diff --git a/aiopvapi/hub.py b/aiopvapi/hub.py index 6124540..f19b256 100644 --- a/aiopvapi/hub.py +++ b/aiopvapi/hub.py @@ -1,6 +1,6 @@ """Hub class acting as the base for the PowerView API.""" import logging -from aiopvapi.helpers.aiorequest import PvApiConnectionError +from aiopvapi.helpers.aiorequest import PvApiConnectionError, PvApiEmptyData from aiopvapi.helpers.api_base import ApiBase from aiopvapi.helpers.constants import ( @@ -41,9 +41,7 @@ def __init__(self, revision, sub_revision, build, name=None) -> None: self._name = name def __repr__(self): - return "REVISION: {} SUB_REVISION: {} BUILD: {} ".format( - self._revision, self._sub_revision, self._build - ) + return f"REVISION: {self._revision} SUB_REVISION: {self._sub_revision} BUILD: {self._build}" @property def name(self) -> str: @@ -177,6 +175,9 @@ async def _query_firmware_g2(self): # self._raw_data = await self.request.get(join_path(self._base_path, "userdata")) self._raw_data = await self.request_raw_data() + if not self._raw_data or self._raw_data == {}: + raise PvApiEmptyData("Hub returned empty data") + _main = self._parse(USER_DATA, FIRMWARE, FIRMWARE_MAINPROCESSOR) if not _main: # do some checking for legacy v1 failures @@ -213,6 +214,9 @@ async def _query_firmware_g3(self): # self._raw_data = await self.request.get(gateway) self._raw_data = await self.request_raw_data() + if not self._raw_data or self._raw_data == {}: + raise PvApiEmptyData("Hub returned empty data") + _main = self._parse(CONFIG, FIRMWARE, FIRMWARE_MAINPROCESSOR) if _main: self._main_processor_version = self._make_version(_main) @@ -235,16 +239,19 @@ async def _query_firmware_g3(self): home = await self.request_home_data() # Find the hub based on the serial number or MAC hub = None - for gateway in home["gateways"]: - if gateway.get("serial") == self.serial_number: - self.hub_name = gateway.get("name") - break - if gateway.get("mac") == self.mac_address: - self.hub_name = gateway.get("name") - break + if "gateways" in home: + for gateway in home["gateways"]: + if gateway.get("serial") == self.serial_number: + hub = gateway.get("name") + self.hub_name = gateway.get("name") + break + if gateway.get("mac") == self.mac_address: + hub = gateway.get("name") + self.hub_name = gateway.get("name") + break if hub is None: - _LOGGER.debug(f"Hub with serial {self.serial_number} not found.") + _LOGGER.debug("Hub with serial %s not found.",self.serial_number) def _make_version(self, data: dict) -> Version: return Version( @@ -254,12 +261,12 @@ def _make_version(self, data: dict) -> Version: data.get(FIRMWARE_NAME), ) - def _make_version_data_from_str(self, fwVersion: str, name: str = None) -> dict: + def _make_version_data_from_str(self, fw_version: str, name: str = None) -> dict: # Split the version string into components - components = fwVersion.split(".") + components = fw_version.split(".") if len(components) != 3: - raise ValueError("Invalid version format: {}".format(fwVersion)) + raise ValueError(f"Invalid version format: {fw_version}") revision, sub_revision, build = map(int, components) From 99c17f67f563d10910d89430a7c9c33a65168237 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 15 Feb 2024 22:02:53 +1100 Subject: [PATCH 11/13] bumpe version tag --- aiopvapi/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiopvapi/__version__.py b/aiopvapi/__version__.py index dd0d624..00f1cb1 100644 --- a/aiopvapi/__version__.py +++ b/aiopvapi/__version__.py @@ -1,3 +1,3 @@ """Aio PowerView api version.""" -__version__ = "3.0.1" +__version__ = "3.0.2" From 422829e3e966c28217416a34e0855c0de3ee096f Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 15 Feb 2024 22:25:35 +1100 Subject: [PATCH 12/13] notes --- aiopvapi/helpers/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiopvapi/helpers/constants.py b/aiopvapi/helpers/constants.py index fccedc7..4e6c361 100644 --- a/aiopvapi/helpers/constants.py +++ b/aiopvapi/helpers/constants.py @@ -66,7 +66,7 @@ # this number is generally below 491.5125, and if not a calibration # can bring the shade within this realm # essentially treat a v2 shade that reports a position of 491.5125 or -# less as closed +# less as closed. Still use percentage based for compatability CLOSED_POSITION_V2 = 0.75 # v2 From fb92d0c16170751a2d8ff9605cf5c87f439d5b34 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 15 Feb 2024 22:25:46 +1100 Subject: [PATCH 13/13] readme updates --- README.rst | 10 ++++++++-- readme.md | 9 +++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 46052d3..486e9bf 100644 --- a/README.rst +++ b/README.rst @@ -64,5 +64,11 @@ Changelog **v3.0.2** -- Bugfix logging on initial setup -- Fixes for ShadeVerticalTiltAnywhere + ShadeTiltOnly \ No newline at end of file +- Add type 19 (Provenance Woven Wood) +- Fix Positioning for ShadeVerticalTiltAnywhere + ShadeTiltOnly (Mid only) +- Fix logging regression on initial setup +- Fixes for ShadeVerticalTiltAnywhere + ShadeTiltOnly +- Fix tests +- Remove unneeded declerations +- Fix shade position reporting for v2 shades +- Dandle empty hub data being returned \ No newline at end of file diff --git a/readme.md b/readme.md index e436cb9..6cbc73b 100644 --- a/readme.md +++ b/readme.md @@ -137,8 +137,13 @@ Shades not listed will get their features from their **capabilities**, unfortuna ### v3.0.2 - Add type 19 (Provenance Woven Wood) -- Fix Positioning for ShadeVerticalTiltAnywhere + ShadeTiltOnly (Mid only) - Logging regression fix +- Fix Positioning for ShadeVerticalTiltAnywhere + ShadeTiltOnly (Mid only) +- Fix logging regression on initial setup +- Fixes for ShadeVerticalTiltAnywhere + ShadeTiltOnly +- Fix tests +- Remove unneeded declerations +- Fix shade position reporting for v2 shades +- Dandle empty hub data being returned ## Links