Skip to content

Commit

Permalink
Merge pull request #30 from sander76/3.0.1
Browse files Browse the repository at this point in the history
3.0.1
  • Loading branch information
kingy444 authored Sep 13, 2023
2 parents 7e145bd + 7867d65 commit 538dd3d
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 55 deletions.
9 changes: 8 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,11 @@ Changelog
- UserData class is deprecated and replaced with Hub.
- ShadePosition class now replaces the raw json management of shades in support of cross generational management.
- Schedules / Automations are now supported by the API
- New get_*objecttype* methods available to returned structured data objects for consistent management
- New get_*objecttype* methods available to returned structured data objects for consistent management

**v3.0.1**

- 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
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.0"
__version__ = "3.0.1"
37 changes: 4 additions & 33 deletions aiopvapi/helpers/aiorequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
import logging

import aiohttp
import async_timeout

from aiopvapi.helpers.constants import FWVERSION
from aiopvapi.helpers.tools import join_path, get_base_path

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -101,7 +97,7 @@ async def get(self, url: str, params: str = None) -> dict:
response = None
try:
_LOGGER.debug("Sending GET request to: %s params: %s", url, params)
with async_timeout.timeout(self._timeout):
async with asyncio.timeout(self._timeout):
response = await self.websession.get(url, params=params)
return await self.check_response(response, [200, 204])
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
Expand All @@ -123,7 +119,7 @@ async def post(self, url: str, data: dict = None):
response = None
try:
_LOGGER.debug("Sending POST request to: %s data: %s", url, data)
with async_timeout.timeout(self._timeout):
async with asyncio.timeout(self._timeout):
response = await self.websession.post(url, json=data)
return await self.check_response(response, [200, 201])
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
Expand All @@ -150,7 +146,7 @@ async def put(self, url: str, data: dict = None, params=None):
params,
data,
)
with async_timeout.timeout(self._timeout):
async with asyncio.timeout(self._timeout):
response = await self.websession.put(url, json=data, params=params)
return await self.check_response(response, [200, 204])
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
Expand All @@ -174,7 +170,7 @@ async def delete(self, url: str, params: dict = None):
response = None
try:
_LOGGER.debug("Sending DELETE request to: %s with param %s", url, params)
with async_timeout.timeout(self._timeout):
async with asyncio.timeout(self._timeout):
response = await self.websession.delete(url, params=params)
return await self.check_response(response, [200, 204])
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
Expand All @@ -184,28 +180,3 @@ async def delete(self, url: str, params: dict = None):
finally:
if response is not None:
await response.release()

async def set_api_version(self):
"""
Set the API generation based on what the gateway responds to.
"""
_LOGGER.debug("Attempting Gen 2 connection")
try:
await self.get(get_base_path(self.hub_ip, join_path("api", FWVERSION)))
self.api_version = 2
_LOGGER.debug("Powerview api version changed to %s", self.api_version)
return
except Exception: # pylint: disable=broad-except
_LOGGER.debug("Gen 2 connection failed")

_LOGGER.debug("Attempting Gen 3 connection")
try:
await self.get(get_base_path(self.hub_ip, join_path("gateway", "info")))
self.api_version = 3
_LOGGER.debug("Powerview api version changed to %s", self.api_version)
# TODO: what about dual hubs
return
except Exception as err: # pylint: disable=broad-except
_LOGGER.debug("Gen 3 connection failed %s", err)

