blueair-api 1.36.0__tar.gz → 1.36.2__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 (26) hide show
  1. {blueair_api-1.36.0 → blueair_api-1.36.2}/PKG-INFO +1 -1
  2. {blueair_api-1.36.0 → blueair_api-1.36.2}/pyproject.toml +1 -1
  3. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/const.py +16 -4
  4. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/device_aws.py +10 -4
  5. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/http_aws_blueair.py +29 -8
  6. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/intermediate_representation_aws.py +32 -2
  7. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/stub.py +1 -1
  8. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/util_http.py +3 -0
  9. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api.egg-info/PKG-INFO +1 -1
  10. {blueair_api-1.36.0 → blueair_api-1.36.2}/tests/test_device_aws.py +26 -19
  11. {blueair_api-1.36.0 → blueair_api-1.36.2}/tests/test_intermediate_representation_aws.py +68 -1
  12. {blueair_api-1.36.0 → blueair_api-1.36.2}/LICENSE +0 -0
  13. {blueair_api-1.36.0 → blueair_api-1.36.2}/README.md +0 -0
  14. {blueair_api-1.36.0 → blueair_api-1.36.2}/setup.cfg +0 -0
  15. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/__init__.py +0 -0
  16. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/callbacks.py +0 -0
  17. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/device.py +0 -0
  18. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/errors.py +0 -0
  19. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/http_blueair.py +0 -0
  20. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/model_enum.py +0 -0
  21. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/util.py +0 -0
  22. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api/util_bootstrap.py +0 -0
  23. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api.egg-info/SOURCES.txt +0 -0
  24. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api.egg-info/dependency_links.txt +0 -0
  25. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api.egg-info/requires.txt +0 -0
  26. {blueair_api-1.36.0 → blueair_api-1.36.2}/src/blueair_api.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blueair_api
3
- Version: 1.36.0
3
+ Version: 1.36.2
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.36.0"
11
+ version = "1.36.2"
12
12
  authors = [
13
13
  { name="Brendan Dahl", email="dahl.brendan@gmail.com" },
14
14
  ]
@@ -11,17 +11,29 @@ API_KEY = "eyJhbGciOiJIUzI1NiJ9.eyJncmFudGVlIjoiYmx1ZWFpciIsImlhdCI6MTQ1MzEyNTYz
11
11
 
12
12
  AWS_APIKEYS = {
13
13
  "us": {
14
- "gigyaRegion": "us1",
14
+ "gigyaRegion": "accounts.us1.gigya.com",
15
15
  "restApiId": "on1keymlmh",
16
- "awsRegion": "us-east-2",
16
+ "awsRegion": "us-east-2.amazonaws.com",
17
17
  "apiKey": "3_-xUbbrIY8QCbHDWQs1tLXE-CZBQ50SGElcOY5hF1euE11wCoIlNbjMGAFQ6UwhMY",
18
18
  },
19
19
  "eu": {
20
- "gigyaRegion": "eu1",
20
+ "gigyaRegion": "accounts.eu1.gigya.com",
21
21
  "restApiId": "hkgmr8v960",
22
- "awsRegion": "eu-west-1",
22
+ "awsRegion": "eu-west-1.amazonaws.com",
23
23
  "apiKey": "3_qRseYzrUJl1VyxvSJANalu_kNgQ83swB1B9uzgms58--5w1ClVNmrFdsDnWVQQCl",
24
24
  },
25
+ "cn": {
26
+ "gigyaRegion": "accounts.cn1.sapcdm.cn",
27
+ "restApiId": "ftbkyp79si",
28
+ "awsRegion": "cn-north-1.amazonaws.com.cn",
29
+ "apiKey": "3_h3UEfJnA-zDpFPR9L4412HO7Mz2VVeN4wprbWYafPN1gX0kSnLcZ9VSfFi7bEIIU",
30
+ },
31
+ "au": {
32
+ "gigyaRegion": "accounts.au1.gigya.com",
33
+ "restApiId": "hkgmr8v960",
34
+ "awsRegion": "eu-west-1.amazonaws.com",
35
+ "apiKey": "3_Z2N0mIFC6j2fx1z2sq76R3pwkCMaMX2y9btPb0_PgI_3wfjSJoofFnBbxbtuQksN",
36
+ },
25
37
  }
26
38
 
27
39
 
@@ -32,6 +32,7 @@ class DeviceAws(CallbacksMixin):
32
32
 
33
33
  api: HttpAwsBlueair = field(repr=False)
34
34
  raw_info : dict[str, Any] = field(repr=False, init=False)
