Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
Sibgatulin committed Sep 2, 2022
2 parents 50a796b + 9bcc823 commit dc46252
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 24 deletions.
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,25 @@ Python library for asynchronous data access to local air-Q devices.

## Retrieve data from air-Q

At its present state, `AirQ` requires an `aiohttp` session to be provided by the user:

```python
import asyncio
import aiohttp
from aioairq import AirQ

address = "123ab_air-q.local"
password = "airqsetup"
airq = AirQ(address, password)
ADDRESS = "123ab_air-q.local"
PASSWORD = "airqsetup"

async def main():
async with aiohttp.ClientSession() as session:
airq = AirQ(ADDRESS, PASSWORD, session)

config = await airq.config
print(f"Available sensors: {config['sensors']}")

loop = asyncio.get_event_loop()
data = await airq.data
print(f"Momentary data: {data}")

data = loop.run_until_complete(airq.data)
average = loop.run_until_complete(airq.average)
config = loop.run_until_complete(airq.config)
asyncio.run(main())
```
5 changes: 3 additions & 2 deletions aioairq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
__email__ = "daniel.lehmann@corant.de"
__url__ = "https://www.air-q.com"
__license__ = "Apache License 2.0"
__version__ = "0.1.1"
__version__ = "0.2.0"
__all__ = ["AirQ", "DeviceInfo", "InvalidAuth"]

from aioairq.core import AirQ # to allow more direct access: `from aioairq import AirQ`
from aioairq.core import AirQ, DeviceInfo, InvalidAuth
62 changes: 47 additions & 15 deletions aioairq/core.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import json
from typing import NamedTuple

from aioairq.encrypt import AESCipher
import aiohttp

from aioairq.encrypt import AESCipher

class DeviceInfo(NamedTuple):
"""Container for device information"""

id: str
name: str
model: str
room_type: str
sw_version: str
hw_version: str


class AirQ:
def __init__(self, airq_ip: str, passw: str):
def __init__(self, airq_ip: str, passw: str, session: aiohttp.ClientSession):
"""Class representing the API for a single AirQ device
The class holds the AESCipher object, responsible for message decoding,
Expand All @@ -24,36 +35,53 @@ def __init__(self, airq_ip: str, passw: str):
self.airq_ip = airq_ip
self.anchor = f"http://{airq_ip}"
self.aes = AESCipher(passw)
self._session = session

async def test_authentication(self) -> bool:
"""Test if the password provided to the constructor matches.
async def validate(self) -> None:
"""Test if the password provided to the constructor is valid.
Returns True or False depending on the ability to authenticate
Raises InvalidAuth if the password is not correct.
This method is a kludge, as currently the device does not support
This method is a workaround, as currently the device does not support
authentication. This module infers the success of failure of the
authentication based on the ability to decode the response from the device.
"""
try:
await self.get("ping")
except UnicodeDecodeError:
return False
return True
raise InvalidAuth

def __repr__(self) -> str:
return f"AirQ(id={self.airq_ip})"

async def fetch_device_info(self) -> DeviceInfo:
"""Fetch condensed device description"""
config = await self.get("config")
return DeviceInfo(
id=config["id"],
name=config["devicename"],
model=config["type"],
room_type=config["RoomType"].replace("-", " ").title(),
sw_version=config["air-Q-Software-Version"],
hw_version=config["air-Q-Hardware-Version"],
)

@staticmethod
def drop_errors_from_data(data: dict) -> dict:
def drop_uncertainties_from_data(data: dict) -> dict:
"""Filter returned dict and substitute (value, uncertainty) with the value.
The device attempts to estimate the uncertainty, or error, of certain readings.
These readings are returned as tuples of (value, uncertainty). Often, the latter
is not desired, and this is a convenience method to homogenise the dict a little
"""
return {k: v[0] if isinstance(v, list) else v for k, v in data.items()}

async def get(self, subject: str) -> dict:
"""Returns the given subject from the air-Q device"""
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.anchor}/{subject}") as response:
html = await response.text()
encoded_message = json.loads(html)["content"]
return json.loads(self.aes.decode(encoded_message))
"""Return the given subject from the air-Q device"""
async with self._session.get(f"{self.anchor}/{subject}") as response:
html = await response.text()
encoded_message = json.loads(html)["content"]
return json.loads(self.aes.decode(encoded_message))

@property
async def data(self):
Expand All @@ -66,3 +94,7 @@ async def average(self):
@property
async def config(self):
return await self.get("config")


class InvalidAuth(Exception):
"""Error to indicate there is invalid auth."""

0 comments on commit dc46252

Please sign in to comment.