blueair-api 1.36.0__py3-none-any.whl → 1.36.2__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/const.py +16 -4
- blueair_api/device_aws.py +10 -4
- blueair_api/http_aws_blueair.py +29 -8
- blueair_api/intermediate_representation_aws.py +32 -2
- blueair_api/stub.py +1 -1
- blueair_api/util_http.py +3 -0
- {blueair_api-1.36.0.dist-info → blueair_api-1.36.2.dist-info}/METADATA +1 -1
- blueair_api-1.36.2.dist-info/RECORD +19 -0
- {blueair_api-1.36.0.dist-info → blueair_api-1.36.2.dist-info}/WHEEL +1 -1
- blueair_api-1.36.0.dist-info/RECORD +0 -19
- {blueair_api-1.36.0.dist-info → blueair_api-1.36.2.dist-info}/licenses/LICENSE +0 -0
- {blueair_api-1.36.0.dist-info → blueair_api-1.36.2.dist-info}/top_level.txt +0 -0
blueair_api/const.py
CHANGED
@@ -11,17 +11,29 @@ API_KEY = "eyJhbGciOiJIUzI1NiJ9.eyJncmFudGVlIjoiYmx1ZWFpciIsImlhdCI6MTQ1MzEyNTYz
|
|
11
11
|
|
12
12
|
AWS_APIKEYS = {
|
13
13
|
"us": {
|
14
|
-
"gigyaRegion": "us1",
|
14
|
+
"gigyaRegion": "accounts.us1.gigya.com",
|
15
15
|
"restApiId": "on1keymlmh",
|
16
|
-
"awsRegion": "us-east-2",
|
16
|
+
"awsRegion": "us-east-2.amazonaws.com",
|
17
17
|
"apiKey": "3_-xUbbrIY8QCbHDWQs1tLXE-CZBQ50SGElcOY5hF1euE11wCoIlNbjMGAFQ6UwhMY",
|
18
18
|
},
|
19
19
|
"eu": {
|
20
|
-
"gigyaRegion": "eu1",
|
20
|
+
"gigyaRegion": "accounts.eu1.gigya.com",
|
21
21
|
"restApiId": "hkgmr8v960",
|
22
|
-
"awsRegion": "eu-west-1",
|
22
|
+
"awsRegion": "eu-west-1.amazonaws.com",
|
23
23
|
"apiKey": "3_qRseYzrUJl1VyxvSJANalu_kNgQ83swB1B9uzgms58--5w1ClVNmrFdsDnWVQQCl",
|
24
24
|
},
|
25
|
+
"cn": {
|
26
|
+
"gigyaRegion": "accounts.cn1.sapcdm.cn",
|
27
|
+
"restApiId": "ftbkyp79si",
|
28
|
+
"awsRegion": "cn-north-1.amazonaws.com.cn",
|
29
|
+
"apiKey": "3_h3UEfJnA-zDpFPR9L4412HO7Mz2VVeN4wprbWYafPN1gX0kSnLcZ9VSfFi7bEIIU",
|
30
|
+
},
|
31
|
+
"au": {
|
32
|
+
"gigyaRegion": "accounts.au1.gigya.com",
|
33
|
+
"restApiId": "hkgmr8v960",
|
34
|
+
"awsRegion": "eu-west-1.amazonaws.com",
|
35
|
+
"apiKey": "3_Z2N0mIFC6j2fx1z2sq76R3pwkCMaMX2y9btPb0_PgI_3wfjSJoofFnBbxbtuQksN",
|
36
|
+
},
|
25
37
|
}
|
26
38
|
|
27
39
|
|
blueair_api/device_aws.py
CHANGED
@@ -32,6 +32,7 @@ class DeviceAws(CallbacksMixin):
|
|
32
32
|
|
33
33
|
api: HttpAwsBlueair = field(repr=False)
|
34
34
|
raw_info : dict[str, Any] = field(repr=False, init=False)
|
35
|
+
raw_sensors : dict[str, Any] = field(repr=False, init=False)
|
35
36
|
|
36
37
|
uuid : str | None = None
|
37
38
|
name : str | None = None
|
@@ -81,16 +82,18 @@ class DeviceAws(CallbacksMixin):
|
|
81
82
|
async def refresh(self):
|
82
83
|
_LOGGER.debug(f"refreshing blueair device aws: {self}")
|
83
84
|
self.raw_info = await self.api.device_info(self.name_api, self.uuid)
|
85
|
+
self.raw_sensors = await self.api.device_sensors(self.name_api, self.uuid)
|
84
86
|
_LOGGER.debug(dumps(self.raw_info, indent=2))
|
87
|
+
if self.raw_sensors is not None:
|
88
|
+
_LOGGER.debug(dumps(self.raw_sensors, indent=2))
|
85
89
|
|
86
|
-
# ir.parse_json(ir.Attribute, ir.query_json(info, "configuration.da"))
|
87
90
|
ds = ir.parse_json(ir.Sensor, ir.query_json(self.raw_info, "configuration.ds"))
|
88
91
|
dc = ir.parse_json(ir.Control, ir.query_json(self.raw_info, "configuration.dc"))
|
89
92
|
|
90
|
-
sensor_data = ir.
|
93
|
+
sensor_data = ir.SensorHistory(self.raw_sensors).to_latest()
|
91
94
|
|
92
95
|
def sensor_data_safe_get(key):
|
93
|
-
return sensor_data.get(key) if key in ds else NotImplemented
|
96
|
+
return sensor_data.values.get(key) if key in ds else NotImplemented
|
94
97
|
|
95
98
|
self.pm1 = sensor_data_safe_get("pm1")
|
96
99
|
self.pm2_5 = sensor_data_safe_get("pm2_5")
|
@@ -142,7 +145,10 @@ class DeviceAws(CallbacksMixin):
|
|
142
145
|
self.cool_sub_mode = states_safe_get("coolsubmode")
|
143
146
|
self.cool_fan_speed = states_safe_get("coolfs")
|
144
147
|
self.ap_sub_mode = states_safe_get("apsubmode")
|
145
|
-
|
148
|
+
if states_safe_get("fsp0") is NotImplemented:
|
149
|
+
self.fan_speed_0 = sensor_data_safe_get("fsp0")
|
150
|
+
else:
|
151
|
+
self.fan_speed_0 = states_safe_get("fsp0")
|
146
152
|
self.temperature_unit = states_safe_get("tu")
|
147
153
|
|
148
154
|
self.publish_updates()
|
blueair_api/http_aws_blueair.py
CHANGED
@@ -3,6 +3,7 @@ import functools
|
|
3
3
|
from logging import getLogger
|
4
4
|
from typing import Any
|
5
5
|
from aiohttp import ClientSession, ClientResponse, FormData
|
6
|
+
from datetime import datetime, timedelta
|
6
7
|
|
7
8
|
from .const import AWS_APIKEYS
|
8
9
|
from .util_http import request_with_logging
|
@@ -92,9 +93,9 @@ class HttpAwsBlueair:
|
|
92
93
|
@request_with_errors
|
93
94
|
@request_with_logging
|
94
95
|
async def _get_request_with_logging_and_errors_raised(
|
95
|
-
self, url: str, headers: dict | None = None
|
96
|
+
self, url: str, headers: dict | None = None, params: dict | None = None
|
96
97
|
) -> ClientResponse:
|
97
|
-
return await self.api_session.get(url=url, headers=headers)
|
98
|
+
return await self.api_session.get(url=url, headers=headers, params=params)
|
98
99
|
|
99
100
|
@request_with_errors
|
100
101
|
@request_with_logging
|
@@ -111,7 +112,7 @@ class HttpAwsBlueair:
|
|
111
112
|
|
112
113
|
async def refresh_session(self) -> None:
|
113
114
|
_LOGGER.debug("refresh_session")
|
114
|
-
url = f"https://
|
115
|
+
url = f"https://{AWS_APIKEYS[self.region]['gigyaRegion']}/accounts.login"
|
115
116
|
form_data = FormData()
|
116
117
|
form_data.add_field("apikey", AWS_APIKEYS[self.region]["apiKey"])
|
117
118
|
form_data.add_field("loginID", self.username)
|
@@ -130,7 +131,7 @@ class HttpAwsBlueair:
|
|
130
131
|
_LOGGER.debug("refresh_jwt")
|
131
132
|
if self.session_token is None or self.session_secret is None:
|
132
133
|
await self.refresh_session()
|
133
|
-
url = f"https://
|
134
|
+
url = f"https://{AWS_APIKEYS[self.region]['gigyaRegion']}/accounts.getJWT"
|
134
135
|
form_data = FormData()
|
135
136
|
form_data.add_field("oauth_token", self.session_token)
|
136
137
|
form_data.add_field("secret", self.session_secret)
|
@@ -147,7 +148,7 @@ class HttpAwsBlueair:
|
|
147
148
|
_LOGGER.debug("refresh_access_token")
|
148
149
|
if self.jwt is None:
|
149
150
|
await self.refresh_jwt()
|
150
|
-
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}
|
151
|
+
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}/prod/c/login"
|
151
152
|
headers = {"idtoken": self.jwt}
|
152
153
|
response: ClientResponse = (
|
153
154
|
await self._post_request_with_logging_and_errors_raised(
|
@@ -167,7 +168,7 @@ class HttpAwsBlueair:
|
|
167
168
|
@request_with_active_session
|
168
169
|
async def devices(self) -> dict[str, Any]:
|
169
170
|
_LOGGER.debug("devices")
|
170
|
-
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}
|
171
|
+
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}/prod/c/registered-devices"
|
171
172
|
headers = {
|
172
173
|
"Authorization": f"Bearer {await self.get_access_token()}",
|
173
174
|
}
|
@@ -179,10 +180,30 @@ class HttpAwsBlueair:
|
|
179
180
|
response_json = await response.json()
|
180
181
|
return response_json["devices"]
|
181
182
|
|
183
|
+
@request_with_active_session
|
184
|
+
async def device_sensors(self, device_name, device_uuid, duration: timedelta = timedelta(hours=10)):
|
185
|
+
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}/prod/c/{device_name}/r/telemetry/5m/historical"
|
186
|
+
headers = {
|
187
|
+
"Authorization": f"Bearer {await self.get_access_token()}",
|
188
|
+
}
|
189
|
+
params = {
|
190
|
+
"did": device_uuid,
|
191
|
+
"from": int((datetime.now()-duration).timestamp()),
|
192
|
+
"to": int(datetime.now().timestamp()),
|
193
|
+
"s": ["pm1", "pm2_5", "pm10", "tVOC", "hcho", "h", "t", "fsp0"]
|
194
|
+
}
|
195
|
+
response: ClientResponse = (
|
196
|
+
await self._get_request_with_logging_and_errors_raised(
|
197
|
+
url=url, headers=headers, params=params
|
198
|
+
)
|
199
|
+
)
|
200
|
+
response_json = await response.json()
|
201
|
+
return response_json
|
202
|
+
|
182
203
|
@request_with_active_session
|
183
204
|
async def device_info(self, device_name, device_uuid) -> dict[str, Any]:
|
184
205
|
_LOGGER.debug("device_info")
|
185
|
-
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}
|
206
|
+
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}/prod/c/{device_name}/r/initial"
|
186
207
|
headers = {
|
187
208
|
"Authorization": f"Bearer {await self.get_access_token()}",
|
188
209
|
}
|
@@ -212,7 +233,7 @@ class HttpAwsBlueair:
|
|
212
233
|
self, device_uuid, service_name, action_verb, action_value
|
213
234
|
) -> bool:
|
214
235
|
_LOGGER.debug("set_device_info")
|
215
|
-
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}
|
236
|
+
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}/prod/c/{device_uuid}/a/{service_name}"
|
216
237
|
headers = {
|
217
238
|
"Authorization": f"Bearer {await self.get_access_token()}",
|
218
239
|
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import typing
|
2
|
-
from typing import Any
|
2
|
+
from typing import Any
|
3
3
|
from collections.abc import Iterable
|
4
4
|
import dataclasses
|
5
5
|
import base64
|
@@ -85,7 +85,7 @@ class Attribute:
|
|
85
85
|
class Sensor:
|
86
86
|
"""DeviceSensor(ds); seems to define a sensor.
|
87
87
|
|
88
|
-
We never directly access these objects.
|
88
|
+
We never directly access these objects. Though this defines
|
89
89
|
the schema for 'h', 't', 'pm10' etc that gets returned in
|
90
90
|
the sensor_data senml SensorPack.
|
91
91
|
"""
|
@@ -115,6 +115,36 @@ class Control:
|
|
115
115
|
d: str | None = None # device info json path
|
116
116
|
|
117
117
|
|
118
|
+
@dataclasses.dataclass
|
119
|
+
class SensorRecord:
|
120
|
+
values: dict[str, int]
|
121
|
+
timestamp: float | None
|
122
|
+
|
123
|
+
|
124
|
+
class SensorHistory(list[SensorRecord]):
|
125
|
+
def __init__(self, response):
|
126
|
+
sensors = response[0]["sensors"]
|
127
|
+
datapoints = response[0]["datapoints"]
|
128
|
+
sensor_records = []
|
129
|
+
for datapoint in datapoints:
|
130
|
+
values = {}
|
131
|
+
for idx, sensor in enumerate(sensors):
|
132
|
+
if datapoint[idx+1] is not None:
|
133
|
+
values[sensor] = int(datapoint[idx+1])
|
134
|
+
sensor_records.append(SensorRecord(timestamp=int(datapoint[0]), values=values))
|
135
|
+
super().__init__(sensor_records)
|
136
|
+
|
137
|
+
def to_latest(self) -> SensorRecord:
|
138
|
+
def key(e):
|
139
|
+
return e.timestamp
|
140
|
+
self.sort(key=key, reverse=True)
|
141
|
+
if len(self) > 0:
|
142
|
+
return self[0]
|
143
|
+
else:
|
144
|
+
return SensorRecord(values={}, timestamp=0)
|
145
|
+
|
146
|
+
|
147
|
+
|
118
148
|
########################
|
119
149
|
# SenML RFC8428
|
120
150
|
|
blueair_api/stub.py
CHANGED
@@ -8,7 +8,7 @@ import sys
|
|
8
8
|
|
9
9
|
path_root = Path(__file__).parents[2]
|
10
10
|
sys.path.append(str(path_root))
|
11
|
-
from src.blueair_api import get_devices, get_aws_devices
|
11
|
+
from src.blueair_api import get_devices, get_aws_devices
|
12
12
|
|
13
13
|
|
14
14
|
logger = logging.getLogger("src.blueair_api")
|
blueair_api/util_http.py
CHANGED
@@ -12,6 +12,9 @@ def request_with_logging(func):
|
|
12
12
|
headers = kwargs.get("headers")
|
13
13
|
if headers is not None:
|
14
14
|
request_message = request_message + f"headers: {headers}"
|
15
|
+
params = kwargs.get("params")
|
16
|
+
if params is not None:
|
17
|
+
request_message = request_message + f"params: {params}"
|
15
18
|
json_body = kwargs.get("json_body")
|
16
19
|
if json_body is not None:
|
17
20
|
request_message = (
|
@@ -0,0 +1,19 @@
|
|
1
|
+
blueair_api/__init__.py,sha256=gk2CZWEnEo4FlP7VcJsDs1kujZ_ryXF5H9n3Kd-4WWo,313
|
2
|
+
blueair_api/callbacks.py,sha256=fvrJsqH5eDRxWOGWiZkF2uLU4n2ve0zzU17ERqWbHP8,756
|
3
|
+
blueair_api/const.py,sha256=x-2ae2faZqsalYCuqA_ZpGSZSu0_x_rTrvikagnM9PU,1585
|
4
|
+
blueair_api/device.py,sha256=4mjoYGOiVGi_yw5ZRnpgwSRcV9q68nseC6VgdCDyWAQ,5554
|
5
|
+
blueair_api/device_aws.py,sha256=ClvpDKT9prv4d15j1fdF1S4gtdH810ZF-iKRN6WXbr0,10506
|
6
|
+
blueair_api/errors.py,sha256=lJ_iFU_W6zQfGRi_wsMhWDw-fAVPFeCkCbT1erIlYQQ,233
|
7
|
+
blueair_api/http_aws_blueair.py,sha256=4-qrtqYkXHejUhImyDisnLJ-6sDf_YZkzD_Nv-KcBUw,9450
|
8
|
+
blueair_api/http_blueair.py,sha256=IMnKXs9S0Z-t1GVshSItjNr9tTdtXjh7-T-tHpsbzhE,11752
|
9
|
+
blueair_api/intermediate_representation_aws.py,sha256=g4S7TsRxJd4uh-0k8ByvgG36AXDiPPbDtlHn5ZOTk_A,7035
|
10
|
+
blueair_api/model_enum.py,sha256=gH82gAnnxnK7JFIHbfjkFXFA3p_hSmujvvuQ0GQyzm4,494
|
11
|
+
blueair_api/stub.py,sha256=J1BXHUzRR4P32q9WkbucmMqzAjqxS722NSMK4TLZG8o,1176
|
12
|
+
blueair_api/util.py,sha256=2FerJEliR4AQc8nBeAMXb7nyl-CPhUszaHxFcVq4p0s,2105
|
13
|
+
blueair_api/util_bootstrap.py,sha256=oo6qqEYUxWb4wLLivk7ofB0YFrKsTZo5tW0U2wc3-D0,1757
|
14
|
+
blueair_api/util_http.py,sha256=LohO13eBk7_MCfBRg3tMKjLklDTr87RC6Q8vLffwX1M,1464
|
15
|
+
blueair_api-1.36.2.dist-info/licenses/LICENSE,sha256=W6UV41yCe1R_Avet8VtsxwdJar18n40b3MRnbEMHZmI,1066
|
16
|
+
blueair_api-1.36.2.dist-info/METADATA,sha256=7KSURW4i0JNyKUMtJEaawYDY-T0Ab4EMQHz8lisv_O0,2017
|
17
|
+
blueair_api-1.36.2.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
18
|
+
blueair_api-1.36.2.dist-info/top_level.txt,sha256=-gn0jNtmE83qEu70uMW5F4JrXnHGRfuFup1EPWF70oc,12
|
19
|
+
blueair_api-1.36.2.dist-info/RECORD,,
|
@@ -1,19 +0,0 @@
|
|
1
|
-
blueair_api/__init__.py,sha256=gk2CZWEnEo4FlP7VcJsDs1kujZ_ryXF5H9n3Kd-4WWo,313
|
2
|
-
blueair_api/callbacks.py,sha256=fvrJsqH5eDRxWOGWiZkF2uLU4n2ve0zzU17ERqWbHP8,756
|
3
|
-
blueair_api/const.py,sha256=q1smSEhwyYvuQiR867lToFm-mGV-d3dNJvN0NJgscbU,1037
|
4
|
-
blueair_api/device.py,sha256=4mjoYGOiVGi_yw5ZRnpgwSRcV9q68nseC6VgdCDyWAQ,5554
|
5
|
-
blueair_api/device_aws.py,sha256=J3HhuCnqSQhArgjBQfg3FdIl2lI8EKQp24OFxeA7GNY,10210
|
6
|
-
blueair_api/errors.py,sha256=lJ_iFU_W6zQfGRi_wsMhWDw-fAVPFeCkCbT1erIlYQQ,233
|
7
|
-
blueair_api/http_aws_blueair.py,sha256=f5HhjaYiBS8RMPjYgVJLOkk4Yqas2euh6hAkHr6H6Uc,8543
|
8
|
-
blueair_api/http_blueair.py,sha256=IMnKXs9S0Z-t1GVshSItjNr9tTdtXjh7-T-tHpsbzhE,11752
|
9
|
-
blueair_api/intermediate_representation_aws.py,sha256=DJWxHFP9yVll0O6kxHWjKscIlu-7genc3f7ZvwV5YdA,6137
|
10
|
-
blueair_api/model_enum.py,sha256=gH82gAnnxnK7JFIHbfjkFXFA3p_hSmujvvuQ0GQyzm4,494
|
11
|
-
blueair_api/stub.py,sha256=tWJJLIhKf39P1gRAXfJk6L8cBJI9fVK3Uk9pj2_Teaw,1187
|
12
|
-
blueair_api/util.py,sha256=2FerJEliR4AQc8nBeAMXb7nyl-CPhUszaHxFcVq4p0s,2105
|
13
|
-
blueair_api/util_bootstrap.py,sha256=oo6qqEYUxWb4wLLivk7ofB0YFrKsTZo5tW0U2wc3-D0,1757
|
14
|
-
blueair_api/util_http.py,sha256=45AJG3Vb6LMVzI0WV22AoSyt64f_Jj3KpOAwF5M6EFE,1327
|
15
|
-
blueair_api-1.36.0.dist-info/licenses/LICENSE,sha256=W6UV41yCe1R_Avet8VtsxwdJar18n40b3MRnbEMHZmI,1066
|
16
|
-
blueair_api-1.36.0.dist-info/METADATA,sha256=oj2k2pxbF40Bezigu942MVToRz2h5Zi5817HK2YXbDc,2017
|
17
|
-
blueair_api-1.36.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
18
|
-
blueair_api-1.36.0.dist-info/top_level.txt,sha256=-gn0jNtmE83qEu70uMW5F4JrXnHGRfuFup1EPWF70oc,12
|
19
|
-
blueair_api-1.36.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|