35
+ raw_sensors : dict[str, Any] = field(repr=False, init=False)
35
36
 
36
37
  uuid : str | None = None
37
38
  name : str | None = None
@@ -81,16 +82,18 @@ class DeviceAws(CallbacksMixin):
81
82
  async def refresh(self):
82
83
  _LOGGER.debug(f"refreshing blueair device aws: {self}")
83
84
  self.raw_info = await self.api.device_info(self.name_api, self.uuid)
85
+ self.raw_sensors = await self.api.device_sensors(self.name_api, self.uuid)
84
86
  _LOGGER.debug(dumps(self.raw_info, indent=2))
87
+ if self.raw_sensors is not None:
88
+ _LOGGER.debug(dumps(self.raw_sensors, indent=2))
85
89
 
86
- # ir.parse_json(ir.Attribute, ir.query_json(info, "configuration.da"))
87
90
  ds = ir.parse_json(ir.Sensor, ir.query_json(self.raw_info, "configuration.ds"))
88
91
  dc = ir.parse_json(ir.Control, ir.query_json(self.raw_info, "configuration.dc"))
89
92
 
90
- sensor_data = ir.SensorPack(self.raw_info["sensordata"]).to_latest_value()
93
+ sensor_data = ir.SensorHistory(self.raw_sensors).to_latest()
91
94
 
92
95
  def sensor_data_safe_get(key):
93
- return sensor_data.get(key) if key in ds else NotImplemented
96
+ return sensor_data.values.get(key) if key in ds else NotImplemented
94
97
 
95
98
  self.pm1 = sensor_data_safe_get("pm1")
96
99
  self.pm2_5 = sensor_data_safe_get("pm2_5")
@@ -142,7 +145,10 @@ class DeviceAws(CallbacksMixin):
142
145
  self.cool_sub_mode = states_safe_get("coolsubmode")
143
146
  self.cool_fan_speed = states_safe_get("coolfs")
144
147
  self.ap_sub_mode = states_safe_get("apsubmode")
145
- self.fan_speed_0 = states_safe_get("fsp0")
148
+ if states_safe_get("fsp0") is NotImplemented:
149
+ self.fan_speed_0 = sensor_data_safe_get("fsp0")
150
+ else:
151
+ self.fan_speed_0 = states_safe_get("fsp0")
146
152
  self.temperature_unit = states_safe_get("tu")
147
153
 
148
154
  self.publish_updates()
@@ -3,6 +3,7 @@ import functools
3
3
  from logging import getLogger
4
4
  from typing import Any
5
5
  from aiohttp import ClientSession, ClientResponse, FormData
6
+ from datetime import datetime, timedelta
6
7
 
7
8
  from .const import AWS_APIKEYS
8
9
  from .util_http import request_with_logging
@@ -92,9 +93,9 @@ class HttpAwsBlueair:
92
93
  @request_with_errors
93
94
  @request_with_logging
94
95
  async def _get_request_with_logging_and_errors_raised(
95
- self, url: str, headers: dict | None = None
96
+ self, url: str, headers: dict | None = None, params: dict | None = None
96
97
  ) -> ClientResponse:
97
- return await self.api_session.get(url=url, headers=headers)
98
+ return await self.api_session.get(url=url, headers=headers, params=params)
98
99
 
99
100
  @request_with_errors
100
101
  @request_with_logging
@@ -111,7 +112,7 @@ class HttpAwsBlueair:
111
112
 
112
113
  async def refresh_session(self) -> None:
113
114
  _LOGGER.debug("refresh_session")
114
- url = f"https://accounts.{AWS_APIKEYS[self.region]['gigyaRegion']}.gigya.com/accounts.login"
115
+ url = f"https://{AWS_APIKEYS[self.region]['gigyaRegion']}/accounts.login"
115
116
  form_data = FormData()
116
117
  form_data.add_field("apikey", AWS_APIKEYS[self.region]["apiKey"])
117
118
  form_data.add_field("loginID", self.username)
@@ -130,7 +131,7 @@ class HttpAwsBlueair:
130
131
  _LOGGER.debug("refresh_jwt")
131
132
  if self.session_token is None or self.session_secret is None:
132
133
  await self.refresh_session()
133
- url = f"https://accounts.{AWS_APIKEYS[self.region]['gigyaRegion']}.gigya.com/accounts.getJWT"
134
+ url = f"https://{AWS_APIKEYS[self.region]['gigyaRegion']}/accounts.getJWT"
134
135
  form_data = FormData()
135
136
  form_data.add_field("oauth_token", self.session_token)
