blueair-api 1.26.2__py3-none-any.whl → 1.26.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
blueair_api/callbacks.py CHANGED
@@ -22,6 +22,6 @@ class CallbacksMixin:
22
22
  def publish_updates(self) -> None:
23
23
  if not hasattr(self, "_callbacks"):
24
24
  self._setup_callbacks()
25
- _LOGGER.debug(f"{self.name} publishing updates")
25
+ _LOGGER.debug(f"{id(self)} publishing updates")
26
26
  for callback in self._callbacks:
27
27
  callback()
blueair_api/device.py CHANGED
@@ -70,7 +70,7 @@ class Device(CallbacksMixin):
70
70
  else:
71
71
  self.brightness = 0
72
72
  if "child_lock" in attributes:
73
- self.child_lock = 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"])
blueair_api/device_aws.py CHANGED
@@ -9,7 +9,7 @@ from . import intermediate_representation_aws as ir
9
9
 
10
10
  _LOGGER = logging.getLogger(__name__)
11
11
 
12
- type AttributeType[T] = T | None | type[NotImplemented]
12
+ type AttributeType[T] = T | None
13
13
 
14
14
  @dataclasses.dataclass(slots=True)
15
15
  class DeviceAws(CallbacksMixin):
@@ -1,3 +1,5 @@
1
+ from typing import Any
2
+ import functools
1
3
  import logging
2
4
 
3
5
  from aiohttp import ClientSession, ClientResponse, FormData
@@ -10,7 +12,8 @@ _LOGGER = logging.getLogger(__name__)
10
12
 
11
13
 
12
14
  def request_with_active_session(func):
13
- async def request_with_active_session_wrapper(*args, **kwargs):
15
+ @functools.wraps(func)
16
+ async def request_with_active_session_wrapper(*args, **kwargs) -> ClientResponse:
14
17
  _LOGGER.debug("session")
15
18
  try:
16
19
  return await func(*args, **kwargs)
@@ -28,7 +31,8 @@ def request_with_active_session(func):
28
31
 
29
32
 
30
33
  def request_with_errors(func):
31
- async def request_with_errors_wrapper(*args, **kwargs):
34
+ @functools.wraps(func)
35
+ async def request_with_errors_wrapper(*args, **kwargs) -> ClientResponse:
32
36
  _LOGGER.debug("checking for errors")
33
37
  response: ClientResponse = await func(*args, **kwargs)
34
38
  status_code = response.status
@@ -53,6 +57,7 @@ def request_with_errors(func):
53
57
  else:
54
58
  _LOGGER.debug("session error")
55
59
  raise SessionError(response_text)
60
+ raise ValueError(f"unknown status code {status_code}")
56
61
 
57
62
  return request_with_errors_wrapper
58
63
 
@@ -63,7 +68,7 @@ class HttpAwsBlueair:
63
68
  username: str,
64
69
  password: str,
65
70
  region: str = "us",
66
- client_session: ClientSession = None,
71
+ client_session: ClientSession | None = None,
67
72
  ):
68
73
  self.username = username
69
74
  self.password = password
@@ -87,7 +92,7 @@ class HttpAwsBlueair:
87
92
  @request_with_errors
88
93
  @request_with_logging
89
94
  async def _get_request_with_logging_and_errors_raised(
90
- self, url: str, headers: dict = None
95
+ self, url: str, headers: dict | None = None
91
96
  ) -> ClientResponse:
92
97
  return await self.api_session.get(url=url, headers=headers)
93
98
 
@@ -96,15 +101,15 @@ class HttpAwsBlueair:
96
101
  async def _post_request_with_logging_and_errors_raised(
97
102
  self,
98
103
  url: str,
99
- json_body: dict = None,
100
- form_data: FormData = None,
101
- headers: dict = None,
104
+ json_body: dict | None = None,
105
+ form_data: FormData | None = None,
106
+ headers: dict | None = None,
102
107
  ) -> ClientResponse:
103
108
  return await self.api_session.post(
104
109
  url=url, data=form_data, json=json_body, headers=headers
105
110
  )
106
111
 
107
- async def refresh_session(self):
112
+ async def refresh_session(self) -> None:
108
113
  _LOGGER.debug("refresh_session")
