blueair-api 1.26.2__tar.gz → 1.26.3__tar.gz
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-1.26.2 → blueair_api-1.26.3}/PKG-INFO +1 -1
- {blueair_api-1.26.2 → blueair_api-1.26.3}/pyproject.toml +1 -1
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/callbacks.py +1 -1
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/device.py +1 -1
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/device_aws.py +1 -1
- blueair_api-1.26.3/src/blueair_api/http_aws_blueair.py +226 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/http_blueair.py +12 -10
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/intermediate_representation_aws.py +40 -33
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/util.py +2 -12
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/util_bootstrap.py +7 -6
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api.egg-info/PKG-INFO +1 -1
- blueair_api-1.26.2/src/blueair_api/http_aws_blueair.py +0 -227
- {blueair_api-1.26.2 → blueair_api-1.26.3}/LICENSE +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/README.md +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/setup.cfg +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/__init__.py +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/const.py +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/errors.py +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/model_enum.py +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/stub.py +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/util_http.py +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api.egg-info/SOURCES.txt +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api.egg-info/dependency_links.txt +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api.egg-info/requires.txt +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api.egg-info/top_level.txt +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/tests/test_device_aws.py +0 -0
- {blueair_api-1.26.2 → blueair_api-1.26.3}/tests/test_intermediate_representation_aws.py +0 -0
@@ -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()
|
@@ -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"])
|
@@ -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):
|
@@ -0,0 +1,226 @@
|
|
1
|
+
from typing import Any
|
2
|
+
import functools
|
3
|
+
import logging
|
4
|
+
|
5
|
+
from aiohttp import ClientSession, ClientResponse, FormData
|
6
|
+
from .const import AWS_APIKEYS
|
7
|
+
|
8
|
+
from .util_http import request_with_logging
|
9
|
+
from .errors import SessionError, LoginError
|
10
|
+
|
11
|
+
_LOGGER = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
def request_with_active_session(func):
|
15
|
+
@functools.wraps(func)
|
16
|
+
async def request_with_active_session_wrapper(*args, **kwargs) -> ClientResponse:
|
17
|
+
_LOGGER.debug("session")
|
18
|
+
try:
|
19
|
+
return await func(*args, **kwargs)
|
20
|
+
except SessionError:
|
21
|
+
_LOGGER.debug("got invalid session, attempting to repair and resend")
|
22
|
+
self = args[0]
|
23
|
+
self.session_token = None
|
24
|
+
self.session_secret = None
|
25
|
+
self.access_token = None
|
26
|
+
self.jwt = None
|
27
|
+
response = await func(*args, **kwargs)
|
28
|
+
return response
|
29
|
+
|
30
|
+
return request_with_active_session_wrapper
|
31
|
+
|
32
|
+
|
33
|
+
def request_with_errors(func):
|
34
|
+
@functools.wraps(func)
|
35
|
+
async def request_with_errors_wrapper(*args, **kwargs) -> ClientResponse:
|
36
|
+
_LOGGER.debug("checking for errors")
|
37
|
+
response: ClientResponse = await func(*args, **kwargs)
|
38
|
+
status_code = response.status
|
39
|
+
try:
|
40
|
+
response_json = await response.json(content_type=None)
|
41
|
+
if "statusCode" in response_json:
|
42
|
+
_LOGGER.debug("response json found, checking status code from response")
|
43
|
+
status_code = response_json["statusCode"]
|
44
|
+
except Exception as e:
|
45
|
+
_LOGGER.error(f"Error parsing response for errors {e}")
|
46
|
+
raise e
|
47
|
+
if status_code == 200:
|
48
|
+
_LOGGER.debug("response 200")
|
49
|
+
return response
|
50
|
+
if 400 <= status_code <= 500:
|
51
|
+
_LOGGER.debug("auth error")
|
52
|
+
url = kwargs["url"]
|
53
|
+
response_text = await response.text()
|
54
|
+
if "accounts.login" in url:
|
55
|
+
_LOGGER.debug("login error")
|
56
|
+
raise LoginError(response_text)
|
57
|
+
else:
|
58
|
+
_LOGGER.debug("session error")
|
59
|
+
raise SessionError(response_text)
|
60
|
+
raise ValueError(f"unknown status code {status_code}")
|
61
|
+
|
62
|
+
return request_with_errors_wrapper
|
63
|
+
|
64
|
+
|
65
|
+
class HttpAwsBlueair:
|
66
|
+
def __init__(
|
67
|
+
self,
|
68
|
+
username: str,
|
69
|
+
password: str,
|
70
|
+
region: str = "us",
|
71
|
+
client_session: ClientSession | None = None,
|
72
|
+
):
|
73
|
+
self.username = username
|
74
|
+
self.password = password
|
75
|
+
self.region = region
|
76
|
+
|
77
|
+
self.session_token = None
|
78
|
+
self.session_secret = None
|
79
|
+
|
80
|
+
self.access_token = None
|
81
|
+
|
82
|
+
self.jwt = None
|
83
|
+
|
84
|
+
if client_session is None:
|
85
|
+
self.api_session = ClientSession(raise_for_status=False)
|
86
|
+
else:
|
87
|
+
self.api_session = client_session
|
88
|
+
|
89
|
+
async def cleanup_client_session(self):
|
90
|
+
await self.api_session.close()
|
91
|
+
|
92
|
+
@request_with_errors
|
93
|
+
@request_with_logging
|
94
|
+
async def _get_request_with_logging_and_errors_raised(
|
95
|
+
self, url: str, headers: dict | None = None
|
96
|
+
) -> ClientResponse:
|
97
|
+
return await self.api_session.get(url=url, headers=headers)
|
98
|
+
|
99
|
+
@request_with_errors
|
100
|
+
@request_with_logging
|
101
|
+
async def _post_request_with_logging_and_errors_raised(
|
102
|
+
self,
|
103
|
+
url: str,
|
104
|
+
json_body: dict | None = None,
|
105
|
+
form_data: FormData | None = None,
|
106
|
+
headers: dict | None = None,
|
107
|
+
) -> ClientResponse:
|
108
|
+
return await self.api_session.post(
|
109
|
+
url=url, data=form_data, json=json_body, headers=headers
|
110
|
+
)
|
111
|
+
|
112
|
+
async def refresh_session(self) -> None:
|
113
|
+
_LOGGER.debug("refresh_session")
|
114
|
+
url = f"https://accounts.{AWS_APIKEYS[self.region]['gigyaRegion']}.gigya.com/accounts.login"
|
115
|
+
form_data = FormData()
|
116
|
+
form_data.add_field("apikey", AWS_APIKEYS[self.region]["apiKey"])
|
117
|
+
form_data.add_field("loginID", self.username)
|
118
|
+
form_data.add_field("password", self.password)
|
119
|
+
form_data.add_field("targetEnv", "mobile")
|
120
|
+
response: ClientResponse = (
|
121
|
+
await self._post_request_with_logging_and_errors_raised(
|
122
|
+
url=url, form_data=form_data
|
123
|
+
)
|
124
|
+
)
|
125
|
+
response_json = await response.json(content_type="text/javascript")
|
126
|
+
self.session_token = response_json["sessionInfo"]["sessionToken"]
|
127
|
+
self.session_secret = response_json["sessionInfo"]["sessionSecret"]
|
128
|
+
|
129
|
+
async def refresh_jwt(self) -> None:
|
130
|
+
_LOGGER.debug("refresh_jwt")
|
131
|
+
if self.session_token is None or self.session_secret is None:
|
132
|
+
await self.refresh_session()
|
133
|
+
url = f"https://accounts.{AWS_APIKEYS[self.region]['gigyaRegion']}.gigya.com/accounts.getJWT"
|
134
|
+
form_data = FormData()
|
135
|
+
form_data.add_field("oauth_token", self.session_token)
|
136
|
+
form_data.add_field("secret", self.session_secret)
|
137
|
+
form_data.add_field("targetEnv", "mobile")
|
138
|
+
response: ClientResponse = (
|
139
|
+
await self._post_request_with_logging_and_errors_raised(
|
140
|
+
url=url, form_data=form_data
|
141
|
+
)
|
142
|
+
)
|
143
|
+
response_json = await response.json(content_type="text/javascript")
|
144
|
+
self.jwt = response_json["id_token"]
|
145
|
+
|
146
|
+
async def refresh_access_token(self) -> None:
|
147
|
+
_LOGGER.debug("refresh_access_token")
|
148
|
+
if self.jwt is None:
|
149
|
+
await self.refresh_jwt()
|
150
|
+
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/login"
|
151
|
+
headers = {"idtoken": self.jwt}
|
152
|
+
response: ClientResponse = (
|
153
|
+
await self._post_request_with_logging_and_errors_raised(
|
154
|
+
url=url, headers=headers
|
155
|
+
)
|
156
|
+
)
|
157
|
+
response_json = await response.json()
|
158
|
+
self.access_token = response_json["access_token"]
|
159
|
+
|
160
|
+
async def get_access_token(self) -> str:
|
161
|
+
_LOGGER.debug("get_access_token")
|
162
|
+
if self.access_token is None:
|
163
|
+
await self.refresh_access_token()
|
164
|
+
assert self.access_token is not None
|
165
|
+
return self.access_token
|
166
|
+
|
167
|
+
@request_with_active_session
|
168
|
+
async def devices(self) -> dict[str, Any]:
|
169
|
+
_LOGGER.debug("devices")
|
170
|
+
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/registered-devices"
|
171
|
+
headers = {
|
172
|
+
"Authorization": f"Bearer {await self.get_access_token()}",
|
173
|
+
}
|
174
|
+
response: ClientResponse = (
|
175
|
+
await self._get_request_with_logging_and_errors_raised(
|
176
|
+
url=url, headers=headers
|
177
|
+
)
|
178
|
+
)
|
179
|
+
response_json = await response.json()
|
180
|
+
return response_json["devices"]
|
181
|
+
|
182
|
+
@request_with_active_session
|
183
|
+
async def device_info(self, device_name, device_uuid) -> dict[str, Any]:
|
184
|
+
_LOGGER.debug("device_info")
|
185
|
+
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/{device_name}/r/initial"
|
186
|
+
headers = {
|
187
|
+
"Authorization": f"Bearer {await self.get_access_token()}",
|
188
|
+
}
|
189
|
+
json_body = {
|
190
|
+
"deviceconfigquery": [
|
191
|
+
{
|
192
|
+
"id": device_uuid,
|
193
|
+
"r": {
|
194
|
+
"r": [
|
195
|
+
"sensors",
|
196
|
+
],
|
197
|
+
},
|
198
|
+
},
|
199
|
+
],
|
200
|
+
"includestates": True
|
201
|
+
}
|
202
|
+
response: ClientResponse = (
|
203
|
+
await self._post_request_with_logging_and_errors_raised(
|
204
|
+
url=url, headers=headers, json_body=json_body
|
205
|
+
)
|
206
|
+
)
|
207
|
+
response_json = await response.json()
|
208
|
+
return response_json["deviceInfo"][0]
|
209
|
+
|
210
|
+
@request_with_active_session
|
211
|
+
async def set_device_info(
|
212
|
+
self, device_uuid, service_name, action_verb, action_value
|
213
|
+
) -> bool:
|
214
|
+
_LOGGER.debug("set_device_info")
|
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}"
|
216
|
+
headers = {
|
217
|
+
"Authorization": f"Bearer {await self.get_access_token()}",
|
218
|
+
}
|
219
|
+
json_body = {"n": service_name, action_verb: action_value}
|
220
|
+
response: ClientResponse = (
|
221
|
+
await self._post_request_with_logging_and_errors_raised(
|
222
|
+
url=url, headers=headers, json_body=json_body
|
223
|
+
)
|
224
|
+
)
|
225
|
+
response_text = await response.text()
|
226
|
+
return response_text == "Success"
|
@@ -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
|
|
{blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/intermediate_representation_aws.py
RENAMED
@@ -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
|
@@ -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("."):
|
@@ -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,
|
@@ -1,227 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
|
3
|
-
from aiohttp import ClientSession, ClientResponse, FormData
|
4
|
-
from .const import AWS_APIKEYS
|
5
|
-
|
6
|
-
from .util_http import request_with_logging
|
7
|
-
from .errors import SessionError, LoginError
|
8
|
-
|
9
|
-
_LOGGER = logging.getLogger(__name__)
|
10
|
-
|
11
|
-
|
12
|
-
def request_with_active_session(func):
|
13
|
-
async def request_with_active_session_wrapper(*args, **kwargs):
|
14
|
-
_LOGGER.debug("session")
|
15
|
-
try:
|
16
|
-
return await func(*args, **kwargs)
|
17
|
-
except SessionError:
|
18
|
-
_LOGGER.debug("got invalid session, attempting to repair and resend")
|
19
|
-
self = args[0]
|
20
|
-
self.session_token = None
|
21
|
-
self.session_secret = None
|
22
|
-
self.access_token = None
|
23
|
-
self.jwt = None
|
24
|
-
response = await func(*args, **kwargs)
|
25
|
-
return response
|
26
|
-
|
27
|
-
return request_with_active_session_wrapper
|
28
|
-
|
29
|
-
|
30
|
-
def request_with_errors(func):
|
31
|
-
async def request_with_errors_wrapper(*args, **kwargs):
|
32
|
-
_LOGGER.debug("checking for errors")
|
33
|
-
response: ClientResponse = await func(*args, **kwargs)
|
34
|
-
status_code = response.status
|
35
|
-
try:
|
36
|
-
response_json = await response.json(content_type=None)
|
37
|
-
if "statusCode" in response_json:
|
38
|
-
_LOGGER.debug("response json found, checking status code from response")
|
39
|
-
status_code = response_json["statusCode"]
|
40
|
-
except Exception as e:
|
41
|
-
_LOGGER.error(f"Error parsing response for errors {e}")
|
42
|
-
raise e
|
43
|
-
if status_code == 200:
|
44
|
-
_LOGGER.debug("response 200")
|
45
|
-
return response
|
46
|
-
if 400 <= status_code <= 500:
|
47
|
-
_LOGGER.debug("auth error")
|
48
|
-
url = kwargs["url"]
|
49
|
-
response_text = await response.text()
|
50
|
-
if "accounts.login" in url:
|
51
|
-
_LOGGER.debug("login error")
|
52
|
-
raise LoginError(response_text)
|
53
|
-
else:
|
54
|
-
_LOGGER.debug("session error")
|
55
|
-
raise SessionError(response_text)
|
56
|
-
|
57
|
-
return request_with_errors_wrapper
|
58
|
-
|
59
|
-
|
60
|
-
class HttpAwsBlueair:
|
61
|
-
def __init__(
|
62
|
-
self,
|
63
|
-
username: str,
|
64
|
-
password: str,
|
65
|
-
region: str = "us",
|
66
|
-
client_session: ClientSession = None,
|
67
|
-
):
|
68
|
-
self.username = username
|
69
|
-
self.password = password
|
70
|
-
self.region = region
|
71
|
-
|
72
|
-
self.session_token = None
|
73
|
-
self.session_secret = None
|
74
|
-
|
75
|
-
self.access_token = None
|
76
|
-
|
77
|
-
self.jwt = None
|
78
|
-
|
79
|
-
if client_session is None:
|
80
|
-
self.api_session = ClientSession(raise_for_status=False)
|
81
|
-
else:
|
82
|
-
self.api_session = client_session
|
83
|
-
|
84
|
-
async def cleanup_client_session(self):
|
85
|
-
await self.api_session.close()
|
86
|
-
|
87
|
-
@request_with_errors
|
88
|
-
@request_with_logging
|
89
|
-
async def _get_request_with_logging_and_errors_raised(
|
90
|
-
self, url: str, headers: dict = None
|
91
|
-
) -> ClientResponse:
|
92
|
-
return await self.api_session.get(url=url, headers=headers)
|
93
|
-
|
94
|
-
@request_with_errors
|
95
|
-
@request_with_logging
|
96
|
-
async def _post_request_with_logging_and_errors_raised(
|
97
|
-
self,
|
98
|
-
url: str,
|
99
|
-
json_body: dict = None,
|
100
|
-
form_data: FormData = None,
|
101
|
-
headers: dict = None,
|
102
|
-
) -> ClientResponse:
|
103
|
-
return await self.api_session.post(
|
104
|
-
url=url, data=form_data, json=json_body, headers=headers
|
105
|
-
)
|
106
|
-
|
107
|
-
async def refresh_session(self):
|
108
|
-
_LOGGER.debug("refresh_session")
|
109
|
-
url = f"https://accounts.{AWS_APIKEYS[self.region]['gigyaRegion']}.gigya.com/accounts.login"
|
110
|
-
form_data = FormData()
|
111
|
-
form_data.add_field("apikey", AWS_APIKEYS[self.region]["apiKey"])
|
112
|
-
form_data.add_field("loginID", self.username)
|
113
|
-
form_data.add_field("password", self.password)
|
114
|
-
form_data.add_field("targetEnv", "mobile")
|
115
|
-
response: ClientResponse = (
|
116
|
-
await self._post_request_with_logging_and_errors_raised(
|
117
|
-
url=url, form_data=form_data
|
118
|
-
)
|
119
|
-
)
|
120
|
-
response_json = await response.json(content_type="text/javascript")
|
121
|
-
self.session_token = response_json["sessionInfo"]["sessionToken"]
|
122
|
-
self.session_secret = response_json["sessionInfo"]["sessionSecret"]
|
123
|
-
|
124
|
-
async def refresh_jwt(self):
|
125
|
-
_LOGGER.debug("refresh_jwt")
|
126
|
-
if self.session_token is None or self.session_secret is None:
|
127
|
-
await self.refresh_session()
|
128
|
-
url = f"https://accounts.{AWS_APIKEYS[self.region]['gigyaRegion']}.gigya.com/accounts.getJWT"
|
129
|
-
form_data = FormData()
|
130
|
-
form_data.add_field("oauth_token", self.session_token)
|
131
|
-
form_data.add_field("secret", self.session_secret)
|
132
|
-
form_data.add_field("targetEnv", "mobile")
|
133
|
-
response: ClientResponse = (
|
134
|
-
await self._post_request_with_logging_and_errors_raised(
|
135
|
-
url=url, form_data=form_data
|
136
|
-
)
|
137
|
-
)
|
138
|
-
response_json = await response.json(content_type="text/javascript")
|
139
|
-
self.jwt = response_json["id_token"]
|
140
|
-
|
141
|
-
async def refresh_access_token(self):
|
142
|
-
_LOGGER.debug("refresh_access_token")
|
143
|
-
if self.jwt is None:
|
144
|
-
await self.refresh_jwt()
|
145
|
-
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/login"
|
146
|
-
headers = {"idtoken": self.jwt}
|
147
|
-
response: ClientResponse = (
|
148
|
-
await self._post_request_with_logging_and_errors_raised(
|
149
|
-
url=url, headers=headers
|
150
|
-
)
|
151
|
-
)
|
152
|
-
response_json = await response.json()
|
153
|
-
self.access_token = response_json["access_token"]
|
154
|
-
|
155
|
-
async def get_access_token(self):
|
156
|
-
_LOGGER.debug("get_access_token")
|
157
|
-
if self.access_token is None:
|
158
|
-
await self.refresh_access_token()
|
159
|
-
return self.access_token
|
160
|
-
|
161
|
-
@request_with_active_session
|
162
|
-
async def devices(self):
|
163
|
-
_LOGGER.debug("devices")
|
164
|
-
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/registered-devices"
|
165
|
-
headers = {
|
166
|
-
"Authorization": f"Bearer {await self.get_access_token()}",
|
167
|
-
}
|
168
|
-
response: ClientResponse = (
|
169
|
-
await self._get_request_with_logging_and_errors_raised(
|
170
|
-
url=url, headers=headers
|
171
|
-
)
|
172
|
-
)
|
173
|
-
response_json = await response.json()
|
174
|
-
return response_json["devices"]
|
175
|
-
|
176
|
-
@request_with_active_session
|
177
|
-
async def device_info(self, device_name, device_uuid):
|
178
|
-
_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
|
-
url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/{device_name}/r/initial"
|
187
|
-
headers = {
|
188
|
-
"Authorization": f"Bearer {await self.get_access_token()}",
|
189
|
-
}
|
190
|
-
json_body = {
|
191
|
-
"deviceconfigquery": [
|
192
|
-
{
|
193
|
-
"id": device_uuid,
|
194
|
-
"r": {
|
195
|
-
"r": [
|
196
|
-
"sensors",
|
197
|
-
],
|
198
|
-
},
|
199
|
-
},
|
200
|
-
],
|
201
|
-
"includestates": True
|
202
|
-
}
|
203
|
-
response: ClientResponse = (
|
204
|
-
await self._post_request_with_logging_and_errors_raised(
|
205
|
-
url=url, headers=headers, json_body=json_body
|
206
|
-
)
|
207
|
-
)
|
208
|
-
response_json = await response.json()
|
209
|
-
return response_json["deviceInfo"][0]
|
210
|
-
|
211
|
-
@request_with_active_session
|
212
|
-
async def set_device_info(
|
213
|
-
self, device_uuid, service_name, action_verb, action_value
|
214
|
-
):
|
215
|
-
_LOGGER.debug("set_device_info")
|
216
|
-
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
|
-
headers = {
|
218
|
-
"Authorization": f"Bearer {await self.get_access_token()}",
|
219
|
-
}
|
220
|
-
json_body = {"n": service_name, action_verb: action_value}
|
221
|
-
response: ClientResponse = (
|
222
|
-
await self._post_request_with_logging_and_errors_raised(
|
223
|
-
url=url, headers=headers, json_body=json_body
|
224
|
-
)
|
225
|
-
)
|
226
|
-
response_text = await response.text()
|
227
|
-
return response_text == "Success"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|