136
137
  form_data.add_field("secret", self.session_secret)
@@ -147,7 +148,7 @@ class HttpAwsBlueair:
147
148
  _LOGGER.debug("refresh_access_token")
148
149
  if self.jwt is None:
149
150
  await self.refresh_jwt()
150
- url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/login"
151
+ url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}/prod/c/login"
151
152
  headers = {"idtoken": self.jwt}
152
153
  response: ClientResponse = (
153
154
  await self._post_request_with_logging_and_errors_raised(
@@ -167,7 +168,7 @@ class HttpAwsBlueair:
167
168
  @request_with_active_session
168
169
  async def devices(self) -> dict[str, Any]:
169
170
  _LOGGER.debug("devices")
170
- url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/registered-devices"
171
+ url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}/prod/c/registered-devices"
171
172
  headers = {
172
173
  "Authorization": f"Bearer {await self.get_access_token()}",
173
174
  }
@@ -179,10 +180,30 @@ class HttpAwsBlueair:
179
180
  response_json = await response.json()
180
181
  return response_json["devices"]
181
182
 
183
+ @request_with_active_session
184
+ async def device_sensors(self, device_name, device_uuid, duration: timedelta = timedelta(hours=10)):
185
+ url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}/prod/c/{device_name}/r/telemetry/5m/historical"
186
+ headers = {
187
+ "Authorization": f"Bearer {await self.get_access_token()}",
188
+ }
189
+ params = {
190
+ "did": device_uuid,
191
+ "from": int((datetime.now()-duration).timestamp()),
192
+ "to": int(datetime.now().timestamp()),
193
+ "s": ["pm1", "pm2_5", "pm10", "tVOC", "hcho", "h", "t", "fsp0"]
194
+ }
195
+ response: ClientResponse = (
196
+ await self._get_request_with_logging_and_errors_raised(
197
+ url=url, headers=headers, params=params
198
+ )
199
+ )
200
+ response_json = await response.json()
201
+ return response_json
202
+
182
203
  @request_with_active_session
183
204
  async def device_info(self, device_name, device_uuid) -> dict[str, Any]:
184
205
  _LOGGER.debug("device_info")
185
- url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/{device_name}/r/initial"
206
+ url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}/prod/c/{device_name}/r/initial"
186
207
  headers = {
187
208
  "Authorization": f"Bearer {await self.get_access_token()}",
188
209
  }
@@ -212,7 +233,7 @@ class HttpAwsBlueair:
212
233
  self, device_uuid, service_name, action_verb, action_value
213
234
  ) -> bool:
214
235
  _LOGGER.debug("set_device_info")
215
- url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}.amazonaws.com/prod/c/{device_uuid}/a/{service_name}"
236
+ url = f"https://{AWS_APIKEYS[self.region]['restApiId']}.execute-api.{AWS_APIKEYS[self.region]['awsRegion']}/prod/c/{device_uuid}/a/{service_name}"
216
237
  headers = {
217
238
  "Authorization": f"Bearer {await self.get_access_token()}",
218
239
  }
@@ -1,5 +1,5 @@
1
1
  import typing
2
- from typing import Any, TypeVar
2
+ from typing import Any
3
3
  from collections.abc import Iterable
4
4
  import dataclasses
5
5
  import base64
@@ -85,7 +85,7 @@ class Attribute:
85
85
  class Sensor:
86
86
  """DeviceSensor(ds); seems to define a sensor.
87
87
 
88
- We never directly access these objects. Thos this defines
88
+ We never directly access these objects. Though this defines
89
89
  the schema for 'h', 't', 'pm10' etc that gets returned in
90
90
  the sensor_data senml SensorPack.
91
91
  """
@@ -115,6 +115,36 @@ class Control:
115
115
  d: str | None = None # device info json path
116
116
 
117
117
 
118
+ @dataclasses.dataclass
119
+ class SensorRecord:
120
+ values: dict[str, int]
121
+ timestamp: float | None
122
+
123
+
124
+ class SensorHistory(list[SensorRecord]):
125
+ def __init__(self, response):
126
+ sensors = response[0]["sensors"]
127
+ datapoints = response[0]["datapoints"]
128
+ sensor_records = []
129
+ for datapoint in datapoints:
130
+ values = {}
131
+ for idx, sensor in enumerate(sensors):
132
+ if datapoint[idx+1] is not None:
133
+ values[sensor] = int(datapoint[idx+1])
134
+ sensor_records.append(SensorRecord(timestamp=int(datapoint[0]), values=values))
135
+ super().__init__(sensor_records)
136
+
137
+ def to_latest(self) -> SensorRecord:
138
+ def key(e):
139
+ return e.timestamp
140
+ self.sort(key=key, reverse=True)
141
+ if len(self) > 0:
142
+ return self[0]
143
+ else:
144
+ return SensorRecord(values={}, timestamp=0)
145
+
146
+
147
+
118
148
  ########################