109
114
  url = f"https://accounts.{AWS_APIKEYS[self.region]['gigyaRegion']}.gigya.com/accounts.login"
110
115
  form_data = FormData()
@@ -121,7 +126,7 @@ class HttpAwsBlueair:
121
126
  self.session_token = response_json["sessionInfo"]["sessionToken"]
122
127
  self.session_secret = response_json["sessionInfo"]["sessionSecret"]
123
128
 
124
- async def refresh_jwt(self):
129
+ async def refresh_jwt(self) -> None:
125
130
  _LOGGER.debug("refresh_jwt")
126
131
  if self.session_token is None or self.session_secret is None:
127
132
  await self.refresh_session()
@@ -138,7 +143,7 @@ class HttpAwsBlueair:
138
143
  response_json = await response.json(content_type="text/javascript")
139
144
  self.jwt = response_json["id_token"]
140
145
 
141
- async def refresh_access_token(self):
146
+ async def refresh_access_token(self) -> None:
142
147
  _LOGGER.debug("refresh_access_token")
143
148
  if self.jwt is None:
144
149
  await self.refresh_jwt()
@@ -152,14 +157,15 @@ class HttpAwsBlueair:
152
157
  response_json = await response.json()
153
158
  self.access_token = response_json["access_token"]
154
159
 
155
- async def get_access_token(self):
160
+ async def get_access_token(self) -> str:
156
161
  _LOGGER.debug("get_access_token")
157
162
  if self.access_token is None:
158
163
  await self.refresh_access_token()
164
+ assert self.access_token is not None
159
165
  return self.access_token
160
166
 
161
167
  @request_with_active_session
162
- async def devices(self):
168
+ async def devices(self) -> dict[str, Any]:
163
169
  _LOGGER.debug("devices")
164
170
  url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/registered-devices"
