Skip to content

Commit

Permalink
version 3.0 release
Browse files Browse the repository at this point in the history
- Major overhaul to incorporate gateway version 3 API.  Version can be automatically detected or manually specified.
- 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
  • Loading branch information
kingy444 authored Aug 28, 2023
2 parents 8dceb35 + 90b03dd commit 7e145bd
Show file tree
Hide file tree
Showing 30 changed files with 2,238 additions and 726 deletions.
8 changes: 8 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,11 @@ Changelog
- Add Type 10 - SkyLift
- Handle calls to update shade position during maintenance
- Raise error directly on hub calls instead of logger

**v3.0.0**

- Major overhaul to incorporate gateway version 3 API. Version can be automatically detected or manually specified.
- 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
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__ = "2.0.4"
__version__ = "3.0.0"
59 changes: 59 additions & 0 deletions aiopvapi/automations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Scenes class managing all scene data."""

import logging

from aiopvapi.helpers.aiorequest import AioRequest
from aiopvapi.helpers.api_base import ApiEntryPoint
from aiopvapi.helpers.constants import (
ATTR_ID,
ATTR_SCHEDULED_EVENT_DATA,
)
from aiopvapi.resources.automation import Automation

from aiopvapi.resources.model import PowerviewData

_LOGGER = logging.getLogger(__name__)


class Automations(ApiEntryPoint):
"""Powerview Automations"""

def __init__(self, request: AioRequest) -> None:
self.api_endpoint = "scheduledevents"
if request.api_version >= 3:
self.api_endpoint = "automations"
super().__init__(request, self.api_endpoint)

def _resource_factory(self, raw):
return Automation(raw, self.request)

def _loop_raw(self, raw):
if self.api_version < 3:
raw = raw[ATTR_SCHEDULED_EVENT_DATA]

for _raw in raw:
yield _raw

def _get_to_actual_data(self, raw):
if self.api_version >= 3:
return raw
return raw.get("scene")

async def get_automations(self, fetch_scene_data: bool = True) -> PowerviewData:
"""Get a list of automations.
:returns PowerviewData object
:raises PvApiError when an error occurs.
"""
resources = await self.get_resources()
if self.api_version < 3:
resources = resources[ATTR_SCHEDULED_EVENT_DATA]

processed = {entry[ATTR_ID]: Automation(entry, self.request) for entry in resources}

if fetch_scene_data is True:
for automation in processed.values():
await automation.fetch_associated_scene_data()

_LOGGER.debug("Raw automation data: %s", resources)
return PowerviewData(raw=resources, processed=processed)
6 changes: 3 additions & 3 deletions aiopvapi/example/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ async def get_firmware(hub_ip):
async def get_user_data(hub_ip):
request = AioRequest(hub_ip)
hub = Hub(request)
await hub.query_user_data()
await hub.query_firmware()

print("UserData")
print("hub name: {}".format(hub.user_data.hub_name))
pprint(hub.user_data._raw)
print("hub name: {}".format(hub.hub_name))
pprint(hub._raw)
143 changes: 109 additions & 34 deletions aiopvapi/helpers/aiorequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,40 @@
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__)


class PvApiError(Exception):
"""General Api error. Means we have a problem communication with
the PowerView hub."""

pass


class PvApiResponseStatusError(PvApiError):
"""Wrong http response error."""


class PvApiConnectionError(PvApiError):
"""Problem connecting to PowerView hub."""
class PvApiMaintenance(PvApiError):
"""Hub is undergoing maintenance."""


async def check_response(response, valid_response_codes):
"""Check the response for correctness."""
if response.status in [204, 423]:
return True
if response.status in valid_response_codes:
_js = await response.json()
return _js
else:
raise PvApiResponseStatusError(response.status)
class PvApiConnectionError(PvApiError):
"""Problem connecting to PowerView hub."""


class AioRequest:
"""Request class managing hub connection."""

def __init__(self, hub_ip, loop=None, websession=None, timeout=15):
def __init__(
self,
hub_ip,
loop=None,
websession=None,
timeout: int = 15,
api_version: int | None = None,
) -> None:
self.hub_ip = hub_ip
self._timeout = timeout
if loop:
Expand All @@ -49,6 +50,45 @@ def __init__(self, hub_ip, loop=None, websession=None, timeout=15):
self.websession = websession
else:
self.websession = aiohttp.ClientSession()
self.api_version: int | None = api_version
self._last_request_status: int = 0
_LOGGER.debug("Powerview api version: %s", self.api_version)

@property
def api_path(self) -> str:
"""Returns the initial api call path"""
if self.api_version and self.api_version >= 3:
return "home"
return "api"

async def check_response(self, response, valid_response_codes):
"""Check the response for correctness."""
_val = None
if response.status == 403 and self._last_request_status == 423:
# if last status was hub undergoing maint then it is common
# on reboot for a 403 response. Generally this should raise
# PvApiResponseStatusError but as this is unavoidable we
# class this situation as still undergoing maintenance
_val = False
elif response.status in [204, 423]:
# 423 hub under maintenance, returns data, but not shade
_val = True
elif response.status in valid_response_codes:
_val = await response.json()

# store the status for next check
self._last_request_status = response.status

# raise a maintenance error
if isinstance(_val, bool):
raise PvApiMaintenance("Powerview Hub is undergoing maintenance")

# if none of the above checks passed, raise a response error
if _val is None:
raise PvApiResponseStatusError(response.status)

# finally, return the result
return _val

async def get(self, url: str, params: str = None) -> dict:
"""
Expand All @@ -58,38 +98,43 @@ async def get(self, url: str, params: str = None) -> dict:
:param params:
:return:
"""
_LOGGER.debug("Sending a get request")
response = None
try:
_LOGGER.debug("Sending GET request to: %s" % url)
_LOGGER.debug("Sending GET request to: %s params: %s", url, params)
with async_timeout.timeout(self._timeout):
response = await self.websession.get(url, params=params)
return await check_response(response, [200, 204])
return await self.check_response(response, [200, 204])
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
raise PvApiConnectionError(
f"Failed to communicate with PowerView hub: {error}"
)
"Failed to communicate with PowerView hub"
) from error
finally:
if response is not None:
await response.release()

async def post(self, url: str, data: dict = None):
"""
Post a resource update.
:param url:
:param data: a Dict. later converted to json.
:return:
"""
response = None
try:
_LOGGER.debug("Sending POST request to: %s data: %s", url, data)
with async_timeout.timeout(self._timeout):
_LOGGER.debug("url: %s", url)
_LOGGER.debug("data: %s", data)
response = await self.websession.post(url, json=data)
return await check_response(response, [200, 201])
return await self.check_response(response, [200, 201])
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
raise PvApiConnectionError(
f"Failed to communicate with PowerView hub: {error}"
)
"Failed to communicate with PowerView hub"
) from error
finally:
if response is not None:
await response.release()

async def put(self, url: str, data: dict = None):
async def put(self, url: str, data: dict = None, params=None):
"""
Do a put request.
Expand All @@ -99,15 +144,19 @@ async def put(self, url: str, data: dict = None):
"""
response = None
try:
_LOGGER.debug(
"Sending PUT request to: %s params: %s data: %s",
url,
params,
data,
)
with async_timeout.timeout(self._timeout):
_LOGGER.debug("url: %s", url)
_LOGGER.debug("data: %s", data)
response = await self.websession.put(url, json=data)
return await check_response(response, [200, 204])
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:
raise PvApiConnectionError(
f"Failed to communicate with PowerView hub: {error}"
)
"Failed to communicate with PowerView hub"
) from error
finally:
if response is not None:
await response.release()
Expand All @@ -124,13 +173,39 @@ 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):
response = await self.websession.delete(url, params=params)
return await check_response(response, [200, 204])
return await self.check_response(response, [200, 204])
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
raise PvApiConnectionError(
f"Failed to communicate with PowerView hub: {error}"
)
"Failed to communicate with PowerView hub"
) from error
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")
Loading

0 comments on commit 7e145bd

Please sign in to comment.