119
149
  # SenML RFC8428
120
150
 
@@ -8,7 +8,7 @@ import sys
8
8
 
9
9
  path_root = Path(__file__).parents[2]
10
10
  sys.path.append(str(path_root))
11
- from src.blueair_api import get_devices, get_aws_devices, DeviceAws
11
+ from src.blueair_api import get_devices, get_aws_devices
12
12
 
13
13
 
14
14
  logger = logging.getLogger("src.blueair_api")
@@ -12,6 +12,9 @@ def request_with_logging(func):
12
12
  headers = kwargs.get("headers")
13
13
  if headers is not None:
14
14
  request_message = request_message + f"headers: {headers}"
15
+ params = kwargs.get("params")
16
+ if params is not None:
17
+ request_message = request_message + f"params: {params}"
15
18
  json_body = kwargs.get("json_body")
16
19
  if json_body is not None:
17
20
  request_message = (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blueair_api
3
- Version: 1.36.0
3
+ Version: 1.36.2
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
@@ -97,6 +97,13 @@ class DeviceAwsTestBase(IsolatedAsyncioTestCase):
97
97
  "states": [],
98
98
  })
99
99
 
100
+ async def fake_sensors(device_name, device_uuid):
101
+ return [{
102
+ "datapoints": [],
103
+ "sensors": []
104
+ }]
105
+
106
+ self.api.device_sensors.side_effect = fake_sensors
100
107
  self.api.device_info.side_effect = self.device_info_helper.device_info
101
108
  self.api.set_device_info.side_effect = self.device_info_helper.set_device_info
102
109
 
@@ -409,8 +416,8 @@ class H35iTest(DeviceAwsTestBase):
409
416
  assert device.pm2_5 is NotImplemented
410
417
  assert device.pm10 is NotImplemented
411
418
  assert device.tVOC is NotImplemented
412
- assert device.temperature == 19
413
- assert device.humidity == 50
419
+ assert device.temperature is None
420
+ assert device.humidity is None
414
421
  assert device.name == "Bedroom"
415
422
  assert device.firmware == "1.0.1"
416
423
  assert device.mcu_firmware == "1.0.1"
@@ -437,7 +444,7 @@ class H35iTest(DeviceAwsTestBase):
437
444
  assert device.heat_fan_speed is NotImplemented
438
445
  assert device.cool_sub_mode is NotImplemented
439
446
  assert device.cool_fan_speed is NotImplemented
440
- assert device.fan_speed_0 is NotImplemented
447
+ assert device.fan_speed_0 is None
441
448
  assert device.temperature_unit is NotImplemented
442
449
 
443
450
 
@@ -459,7 +466,7 @@ class Max311iTest(DeviceAwsTestBase):
459
466
  assert device.model == ModelEnum.MAX_311I
460
467
 
461
468
  assert device.pm1 is NotImplemented
462
- assert device.pm2_5 == 0
469
+ assert device.pm2_5 is None
463
470
  assert device.pm10 is NotImplemented
464
471
  assert device.tVOC is NotImplemented
465
472
  assert device.temperature is NotImplemented
@@ -490,7 +497,7 @@ class Max311iTest(DeviceAwsTestBase):
490
497
  assert device.heat_fan_speed is NotImplemented
491
498
  assert device.cool_sub_mode is NotImplemented
492
499
  assert device.cool_fan_speed is NotImplemented
493
- assert device.fan_speed_0 is NotImplemented
500
+ assert device.fan_speed_0 is None
494
501
  assert device.temperature_unit is NotImplemented
495
502
 
496
503
 
@@ -513,11 +520,11 @@ class T10iTest(DeviceAwsTestBase):
513
520
  assert device.model == ModelEnum.T10I
514
521
 
515
522
  assert device.pm1 is NotImplemented
516
- assert device.pm2_5 == 0
523
+ assert device.pm2_5 is None
517
524
  assert device.pm10 is NotImplemented
518
525
  assert device.tVOC is NotImplemented
519
- assert device.temperature == 18
520
- assert device.humidity == 28
526
+ assert device.temperature is None
527
+ assert device.humidity is None
521
528
  assert device.name == "Allen's Office"
522
529
  assert device.firmware == "1.0.4"