165
171
  headers = {
@@ -174,15 +180,8 @@ class HttpAwsBlueair:
174
180
  return response_json["devices"]
175
181
 
176
182
  @request_with_active_session
177
- async def device_info(self, device_name, device_uuid):
183
+ async def device_info(self, device_name, device_uuid) -> dict[str, Any]:
178
184
  _LOGGER.debug("device_info")
179
- """
180
- sample; [{'id': '1b41a7c6-8f02-42fa-9af8-868dad9be98a', 'configuration': {'df': {'a': 877, 'ot': 'G4Filter', 'alg': 'FilterAlg1'}, 'di': {'cfv': '2.1.1', 'cma': '3c:61:05:45:56:98', 'mt': '2', 'name': 'sage', 'sku': '105826', 'mfv': '1.0.12', 'ofv': '2.1.1', 'hw': 'high_1.5', 'ds': '110582600000110110016855'}, '_ot': 'CmConfig', '_f': False, '_it': 'urn:blueair:openapi:version:healthprotect:0.0.5', '_eid': 'f8cb142f-c77d-11eb-b045-c98ff4f5a769', '_sc': 'Instance', 'ds': {'tVOC': {'tf': 'senml+json', 'ot': 'TVOC', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/tVOC', 'ttl': 0, 'n': 'tVOC', 'fe': True}, 'fu0': {'tf': 'senml+json', 'ot': 'FU', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/fu0', 'ttl': -1, 'n': 'fu0', 'fe': True}, 'ledb': {'tf': 'senml+json', 'ot': 'Led', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/ledb', 'ttl': 0, 'n': 'ledb', 'fe': True}, 'co2': {'tf': 'senml+json', 'ot': 'Co2', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/co2', 'ttl': 0, 'n': 'co2', 'fe': True}, 'pm2_5c': {'tf': 'senml+json', 'ot': 'PMParticleCount', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/pm2_5c', 'ttl': 0, 'n': 'pm2_5c', 'fe': True}, 'sb': {'tf': 'senml+json', 'ot': 'Standby', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/sb', 'ttl': -1, 'n': 'sb', 'fe': True}, 'ss0': {'tf': 'senml+json', 'ot': 'SafetySwitch', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/ss0', 'ttl': -1, 'n': 'ss0', 'fe': True}, 'rt1s': {'tf': 'senml+json', 'ot': 'RT1s', 'e': True, 'i': 1000, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/1s', 'sn': ['pm1', 'pm2_5', 'pm10', 't', 'h', 'tVOC', 'rssi'], 'ttl': 0, 'n': 'rt1s', 'fe': True}, 'rt5s': {'tf': 'senml+json', 'ot': 'RT5s', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/5s', 'sn': ['pm1', 'pm2_5', 'pm10', 't', 'h', 'tVOC', 'rssi'], 'ttl': -1, 'n': 'rt5s', 'fe': True}, 'pm1': {'tf': 'senml+json', 'ot': 'PM1', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/pm1', 'ttl': 0, 'n': 'pm1', 'fe': True}, 'pm2_5': {'tf': 'senml+json', 'ot': 'PM2_5', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/pm2_5', 'ttl': 0, 'n': 'pm2_5', 'fe': True}, 'pm10c': {'tf': 'senml+json', 'ot': 'PMParticleCount', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/pm10c', 'ttl': 0, 'n': 'pm10c', 'fe': True}, 'sp': {'tf': 'senml+json', 'ot': 'SensorBoxPresense', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/sp', 'ttl': -1, 'n': 'sp', 'fe': True}, 'rssi': {'tf': 'senml+json', 'ot': 'RSSI', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/rssi', 'ttl': 0, 'n': 'rssi', 'fe': True}, 'chl': {'tf': 'senml+json', 'ot': 'ChildLock', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/chl', 'ttl': -1, 'n': 'chl', 'fe': True}, 'fp0': {'tf': 'senml+json', 'ot': 'FilterPresence', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/fp0', 'ttl': -1, 'n': 'fp0', 'fe': True}, 'pm10': {'tf': 'senml+json', 'ot': 'PM10', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/pm10', 'ttl': 0, 'n': 'pm10', 'fe': True}, 'h': {'tf': 'senml+json', 'ot': 'Humidity', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/h', 'ttl': 0, 'n': 'h', 'fe': True}, 'is': {'tf': 'senml+json', 'ot': 'IonizerState', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/is', 'ttl': -1, 'n': 'is', 'fe': True}, 'gs': {'tf': 'senml+json', 'ot': 'GermShield', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/gs', 'ttl': -1, 'n': 'gs', 'fe': True}, 'am': {'tf': 'senml+json', 'ot': 'AutoMode', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/am', 'ttl': -1, 'n': 'am', 'fe': True}, 'tVOCbaseline': {'t': 0, 'e': False, 'ot': 'TVOCbaseline', 'i': 60000, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/TVOCbaseline', 'ttl': 600, 'n': 'tVOCbaseline', 'fe': True}, 'rt5m': {'tf': 'senml+json', 'ot': 'RT5m', 'e': True, 'i': 300000, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/5m', 'sn': ['pm1', 'pm2_5', 'pm10', 't', 'h', 'tVOC'], 'ttl': 0, 'n': 'rt5m', 'fe': True}, 't': {'tf': 'senml+json', 'ot': 'Temperature', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/t', 'ttl': 0, 'n': 't', 'fe': True}, 'pm1c': {'tf': 'senml+json', 'ot': 'PMParticleCount', 'e': False, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/pm1c', 'ttl': 0, 'n': 'pm1c', 'fe': True}, 'b5m': {'tf': 'senml+json', 'th': ['5kb', '4h'], 'ot': 'Batch5m', 'e': True, 'i': 300000, 'tn': '$aws/rules/telemetry_ingest_rule/d/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/batch/b5m', 'sn': ['pm1', 'pm2_5', 'pm10', 't', 'h', 'tVOC', 'fsp0'], 'ttl': -1, 'n': 'b5m', 'fe': True}, 'fsp0': {'st': 'fsp0', 'tf': 'senml+json', 't': 10, 'ot': 'Fanspeed', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/fsp0', 'sn': ['fsp0'], 'ttl': -1, 'n': 'fsp0', 'fe': True}, 'nm': {'tf': 'senml+json', 'ot': 'NightMode', 'e': True, 'i': 0, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/s/nm', 'ttl': -1, 'n': 'nm', 'fe': True}}, '_r': 'us-east-2', '_s': {'sig': 'f812cd40acae289ad965c159051abadb28ed6c8b1eb2df917fedaa135f5a69d2', 'salg': 'SHA256'}, '_t': 'Diff', '_v': 1655924801, '_cas': 1623062101, '_id': '1b41a7c6-8f02-42fa-9af8-868dad9be98a', 'fc': {'pwd': '9102revreSyrotcaF', 'ssid': 'BAFactory', 'url': ' '}, 'da': {'reboot': {'p': False, 'a': False, 'ot': 'Reboot', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/reboot', 'n': 'reboot', 'fe': True}, 'uitest': {'a': 'off', 'ot': 'UiTest', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/uitest', 'n': 'uitest', 'fe': True}, 'ledb': {'p': True, 'a': 0, 'tf': 'senml+json', 'ot': 'LedBrightness', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/ledb', 'n': 'ledb', 'fe': True}, 'chl': {'p': True, 'a': False, 'tf': 'senml+json', 'ot': 'ChildLock', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/chl', 'n': 'chl', 'fe': True}, 'sflu': {'p': False, 'a': False, 'ot': 'SensorFlush', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/sflu', 'n': 'sflu', 'fe': True}, 'buttontest': {'p': False, 'a': False, 'ot': 'ButtonTest', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/buttontest', 'n': 'buttontest', 'fe': True}, 'is': {'p': True, 'a': False, 'tf': 'senml+json', 'ot': 'IonizerState', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/is', 'n': 'is', 'fe': True}, 'gs': {'p': True, 'a': False, 'tf': 'senml+json', 'ot': 'GermShield', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/gs', 'n': 'gs', 'fe': True}, 'am': {'p': True, 'a': False, 'tf': 'senml+json', 'ot': 'AutoMode', 'e': True, 'amt': 'PM', 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/am', 'n': 'am', 'fe': True}, 'tVOCbaseline': {'a': 0, 'ot': 'TVOCbaseline', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/tVOCbaseline', 'n': 'tVOCbaseline', 'fe': True}, 'sb': {'p': True, 'a': False, 'tf': 'senml+json', 'sbt': 'sensor_on', 'ot': 'Standby', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/sb', 'n': 'sb', 'fe': True}, 'fsp0': {'p': True, 'st': 'fsp0', 'a': 20, 'tf': 'senml+json', 'ot': 'Fanspeed', 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/fsp0', 'n': 'fsp0', 'fe': True}, 'nm': {'p': True, 'maxfsp': 25, 'a': False, 'tf': 'senml+json', 'nmt': 'PM', 'ot': 'NightMode', 'ledb': 12, 'e': True, 'tn': 'd/1b41a7c6-8f02-42fa-9af8-868dad9be98a/a/nm', 'n': 'nm', 'fe': True}}, 'dc': {'cfv': {'d': 'di.cfv', 't': 'integer', 'v': 0, 'n': 'cfv'}, 'germshield': {'a': 'gs', 's': 'gs', 't': 'boolean', 'v': False, 'n': 'germshield'}, 'filterusage': {'s': 'fu0', 't': 'integer', 'v': 0, 'n': 'filterusage'}, 'brightness': {'a': 'ledb', 's': 'ledb', 't': 'integer', 'v': 100, 'n': 'brightness'}, 'standby': {'a': 'sb', 's': 'sb', 't': 'boolean', 'v': False, 'n': 'standby'}, 'fanspeed': {'a': 'fsp0', 's': 'fsp0', 't': 'integer', 'v': 11, 'n': 'fanspeed'}, 'nightmode': {'a': 'nm', 's': 'nm', 't': 'boolean', 'v': False, 'n': 'nightmode'}, 'childlock': {'a': 'chl', 's': 'chl', 't': 'boolean', 'v': False, 'n': 'childlock'}, 'safetyswitch': {'s': 'ss0', 't': 'boolean', 'v': True, 'n': 'safetyswitch'}, 'mfv': {'d': 'di.mfv', 't': 'integer', 'v': 0, 'n': 'mfv'}, 'ofv': {'d': 'di.ofv', 't': 'integer', 'v': 0, 'n': 'ofv'}, 'automode': {'a': 'am', 's': 'am', 't': 'boolean', 'v': False, 'n': 'automode'}}}, 'alarms': [], 'events': [], 'sensordata': [{'v': '0', 'n': 'pm1', 't': 1656198091}, {'v': '0', 'n': 'pm2_5', 't': 1656198091}, {'v': '0', 'n': 'pm10', 't': 1656198091}, {'v': '23', 'n': 't', 't': 1656198091}, {'v': '45', 'n': 'h', 't': 1656198091}, {'v': '170', 'n': 'tVOC', 't': 1656198091}, {'v': '11', 'n': 'fsp0', 't': 1656198091}], 'states': [{'n': 'cfv', 'v': 33554689, 't': 1656203510}, {'n': 'germshield', 'vb': True, 't': 1656203510}, {'n': 'filterusage', 'v': 1, 't': 1656203510}, {'n': 'brightness', 'v': 0, 't': 1656203510}, {'n': 'standby', 'vb': False, 't': 1656203510}, {'n': 'fanspeed', 'v': 11, 't': 1656203510}, {'n': 'nightmode', 'vb': False, 't': 1656203510}, {'n': 'childlock', 'vb': False, 't': 1656203510}, {'n': 'safetyswitch', 'vb': True, 't': 1656203510}, {'n': 'mfv', 'v': 16777228, 't': 1656203510}, {'n': 'ofv', 'v': 33554689, 't': 1656203510}, {'n': 'automode', 'vb': False, 't': 1656203510}, {'t': 1656203510, 'vb': True, 'n': 'online'}], 'welcomehome': {'setting': []}, 'fleet_info': []}]
181
-
182
- :param device_name:
183
- :param device_uuid:
184
- :return:
185
- """
186
185
  url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/{device_name}/r/initial"
187
186
  headers = {
188
187
  "Authorization": f"Bearer {await self.get_access_token()}",
@@ -211,7 +210,7 @@ class HttpAwsBlueair:
211
210
  @request_with_active_session
212
211
  async def set_device_info(
213
212
  self, device_uuid, service_name, action_verb, action_value
214
- ):
213
+ ) -> bool:
215
214
  _LOGGER.debug("set_device_info")
216
215
  url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/{device_uuid}/a/{service_name}"
217
216
  headers = {
@@ -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
blueair_api/util.py CHANGED
@@ -1,3 +1,4 @@
1
+ from typing import Any
1
2
  import logging
2
3
 
3
4
  from .const import SENSITIVE_FIELD_NAMES
@@ -5,7 +6,7 @@ from .const import SENSITIVE_FIELD_NAMES
5
6
  _LOGGER = logging.getLogger(__name__)
6
7
 
7
8
 
8
- def clean_dictionary_for_logging(dictionary: dict[str, 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
@@ -0,0 +1,19 @@
1
+ blueair_api/__init__.py,sha256=GucsIENhTF4AVxPn4Xyr4imUxJJ8RO8RYt1opHCF2hQ,326
2
+ blueair_api/callbacks.py,sha256=fvrJsqH5eDRxWOGWiZkF2uLU4n2ve0zzU17ERqWbHP8,756
3
+ blueair_api/const.py,sha256=q1smSEhwyYvuQiR867lToFm-mGV-d3dNJvN0NJgscbU,1037
4
+ blueair_api/device.py,sha256=t2KrTsdPH9FJZqCcLSm_3eQGHtbNuVcwK3OyzgCpwJ0,3835
5
+ blueair_api/device_aws.py,sha256=FYz1YtJQMTUfzZ-__kdOGe5HtO8MTUygX-XdRsYejWg,7356
6
+ blueair_api/errors.py,sha256=lJ_iFU_W6zQfGRi_wsMhWDw-fAVPFeCkCbT1erIlYQQ,233
7
+ blueair_api/http_aws_blueair.py,sha256=jztGyoH0iC7aCJ2oGf9hnEeHFOie3YikFvwtWo3W2w4,8536
8
+ blueair_api/http_blueair.py,sha256=cTtiHNTlZKFQTh7cvfRqA_P1lGEpvMXvjM5o6_qEN2U,9497
9
+ blueair_api/intermediate_representation_aws.py,sha256=DJWxHFP9yVll0O6kxHWjKscIlu-7genc3f7ZvwV5YdA,6137
10
+ blueair_api/model_enum.py,sha256=Z9Ne4icNEjbGNwdHJZSDibcKJKwv-W1BRpZx01RGFuY,2480
11
+ blueair_api/stub.py,sha256=sTWyRSDObzrXpZToAgDmZhCk3q8SsGN35h-kzMOqSOc,1272
12
+ blueair_api/util.py,sha256=7MrB2DLqUVOlBQuhv7bRmUXvcGcsjXiV3M0H3LloiOo,1962
13
+ blueair_api/util_bootstrap.py,sha256=RNIKrMWMBSUad4loYGwEVIKVxQ1_LVhXNQtUwuaquyo,1754
14
+ blueair_api/util_http.py,sha256=45AJG3Vb6LMVzI0WV22AoSyt64f_Jj3KpOAwF5M6EFE,1327
15
+ blueair_api-1.26.3.dist-info/LICENSE,sha256=W6UV41yCe1R_Avet8VtsxwdJar18n40b3MRnbEMHZmI,1066
16
+ blueair_api-1.26.3.dist-info/METADATA,sha256=fUmIeTOXwfjXBWUh5sN24dhgek6fhOK-jylC-6izNAg,1995
17
+ blueair_api-1.26.3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
18
+ blueair_api-1.26.3.dist-info/top_level.txt,sha256=-gn0jNtmE83qEu70uMW5F4JrXnHGRfuFup1EPWF70oc,12
19
+ blueair_api-1.26.3.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- blueair_api/__init__.py,sha256=GucsIENhTF4AVxPn4Xyr4imUxJJ8RO8RYt1opHCF2hQ,326
2
- blueair_api/callbacks.py,sha256=eqgfe1qN_NNWOp2qNSP123zQJxh_Ie6vZXp5EHp6ePc,757
3
- blueair_api/const.py,sha256=q1smSEhwyYvuQiR867lToFm-mGV-d3dNJvN0NJgscbU,1037
4
- blueair_api/device.py,sha256=Ey8FIDnRwyVWSdLqfHxlCQSOFFaI_l_oVfvqpPIoTvI,3834
5
- blueair_api/device_aws.py,sha256=c8dZQ9rbljeYBjzXelcnhwQWIJ0dw3JACLFRzxD7s38,7379
6
- blueair_api/errors.py,sha256=lJ_iFU_W6zQfGRi_wsMhWDw-fAVPFeCkCbT1erIlYQQ,233
7
- blueair_api/http_aws_blueair.py,sha256=m_qoCFOYICCu_U_maBvkmOha3YmNtxxtPYyapVBGKNc,17821
8
- blueair_api/http_blueair.py,sha256=n9F5fvEROIyAkqDMM22l84PB7ZoeEkWbj2YuCZpeDNg,9411
9
- blueair_api/intermediate_representation_aws.py,sha256=TteTMuy1UQ2L0vpGF2Te8v_E1Ageg0QTp6ZM_UUZ9dI,5667
10
- blueair_api/model_enum.py,sha256=Z9Ne4icNEjbGNwdHJZSDibcKJKwv-W1BRpZx01RGFuY,2480
11
- blueair_api/stub.py,sha256=sTWyRSDObzrXpZToAgDmZhCk3q8SsGN35h-kzMOqSOc,1272
12
- blueair_api/util.py,sha256=4g8dTlxawBYKslOJS7WCWss0670mtUc53c3L8NiPTsM,2201
13
- blueair_api/util_bootstrap.py,sha256=Vewg7mT1qSRgzOOJDrpXrVhGwcFPRnMrqhJVSIB-0AA,1688
14
- blueair_api/util_http.py,sha256=45AJG3Vb6LMVzI0WV22AoSyt64f_Jj3KpOAwF5M6EFE,1327
15
- blueair_api-1.26.2.dist-info/LICENSE,sha256=W6UV41yCe1R_Avet8VtsxwdJar18n40b3MRnbEMHZmI,1066
16
- blueair_api-1.26.2.dist-info/METADATA,sha256=1hD8KxOs0fsLvrEVOorTCf9_tQkPUDJQIot3OpTaeAU,1995
17
- blueair_api-1.26.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
18
- blueair_api-1.26.2.dist-info/top_level.txt,sha256=-gn0jNtmE83qEu70uMW5F4JrXnHGRfuFup1EPWF70oc,12
19
- blueair_api-1.26.2.dist-info/RECORD,,