blueair-api 1.26.2__py3-none-any.whl → 1.26.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blueair_api/callbacks.py +1 -1
- blueair_api/device.py +1 -1
- blueair_api/device_aws.py +1 -1
- blueair_api/http_aws_blueair.py +20 -21
- blueair_api/http_blueair.py +12 -10
- blueair_api/intermediate_representation_aws.py +40 -33
- blueair_api/util.py +2 -12
- blueair_api/util_bootstrap.py +7 -6
- {blueair_api-1.26.2.dist-info → blueair_api-1.26.3.dist-info}/METADATA +1 -1
- blueair_api-1.26.3.dist-info/RECORD +19 -0
- blueair_api-1.26.2.dist-info/RECORD +0 -19
- {blueair_api-1.26.2.dist-info → blueair_api-1.26.3.dist-info}/LICENSE +0 -0
- {blueair_api-1.26.2.dist-info → blueair_api-1.26.3.dist-info}/WHEEL +0 -0
- {blueair_api-1.26.2.dist-info → blueair_api-1.26.3.dist-info}/top_level.txt +0 -0
blueair_api/callbacks.py
CHANGED
@@ -22,6 +22,6 @@ class CallbacksMixin:
|
|
22
22
|
def publish_updates(self) -> None:
|
23
23
|
if not hasattr(self, "_callbacks"):
|
24
24
|
self._setup_callbacks()
|
25
|
-
_LOGGER.debug(f"{self
|
25
|
+
_LOGGER.debug(f"{id(self)} publishing updates")
|
26
26
|
for callback in self._callbacks:
|
27
27
|
callback()
|
blueair_api/device.py
CHANGED
@@ -70,7 +70,7 @@ class Device(CallbacksMixin):
|
|
70
70
|
else:
|
71
71
|
self.brightness = 0
|
72
72
|
if "child_lock" in attributes:
|
73
|
-
self.child_lock =
|
73
|
+
self.child_lock = attributes["child_lock"] == "1"
|
74
74
|
if "night_mode" in attributes:
|
75
75
|
self.night_mode = bool(attributes["night_mode"])
|
76
76
|
self.fan_speed = int(attributes["fan_speed"])
|
blueair_api/device_aws.py
CHANGED
@@ -9,7 +9,7 @@ from . import intermediate_representation_aws as ir
|
|
9
9
|
|
10
10
|
_LOGGER = logging.getLogger(__name__)
|
11
11
|
|
12
|
-
type AttributeType[T] = T | None
|
12
|
+
type AttributeType[T] = T | None
|
13
13
|
|
14
14
|
@dataclasses.dataclass(slots=True)
|
15
15
|
class DeviceAws(CallbacksMixin):
|
blueair_api/http_aws_blueair.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
from typing import Any
|
2
|
+
import functools
|
1
3
|
import logging
|
2
4
|
|
3
5
|
from aiohttp import ClientSession, ClientResponse, FormData
|
@@ -10,7 +12,8 @@ _LOGGER = logging.getLogger(__name__)
|
|
10
12
|
|
11
13
|
|
12
14
|
def request_with_active_session(func):
|
13
|
-
|
15
|
+
@functools.wraps(func)
|
16
|
+
async def request_with_active_session_wrapper(*args, **kwargs) -> ClientResponse:
|
14
17
|
_LOGGER.debug("session")
|
15
18
|
try:
|
16
19
|
return await func(*args, **kwargs)
|
@@ -28,7 +31,8 @@ def request_with_active_session(func):
|
|
28
31
|
|
29
32
|
|
30
33
|
def request_with_errors(func):
|
31
|
-
|
34
|
+
@functools.wraps(func)
|
35
|
+
async def request_with_errors_wrapper(*args, **kwargs) -> ClientResponse:
|
32
36
|
_LOGGER.debug("checking for errors")
|
33
37
|
response: ClientResponse = await func(*args, **kwargs)
|
34
38
|
status_code = response.status
|
@@ -53,6 +57,7 @@ def request_with_errors(func):
|
|
53
57
|
else:
|
54
58
|
_LOGGER.debug("session error")
|
55
59
|
raise SessionError(response_text)
|
60
|
+
raise ValueError(f"unknown status code {status_code}")
|
56
61
|
|
57
62
|
return request_with_errors_wrapper
|
58
63
|
|
@@ -63,7 +68,7 @@ class HttpAwsBlueair:
|
|
63
68
|
username: str,
|
64
69
|
password: str,
|
65
70
|
region: str = "us",
|
66
|
-
client_session: ClientSession = None,
|
71
|
+
client_session: ClientSession | None = None,
|
67
72
|
):
|
68
73
|
self.username = username
|
69
74
|
self.password = password
|
@@ -87,7 +92,7 @@ class HttpAwsBlueair:
|
|
87
92
|
@request_with_errors
|
88
93
|
@request_with_logging
|
89
94
|
async def _get_request_with_logging_and_errors_raised(
|
90
|
-
self, url: str, headers: dict = None
|
95
|
+
self, url: str, headers: dict | None = None
|
91
96
|
) -> ClientResponse:
|
92
97
|
return await self.api_session.get(url=url, headers=headers)
|
93
98
|
|
@@ -96,15 +101,15 @@ class HttpAwsBlueair:
|
|
96
101
|
async def _post_request_with_logging_and_errors_raised(
|
97
102
|
self,
|
98
103
|
url: str,
|
99
|
-
json_body: dict = None,
|
100
|
-
form_data: FormData = None,
|
101
|
-
headers: dict = None,
|
104
|
+
json_body: dict | None = None,
|
105
|
+
form_data: FormData | None = None,
|
106
|
+
headers: dict | None = None,
|
102
107
|
) -> ClientResponse:
|
103
108
|
return await self.api_session.post(
|
104
109
|
url=url, data=form_data, json=json_body, headers=headers
|
105
110
|
)
|
106
111
|
|
107
|
-
async def refresh_session(self):
|
112
|
+
async def refresh_session(self) -> None:
|
108
113
|
_LOGGER.debug("refresh_session")
|
109
114
|
url = f"https://accounts.{AWS_APIKEYS[self.region]['gigyaRegion']}.gigya.com/accounts.login"
|
110
115
|
form_data = FormData()
|
@@ -121,7 +126,7 @@ class HttpAwsBlueair:
|
|
121
126
|
self.session_token = response_json["sessionInfo"]["sessionToken"]
|
122
127
|
self.session_secret = response_json["sessionInfo"]["sessionSecret"]
|
123
128
|
|
124
|
-
async def refresh_jwt(self):
|
129
|
+
async def refresh_jwt(self) -> None:
|
125
130
|
_LOGGER.debug("refresh_jwt")
|
126
131
|
if self.session_token is None or self.session_secret is None:
|
127
132
|
await self.refresh_session()
|
@@ -138,7 +143,7 @@ class HttpAwsBlueair:
|
|
138
143
|
response_json = await response.json(content_type="text/javascript")
|
139
144
|
self.jwt = response_json["id_token"]
|
140
145
|
|
141
|
-
async def refresh_access_token(self):
|
146
|
+
async def refresh_access_token(self) -> None:
|
142
147
|
_LOGGER.debug("refresh_access_token")
|
143
148
|
if self.jwt is None:
|
144
149
|
await self.refresh_jwt()
|
@@ -152,14 +157,15 @@ class HttpAwsBlueair:
|
|
152
157
|
response_json = await response.json()
|
153
158
|
self.access_token = response_json["access_token"]
|
154
159
|
|
155
|
-
async def get_access_token(self):
|
160
|
+
async def get_access_token(self) -> str:
|
156
161
|
_LOGGER.debug("get_access_token")
|
157
162
|
if self.access_token is None:
|
158
163
|
await self.refresh_access_token()
|
164
|
+
assert self.access_token is not None
|
159
165
|
return self.access_token
|
160
166
|
|
161
167
|
@request_with_active_session
|
162
|
-
async def devices(self):
|
168
|
+
async def devices(self) -> dict[str, Any]:
|
163
169
|
_LOGGER.debug("devices")
|
164
170
|
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/registered-devices"
|
165
171
|
headers = {
|
@@ -174,15 +180,8 @@ class HttpAwsBlueair:
|
|
174
180
|
return response_json["devices"]
|
175
181
|
|
176
182
|
@request_with_active_session
|
177
|
-
async def device_info(self, device_name, device_uuid):
|
183
|
+
async def device_info(self, device_name, device_uuid) -> dict[str, Any]:
|
178
184
|
_LOGGER.debug("device_info")
|
179
|
-
"""
|
180
|
-
sample; [{'id': '1b41a7c6-8f02-42fa-9af8-868dad9be98a', 'configuration': {'df': {'a': 877, 'ot': 'G4Filter', 'alg': 'FilterAlg1'}, 'di': {'cfv': '2.1.1', 'cma': '3c:61:05:45:56:98', 'mt': '2', 'name': 'sage', 'sku': '105826', 'mfv': '1.0.12', 'ofv': '2.1.1', 'hw': 'high_1.5', 'ds': '110582600000110110016855'}, '_ot': 'CmConfig', '_f': False, '_it': 'urn:blueair:openapi:version:healthprotect:0.0.5', '_eid': 'f8cb142f-c77d-11eb-b045-c98ff4f5a769', '_sc': 'Instance', 'ds': {'tVOC': {'tf': 'senml+json', 'ot': 'TVOC', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/tVOC', 'ttl': 0, 'n': 'tVOC', 'fe': True}, 'fu0': {'tf': 'senml+json', 'ot': 'FU', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/fu0', 'ttl': -1, 'n': 'fu0', 'fe': True}, 'ledb': {'tf': 'senml+json', 'ot': 'Led', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/ledb', 'ttl': 0, 'n': 'ledb', 'fe': True}, 'co2': {'tf': 'senml+json', 'ot': 'Co2', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/co2', 'ttl': 0, 'n': 'co2', 'fe': True}, 'pm2_5c': {'tf': 'senml+json', 'ot': 'PMParticleCount', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/pm2_5c', 'ttl': 0, 'n': 'pm2_5c', 'fe': True}, 'sb': {'tf': 'senml+json', 'ot': 'Standby', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/sb', 'ttl': -1, 'n': 'sb', 'fe': True}, 'ss0': {'tf': 'senml+json', 'ot': 'SafetySwitch', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/ss0', 'ttl': -1, 'n': 'ss0', 'fe': True}, 'rt1s': {'tf': 'senml+json', 'ot': 'RT1s', 'e': True, 'i': 1000, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/1s', 'sn': ['pm1', 'pm2_5', 'pm10', 't', 'h', 'tVOC', 'rssi'], 'ttl': 0, 'n': 'rt1s', 'fe': True}, 'rt5s': {'tf': 'senml+json', 'ot': 'RT5s', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/5s', 'sn': ['pm1', 'pm2_5', 'pm10', 't', 'h', 'tVOC', 'rssi'], 'ttl': -1, 'n': 'rt5s', 'fe': True}, 'pm1': {'tf': 'senml+json', 'ot': 'PM1', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/pm1', 'ttl': 0, 'n': 'pm1', 'fe': True}, 'pm2_5': {'tf': 'senml+json', 'ot': 'PM2_5', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/pm2_5', 'ttl': 0, 'n': 'pm2_5', 'fe': True}, 'pm10c': {'tf': 'senml+json', 'ot': 'PMParticleCount', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/pm10c', 'ttl': 0, 'n': 'pm10c', 'fe': True}, 'sp': {'tf': 'senml+json', 'ot': 'SensorBoxPresense', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/sp', 'ttl': -1, 'n': 'sp', 'fe': True}, 'rssi': {'tf': 'senml+json', 'ot': 'RSSI', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/rssi', 'ttl': 0, 'n': 'rssi', 'fe': True}, 'chl': {'tf': 'senml+json', 'ot': 'ChildLock', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/chl', 'ttl': -1, 'n': 'chl', 'fe': True}, 'fp0': {'tf': 'senml+json', 'ot': 'FilterPresence', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/fp0', 'ttl': -1, 'n': 'fp0', 'fe': True}, 'pm10': {'tf': 'senml+json', 'ot': 'PM10', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/pm10', 'ttl': 0, 'n': 'pm10', 'fe': True}, 'h': {'tf': 'senml+json', 'ot': 'Humidity', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/h', 'ttl': 0, 'n': 'h', 'fe': True}, 'is': {'tf': 'senml+json', 'ot': 'IonizerState', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/is', 'ttl': -1, 'n': 'is', 'fe': True}, 'gs': {'tf': 'senml+json', 'ot': 'GermShield', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/gs', 'ttl': -1, 'n': 'gs', 'fe': True}, 'am': {'tf': 'senml+json', 'ot': 'AutoMode', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/am', 'ttl': -1, 'n': 'am', 'fe': True}, 'tVOCbaseline': {'t': 0, 'e': False, 'ot': 'TVOCbaseline', 'i': 60000, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/TVOCbaseline', 'ttl': 600, 'n': 'tVOCbaseline', 'fe': True}, 'rt5m': {'tf': 'senml+json', 'ot': 'RT5m', 'e': True, 'i': 300000, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/5m', 'sn': ['pm1', 'pm2_5', 'pm10', 't', 'h', 'tVOC'], 'ttl': 0, 'n': 'rt5m', 'fe': True}, 't': {'tf': 'senml+json', 'ot': 'Temperature', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/t', 'ttl': 0, 'n': 't', 'fe': True}, 'pm1c': {'tf': 'senml+json', 'ot': 'PMParticleCount', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/pm1c', 'ttl': 0, 'n': 'pm1c', 'fe': True}, 'b5m': {'tf': 'senml+json', 'th': ['5kb', '4h'], 'ot': 'Batch5m', 'e': True, 'i': 300000, 'tn': '$aws/rules/telemetry_ingest_rule/d/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/batch/b5m', 'sn': ['pm1', 'pm2_5', 'pm10', 't', 'h', 'tVOC', 'fsp0'], 'ttl': -1, 'n': 'b5m', 'fe': True}, 'fsp0': {'st': 'fsp0', 'tf': 'senml+json', 't': 10, 'ot': 'Fanspeed', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/fsp0', 'sn': ['fsp0'], 'ttl': -1, 'n': 'fsp0', 'fe': True}, 'nm': {'tf': 'senml+json', 'ot': 'NightMode', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/nm', 'ttl': -1, 'n': 'nm', 'fe': True}}, '_r': 'us-east-2', '_s': {'sig': 'f812cd40acae289ad965c159051abadb28ed6c8b1eb2df917fedaa135f5a69d2', 'salg': 'SHA256'}, '_t': 'Diff', '_v': 1655924801, '_cas': 1623062101, '_id': '1b41a7c6-8f02-42fa-9af8-868dad9be98a', 'fc': {'pwd': '9102revreSyrotcaF', 'ssid': 'BAFactory', 'url': ' '}, 'da': {'reboot': {'p': False, 'a': False, 'ot': 'Reboot', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/reboot', 'n': 'reboot', 'fe': True}, 'uitest': {'a': 'off', 'ot': 'UiTest', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/uitest', 'n': 'uitest', 'fe': True}, 'ledb': {'p': True, 'a': 0, 'tf': 'senml+json', 'ot': 'LedBrightness', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/ledb', 'n': 'ledb', 'fe': True}, 'chl': {'p': True, 'a': False, 'tf': 'senml+json', 'ot': 'ChildLock', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/chl', 'n': 'chl', 'fe': True}, 'sflu': {'p': False, 'a': False, 'ot': 'SensorFlush', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/sflu', 'n': 'sflu', 'fe': True}, 'buttontest': {'p': False, 'a': False, 'ot': 'ButtonTest', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/buttontest', 'n': 'buttontest', 'fe': True}, 'is': {'p': True, 'a': False, 'tf': 'senml+json', 'ot': 'IonizerState', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/is', 'n': 'is', 'fe': True}, 'gs': {'p': True, 'a': False, 'tf': 'senml+json', 'ot': 'GermShield', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/gs', 'n': 'gs', 'fe': True}, 'am': {'p': True, 'a': False, 'tf': 'senml+json', 'ot': 'AutoMode', 'e': True, 'amt': 'PM', 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/am', 'n': 'am', 'fe': True}, 'tVOCbaseline': {'a': 0, 'ot': 'TVOCbaseline', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/tVOCbaseline', 'n': 'tVOCbaseline', 'fe': True}, 'sb': {'p': True, 'a': False, 'tf': 'senml+json', 'sbt': 'sensor_on', 'ot': 'Standby', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/sb', 'n': 'sb', 'fe': True}, 'fsp0': {'p': True, 'st': 'fsp0', 'a': 20, 'tf': 'senml+json', 'ot': 'Fanspeed', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/fsp0', 'n': 'fsp0', 'fe': True}, 'nm': {'p': True, 'maxfsp': 25, 'a': False, 'tf': 'senml+json', 'nmt': 'PM', 'ot': 'NightMode', 'ledb': 12, 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/nm', 'n': 'nm', 'fe': True}}, 'dc': {'cfv': {'d': 'di.cfv', 't': 'integer', 'v': 0, 'n': 'cfv'}, 'germshield': {'a': 'gs', 's': 'gs', 't': 'boolean', 'v': False, 'n': 'germshield'}, 'filterusage': {'s': 'fu0', 't': 'integer', 'v': 0, 'n': 'filterusage'}, 'brightness': {'a': 'ledb', 's': 'ledb', 't': 'integer', 'v': 100, 'n': 'brightness'}, 'standby': {'a': 'sb', 's': 'sb', 't': 'boolean', 'v': False, 'n': 'standby'}, 'fanspeed': {'a': 'fsp0', 's': 'fsp0', 't': 'integer', 'v': 11, 'n': 'fanspeed'}, 'nightmode': {'a': 'nm', 's': 'nm', 't': 'boolean', 'v': False, 'n': 'nightmode'}, 'childlock': {'a': 'chl', 's': 'chl', 't': 'boolean', 'v': False, 'n': 'childlock'}, 'safetyswitch': {'s': 'ss0', 't': 'boolean', 'v': True, 'n': 'safetyswitch'}, 'mfv': {'d': 'di.mfv', 't': 'integer', 'v': 0, 'n': 'mfv'}, 'ofv': {'d': 'di.ofv', 't': 'integer', 'v': 0, 'n': 'ofv'}, 'automode': {'a': 'am', 's': 'am', 't': 'boolean', 'v': False, 'n': 'automode'}}}, 'alarms': [], 'events': [], 'sensordata': [{'v': '0', 'n': 'pm1', 't': 1656198091}, {'v': '0', 'n': 'pm2_5', 't': 1656198091}, {'v': '0', 'n': 'pm10', 't': 1656198091}, {'v': '23', 'n': 't', 't': 1656198091}, {'v': '45', 'n': 'h', 't': 1656198091}, {'v': '170', 'n': 'tVOC', 't': 1656198091}, {'v': '11', 'n': 'fsp0', 't': 1656198091}], 'states': [{'n': 'cfv', 'v': 33554689, 't': 1656203510}, {'n': 'germshield', 'vb': True, 't': 1656203510}, {'n': 'filterusage', 'v': 1, 't': 1656203510}, {'n': 'brightness', 'v': 0, 't': 1656203510}, {'n': 'standby', 'vb': False, 't': 1656203510}, {'n': 'fanspeed', 'v': 11, 't': 1656203510}, {'n': 'nightmode', 'vb': False, 't': 1656203510}, {'n': 'childlock', 'vb': False, 't': 1656203510}, {'n': 'safetyswitch', 'vb': True, 't': 1656203510}, {'n': 'mfv', 'v': 16777228, 't': 1656203510}, {'n': 'ofv', 'v': 33554689, 't': 1656203510}, {'n': 'automode', 'vb': False, 't': 1656203510}, {'t': 1656203510, 'vb': True, 'n': 'online'}], 'welcomehome': {'setting': []}, 'fleet_info': []}]
|
181
|
-
|
182
|
-
:param device_name:
|
183
|
-
:param device_uuid:
|
184
|
-
:return:
|
185
|
-
"""
|
186
185
|
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/{device_name}/r/initial"
|
187
186
|
headers = {
|
188
187
|
"Authorization": f"Bearer {await self.get_access_token()}",
|
@@ -211,7 +210,7 @@ class HttpAwsBlueair:
|
|
211
210
|
@request_with_active_session
|
212
211
|
async def set_device_info(
|
213
212
|
self, device_uuid, service_name, action_verb, action_value
|
214
|
-
):
|
213
|
+
) -> bool:
|
215
214
|
_LOGGER.debug("set_device_info")
|
216
215
|
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/{device_uuid}/a/{service_name}"
|
217
216
|
headers = {
|
blueair_api/http_blueair.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
from typing import Any
|
1
2
|
import logging
|
2
3
|
|
3
4
|
from aiohttp import ClientSession, ClientResponse
|
@@ -6,6 +7,7 @@ import base64
|
|
6
7
|
from .util_http import request_with_logging
|
7
8
|
from .const import API_KEY
|
8
9
|
from .errors import LoginError
|
10
|
+
from typing import Optional
|
9
11
|
|
10
12
|
_LOGGER = logging.getLogger(__name__)
|
11
13
|
|
@@ -15,9 +17,9 @@ class HttpBlueair:
|
|
15
17
|
self,
|
16
18
|
username: str,
|
17
19
|
password: str,
|
18
|
-
home_host: str = None,
|
19
|
-
auth_token: str = None,
|
20
|
-
client_session: ClientSession = None,
|
20
|
+
home_host: str | None = None,
|
21
|
+
auth_token: str | None = None,
|
22
|
+
client_session: ClientSession | None = None,
|
21
23
|
):
|
22
24
|
self.username = username
|
23
25
|
self.password = password
|
@@ -44,13 +46,13 @@ class HttpBlueair:
|
|
44
46
|
|
45
47
|
@request_with_logging
|
46
48
|
async def _get_request_with_logging_and_errors_raised(
|
47
|
-
self, url: str, headers: dict = None
|
49
|
+
self, url: str, headers: dict | None = None
|
48
50
|
) -> ClientResponse:
|
49
51
|
return await self.api_session.get(url=url, headers=headers)
|
50
52
|
|
51
53
|
@request_with_logging
|
52
54
|
async def _post_request_with_logging_and_errors_raised(
|
53
|
-
self, url: str, json_body: dict, headers: dict = None
|
55
|
+
self, url: str, json_body: dict, headers: dict | None = None
|
54
56
|
) -> ClientResponse:
|
55
57
|
return await self.api_session.post(url=url, json=json_body, headers=headers)
|
56
58
|
|
@@ -99,7 +101,7 @@ class HttpBlueair:
|
|
99
101
|
else:
|
100
102
|
raise LoginError("invalid password")
|
101
103
|
|
102
|
-
async def get_devices(self) -> list[dict[str,
|
104
|
+
async def get_devices(self) -> list[dict[str, Any]]:
|
103
105
|
"""
|
104
106
|
Fetch a list of devices.
|
105
107
|
|
@@ -123,7 +125,7 @@ class HttpBlueair:
|
|
123
125
|
return await response.json()
|
124
126
|
|
125
127
|
# Note: refreshes every 5 minutes
|
126
|
-
async def get_attributes(self, device_uuid: str) -> dict[str,
|
128
|
+
async def get_attributes(self, device_uuid: str) -> dict[str, Any]:
|
127
129
|
"""
|
128
130
|
Fetch a list of attributes for the provided device ID.
|
129
131
|
|
@@ -154,7 +156,7 @@ class HttpBlueair:
|
|
154
156
|
return attributes
|
155
157
|
|
156
158
|
# Note: refreshes every 5 minutes, timestamps are in seconds
|
157
|
-
async def get_info(self, device_uuid: str) -> dict[str,
|
159
|
+
async def get_info(self, device_uuid: str) -> dict[str, Any]:
|
158
160
|
"""
|
159
161
|
Fetch device information for the provided device ID.
|
160
162
|
|
@@ -178,7 +180,7 @@ class HttpBlueair:
|
|
178
180
|
return await response.json()
|
179
181
|
|
180
182
|
# Note: refreshes every 5 minutes, timestamps are in seconds
|
181
|
-
async def get_current_data_point(self, device_uuid: str) -> dict[str,
|
183
|
+
async def get_current_data_point(self, device_uuid: str) -> dict[str, Any]:
|
182
184
|
"""
|
183
185
|
Fetch device information for the provided device ID.
|
184
186
|
|
@@ -201,7 +203,7 @@ class HttpBlueair:
|
|
201
203
|
)
|
202
204
|
return await response.json()
|
203
205
|
|
204
|
-
async def get_data_points_since(self, device_uuid: str, seconds_ago: int = 0, sample_period: int = 300) -> dict[str,
|
206
|
+
async def get_data_points_since(self, device_uuid: str, seconds_ago: int = 0, sample_period: int = 300) -> dict[str, Any]:
|
205
207
|
"""
|
206
208
|
Fetch the list of data points between a relative timestamp (in seconds) and the current time.
|
207
209
|
|
@@ -1,57 +1,62 @@
|
|
1
|
+
import typing
|
1
2
|
from typing import Any, TypeVar
|
2
3
|
from collections.abc import Iterable
|
3
4
|
import dataclasses
|
4
5
|
import base64
|
5
6
|
|
6
|
-
type ScalarType = str | float | bool
|
7
|
+
type ScalarType = str | float | bool | int | None
|
7
8
|
type MappingType = dict[str, "ObjectType"]
|
8
9
|
type SequenceType = list["ObjectType"]
|
9
10
|
type ObjectType = ScalarType | MappingType | SequenceType
|
10
11
|
|
11
12
|
|
12
|
-
def query_json(jsonobj: ObjectType, path: str):
|
13
|
+
def query_json(jsonobj: ObjectType, path: str) -> ObjectType:
|
13
14
|
value = jsonobj
|
14
15
|
segs = path.split(".")
|
15
|
-
for i, seg in enumerate(segs
|
16
|
-
if not isinstance(value, dict | list):
|
17
|
-
raise KeyError(
|
18
|
-
f"cannot resolve path segment on a scalar "
|
19
|
-
f"when resolving segment {i}:{seg} of {path}.")
|
16
|
+
for i, seg in enumerate(segs):
|
20
17
|
if isinstance(value, list):
|
21
18
|
value = value[int(seg)]
|
22
|
-
|
23
|
-
|
19
|
+
elif isinstance(value, dict):
|
20
|
+
if seg in value:
|
24
21
|
value = value[seg]
|
25
|
-
|
22
|
+
elif i == len(segs) - 1:
|
23
|
+
# last segment returns None if it is not found.
|
24
|
+
value = None
|
25
|
+
else:
|
26
26
|
raise KeyError(
|
27
|
+
f"cannot resolve path segment on a scalar "
|
28
|
+
f"when resolving segment {i}:{seg} of {path}. "
|
29
|
+
f"available keys are {value.keys()}.")
|
30
|
+
else:
|
31
|
+
raise KeyError(
|
27
32
|
f"cannot resolve path segment on a scalar "
|
28
|
-
f"when resolving segment {i}:{seg} of {path}.
|
29
|
-
|
30
|
-
|
31
|
-
# last segment returns None if it is not found.
|
32
|
-
return value.get(segs[-1])
|
33
|
-
|
33
|
+
f"when resolving segment {i}:{seg} of {path}.")
|
34
|
+
return value
|
34
35
|
|
35
36
|
def parse_json[T](kls: type[T], jsonobj: MappingType) -> dict[str, T]:
|
36
37
|
"""Parses a json mapping object to dict.
|
37
38
|
|
38
39
|
The key is preserved. The value is parsed as dataclass type kls.
|
39
40
|
"""
|
41
|
+
assert dataclasses.is_dataclass(kls)
|
40
42
|
result = {}
|
41
43
|
fields = dataclasses.fields(kls)
|
42
44
|
|
43
45
|
for key, value in jsonobj.items():
|
44
|
-
|
45
|
-
|
46
|
+
if not isinstance(value, dict):
|
47
|
+
raise TypeError("expecting mapping value to be dict.")
|
48
|
+
extra_fields = dict(value) # make extra_fields copy.
|
49
|
+
kwargs : dict[str, Any] = {}
|
46
50
|
for field in fields:
|
47
51
|
if field.name == "extra_fields":
|
48
|
-
|
49
|
-
|
50
|
-
kwargs[field.name] =
|
52
|
+
kwargs[field.name] = extra_fields
|
53
|
+
elif field.default is dataclasses.MISSING:
|
54
|
+
kwargs[field.name] = extra_fields.pop(field.name)
|
51
55
|
else:
|
52
|
-
kwargs[field.name] =
|
56
|
+
kwargs[field.name] = extra_fields.pop(field.name, field.default)
|
53
57
|
|
54
|
-
|
58
|
+
obj = kls(**kwargs)
|
59
|
+
result[key] = typing.cast(T, obj)
|
55
60
|
return result
|
56
61
|
|
57
62
|
|
@@ -118,7 +123,7 @@ class Record:
|
|
118
123
|
"""A RFC8428 SenML record, resolved to Python types."""
|
119
124
|
name: str
|
120
125
|
unit: str | None
|
121
|
-
value:
|
126
|
+
value: float | bool | str | bytes
|
122
127
|
timestamp: float | None
|
123
128
|
integral: float | None
|
124
129
|
|
@@ -131,9 +136,11 @@ class SensorPack(list[Record]):
|
|
131
136
|
for record in stream:
|
132
137
|
rs = None
|
133
138
|
rt = None
|
134
|
-
rn
|
139
|
+
rn : str
|
135
140
|
ru = None
|
141
|
+
rv : float | bool | str | bytes
|
136
142
|
for label, value in record.items():
|
143
|
+
assert isinstance(value, str | int | float | bool)
|
137
144
|
match label:
|
138
145
|
case 'bn' | 'bt' | 'bu' | 'bv' | 'bs' | 'bver':
|
139
146
|
raise ValueError("TODO: base fields not supported. c.f. RFC8428, 4.1")
|
@@ -148,29 +155,29 @@ class SensorPack(list[Record]):
|
|
148
155
|
case 'vs':
|
149
156
|
rv = str(value)
|
150
157
|
case 'vd':
|
151
|
-
rv = bytes(base64.b64decode(value))
|
158
|
+
rv = bytes(base64.b64decode(str(value)))
|
152
159
|
case 'n':
|
153
160
|
rn = str(value)
|
154
161
|
case 'u':
|
155
162
|
ru = str(value)
|
156
|
-
case 't':
|
157
|
-
rn = float(value)
|
158
163
|
seq.append(Record(name=rn, unit=ru, value=rv, integral=rs, timestamp=rt))
|
159
164
|
super().__init__(seq)
|
160
165
|
|
161
|
-
def to_latest_value(self) -> dict[str,
|
166
|
+
def to_latest_value(self) -> dict[str, str | bool | float | bytes]:
|
162
167
|
return {rn : record.value for rn, record in self.to_latest().items()}
|
163
168
|
|
164
169
|
def to_latest(self) -> dict[str, Record]:
|
165
|
-
latest = {}
|
170
|
+
latest : dict[str, Record] = {}
|
166
171
|
for record in self:
|
167
172
|
rn = record.name
|
168
173
|
if record.name not in latest:
|
169
174
|
latest[rn] = record
|
170
|
-
|
175
|
+
continue
|
176
|
+
lt = latest[record.name].timestamp
|
177
|
+
if record.timestamp is None:
|
171
178
|
latest[rn] = record
|
172
|
-
elif
|
179
|
+
elif lt is None:
|
173
180
|
latest[rn] = record
|
174
|
-
elif
|
181
|
+
elif lt < record.timestamp:
|
175
182
|
latest[rn] = record
|
176
183
|
return latest
|
blueair_api/util.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
from typing import Any
|
1
2
|
import logging
|
2
3
|
|
3
4
|
from .const import SENSITIVE_FIELD_NAMES
|
@@ -5,7 +6,7 @@ from .const import SENSITIVE_FIELD_NAMES
|
|
5
6
|
_LOGGER = logging.getLogger(__name__)
|
6
7
|
|
7
8
|
|
8
|
-
def clean_dictionary_for_logging(dictionary: dict[str,
|
9
|
+
def clean_dictionary_for_logging(dictionary: dict[str, Any]) -> dict[str, Any]:
|
9
10
|
mutable_dictionary = dictionary.copy()
|
10
11
|
for key in dictionary:
|
11
12
|
if key.lower() in SENSITIVE_FIELD_NAMES:
|
@@ -26,17 +27,6 @@ def clean_dictionary_for_logging(dictionary: dict[str, any]) -> dict[str, any]:
|
|
26
27
|
return mutable_dictionary
|
27
28
|
|
28
29
|
|
29
|
-
def convert_api_array_to_dict(array):
|
30
|
-
dictionary = {}
|
31
|
-
for obj in array:
|
32
|
-
if "v" in obj:
|
33
|
-
dictionary[obj["n"]] = obj["v"]
|
34
|
-
else:
|
35
|
-
if "vb" in obj:
|
36
|
-
dictionary[obj["n"]] = obj["vb"]
|
37
|
-
return dictionary
|
38
|
-
|
39
|
-
|
40
30
|
def safely_get_json_value(json, key, callable_to_cast=None):
|
41
31
|
value = json
|
42
32
|
for x in key.split("."):
|
blueair_api/util_bootstrap.py
CHANGED
@@ -6,6 +6,7 @@ from .http_blueair import HttpBlueair
|
|
6
6
|
from .http_aws_blueair import HttpAwsBlueair
|
7
7
|
from .device import Device
|
8
8
|
from .device_aws import DeviceAws
|
9
|
+
from typing import Optional
|
9
10
|
|
10
11
|
_LOGGER = logging.getLogger(__name__)
|
11
12
|
|
@@ -13,10 +14,10 @@ _LOGGER = logging.getLogger(__name__)
|
|
13
14
|
async def get_devices(
|
14
15
|
username: str,
|
15
16
|
password: str,
|
16
|
-
home_host: str = None,
|
17
|
-
auth_token: str = None,
|
18
|
-
client_session: ClientSession = None,
|
19
|
-
) ->
|
17
|
+
home_host: str | None = None,
|
18
|
+
auth_token: str | None = None,
|
19
|
+
client_session: ClientSession | None = None,
|
20
|
+
) -> tuple[HttpBlueair, list[Device]]:
|
20
21
|
api = HttpBlueair(
|
21
22
|
client_session=client_session,
|
22
23
|
username=username,
|
@@ -43,8 +44,8 @@ async def get_aws_devices(
|
|
43
44
|
username: str,
|
44
45
|
password: str,
|
45
46
|
region: str = "us",
|
46
|
-
client_session: ClientSession = None,
|
47
|
-
) ->
|
47
|
+
client_session: ClientSession | None = None,
|
48
|
+
) -> tuple[HttpAwsBlueair, list[Device]]:
|
48
49
|
api = HttpAwsBlueair(
|
49
50
|
username=username,
|
50
51
|
password=password,
|
@@ -0,0 +1,19 @@
|
|
1
|
+
blueair_api/__init__.py,sha256=GucsIENhTF4AVxPn4Xyr4imUxJJ8RO8RYt1opHCF2hQ,326
|
2
|
+
blueair_api/callbacks.py,sha256=fvrJsqH5eDRxWOGWiZkF2uLU4n2ve0zzU17ERqWbHP8,756
|
3
|
+
blueair_api/const.py,sha256=q1smSEhwyYvuQiR867lToFm-mGV-d3dNJvN0NJgscbU,1037
|
4
|
+
blueair_api/device.py,sha256=t2KrTsdPH9FJZqCcLSm_3eQGHtbNuVcwK3OyzgCpwJ0,3835
|
5
|
+
blueair_api/device_aws.py,sha256=FYz1YtJQMTUfzZ-__kdOGe5HtO8MTUygX-XdRsYejWg,7356
|
6
|
+
blueair_api/errors.py,sha256=lJ_iFU_W6zQfGRi_wsMhWDw-fAVPFeCkCbT1erIlYQQ,233
|
7
|
+
blueair_api/http_aws_blueair.py,sha256=jztGyoH0iC7aCJ2oGf9hnEeHFOie3YikFvwtWo3W2w4,8536
|
8
|
+
blueair_api/http_blueair.py,sha256=cTtiHNTlZKFQTh7cvfRqA_P1lGEpvMXvjM5o6_qEN2U,9497
|
9
|
+
blueair_api/intermediate_representation_aws.py,sha256=DJWxHFP9yVll0O6kxHWjKscIlu-7genc3f7ZvwV5YdA,6137
|
10
|
+
blueair_api/model_enum.py,sha256=Z9Ne4icNEjbGNwdHJZSDibcKJKwv-W1BRpZx01RGFuY,2480
|
11
|
+
blueair_api/stub.py,sha256=sTWyRSDObzrXpZToAgDmZhCk3q8SsGN35h-kzMOqSOc,1272
|
12
|
+
blueair_api/util.py,sha256=7MrB2DLqUVOlBQuhv7bRmUXvcGcsjXiV3M0H3LloiOo,1962
|
13
|
+
blueair_api/util_bootstrap.py,sha256=RNIKrMWMBSUad4loYGwEVIKVxQ1_LVhXNQtUwuaquyo,1754
|
14
|
+
blueair_api/util_http.py,sha256=45AJG3Vb6LMVzI0WV22AoSyt64f_Jj3KpOAwF5M6EFE,1327
|
15
|
+
blueair_api-1.26.3.dist-info/LICENSE,sha256=W6UV41yCe1R_Avet8VtsxwdJar18n40b3MRnbEMHZmI,1066
|
16
|
+
blueair_api-1.26.3.dist-info/METADATA,sha256=fUmIeTOXwfjXBWUh5sN24dhgek6fhOK-jylC-6izNAg,1995
|
17
|
+
blueair_api-1.26.3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
18
|
+
blueair_api-1.26.3.dist-info/top_level.txt,sha256=-gn0jNtmE83qEu70uMW5F4JrXnHGRfuFup1EPWF70oc,12
|
19
|
+
blueair_api-1.26.3.dist-info/RECORD,,
|
@@ -1,19 +0,0 @@
|
|
1
|
-
blueair_api/__init__.py,sha256=GucsIENhTF4AVxPn4Xyr4imUxJJ8RO8RYt1opHCF2hQ,326
|
2
|
-
blueair_api/callbacks.py,sha256=eqgfe1qN_NNWOp2qNSP123zQJxh_Ie6vZXp5EHp6ePc,757
|
3
|
-
blueair_api/const.py,sha256=q1smSEhwyYvuQiR867lToFm-mGV-d3dNJvN0NJgscbU,1037
|
4
|
-
blueair_api/device.py,sha256=Ey8FIDnRwyVWSdLqfHxlCQSOFFaI_l_oVfvqpPIoTvI,3834
|
5
|
-
blueair_api/device_aws.py,sha256=c8dZQ9rbljeYBjzXelcnhwQWIJ0dw3JACLFRzxD7s38,7379
|
6
|
-
blueair_api/errors.py,sha256=lJ_iFU_W6zQfGRi_wsMhWDw-fAVPFeCkCbT1erIlYQQ,233
|
7
|
-
blueair_api/http_aws_blueair.py,sha256=m_qoCFOYICCu_U_maBvkmOha3YmNtxxtPYyapVBGKNc,17821
|
8
|
-
blueair_api/http_blueair.py,sha256=n9F5fvEROIyAkqDMM22l84PB7ZoeEkWbj2YuCZpeDNg,9411
|
9
|
-
blueair_api/intermediate_representation_aws.py,sha256=TteTMuy1UQ2L0vpGF2Te8v_E1Ageg0QTp6ZM_UUZ9dI,5667
|
10
|
-
blueair_api/model_enum.py,sha256=Z9Ne4icNEjbGNwdHJZSDibcKJKwv-W1BRpZx01RGFuY,2480
|
11
|
-
blueair_api/stub.py,sha256=sTWyRSDObzrXpZToAgDmZhCk3q8SsGN35h-kzMOqSOc,1272
|
12
|
-
blueair_api/util.py,sha256=4g8dTlxawBYKslOJS7WCWss0670mtUc53c3L8NiPTsM,2201
|
13
|
-
blueair_api/util_bootstrap.py,sha256=Vewg7mT1qSRgzOOJDrpXrVhGwcFPRnMrqhJVSIB-0AA,1688
|
14
|
-
blueair_api/util_http.py,sha256=45AJG3Vb6LMVzI0WV22AoSyt64f_Jj3KpOAwF5M6EFE,1327
|
15
|
-
blueair_api-1.26.2.dist-info/LICENSE,sha256=W6UV41yCe1R_Avet8VtsxwdJar18n40b3MRnbEMHZmI,1066
|
16
|
-
blueair_api-1.26.2.dist-info/METADATA,sha256=1hD8KxOs0fsLvrEVOorTCf9_tQkPUDJQIot3OpTaeAU,1995
|
17
|
-
blueair_api-1.26.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
18
|
-
blueair_api-1.26.2.dist-info/top_level.txt,sha256=-gn0jNtmE83qEu70uMW5F4JrXnHGRfuFup1EPWF70oc,12
|
19
|
-
blueair_api-1.26.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|