523
530
  assert device.mcu_firmware == "1.0.4"
@@ -566,12 +573,12 @@ class Protect7470iTest(DeviceAwsTestBase):
566
573
 
567
574
  assert device.model == ModelEnum.PROTECT_7470I
568
575
 
569
- assert device.pm1 == 0
570
- assert device.pm2_5 == 0
571
- assert device.pm10 == 0
572
- assert device.tVOC == 59
573
- assert device.temperature == 23
574
- assert device.humidity == 46
576
+ assert device.pm1 is None
577
+ assert device.pm2_5 is None
578
+ assert device.pm10 is None
579
+ assert device.tVOC is None
580
+ assert device.temperature is None
581
+ assert device.humidity is None
575
582
  assert device.name == "air filter in room"
576
583
  assert device.firmware == "2.1.1"
577
584
  assert device.mcu_firmware == "1.0.12"
@@ -598,7 +605,7 @@ class Protect7470iTest(DeviceAwsTestBase):
598
605
  assert device.cool_sub_mode is NotImplemented
599
606
  assert device.cool_fan_speed is NotImplemented
600
607
  assert device.ap_sub_mode is NotImplemented
601
- assert device.fan_speed_0 is NotImplemented
608
+ assert device.fan_speed_0 is None
602
609
  assert device.temperature_unit is NotImplemented
603
610
 
604
611
 
@@ -620,9 +627,9 @@ class Max211iTest(DeviceAwsTestBase):
620
627
 
621
628
  assert device.model == ModelEnum.MAX_211I
622
629
 
623
- assert device.pm1 == 0
624
- assert device.pm2_5 == 0
625
- assert device.pm10 == 0
630
+ assert device.pm1 is None
631
+ assert device.pm2_5 is None
632
+ assert device.pm10 is None
626
633
  assert device.tVOC is NotImplemented
627
634
  assert device.temperature is NotImplemented
628
635
  assert device.humidity is NotImplemented
@@ -652,5 +659,5 @@ class Max211iTest(DeviceAwsTestBase):
652
659
  assert device.cool_sub_mode is NotImplemented
653
660
  assert device.cool_fan_speed is NotImplemented
654
661
  assert device.ap_sub_mode is NotImplemented
655
- assert device.fan_speed_0 is NotImplemented
662
+ assert device.fan_speed_0 is None
656
663
  assert device.temperature_unit is NotImplemented
@@ -5,8 +5,75 @@ import pytest
5
5
 
6
6
  from blueair_api import intermediate_representation_aws as ir
7
7
 
8
- class SensorPackTest(TestCase):
9
8
 
9
+ class SensorHistoryTest(TestCase):
10
+ def test_history_simple(self):
11
+ sh = ir.SensorHistory([
12
+ {
13
+ "datapoints": [
14
+ [
15
+ "1",
16
+ "4",
17
+ "5",
18
+ "6",
19
+ "42",
20
+ None,
21
+ "51",
22
+ "27",
23
+ "87"
24
+ ],
25
+ [
26
+ "3",
27
+ "1",
28
+ "2",
29
+ "3",
30
+ "43",
31
+ None,
32
+ "50",
33
+ "24",
34
+ "91"
35
+ ],
36
+ ],
37
+ "sensors": [
38
+ "pm1",
39
+ "pm2_5",
40
+ "pm10",
41
+ "tVOC",
42
+ "hcho",
43
+ "h",
44
+ "t",
45
+ "fsp0"
46
+ ],
47
+ "start": "1746401990",
48
+ "end": "1746409190",
49
+ "did": "1d528642-56a9"
50
+ }
51
+ ])
52
+ assert sh[0].timestamp == 1
53
+ assert sh[0].values == {
54
+ "pm1": 4,
55
+ "pm2_5": 5,
56
+ "pm10": 6,
57
+ "tVOC": 42,
58
+ "h": 51,
59
+ "t": 27,
60
+ "fsp0": 87
61
+ }
62
+ assert sh[1].timestamp == 3
63
+ assert sh[1].values == {
64
+ "pm1": 1,
65
+ "pm2_5": 2,
66
+ "pm10": 3,
67
+ "tVOC": 43,
68
+ "h": 50,
69
+ "t": 24,
70
+ "fsp0": 91
71
+ }
72
+
73
+ assert sh.to_latest().timestamp == 3
74
+
75
+
76
+ class SensorPackTest(TestCase):
10
77
  def testSimple(self):
11
78
  sp = ir.SensorPack( [
12
79
  {'n': 'v', 't': 1, 'v': 1},
File without changes
File without changes
File without changes