From 762d4206c8bd88a218e13498e9f4705f1cceb3a7 Mon Sep 17 00:00:00 2001 From: Jordan Date: Tue, 16 Jan 2024 21:07:03 -0500 Subject: [PATCH] Adding functions to get firmware version info and get/set device preferences. Various refactoring. --- aiophyn/api.py | 53 +++++++++++++++++++++++++++++++++++------------ aiophyn/device.py | 40 +++++++++++++++++++++++++++++++++-- aiophyn/mqtt.py | 4 ++-- pyproject.toml | 2 +- 4 files changed, 81 insertions(+), 18 deletions(-) diff --git a/aiophyn/api.py b/aiophyn/api.py index 95b8786..30f077d 100644 --- a/aiophyn/api.py +++ b/aiophyn/api.py @@ -2,9 +2,8 @@ import asyncio import logging from concurrent.futures import ThreadPoolExecutor -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta from typing import Optional -from urllib.parse import urlparse import boto3 from aiohttp import ClientSession, ClientTimeout @@ -17,8 +16,6 @@ from .errors import BrandError, RequestError from .home import Home -from .const import API_BASE - _LOGGER = logging.getLogger(__name__) @@ -49,7 +46,8 @@ class API: def __init__( self, username: str, password: str, *, phyn_brand: str, session: Optional[ClientSession] = None, - client_id: Optional[str] = None, verify_ssl: bool = True, proxy: Optional[str] = None, proxy_port: Optional[int] = None + client_id: Optional[str] = None, verify_ssl: bool = True, proxy: Optional[str] = None, + proxy_port: Optional[int] = None ) -> None: """Initialize.""" if phyn_brand not in BRANDS: @@ -75,13 +73,16 @@ def __init__( self._iot_id = None self._iot_credentials = None self.mqtt = None + self._id_token = None + self._refresh_token = None + self._mqtt_settings = {} self.verify_ssl = verify_ssl self.proxy = proxy self.proxy_port = proxy_port self.proxy_url: Optional[str] = None if self.proxy is not None and self.proxy_port is not None: - self.proxy_url = "https://%s:%s" % (proxy, proxy_port) + self.proxy_url = f"https://{proxy}:{proxy_port}" self._token: Optional[str] = None self._token_expiration: Optional[datetime] = None @@ -90,8 +91,24 @@ def __init__( self.device: Device = Device(self._request) self.mqtt = MQTTClient(self, client_id=client_id, verify_ssl=verify_ssl, proxy=proxy, proxy_port=proxy_port) + @property + def username(self) -> Optional[str]: + """Get the API username""" + return self._username + async def _request(self, method: str, url: str, token_type:str = "access", **kwargs) -> dict: - """Make a request against the API.""" + """Make a request against the API. + + :param method: GET or POST request + :type method: str + :param url: API URL + :type url: str + :param token_type: ID or Access token, defaults to "access" + :type token_type: str, optional + :raises RequestError: Error if issue accessing URL + :return: JSON response + :rtype: dict + """ if self._token_expiration and datetime.now() >= self._token_expiration: _LOGGER.info("Requesting new access token to replace expired one") @@ -122,10 +139,10 @@ async def _request(self, method: str, url: str, token_type:str = "access", **kwa elif token_type == "id": if self._id_token: kwargs["headers"]["Authorization"] = self._id_token - + if self.proxy_url is not None: kwargs["proxy"] = self.proxy_url - + if not self.verify_ssl: kwargs["ssl"] = False @@ -150,9 +167,9 @@ async def _request(self, method: str, url: str, token_type:str = "access", **kwa async def async_authenticate(self) -> None: """Authenticate the user and set the access token with its expiration.""" if self._brand == BRANDS["kohler"]: - if self._password == None: + if self._password is None: _LOGGER.info("Auhenticating to Kohler") - self._partner_api = KOHLER_API(self._username, self._partner_password, verify_ssl=self.verify_ssl, + self._partner_api = KOHLER_API(self._username, self._partner_password, verify_ssl=self.verify_ssl, proxy=self.proxy, proxy_port=self.proxy_port) await self._partner_api.authenticate() self._password = self._partner_api.get_phyn_password() @@ -190,7 +207,8 @@ def _authenticate(self): async def async_get_api( username: str, password: str, *, phyn_brand: str = "phyn", session: Optional[ClientSession] = None, - client_id: Optional[str] = None, verify_ssl: bool = True, proxy: Optional[str] = None, proxy_port: Optional[int] = None + client_id: Optional[str] = None, verify_ssl: bool = True, proxy: Optional[str] = None, + proxy_port: Optional[int] = None ) -> API: """Instantiate an authenticated API object. @@ -202,8 +220,17 @@ async def async_get_api( :type password: ``str`` :param phyn_brand: A brand for phyn :type phyn_brand: ``str`` + :param client_id: A MQTT client id name + :type client_id: ``str`` + :param verify_ssl: Should SSL certificates be verified + :type verify_ssl: ``bool`` + :param proxy: HTTP proxy hostname/IP + :type proxy: ``str`` + :param proxy_port: Port for HTTP proxy + :type proxy_port: ``int`` :rtype: :meth:`aiophyn.api.API` """ - api = API(username, password, phyn_brand=phyn_brand, session=session, client_id=client_id, verify_ssl=verify_ssl, proxy=proxy, proxy_port=proxy_port) + api = API(username, password, phyn_brand=phyn_brand, session=session, client_id=client_id, + verify_ssl=verify_ssl, proxy=proxy, proxy_port=proxy_port) await api.async_authenticate() return api diff --git a/aiophyn/device.py b/aiophyn/device.py index d79c07d..0b95719 100644 --- a/aiophyn/device.py +++ b/aiophyn/device.py @@ -1,5 +1,5 @@ """Define /devices endpoints.""" -from typing import Awaitable, Callable, Optional +from typing import Awaitable, Any, Callable, Optional from .const import API_BASE @@ -88,7 +88,7 @@ async def close_valve(self, device_id: str) -> None: "post", f"{API_BASE}/devices/{device_id}/sov/Close", ) - + async def get_away_mode(self, device_id: str) -> dict: """Return away mode status of a device. @@ -134,3 +134,39 @@ async def disable_away_mode(self, device_id: str) -> None: return await self._request( "post", f"{API_BASE}/preferences/device/{device_id}", json=data ) + + async def get_latest_firmware_info(self, device_id: str) -> dict: + """Get Latest Firmware Information + + :param device_id: Unique identifier for the device + :type device_id: str + :return: Returns dict with fw_img_name, fw_version, product_code + :rtype: dict + """ + return await self._request( + "get", f"{API_BASE}/firmware/latestVersion/v2?device_id={device_id}" + ) + + async def get_device_preferences(self, device_id: str) -> dict: + """Get phyn device preferences. + + :param device_id: Unique identifier for the device + :type device_id: str + :return: List of dicts with the following keys: created_ts, device_id, name, updated_ts, value + :rtype: dict + """ + return await self._request( + "get", f"{API_BASE}/preferences/device/{device_id}" + ) + + async def set_device_preferences(self, device_id: str, data: list[dict]) -> None: + """Set device preferences + + :param device_id: Unique identifier for the device + :type device_id: str + :param data: List of dicts which have the keys: device_id, name, value + :type data: List[dict] + """ + return await self._request( + "post", f"{API_BASE}/preferences/device/{device_id}", json=data + ) diff --git a/aiophyn/mqtt.py b/aiophyn/mqtt.py index 2dfc20b..bd33138 100644 --- a/aiophyn/mqtt.py +++ b/aiophyn/mqtt.py @@ -114,7 +114,7 @@ def __init__(self, api, client_id: str =None, verify_ssl: bool =True, proxy: str self.connect_task = None self.disconnect_evt: Optional[asyncio.Event] = None self.reconnect_evt: asyncio.Event = asyncio.Event() - self.host = None + self.host = None self.port = 443 if client_id is None: @@ -326,7 +326,7 @@ def _on_message( ) -> None: # pylint: disable=unused-argument msg = message.payload.decode() - _LOGGER.debug("Message received on %s %s", message.topic, msg) + _LOGGER.debug("Message received on %s", message.topic) try: data = json.loads(msg) except json.decoder.JSONDecodeError: diff --git a/pyproject.toml b/pyproject.toml index 382fb77..0abfaf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aiophyn" -version = "2024.1.1" +version = "2024.1.2" description = "An asynchronous library for Phyn Smart Water devices" authors = ["MizterB <5458030+MizterB@users.noreply.github.com>","jordanruthe <31575189+jordanruthe@users.noreply.github.com>"] license = "MIT"