Skip to content

Commit

Permalink
Merge pull request #36 from sander76/3.0.2
Browse files Browse the repository at this point in the history
v3.0.2
  • Loading branch information
kingy444 authored Feb 15, 2024
2 parents e60af51 + fb92d0c commit d9b8e39
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 35 deletions.
13 changes: 12 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,15 @@ 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
- Remove async_timeout and move to asyncio

**v3.0.2**

- 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
2 changes: 1 addition & 1 deletion aiopvapi/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Aio PowerView api version."""

__version__ = "3.0.1"
__version__ = "3.0.2"
4 changes: 4 additions & 0 deletions aiopvapi/helpers/aiorequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
7 changes: 7 additions & 0 deletions aiopvapi/helpers/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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. Still use percentage based for compatability
CLOSED_POSITION_V2 = 0.75

# v2
FWVERSION = "fwversion"
Expand Down
41 changes: 24 additions & 17 deletions aiopvapi/hub.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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)

Expand Down Expand Up @@ -349,7 +356,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)
46 changes: 33 additions & 13 deletions aiopvapi/resources/shade.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
ATTR_TILT,
ATTR_BATTERY_KIND,
ATTR_POWER_TYPE,
CLOSED_POSITION,
CLOSED_POSITION_V2,
FIRMWARE,
FIRMWARE_REVISION,
FIRMWARE_SUB_REVISION,
Expand Down Expand Up @@ -186,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:
Expand Down Expand Up @@ -252,8 +259,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"""
Expand Down Expand Up @@ -415,6 +422,15 @@ 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):
Expand Down Expand Up @@ -629,6 +645,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"),
Expand Down Expand Up @@ -673,7 +690,7 @@ class ShadeBottomUpTiltOnClosed180(BaseShadeTilt):
tilt_onclosed=True,
tilt_180=True,
),
"Bottom Up Tilt 180°",
"Bottom Up TiltOnClosed 180°",
)

def __init__(
Expand Down Expand Up @@ -707,7 +724,7 @@ class ShadeBottomUpTiltOnClosed90(BaseShadeTilt):
tilt_onclosed=True,
tilt_90=True,
),
"Bottom Up Tilt 90°",
"Bottom Up TiltOnClosed 90°",
)

def __init__(
Expand Down Expand Up @@ -742,7 +759,7 @@ class ShadeBottomUpTiltAnywhere(BaseShadeTilt):
tilt_anywhere=True,
tilt_180=True,
),
"Bottom Up Tilt 180°",
"Bottom Up TiltAnywhere 180°",
)

def __init__(
Expand Down Expand Up @@ -809,6 +826,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°
Expand All @@ -833,14 +858,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"""
Expand Down Expand Up @@ -1040,6 +1059,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."""
Expand Down
78 changes: 76 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -71,6 +134,17 @@ 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

- 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

## Links

---
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down

0 comments on commit d9b8e39

Please sign in to comment.