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.
Files changed (27) hide show
  1. {blueair_api-1.26.2 → blueair_api-1.26.3}/PKG-INFO +1 -1
  2. {blueair_api-1.26.2 → blueair_api-1.26.3}/pyproject.toml +1 -1
  3. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/callbacks.py +1 -1
  4. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/device.py +1 -1
  5. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/device_aws.py +1 -1
  6. blueair_api-1.26.3/src/blueair_api/http_aws_blueair.py +226 -0
  7. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/http_blueair.py +12 -10
  8. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/intermediate_representation_aws.py +40 -33
  9. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/util.py +2 -12
  10. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/util_bootstrap.py +7 -6
  11. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api.egg-info/PKG-INFO +1 -1
  12. blueair_api-1.26.2/src/blueair_api/http_aws_blueair.py +0 -227
  13. {blueair_api-1.26.2 → blueair_api-1.26.3}/LICENSE +0 -0
  14. {blueair_api-1.26.2 → blueair_api-1.26.3}/README.md +0 -0
  15. {blueair_api-1.26.2 → blueair_api-1.26.3}/setup.cfg +0 -0
  16. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/__init__.py +0 -0
  17. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/const.py +0 -0
  18. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/errors.py +0 -0
  19. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/model_enum.py +0 -0
  20. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/stub.py +0 -0
  21. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api/util_http.py +0 -0
  22. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api.egg-info/SOURCES.txt +0 -0
  23. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api.egg-info/dependency_links.txt +0 -0
  24. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api.egg-info/requires.txt +0 -0
  25. {blueair_api-1.26.2 → blueair_api-1.26.3}/src/blueair_api.egg-info/top_level.txt +0 -0
  26. {blueair_api-1.26.2 → blueair_api-1.26.3}/tests/test_device_aws.py +0 -0
  27. {blueair_api-1.26.2 → blueair_api-1.26.3}/tests/test_intermediate_representation_aws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: blueair_api
3
- Version: 1.26.2
3
+ Version: 1.26.3
4
4
  Summary: Blueair Api Wrapper
5
5
  Author-email: Brendan Dahl <dahl.brendan@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/dahlb/blueair_api
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
8
8
 
9
9
  [project]
10
10
  name = "blueair_api"
11
- version = "1.26.2"
11
+ version = "1.26.3"
12
12
  authors = [
13
13
  { name="Brendan Dahl", email="dahl.brendan@gmail.com" },
14
14
  ]
@@ -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.name} publishing updates")
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 = bool(attributes["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 | type[NotImplemented]
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, any]]:
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, any]:
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, any]:
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, any]:
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, any]:
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[:-1]):
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
- else:
23
- try:
19
+ elif isinstance(value, dict):
20
+ if seg in value:
24
21
  value = value[seg]
25
- except KeyError:
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
- f"available keys are {value.keys()}.")
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
- a = dict(value) # make a copy.
45
- kwargs = {}
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
- continue
49
- if field.default is dataclasses.MISSING:
50
- kwargs[field.name] = a.pop(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] = a.pop(field.name, field.default)
56
+ kwargs[field.name] = extra_fields.pop(field.name, field.default)
53
57
 
54
- result[key] = kls(**kwargs, extra_fields=a)
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: ScalarType
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 = 0
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, ScalarType]:
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
- elif record.timestamp is None:
175
+ continue
176
+ lt = latest[record.name].timestamp
177
+ if record.timestamp is None:
171
178
  latest[rn] = record
172
- elif latest[record.name].timestamp is None:
179
+ elif lt is None:
173
180
  latest[rn] = record
174
- elif latest[record.name].timestamp < record.timestamp:
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, any]) -> dict[str, any]:
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
- ) -> (HttpBlueair, list[Device]):
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
- ) -> (HttpAwsBlueair, list[Device]):
47
+ client_session: ClientSession | None = None,
48
+ ) -> tuple[HttpAwsBlueair, list[Device]]:
48
49
  api = HttpAwsBlueair(
49
50
  username=username,
50
51
  password=password,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: blueair_api
3
- Version: 1.26.2
3
+ Version: 1.26.3
4
4
  Summary: Blueair Api Wrapper
5
5
  Author-email: Brendan Dahl <dahl.brendan@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/dahlb/blueair_api
@@ -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