raise PvApiConnectionError("Failed to discover gateway version")
2 changes: 1 addition & 1 deletion aiopvapi/helpers/api_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def _parse(self, *keys, converter=None, data=None):
for key in keys:
val = val[key]
except KeyError as err:
_LOGGER.error(err)
_LOGGER.debug("Key '%s' missing", err)
return None
if converter:
return converter(val)
Expand Down
155 changes: 139 additions & 16 deletions aiopvapi/hub.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Hub class acting as the base for the PowerView API."""
import logging
from aiopvapi.helpers.aiorequest import PvApiConnectionError

from aiopvapi.helpers.api_base import ApiBase
from aiopvapi.helpers.constants import (
Expand Down Expand Up @@ -33,22 +34,30 @@
class Version:
"""PowerView versioning scheme class."""

def __init__(self, build, revision, sub_revision, name=None) -> None:
self._build = build
def __init__(self, revision, sub_revision, build, name=None) -> None:
self._revision = revision
self._sub_revision = sub_revision
self._build = build
self._name = name

def __repr__(self):
return "BUILD: {} REVISION: {} SUB_REVISION: {}".format(
self._build, self._revision, self._sub_revision
return "REVISION: {} SUB_REVISION: {} BUILD: {} ".format(
self._revision, self._sub_revision, self._build
)

@property
def name(self) -> str:
"""Return the name of the device"""
return self._name

@property
def api(self) -> str:
"""
Return the hardware build of the device.
This correlates to the api version
"""
return self._revision

@property
def sw_version(self) -> str | None:
"""Version in Home Assistant friendly format"""
Expand All @@ -65,6 +74,8 @@ def __init__(self, request) -> None:
super().__init__(request, self.api_endpoint)
self._main_processor_version: Version | None = None
self._radio_version: list[Version] | None = None
self._raw_firmware: dict | None = None
self._raw_data: dict | None = None
self.hub_name = None
self.ip = None
self.ssid = None
Expand All @@ -74,7 +85,7 @@ def __init__(self, request) -> None:

def is_supported(self, function: str) -> bool:
"""Confirm availble features based on api version"""
if self.api_version >= 3:
if self.api_version is not None and self.api_version >= 3:
return function in (FUNCTION_REBOOT, FUNCTION_IDENTIFY)
return False

Expand Down Expand Up @@ -155,8 +166,7 @@ async def query_firmware(self):
"""
Query the firmware versions. If API version is not set yet, get the API version first.
"""
if not self.api_version:
await self.request.set_api_version()
await self.detect_api_version()
if self.api_version >= 3:
await self._query_firmware_g3()
else:
Expand All @@ -165,12 +175,14 @@ async def query_firmware(self):

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.get(join_path(self.base_path, "userdata"))
self._raw_data = await self.request_raw_data()

_main = self._parse(USER_DATA, FIRMWARE, FIRMWARE_MAINPROCESSOR)
if not _main:
# do some checking for legacy v1 failures
_fw = await self.request.get(join_path(self.base_path, FWVERSION))
if not self._raw_firmware:
self._raw_firmware = await self.request_raw_firmware()
_fw = self._raw_firmware
# _fw = await self.request.get(join_path(self._base_path, FWVERSION))
if FIRMWARE in _fw:
_main = self._parse(FIRMWARE, FIRMWARE_MAINPROCESSOR, data=_fw)
Expand Down Expand Up @@ -198,8 +210,8 @@ async def _query_firmware_g2(self):
self.hub_name = self._parse(USER_DATA, HUB_NAME, converter=base64_to_unicode)

async def _query_firmware_g3(self):
gateway = get_base_path(self.request.hub_ip, "gateway")
self._raw_data = await self.request.get(gateway)
# self._raw_data = await self.request.get(gateway)
self._raw_data = await self.request_raw_data()

_main = self._parse(CONFIG, FIRMWARE, FIRMWARE_MAINPROCESSOR)
if _main:
Expand All @@ -220,13 +232,124 @@ async def _query_firmware_g3(self):
self.hub_name = self.mac_address
if HUB_NAME not in self._parse(CONFIG):
# Get gateway name from home API until it is in the gateway API
home = await self.request.get(self.base_path)
self.hub_name = home["gateways"][0]["name"]

def _make_version(self, data: dict):
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 hub is None:
_LOGGER.debug(f"Hub with serial {self.serial_number} not found.")

def _make_version(self, data: dict) -> Version:
return Version(
data[FIRMWARE_BUILD],
data[FIRMWARE_REVISION],
data[FIRMWARE_SUB_REVISION],
data[FIRMWARE_BUILD],
data.get(FIRMWARE_NAME),
)

def _make_version_data_from_str(self, fwVersion: str, name: str = None) -> dict:
# Split the version string into components
components = fwVersion.split(".")

if len(components) != 3:
raise ValueError("Invalid version format: {}".format(fwVersion))

revision, sub_revision, build = map(int, components)

version_data = {
FIRMWARE_REVISION: revision,
FIRMWARE_SUB_REVISION: sub_revision,
FIRMWARE_BUILD: build,
FIRMWARE_NAME: name,
}

return version_data

async def request_raw_data(self):
"""
Raw data update request. Allows patching of data for testing
"""
await self.detect_api_version()
data_url = join_path(self.base_path, "userdata")
if self.api_version is not None and self.api_version >= 3:
data_url = get_base_path(self.request.hub_ip, "gateway")
return await self.request.get(data_url)

async def request_home_data(self):
"""
Raw data update request. Allows patching of data for testing
"""
await self.detect_api_version()
data_url = join_path(self.base_path, "userdata")
if self.api_version is not None and self.api_version >= 3:
data_url = self.base_path
return await self.request.get(data_url)

async def request_raw_firmware(self):
"""
Raw data update request. Allows patching of data for testing
"""

gen2_url = join_path(self.base_path, FWVERSION)
gen3_url = get_base_path(self.request.hub_ip, join_path("gateway", "info"))

if self.api_version is not None:
data_url = gen3_url if self.api_version >= 3 else gen2_url
return await self.request.get(data_url)

_LOGGER.debug("Searching for firmware file")
try:
_LOGGER.debug("Attempting Gen 2 connection")
gen2 = await self.request.get(gen2_url)
return gen2
except Exception: # pylint: disable=broad-except
_LOGGER.debug("Gen 2 connection failed")

try:
_LOGGER.debug("Attempting Gen 3 connection")
gen3 = await self.request.get(gen3_url)
# Secondary hubs not supported - second hub is essentially a repeater
return gen3
except Exception as err: # pylint: disable=broad-except
_LOGGER.debug("Gen 3 connection failed %s", err)

raise PvApiConnectionError("Failed to discover gateway version")

async def detect_api_version(self):
"""
Set the API generation based on what the gateway responds to.
"""
if not self.api_version:
self._raw_firmware = await self.request_raw_firmware()
_main = None
if USER_DATA in self._raw_firmware:
_main = self._parse(
USER_DATA, FIRMWARE, FIRMWARE_MAINPROCESSOR, data=self._raw_firmware
)
elif CONFIG in self._raw_firmware:
_main = self._parse(
CONFIG, FIRMWARE, FIRMWARE_MAINPROCESSOR, data=self._raw_firmware
)
elif FIRMWARE in self._raw_firmware:
_main = self._parse(
FIRMWARE, FIRMWARE_MAINPROCESSOR, data=self._raw_firmware
)
elif "fwVersion" in self._raw_firmware:
_main = self._make_version_data_from_str(
self._raw_firmware.get("fwVersion"), "Powerview Generation 3"
)

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)

if not self.api_version:
_LOGGER.error(self._raw_firmware)
7 changes: 7 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ Have a look at the examples folder for some guidance how to use it.
- Schedules / Automations are now supported by the API
- New get_*objecttype* methods available to returned structured data objects for consistent management

### v3.0.1

- 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

## Links

---
Expand Down
5 changes: 2 additions & 3 deletions 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 = ["async_timeout", "aiohttp>=3.7.4,<4"]
REQUIRED = ["asyncio", "aiohttp>=3.7.4,<4"]

# What packages are optional?
EXTRAS = {}
Expand Down Expand Up @@ -75,8 +75,7 @@ def run(self):
pass

self.status("Building Source and Wheel (universal) distribution…")
os.system(
"{0} setup.py sdist bdist_wheel --universal".format(sys.executable))
os.system("{0} setup.py sdist bdist_wheel --universal".format(sys.executable))

self.status("Uploading the package to PyPI via Twine…")
os.system("twine upload dist/*")
Expand Down

0 comments on commit 538dd3d

Please sign in to comment.