Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ repos:
- id: black

- repo: https://github.com/pycqa/isort
rev: 5.9.3
rev: 5.12.0
hooks:
- id: isort

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
Forked version of pyUhoo which fixes the login problem.

This can be integrated with your existing plugin by updating the manifest file requirements line
to depend on this repository: `pyuhoo@git+https://github.com/wrouesnel/pyuhoo.git@master#0.0.6a2`

# pyuhoo

[![PyPi version](https://img.shields.io/pypi/v/pyuhoo.svg)](https://pypi.python.org/pypi/pyuhoo/)
Expand Down
1,517 changes: 768 additions & 749 deletions poetry.lock

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyuhoo"
version = "0.0.5"
version = "0.0.8"
description = "Python API for talking to uHoo consumer API"
authors = ["Christopher Sacca <[email protected]>"]
repository = "https://github.com/csacca/pyuhoo"
Expand All @@ -11,7 +11,7 @@ readme = "README.md"
pyuhoo-cli = "pyuhoo.cli:cli"

[tool.poetry.dependencies]
python = "^3.8"
python = ">=3.8"
aiohttp = "^3.7.4"
click = "^8.0.1"
pycryptodome = "^3.10.1"
Expand All @@ -20,11 +20,9 @@ pycryptodome = "^3.10.1"
flake8 = "^3.9.2"
black = "^21.9b0"
isort = "^5.9.3"
mypy = "^0.910"
pre-commit = "^2.15.0"
safety = "^1.10.3"
pytest = "^6.2.5"
pytest-asyncio = "^0.15.1"
mypy = "^1.9.0"
pytest = "*"
pytest-asyncio = "*"

[tool.isort]
profile = "black"
Expand All @@ -35,6 +33,9 @@ warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true

[tool.pytest.ini_options]
asyncio_mode = "auto"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
4 changes: 2 additions & 2 deletions pyuhoo/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async def example(username, password):
print(
f" Serial Number: {device.serial_number}\n"
+ f" Timestamp: {device.timestamp}\n"
+ f" Temperature: {device.temp} {client.user_settings_temp}\n"
# + f" Temperature: {device.temp} {client.user_settings_temp}\n"
)

while True:
Expand All @@ -46,7 +46,7 @@ async def example(username, password):
print(
f" Serial Number: {device.serial_number}\n"
+ f" Timestamp: {device.timestamp}\n"
+ f" Temperature: {device.temp} {client.user_settings_temp}\n"
# + f" Temperature: {device.temp} {client.user_settings_temp}\n"
)


Expand Down
30 changes: 13 additions & 17 deletions pyuhoo/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from aiohttp import ClientSession

from pyuhoo.errors import ForbiddenError, UhooError, UnauthorizedError
from pyuhoo.errors import ForbiddenError, RequestError, UhooError, UnauthorizedError

from .api import API
from .consts import APP_VERSION, CLIENT_ID
Expand Down Expand Up @@ -34,7 +34,8 @@ def __init__(
self._websession: ClientSession = websession
self._token: Optional[str] = None
self._refresh_token: Optional[str] = None
self.user_settings_temp: str = "f" # "f" or "c"
# This *seems* to be defaulted to 'c' now.
self.user_settings_temp: str = "c" # "f" or "c"

self._api: API = API(self._websession)

Expand Down Expand Up @@ -87,45 +88,40 @@ async def refresh_token(self) -> None:
self._refresh_token = user_refresh_token["refreshToken"]
self._api.set_bearer_token(self._refresh_token)

except UnauthorizedError:
except RequestError as e:
self._log.debug(
"\033[91m"
+ "[refresh_token] received 401 error, attempting to re-login"
+ "[refresh_token] received {} error, attempting to re-login".format(type(e))
+ "\033[0m"
)
self._api.set_bearer_token(None)
await self.login()

async def get_latest_data(self) -> None:
try:
data_latest: dict = await self._api.data_latest()
except UnauthorizedError:
except RequestError as e:
self._log.debug(
"\033[93m"
+ "[get_latest_data] received 401 error, refreshing token and trying again"
+ "\033[0m"
)
await self.refresh_token()
data_latest = await self._api.data_latest()
except ForbiddenError:
self._log.debug(
"\033[93m"
+ "[get_latest_data] received 403 error, refreshing token and trying again"
+ "[get_latest_data] received {} error, refreshing token and trying again".format(type(e))
+ "\033[0m"
)
await self.refresh_token()
data_latest = await self._api.data_latest()

# self._log.debug(f"[data_latest] returned\n{json_pp(data_latest)}")
#self._log.debug(f"[data_latest] returned\n{json_pp(data_latest)}")

self.user_settings_temp = data_latest["userSettings"]["temp"]
if "temp" in data_latest.get("userSettings",{}):
self.user_settings_temp = data_latest["userSettings"]["temp"]

device: dict
for device in data_latest["devices"]:
serial_number: str = device["serialNumber"]
if serial_number not in self._devices:
self._devices[serial_number] = Device(device)

for data in data_latest["data"]:
data = device["data"]

serial_number = data["serialNumber"]
device_obj: Device = self._devices[serial_number]
if device_obj.timestamp < data["timestamp"]:
Expand Down
132 changes: 98 additions & 34 deletions pyuhoo/device.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
from typing import Any
from typing import Any, Dict


class Device(object):
def __init__(self, device: dict) -> None:
self.calibration: int
self.created_at: str # "YYYY-MM-DDThh:mm:ss.sssZ" UTC time
self.home: Any # type unknown
self.latitude: Any # type unknown
self.longitude: Any # type unknown
self.mac_address: str
self.name: str
self.serial_number: str
self.server: int
self.ssid: str
self.status: int
self._device: Dict[str,Any] = {}

self.co: float # might be an int
self.co2: float # might be an int
Expand All @@ -30,29 +20,103 @@ def __init__(self, device: dict) -> None:
self.timestamp = -1

def __repr__(self):
return f"<{self.__module__}.{self.__class__.__name__} serial_number: {self.serial_number!r}>"
return f"<{self.__module__}.{self.__class__.__name__}(device={repr(self._device)})>"

@property
def calibration(self) -> int:
return self._device["calibration"]

@property
def created_at(self) -> str: # "YYYY-MM-DDThh:mm:ss.sssZ" UTC time
return self._device["createdAt"]

@property
def home(self) -> Any: # type unknown
return self._device["home"]

@property
def latitude(self) -> Any: # type unknown
return self._device["latitude"]

@property
def longitude(self) -> Any: # type unknown
return self._device["longitude"]

@property
def mac_address(self) -> str:
return self._device["macAddress"]

@property
def name(self) -> str:
return self._device["name"]

@property
def serial_number(self) -> str:
return self._device["serialNumber"]

@property
def server(self) -> int:
return self._device["server"]

@property
def ssid(self) -> str:
return self._device["ssid"]

@property
def status(self) -> int:
return self._device["status"]

@property
def location(self):
return self._device["location"]

@property
def city(self):
return self._device["city"]

@property
def city_ios(self):
return self._device["city_ios"]

@property
def room_type(self):
return self._device["RoomType"]

@property
def offline(self):
return self._device["offline"]

@property
def offline_timestamp(self):
return self._device["offline_timestamp"]

def update_device(self, device: dict) -> None:
self.calibration = device["calibration"]
self.created_at = device["createdAt"]
self.home = device["home"]
self.latitude = device["latitude"]
self.longitude = device["longitude"]
self.mac_address = device["macAddress"]
self.name = device["name"]
self.serial_number = device["serialNumber"]
self.server = device["server"]
self.ssid = device["ssid"]
self.status = device["status"]
self._device = { k:v for k,v in device.items() if k not in ("data","userSettings","threshold") }

# Maps data key values to struct values
_DATA_MAPPING = {
"co" : "co",
"co2" : "co2",
"dust" : "dust",
"humidity" : "humidity",
"no2" : "no2",
"ozone" : "ozone",
"pressure" : "pressure",
"temp" : "temp",
"timestamp" : "timestamp",
"voc" : "voc",
}

_INV_DATA_MAPPING = { attr_name:value_name for value_name, attr_name in _DATA_MAPPING.items() }

def update_data(self, data: dict) -> None:
self.co = data["co"]["value"]
self.co2 = data["co2"]["value"]
self.dust = data["dust"]["value"]
self.humidity = data["humidity"]["value"]
self.no2 = data["no2"]["value"]
self.ozone = data["ozone"]["value"]
self.pressure = data["pressure"]["value"]
self.temp = data["temp"]["value"]
self.timestamp = data["timestamp"]
self.voc = data["voc"]["value"]
# The format has been changed - now these values return directly.
# Since this might not be globally consistent, we'll check each one.
for value_name, attr_name in self._DATA_MAPPING.items():
if isinstance(data[value_name], dict):
setattr(self, attr_name, data[value_name]["value"])
else:
setattr(self, attr_name, data[value_name])

def get_data(self) -> Dict[str,Any]:
return { attr_name:getattr(self,attr_name) for attr_name in self._INV_DATA_MAPPING }
2 changes: 1 addition & 1 deletion pyuhoo/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
DEVICE_TIMEZONES: str = "gettimezones"
DEVICE_ROOMS: str = "getlocationtypes"
DEVICE_TRANSFER_OWNER: str = "transferowner"
DATA_LATEST: str = "getalllatestdata"
DATA_LATEST: str = "allconsumerdata"
DATA_HOUR: str = "wtvsRh/gethourcolor"
DATA_DAY: str = "wtvsRh/getdaycolor"
DATA_WEEK: str = "wtvsRh/getweekcolor"
Expand Down
4 changes: 2 additions & 2 deletions pyuhoo/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ class RequestError(UhooError):
pass


class UnauthorizedError(UhooError):
class UnauthorizedError(RequestError):
"""Define an error for 401 unauthorized responses"""

pass


class ForbiddenError(UhooError):
class ForbiddenError(RequestError):
"""Define an error for 403 forbidden responses"""

pass
21 changes: 11 additions & 10 deletions tests/test_api_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"paymentStatus",
]

DATA_LATEST_KEYS = ["devices", "data", "userSettings", "offline", "systemTime"]
DATA_LATEST_KEYS = ["devices", "systemTime", "userSettings"]

DATA_LATEST_DATA_KEYS = [
"serialNumber",
Expand Down Expand Up @@ -82,8 +82,12 @@ def verify_keys(expected: List["str"], returned: dict):
for key in expected:
assert key in returned

assert len(expected) == len(returned.keys())

extra_keys = set(returned.keys()) - set(expected)
if len(extra_keys) > 0:
print("EXTRA KEYS FOUND")
print("================")
for key in extra_keys:
print(f"{key}={returned[key]}")

#
# Check if required environmental variables are defined
Expand Down Expand Up @@ -173,7 +177,6 @@ async def results(websession, username, password):
# Tests
#


def test_user_config(results):
user_config: dict = results["user_config"]

Expand Down Expand Up @@ -207,13 +210,11 @@ def test_data_latest(results):
def test_data_latest_data(results):
data_latest: dict = results["data_latest"]

assert "data" in data_latest.keys()

if len(data_latest["data"]) > 0:
data = data_latest["data"][0]
assert "devices" in data_latest
for device in data_latest["devices"]:
assert "data" in device.keys()
data = device["data"]
verify_keys(DATA_LATEST_DATA_KEYS, data)
else:
pytest.skip('Skipping: No data to test in data_latest["data"]')


def test_data_latest_devices(results):
Expand Down
Loading