blueair-api 1.24.1__tar.gz → 1.26.0__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.24.1 → blueair_api-1.26.0}/PKG-INFO +1 -1
- {blueair_api-1.24.1 → blueair_api-1.26.0}/pyproject.toml +1 -1
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api/const.py +1 -1
- blueair_api-1.26.0/src/blueair_api/device_aws.py +194 -0
- blueair_api-1.26.0/src/blueair_api/intermediate_representation_aws.py +176 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api.egg-info/PKG-INFO +1 -1
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api.egg-info/SOURCES.txt +4 -1
- blueair_api-1.26.0/tests/test_device_aws.py +366 -0
- blueair_api-1.26.0/tests/test_intermediate_representation_aws.py +127 -0
- blueair_api-1.24.1/src/blueair_api/device_aws.py +0 -156
- {blueair_api-1.24.1 → blueair_api-1.26.0}/LICENSE +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/README.md +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/setup.cfg +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api/__init__.py +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api/callbacks.py +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api/device.py +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api/errors.py +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api/http_aws_blueair.py +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api/http_blueair.py +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api/model_enum.py +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api/stub.py +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api/util.py +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api/util_bootstrap.py +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api/util_http.py +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api.egg-info/dependency_links.txt +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api.egg-info/requires.txt +0 -0
- {blueair_api-1.24.1 → blueair_api-1.26.0}/src/blueair_api.egg-info/top_level.txt +0 -0
@@ -0,0 +1,194 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import logging
|
3
|
+
from json import dumps
|
4
|
+
|
5
|
+
from .callbacks import CallbacksMixin
|
6
|
+
from .http_aws_blueair import HttpAwsBlueair
|
7
|
+
from .model_enum import ModelEnum
|
8
|
+
from . import intermediate_representation_aws as ir
|
9
|
+
|
10
|
+
_LOGGER = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
type AttributeType[T] = T | None | type[NotImplemented]
|
13
|
+
|
14
|
+
@dataclasses.dataclass(slots=True)
|
15
|
+
class DeviceAws(CallbacksMixin):
|
16
|
+
@classmethod
|
17
|
+
async def create_device(cls, api, uuid, name, mac, type_name, refresh=False):
|
18
|
+
_LOGGER.debug("UUID:"+uuid)
|
19
|
+
device_aws = DeviceAws(
|
20
|
+
api=api,
|
21
|
+
uuid=uuid,
|
22
|
+
name_api=name,
|
23
|
+
mac=mac,
|
24
|
+
type_name=type_name,
|
25
|
+
)
|
26
|
+
if refresh:
|
27
|
+
await device_aws.refresh()
|
28
|
+
_LOGGER.debug(f"create_device blueair device_aws: {device_aws}")
|
29
|
+
return device_aws
|
30
|
+
|
31
|
+
api: HttpAwsBlueair
|
32
|
+
uuid : str | None = None
|
33
|
+
name : str | None = None
|
34
|
+
name_api : str | None = None
|
35
|
+
mac : str | None = None
|
36
|
+
type_name : str | None = None
|
37
|
+
|
38
|
+
# Attributes are defined below.
|
39
|
+
# We mandate that unittests shall test all fields of AttributeType.
|
40
|
+
sku : AttributeType[str] = None
|
41
|
+
firmware : AttributeType[str] = None
|
42
|
+
mcu_firmware : AttributeType[str] = None
|
43
|
+
serial_number : AttributeType[str] = None
|
44
|
+
|
45
|
+
brightness : AttributeType[int] = None
|
46
|
+
child_lock : AttributeType[bool] = None
|
47
|
+
fan_speed : AttributeType[int] = None
|
48
|
+
fan_auto_mode : AttributeType[bool] = None
|
49
|
+
standby : AttributeType[bool] = None
|
50
|
+
night_mode : AttributeType[bool] = None
|
51
|
+
germ_shield : AttributeType[bool] = None
|
52
|
+
|
53
|
+
pm1 : AttributeType[int] = None
|
54
|
+
pm2_5 : AttributeType[int] = None
|
55
|
+
pm10 : AttributeType[int] = None
|
56
|
+
tVOC : AttributeType[int] = None
|
57
|
+
temperature : AttributeType[int] = None
|
58
|
+
humidity : AttributeType[int] = None
|
59
|
+
filter_usage_percentage : AttributeType[int] = None
|
60
|
+
wifi_working : AttributeType[bool] = None
|
61
|
+
|
62
|
+
wick_usage_percentage : AttributeType[int] = None
|
63
|
+
wick_dry_mode : AttributeType[bool] = None
|
64
|
+
water_shortage : AttributeType[bool] = None
|
65
|
+
auto_regulated_humidity : AttributeType[int] = None
|
66
|
+
|
67
|
+
async def refresh(self):
|
68
|
+
_LOGGER.debug(f"refreshing blueair device aws: {self}")
|
69
|
+
info = await self.api.device_info(self.name_api, self.uuid)
|
70
|
+
_LOGGER.debug(dumps(info, indent=2))
|
71
|
+
|
72
|
+
# ir.parse_json(ir.Attribute, ir.query_json(info, "configuration.da"))
|
73
|
+
ds = ir.parse_json(ir.Sensor, ir.query_json(info, "configuration.ds"))
|
74
|
+
dc = ir.parse_json(ir.Control, ir.query_json(info, "configuration.dc"))
|
75
|
+
|
76
|
+
sensor_data = ir.SensorPack(info["sensordata"]).to_latest_value()
|
77
|
+
|
78
|
+
def sensor_data_safe_get(key):
|
79
|
+
return sensor_data.get(key) if key in ds else NotImplemented
|
80
|
+
|
81
|
+
self.pm1 = sensor_data_safe_get("pm1")
|
82
|
+
self.pm2_5 = sensor_data_safe_get("pm2_5")
|
83
|
+
self.pm10 = sensor_data_safe_get("pm10")
|
84
|
+
self.tVOC = sensor_data_safe_get("tVOC")
|
85
|
+
self.temperature = sensor_data_safe_get("t")
|
86
|
+
self.humidity = sensor_data_safe_get("h")
|
87
|
+
|
88
|
+
def info_safe_get(path):
|
89
|
+
# directly reads for the schema. If the schema field is
|
90
|
+
# undefined, it is NotImplemented, not merely unavailable.
|
91
|
+
value = ir.query_json(info, path)
|
92
|
+
if value is None:
|
93
|
+
return NotImplemented
|
94
|
+
return value
|
95
|
+
|
96
|
+
self.name = info_safe_get("configuration.di.name")
|
97
|
+
self.firmware = info_safe_get("configuration.di.cfv")
|
98
|
+
self.mcu_firmware = info_safe_get("configuration.di.mfv")
|
99
|
+
self.serial_number = info_safe_get("configuration.di.ds")
|
100
|
+
self.sku = info_safe_get("configuration.di.sku")
|
101
|
+
|
102
|
+
states = ir.SensorPack(info["states"]).to_latest_value()
|
103
|
+
|
104
|
+
def states_safe_get(key):
|
105
|
+
return states.get(key) if key in dc else NotImplemented
|
106
|
+
|
107
|
+
# "online" is not defined in the schema.
|
108
|
+
self.wifi_working = states.get("online")
|
109
|
+
|
110
|
+
self.standby = states_safe_get("standby")
|
111
|
+
self.night_mode = states_safe_get("nightmode")
|
112
|
+
self.germ_shield = states_safe_get("germshield")
|
113
|
+
self.brightness = states_safe_get("brightness")
|
114
|
+
self.child_lock = states_safe_get("childlock")
|
115
|
+
self.fan_speed = states_safe_get("fanspeed")
|
116
|
+
self.fan_auto_mode = states_safe_get("automode")
|
117
|
+
self.filter_usage_percentage = states_safe_get("filterusage")
|
118
|
+
self.wick_usage_percentage = states_safe_get("wickusage")
|
119
|
+
self.wick_dry_mode = states_safe_get("wickdrys")
|
120
|
+
self.auto_regulated_humidity = states_safe_get("autorh")
|
121
|
+
self.water_shortage = states_safe_get("wshortage")
|
122
|
+
|
123
|
+
self.publish_updates()
|
124
|
+
_LOGGER.debug(f"refreshed blueair device aws: {self}")
|
125
|
+
|
126
|
+
async def set_brightness(self, value: int):
|
127
|
+
self.brightness = value
|
128
|
+
await self.api.set_device_info(self.uuid, "brightness", "v", value)
|
129
|
+
self.publish_updates()
|
130
|
+
|
131
|
+
async def set_fan_speed(self, value: int):
|
132
|
+
self.fan_speed = value
|
133
|
+
await self.api.set_device_info(self.uuid, "fanspeed", "v", value)
|
134
|
+
self.publish_updates()
|
135
|
+
|
136
|
+
async def set_standby(self, value: bool):
|
137
|
+
self.standby = value
|
138
|
+
await self.api.set_device_info(self.uuid, "standby", "vb", value)
|
139
|
+
self.publish_updates()
|
140
|
+
|
141
|
+
# FIXME: avoid state translation at the API level and depreate running.
|
142
|
+
# replace with standby which is standard across aws devices.
|
143
|
+
@property
|
144
|
+
def running(self) -> AttributeType[bool]:
|
145
|
+
if self.standby is None or self.standby is NotImplemented:
|
146
|
+
return self.standby
|
147
|
+
return not self.standby
|
148
|
+
|
149
|
+
async def set_running(self, running: bool):
|
150
|
+
await self.set_standby(not running)
|
151
|
+
|
152
|
+
async def set_fan_auto_mode(self, fan_auto_mode: bool):
|
153
|
+
self.fan_auto_mode = fan_auto_mode
|
154
|
+
await self.api.set_device_info(self.uuid, "automode", "vb", fan_auto_mode)
|
155
|
+
self.publish_updates()
|
156
|
+
|
157
|
+
async def set_auto_regulated_humidity(self, value: int):
|
158
|
+
self.auto_regulated_humidity = value
|
159
|
+
await self.api.set_device_info(self.uuid, "autorh", "v", value)
|
160
|
+
self.publish_updates()
|
161
|
+
|
162
|
+
async def set_child_lock(self, child_lock: bool):
|
163
|
+
self.child_lock = child_lock
|
164
|
+
await self.api.set_device_info(self.uuid, "childlock", "vb", child_lock)
|
165
|
+
self.publish_updates()
|
166
|
+
|
167
|
+
async def set_night_mode(self, night_mode: bool):
|
168
|
+
self.night_mode = night_mode
|
169
|
+
await self.api.set_device_info(self.uuid, "nightmode", "vb", night_mode)
|
170
|
+
self.publish_updates()
|
171
|
+
|
172
|
+
async def set_wick_dry_mode(self, value: bool):
|
173
|
+
self.wick_dry_mode = value
|
174
|
+
await self.api.set_device_info(self.uuid, "wickdrys", "vb", value)
|
175
|
+
self.publish_updates()
|
176
|
+
|
177
|
+
@property
|
178
|
+
def model(self) -> ModelEnum:
|
179
|
+
if self.sku == "111633":
|
180
|
+
return ModelEnum.HUMIDIFIER_H35I
|
181
|
+
if self.sku == "105820":
|
182
|
+
return ModelEnum.PROTECT_7440I
|
183
|
+
if self.sku == "105826":
|
184
|
+
return ModelEnum.PROTECT_7470I
|
185
|
+
if self.sku == "110059":
|
186
|
+
return ModelEnum.MAX_211I
|
187
|
+
if self.sku == "110092":
|
188
|
+
return ModelEnum.MAX_311I
|
189
|
+
if self.sku == "110057":
|
190
|
+
return ModelEnum.MAX_411I
|
191
|
+
if self.sku == "112124":
|
192
|
+
return ModelEnum.T10I
|
193
|
+
return ModelEnum.UNKNOWN
|
194
|
+
|
@@ -0,0 +1,176 @@
|
|
1
|
+
from typing import Any, TypeVar
|
2
|
+
from collections.abc import Iterable
|
3
|
+
import dataclasses
|
4
|
+
import base64
|
5
|
+
|
6
|
+
type ScalarType = str | float | bool
|
7
|
+
type MappingType = dict[str, "ObjectType"]
|
8
|
+
type SequenceType = list["ObjectType"]
|
9
|
+
type ObjectType = ScalarType | MappingType | SequenceType
|
10
|
+
|
11
|
+
|
12
|
+
def query_json(jsonobj: ObjectType, path: str):
|
13
|
+
value = jsonobj
|
14
|
+
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}.")
|
20
|
+
if isinstance(value, list):
|
21
|
+
value = value[int(seg)]
|
22
|
+
else:
|
23
|
+
try:
|
24
|
+
value = value[seg]
|
25
|
+
except KeyError:
|
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
|
+
|
31
|
+
# last segment returns None if it is not found.
|
32
|
+
return value.get(segs[-1])
|
33
|
+
|
34
|
+
|
35
|
+
def parse_json[T](kls: type[T], jsonobj: MappingType) -> dict[str, T]:
|
36
|
+
"""Parses a json mapping object to dict.
|
37
|
+
|
38
|
+
The key is preserved. The value is parsed as dataclass type kls.
|
39
|
+
"""
|
40
|
+
result = {}
|
41
|
+
fields = dataclasses.fields(kls)
|
42
|
+
|
43
|
+
for key, value in jsonobj.items():
|
44
|
+
a = dict(value) # make a copy.
|
45
|
+
kwargs = {}
|
46
|
+
for field in fields:
|
47
|
+
if field.name == "extra_fields":
|
48
|
+
continue
|
49
|
+
if field.default is dataclasses.MISSING:
|
50
|
+
kwargs[field.name] = a.pop(field.name)
|
51
|
+
else:
|
52
|
+
kwargs[field.name] = a.pop(field.name, field.default)
|
53
|
+
|
54
|
+
result[key] = kls(**kwargs, extra_fields=a)
|
55
|
+
return result
|
56
|
+
|
57
|
+
|
58
|
+
########################
|
59
|
+
# Blueair AWS API Schema.
|
60
|
+
|
61
|
+
@dataclasses.dataclass
|
62
|
+
class Attribute:
|
63
|
+
"""DeviceAttribute(da); defines an attribute
|
64
|
+
|
65
|
+
An attribute is most likely mutable. An attribute may
|
66
|
+
also have alias names, likely derived from the 'dc' relation
|
67
|
+
e.g. a/sb, a/standby all refer to the 'sb' attribute.
|
68
|
+
"""
|
69
|
+
extra_fields : MappingType
|
70
|
+
n: str # name
|
71
|
+
a: int | bool # default attribute value, example value?
|
72
|
+
e: bool # ??? always True
|
73
|
+
fe:bool # ??? always True
|
74
|
+
ot: str # object type? topic type?
|
75
|
+
p: bool # only false for reboot and sflu
|
76
|
+
tn: str # topic name a path-like name d/????/a/{n}
|
77
|
+
|
78
|
+
|
79
|
+
@dataclasses.dataclass
|
80
|
+
class Sensor:
|
81
|
+
"""DeviceSensor(ds); seems to define a sensor.
|
82
|
+
|
83
|
+
We never directly access these objects. Thos this defines
|
84
|
+
the schema for 'h', 't', 'pm10' etc that gets returned in
|
85
|
+
the sensor_data senml SensorPack.
|
86
|
+
"""
|
87
|
+
extra_fields : MappingType
|
88
|
+
n: str # name
|
89
|
+
i: int # integration time? in millis
|
90
|
+
e: bool # ???
|
91
|
+
fe: bool # ??? always True.
|
92
|
+
ot: str # object type / topic name
|
93
|
+
tf: str # senml+json; topic format
|
94
|
+
tn: str # topic name a path-like name d/????/s/{n}
|
95
|
+
ttl: int # only seen 0 or -1, not sure if used.
|
96
|
+
|
97
|
+
@dataclasses.dataclass
|
98
|
+
class Control:
|
99
|
+
"""DeviceControl (dc); seems to define a state.
|
100
|
+
|
101
|
+
The states SensorPack seem to be using fields defined
|
102
|
+
in dc. The only exception is 'online' which is not defined
|
103
|
+
here.
|
104
|
+
"""
|
105
|
+
extra_fields : MappingType
|
106
|
+
n: str # name
|
107
|
+
v: int | bool
|
108
|
+
a: str | None = None
|
109
|
+
s: str | None = None
|
110
|
+
d: str | None = None # device info json path
|
111
|
+
|
112
|
+
|
113
|
+
########################
|
114
|
+
# SenML RFC8428
|
115
|
+
|
116
|
+
@dataclasses.dataclass
|
117
|
+
class Record:
|
118
|
+
"""A RFC8428 SenML record, resolved to Python types."""
|
119
|
+
name: str
|
120
|
+
unit: str | None
|
121
|
+
value: ScalarType
|
122
|
+
timestamp: float | None
|
123
|
+
integral: float | None
|
124
|
+
|
125
|
+
|
126
|
+
class SensorPack(list[Record]):
|
127
|
+
"""Represents a RFC8428 SensorPack, resolved to Python Types."""
|
128
|
+
|
129
|
+
def __init__(self, stream: Iterable[MappingType]):
|
130
|
+
seq = []
|
131
|
+
for record in stream:
|
132
|
+
rs = None
|
133
|
+
rt = None
|
134
|
+
rn = 0
|
135
|
+
ru = None
|
136
|
+
for label, value in record.items():
|
137
|
+
match label:
|
138
|
+
case 'bn' | 'bt' | 'bu' | 'bv' | 'bs' | 'bver':
|
139
|
+
raise ValueError("TODO: base fields not supported. c.f. RFC8428, 4.1")
|
140
|
+
case 't':
|
141
|
+
rt = float(value)
|
142
|
+
case 's':
|
143
|
+
rs = float(value)
|
144
|
+
case 'v':
|
145
|
+
rv = float(value)
|
146
|
+
case 'vb':
|
147
|
+
rv = bool(value)
|
148
|
+
case 'vs':
|
149
|
+
rv = str(value)
|
150
|
+
case 'vd':
|
151
|
+
rv = bytes(base64.b64decode(value))
|
152
|
+
case 'n':
|
153
|
+
rn = str(value)
|
154
|
+
case 'u':
|
155
|
+
ru = str(value)
|
156
|
+
case 't':
|
157
|
+
rn = float(value)
|
158
|
+
seq.append(Record(name=rn, unit=ru, value=rv, integral=rs, timestamp=rt))
|
159
|
+
super().__init__(seq)
|
160
|
+
|
161
|
+
def to_latest_value(self) -> dict[str, ScalarType]:
|
162
|
+
return {rn : record.value for rn, record in self.to_latest().items()}
|
163
|
+
|
164
|
+
def to_latest(self) -> dict[str, Record]:
|
165
|
+
latest = {}
|
166
|
+
for record in self:
|
167
|
+
rn = record.name
|
168
|
+
if record.name not in latest:
|
169
|
+
latest[rn] = record
|
170
|
+
elif record.timestamp is None:
|
171
|
+
latest[rn] = record
|
172
|
+
elif latest[record.name].timestamp is None:
|
173
|
+
latest[rn] = record
|
174
|
+
elif latest[record.name].timestamp < record.timestamp:
|
175
|
+
latest[rn] = record
|
176
|
+
return latest
|
@@ -9,6 +9,7 @@ src/blueair_api/device_aws.py
|
|
9
9
|
src/blueair_api/errors.py
|
10
10
|
src/blueair_api/http_aws_blueair.py
|
11
11
|
src/blueair_api/http_blueair.py
|
12
|
+
src/blueair_api/intermediate_representation_aws.py
|
12
13
|
src/blueair_api/model_enum.py
|
13
14
|
src/blueair_api/stub.py
|
14
15
|
src/blueair_api/util.py
|
@@ -18,4 +19,6 @@ src/blueair_api.egg-info/PKG-INFO
|
|
18
19
|
src/blueair_api.egg-info/SOURCES.txt
|
19
20
|
src/blueair_api.egg-info/dependency_links.txt
|
20
21
|
src/blueair_api.egg-info/requires.txt
|
21
|
-
src/blueair_api.egg-info/top_level.txt
|
22
|
+
src/blueair_api.egg-info/top_level.txt
|
23
|
+
tests/test_device_aws.py
|
24
|
+
tests/test_intermediate_representation_aws.py
|
@@ -0,0 +1,366 @@
|
|
1
|
+
"""Tests for DeviceAws.
|
2
|
+
|
3
|
+
Here is one way to run it:
|
4
|
+
|
5
|
+
First install the package in developer mode
|
6
|
+
|
7
|
+
$ pip install -e .
|
8
|
+
|
9
|
+
Then use pytest to drive the tests
|
10
|
+
|
11
|
+
$ pytest tests
|
12
|
+
"""
|
13
|
+
import typing
|
14
|
+
from typing import Any
|
15
|
+
|
16
|
+
import contextlib
|
17
|
+
import dataclasses
|
18
|
+
from importlib import resources
|
19
|
+
import json
|
20
|
+
from unittest import mock
|
21
|
+
from unittest import IsolatedAsyncioTestCase
|
22
|
+
|
23
|
+
import pytest
|
24
|
+
|
25
|
+
from blueair_api.device_aws import DeviceAws, AttributeType
|
26
|
+
from blueair_api.model_enum import ModelEnum
|
27
|
+
from blueair_api import http_aws_blueair
|
28
|
+
from blueair_api import intermediate_representation_aws as ir
|
29
|
+
|
30
|
+
|
31
|
+
class FakeDeviceInfoHelper:
|
32
|
+
"""Fake for the 'device info' interface of HttpAwsBlueAir class."""
|
33
|
+
def __init__(self, info: dict[str, Any]):
|
34
|
+
self.info = info
|
35
|
+
|
36
|
+
async def device_info(self, *args, **kwargs):
|
37
|
+
return self.info
|
38
|
+
|
39
|
+
async def set_device_info(self, device_uuid, service_name, action_verb, action_value):
|
40
|
+
# this function seems to be only updating the states consider rename the method.
|
41
|
+
# action_verb seems to be a type annotation:
|
42
|
+
# c.f. senml: https://www.rfc-editor.org/rfc/rfc8428.html#section-5
|
43
|
+
# the senml parsing library (utils.py) could use some additional love.
|
44
|
+
# to make it more conformal to the RFC standard.
|
45
|
+
for state in self.info['states']:
|
46
|
+
if state['n'] == service_name:
|
47
|
+
break
|
48
|
+
else:
|
49
|
+
state = {'n': service_name}
|
50
|
+
self.info['states'].append(state)
|
51
|
+
# Watch out: mutate after append produces desired mutation to info.
|
52
|
+
state[action_verb] = action_value
|
53
|
+
|
54
|
+
|
55
|
+
class AssertFullyCheckedHelper:
|
56
|
+
"""Assert that all attributes of AttributeType are accessed once."""
|
57
|
+
def __init__(self, device : DeviceAws):
|
58
|
+
self.device = device
|
59
|
+
self.logs = []
|
60
|
+
self.fields = set()
|
61
|
+
for field in dataclasses.fields(self.device):
|
62
|
+
if typing.get_origin(field.type) is AttributeType:
|
63
|
+
self.fields.add(field.name)
|
64
|
+
|
65
|
+
def __getattr__(self, attr):
|
66
|
+
if attr in self.fields:
|
67
|
+
self.logs.append(attr)
|
68
|
+
return getattr(self.device, attr)
|
69
|
+
|
70
|
+
|
71
|
+
@contextlib.contextmanager
|
72
|
+
def assert_fully_checked(device):
|
73
|
+
helper = AssertFullyCheckedHelper(device)
|
74
|
+
yield helper
|
75
|
+
assert set(helper.logs) == helper.fields
|
76
|
+
|
77
|
+
|
78
|
+
class DeviceAwsTestBase(IsolatedAsyncioTestCase):
|
79
|
+
|
80
|
+
def setUp(self):
|
81
|
+
|
82
|
+
patcher = mock.patch('blueair_api.http_aws_blueair.HttpAwsBlueair', autospec=True)
|
83
|
+
self.api_class = patcher.start()
|
84
|
+
self.addCleanup(patcher.stop)
|
85
|
+
self.api = self.api_class(username="fake-username", password="fake-password")
|
86
|
+
|
87
|
+
self.device = DeviceAws(self.api,
|
88
|
+
name_api="fake-name-api",
|
89
|
+
uuid="fake-uuid",
|
90
|
+
name="fake-name",
|
91
|
+
mac="fake-mac",
|
92
|
+
type_name='fake-type-name')
|
93
|
+
|
94
|
+
self.device_info_helper = FakeDeviceInfoHelper(
|
95
|
+
{"configuration": {"di" : {}, "ds" : {}, "dc" : {}, "da" : {},},
|
96
|
+
"sensordata": [],
|
97
|
+
"states": [],
|
98
|
+
})
|
99
|
+
|
100
|
+
self.api.device_info.side_effect = self.device_info_helper.device_info
|
101
|
+
self.api.set_device_info.side_effect = self.device_info_helper.set_device_info
|
102
|
+
|
103
|
+
|
104
|
+
class DeviceAwsSetterTest(DeviceAwsTestBase):
|
105
|
+
"""Tests for all of the setters."""
|
106
|
+
|
107
|
+
def setUp(self):
|
108
|
+
super().setUp()
|
109
|
+
# minimally populate dc to define the states.
|
110
|
+
fake = {"n": "n", "v": 0}
|
111
|
+
ir.query_json(self.device_info_helper.info, "configuration.dc").update({
|
112
|
+
"brightness": fake,
|
113
|
+
"fanspeed": fake,
|
114
|
+
"standby": fake,
|
115
|
+
"automode": fake,
|
116
|
+
"autorh": fake,
|
117
|
+
"childlock": fake,
|
118
|
+
"nightmode": fake,
|
119
|
+
"wickdrys": fake,
|
120
|
+
})
|
121
|
+
|
122
|
+
async def test_brightness(self):
|
123
|
+
# test cache works
|
124
|
+
self.device.brightness = None
|
125
|
+
await self.device.set_brightness(1)
|
126
|
+
assert self.device.brightness == 1
|
127
|
+
|
128
|
+
# test refresh works
|
129
|
+
await self.device.set_brightness(2)
|
130
|
+
self.device.brightness = None
|
131
|
+
await self.device.refresh()
|
132
|
+
assert self.device.brightness == 2
|
133
|
+
|
134
|
+
async def test_fan_speed(self):
|
135
|
+
# test cache works
|
136
|
+
self.device.fan_speed = None
|
137
|
+
await self.device.set_fan_speed(1)
|
138
|
+
assert self.device.fan_speed == 1
|
139
|
+
|
140
|
+
# test refresh works
|
141
|
+
await self.device.set_fan_speed(2)
|
142
|
+
self.device.fan_speed = None
|
143
|
+
await self.device.refresh()
|
144
|
+
assert self.device.fan_speed == 2
|
145
|
+
|
146
|
+
async def test_running(self):
|
147
|
+
# test cache works
|
148
|
+
self.device.standby = None
|
149
|
+
await self.device.set_running(False)
|
150
|
+
assert self.device.running is False
|
151
|
+
|
152
|
+
# test refresh works
|
153
|
+
await self.device.set_running(True)
|
154
|
+
self.device.standby = None
|
155
|
+
await self.device.refresh()
|
156
|
+
assert self.device.running is True
|
157
|
+
|
158
|
+
async def test_standby(self):
|
159
|
+
# test cache works
|
160
|
+
self.device.standby = None
|
161
|
+
await self.device.set_standby(False)
|
162
|
+
assert self.device.standby is False
|
163
|
+
|
164
|
+
# test refresh works
|
165
|
+
await self.device.set_standby(True)
|
166
|
+
self.device.standby = None
|
167
|
+
await self.device.refresh()
|
168
|
+
assert self.device.standby is True
|
169
|
+
|
170
|
+
async def test_fan_auto_mode(self):
|
171
|
+
# test cache works
|
172
|
+
self.device.fan_auto_mode = None
|
173
|
+
await self.device.set_fan_auto_mode(False)
|
174
|
+
assert self.device.fan_auto_mode is False
|
175
|
+
|
176
|
+
# test refresh works
|
177
|
+
await self.device.set_fan_auto_mode(True)
|
178
|
+
self.device.fan_auto_mode = None
|
179
|
+
await self.device.refresh()
|
180
|
+
assert self.device.fan_auto_mode is True
|
181
|
+
|
182
|
+
async def test_auto_regulated_humidity(self):
|
183
|
+
# test cache works
|
184
|
+
self.device.auto_regulated_humidity = None
|
185
|
+
await self.device.set_auto_regulated_humidity(1)
|
186
|
+
assert self.device.auto_regulated_humidity == 1
|
187
|
+
|
188
|
+
# test refresh works
|
189
|
+
await self.device.set_auto_regulated_humidity(2)
|
190
|
+
self.device.auto_regulated_humidity = None
|
191
|
+
await self.device.refresh()
|
192
|
+
assert self.device.auto_regulated_humidity == 2
|
193
|
+
|
194
|
+
async def test_child_lock(self):
|
195
|
+
# test cache works
|
196
|
+
self.device.child_lock = None
|
197
|
+
await self.device.set_child_lock(False)
|
198
|
+
assert self.device.child_lock is False
|
199
|
+
|
200
|
+
# test refresh works
|
201
|
+
await self.device.set_child_lock(True)
|
202
|
+
self.device.child_lock = None
|
203
|
+
await self.device.refresh()
|
204
|
+
assert self.device.child_lock is True
|
205
|
+
|
206
|
+
async def test_night_mode(self):
|
207
|
+
# test cache works
|
208
|
+
self.device.night_mode = None
|
209
|
+
await self.device.set_night_mode(False)
|
210
|
+
assert self.device.night_mode is False
|
211
|
+
|
212
|
+
# test refresh works
|
213
|
+
await self.device.set_night_mode(True)
|
214
|
+
self.device.night_mode = None
|
215
|
+
await self.device.refresh()
|
216
|
+
assert self.device.night_mode is True
|
217
|
+
|
218
|
+
async def test_wick_dry_mode(self):
|
219
|
+
# test cache works
|
220
|
+
self.device.wick_dry_mode = None
|
221
|
+
await self.device.set_wick_dry_mode(False)
|
222
|
+
assert self.device.wick_dry_mode is False
|
223
|
+
|
224
|
+
# test refresh works
|
225
|
+
await self.device.set_wick_dry_mode(True)
|
226
|
+
self.device.wick_dry_mode = None
|
227
|
+
await self.device.refresh()
|
228
|
+
assert self.device.wick_dry_mode is True
|
229
|
+
|
230
|
+
|
231
|
+
class EmptyDeviceAwsTest(DeviceAwsTestBase):
|
232
|
+
"""Tests for a emptydevice.
|
233
|
+
|
234
|
+
This is a made-up device. All attrs are not implemented.
|
235
|
+
|
236
|
+
Other device types shall override setUp and populate self.info with the
|
237
|
+
golden dataset.
|
238
|
+
"""
|
239
|
+
|
240
|
+
async def test_attributes(self):
|
241
|
+
|
242
|
+
await self.device.refresh()
|
243
|
+
self.api.device_info.assert_awaited_with("fake-name-api", "fake-uuid")
|
244
|
+
|
245
|
+
with assert_fully_checked(self.device) as device:
|
246
|
+
|
247
|
+
assert device.model == ModelEnum.UNKNOWN
|
248
|
+
|
249
|
+
assert device.pm1 is NotImplemented
|
250
|
+
assert device.pm2_5 is NotImplemented
|
251
|
+
assert device.pm10 is NotImplemented
|
252
|
+
assert device.tVOC is NotImplemented
|
253
|
+
assert device.temperature is NotImplemented
|
254
|
+
assert device.humidity is NotImplemented
|
255
|
+
assert device.name is NotImplemented
|
256
|
+
assert device.firmware is NotImplemented
|
257
|
+
assert device.mcu_firmware is NotImplemented
|
258
|
+
assert device.serial_number is NotImplemented
|
259
|
+
assert device.sku is NotImplemented
|
260
|
+
|
261
|
+
assert device.running is NotImplemented
|
262
|
+
assert device.standby is NotImplemented
|
263
|
+
assert device.night_mode is NotImplemented
|
264
|
+
assert device.germ_shield is NotImplemented
|
265
|
+
assert device.brightness is NotImplemented
|
266
|
+
assert device.child_lock is NotImplemented
|
267
|
+
assert device.fan_speed is NotImplemented
|
268
|
+
assert device.fan_auto_mode is NotImplemented
|
269
|
+
assert device.filter_usage_percentage is NotImplemented
|
270
|
+
assert device.wifi_working is None
|
271
|
+
assert device.wick_usage_percentage is NotImplemented
|
272
|
+
assert device.auto_regulated_humidity is NotImplemented
|
273
|
+
assert device.water_shortage is NotImplemented
|
274
|
+
assert device.wick_dry_mode is NotImplemented
|
275
|
+
|
276
|
+
|
277
|
+
class H35iTest(DeviceAwsTestBase):
|
278
|
+
"""Tests for H35i."""
|
279
|
+
|
280
|
+
def setUp(self):
|
281
|
+
super().setUp()
|
282
|
+
with open(resources.files().joinpath('device_info/H35i.json')) as sample_file:
|
283
|
+
info = json.load(sample_file)
|
284
|
+
self.device_info_helper.info.update(info)
|
285
|
+
|
286
|
+
async def test_attributes(self):
|
287
|
+
|
288
|
+
await self.device.refresh()
|
289
|
+
self.api.device_info.assert_awaited_with("fake-name-api", "fake-uuid")
|
290
|
+
|
291
|
+
with assert_fully_checked(self.device) as device:
|
292
|
+
|
293
|
+
assert device.model == ModelEnum.HUMIDIFIER_H35I
|
294
|
+
|
295
|
+
assert device.pm1 is NotImplemented
|
296
|
+
assert device.pm2_5 is NotImplemented
|
297
|
+
assert device.pm10 is NotImplemented
|
298
|
+
assert device.tVOC is NotImplemented
|
299
|
+
assert device.temperature == 19
|
300
|
+
assert device.humidity == 50
|
301
|
+
assert device.name == "Bedroom"
|
302
|
+
assert device.firmware == "1.0.1"
|
303
|
+
assert device.mcu_firmware == "1.0.1"
|
304
|
+
assert device.serial_number == "111163300201110210004036"
|
305
|
+
assert device.sku == "111633"
|
306
|
+
|
307
|
+
assert device.running is True
|
308
|
+
assert device.standby is False
|
309
|
+
assert device.night_mode is False
|
310
|
+
assert device.germ_shield is NotImplemented
|
311
|
+
assert device.brightness == 49
|
312
|
+
assert device.child_lock is False
|
313
|
+
assert device.fan_speed == 24
|
314
|
+
assert device.fan_auto_mode is False
|
315
|
+
assert device.filter_usage_percentage is NotImplemented
|
316
|
+
assert device.wifi_working is True
|
317
|
+
assert device.wick_usage_percentage == 13
|
318
|
+
assert device.auto_regulated_humidity == 50
|
319
|
+
assert device.water_shortage is False
|
320
|
+
assert device.wick_dry_mode is False
|
321
|
+
|
322
|
+
|
323
|
+
class T10iTest(DeviceAwsTestBase):
|
324
|
+
"""Tests for T10i."""
|
325
|
+
|
326
|
+
def setUp(self):
|
327
|
+
super().setUp()
|
328
|
+
with open(resources.files().joinpath('device_info/T10i.json')) as sample_file:
|
329
|
+
info = json.load(sample_file)
|
330
|
+
self.device_info_helper.info.update(info)
|
331
|
+
|
332
|
+
async def test_attributes(self):
|
333
|
+
|
334
|
+
await self.device.refresh()
|
335
|
+
self.api.device_info.assert_awaited_with("fake-name-api", "fake-uuid")
|
336
|
+
|
337
|
+
with assert_fully_checked(self.device) as device:
|
338
|
+
|
339
|
+
assert device.model == ModelEnum.T10I
|
340
|
+
|
341
|
+
assert device.pm1 is NotImplemented
|
342
|
+
assert device.pm2_5 == 0
|
343
|
+
assert device.pm10 is NotImplemented
|
344
|
+
assert device.tVOC is NotImplemented
|
345
|
+
assert device.temperature == 18
|
346
|
+
assert device.humidity == 28
|
347
|
+
assert device.name == "Allen's Office"
|
348
|
+
assert device.firmware == "1.0.4"
|
349
|
+
assert device.mcu_firmware == "1.0.4"
|
350
|
+
assert device.serial_number == "111212400002313210001961"
|
351
|
+
assert device.sku == "112124"
|
352
|
+
|
353
|
+
assert device.running is True
|
354
|
+
assert device.standby is False
|
355
|
+
assert device.night_mode is NotImplemented
|
356
|
+
assert device.germ_shield is NotImplemented
|
357
|
+
assert device.brightness == 100
|
358
|
+
assert device.child_lock is False
|
359
|
+
assert device.fan_speed is NotImplemented
|
360
|
+
assert device.fan_auto_mode is NotImplemented
|
361
|
+
assert device.filter_usage_percentage == 0
|
362
|
+
assert device.wifi_working is True
|
363
|
+
assert device.wick_usage_percentage is NotImplemented
|
364
|
+
assert device.auto_regulated_humidity is NotImplemented
|
365
|
+
assert device.water_shortage is NotImplemented
|
366
|
+
assert device.wick_dry_mode is NotImplemented
|
@@ -0,0 +1,127 @@
|
|
1
|
+
import dataclasses
|
2
|
+
from unittest import TestCase
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
|
6
|
+
from blueair_api import intermediate_representation_aws as ir
|
7
|
+
|
8
|
+
class SensorPackTest(TestCase):
|
9
|
+
|
10
|
+
def testSimple(self):
|
11
|
+
sp = ir.SensorPack( [
|
12
|
+
{'n': 'v', 't': 1, 'v': 1},
|
13
|
+
{'n': 'vb', 't': 2, 'vb': True},
|
14
|
+
{'n': 'vs', 't': 3, 'vs': "s"},
|
15
|
+
{'n': 'vd', 't': 4, 'vd': "MTIzCg=="}, # b'123\n'
|
16
|
+
{'n': 'no_t', 'v': 1},
|
17
|
+
{'n': 'no_s', 'v': 1},
|
18
|
+
{'n': 's', 'v': 1, 's': 2},
|
19
|
+
{'n': 'u', 'v': 1, 'u': "unit"},
|
20
|
+
{'n': 'no_u', 'v': 1},
|
21
|
+
])
|
22
|
+
|
23
|
+
latest = sp.to_latest()
|
24
|
+
|
25
|
+
assert latest['v'].timestamp == 1
|
26
|
+
assert latest['v'].value == 1
|
27
|
+
assert isinstance(latest['v'].value, float)
|
28
|
+
|
29
|
+
assert latest['vb'].timestamp == 2
|
30
|
+
assert latest['vb'].value is True
|
31
|
+
assert isinstance(latest['vb'].value, bool)
|
32
|
+
|
33
|
+
assert latest['vs'].timestamp == 3
|
34
|
+
assert latest['vs'].value == "s"
|
35
|
+
assert isinstance(latest['vs'].value, str)
|
36
|
+
|
37
|
+
assert latest['vd'].timestamp == 4
|
38
|
+
assert latest['vd'].value == b"123\n"
|
39
|
+
assert isinstance(latest['vd'].value, bytes)
|
40
|
+
|
41
|
+
assert latest['no_t'].timestamp is None
|
42
|
+
|
43
|
+
assert latest['no_s'].integral is None
|
44
|
+
assert latest['s'].integral == 2
|
45
|
+
assert latest['s'].value == 1
|
46
|
+
|
47
|
+
assert latest['no_u'].unit is None
|
48
|
+
assert latest['u'].unit == "unit"
|
49
|
+
|
50
|
+
def testToLatestMissingT1None(self):
|
51
|
+
sp = ir.SensorPack( [
|
52
|
+
{'n': 'missing_t', 't': 1, 'v': 1},
|
53
|
+
{'n': 'missing_t', 'v': 2},
|
54
|
+
])
|
55
|
+
|
56
|
+
latest = sp.to_latest()
|
57
|
+
assert latest['missing_t'].value == 2
|
58
|
+
|
59
|
+
def testToLatestMissingTNoneNone(self):
|
60
|
+
sp = ir.SensorPack( [
|
61
|
+
{'n': 'missing_t', 'v': 1},
|
62
|
+
{'n': 'missing_t', 'v': 2},
|
63
|
+
])
|
64
|
+
|
65
|
+
latest = sp.to_latest()
|
66
|
+
assert latest['missing_t'].value == 2
|
67
|
+
|
68
|
+
def testToLatestMissingTNone1(self):
|
69
|
+
sp = ir.SensorPack( [
|
70
|
+
{'n': 'missing_t', 'v': 1},
|
71
|
+
{'n': 'missing_t', 't': 1, 'v': 2},
|
72
|
+
])
|
73
|
+
|
74
|
+
latest = sp.to_latest()
|
75
|
+
assert latest['missing_t'].value == 2
|
76
|
+
|
77
|
+
def testToLatestReversedOrder(self):
|
78
|
+
sp = ir.SensorPack( [
|
79
|
+
{'n': 'missing_t', 't': 2, 'v': 2},
|
80
|
+
{'n': 'missing_t', 't': 1, 'v': 1},
|
81
|
+
])
|
82
|
+
|
83
|
+
latest = sp.to_latest()
|
84
|
+
assert latest['missing_t'].value == 2
|
85
|
+
|
86
|
+
|
87
|
+
class QueryJsonTest(TestCase):
|
88
|
+
|
89
|
+
def test_mapping_one(self):
|
90
|
+
assert ir.query_json({"a": 0}, "a") == 0
|
91
|
+
|
92
|
+
def test_mapping_two(self):
|
93
|
+
assert ir.query_json({"a": {"b": 0}}, "a.b") == 0
|
94
|
+
|
95
|
+
def test_sequence(self):
|
96
|
+
assert ir.query_json([{"a": 1}], "0.a") == 1
|
97
|
+
|
98
|
+
def test_none(self):
|
99
|
+
# last segment not found produces None.
|
100
|
+
assert ir.query_json([{"a": 1}], "0.r") is None
|
101
|
+
|
102
|
+
def test_scalar_error(self):
|
103
|
+
with pytest.raises(KeyError):
|
104
|
+
ir.query_json(3, "0.r")
|
105
|
+
|
106
|
+
def test_key_error(self):
|
107
|
+
with pytest.raises(KeyError):
|
108
|
+
# intermediate segment not found produces KeyError
|
109
|
+
ir.query_json({"a": {"b": 3}}, "r.b")
|
110
|
+
|
111
|
+
@dataclasses.dataclass
|
112
|
+
class FakeObjectType:
|
113
|
+
a : str
|
114
|
+
extra_fields: ir.MappingType
|
115
|
+
|
116
|
+
class ParseJsonTest(TestCase):
|
117
|
+
|
118
|
+
def test_mapping_one(self):
|
119
|
+
d = ir.parse_json(FakeObjectType, {
|
120
|
+
"one" :{"a": "1", "e" : 1},
|
121
|
+
"two" :{"a": "2", "e" : 2},
|
122
|
+
})
|
123
|
+
assert d == {
|
124
|
+
"one" : FakeObjectType(a="1", extra_fields={"e":1}),
|
125
|
+
"two" : FakeObjectType(a="2", extra_fields={"e":2}),
|
126
|
+
}
|
127
|
+
|
@@ -1,156 +0,0 @@
|
|
1
|
-
import dataclasses
|
2
|
-
import logging
|
3
|
-
|
4
|
-
from .callbacks import CallbacksMixin
|
5
|
-
from .http_aws_blueair import HttpAwsBlueair
|
6
|
-
from .model_enum import ModelEnum
|
7
|
-
from .util import convert_api_array_to_dict, safely_get_json_value
|
8
|
-
|
9
|
-
_LOGGER = logging.getLogger(__name__)
|
10
|
-
|
11
|
-
|
12
|
-
@dataclasses.dataclass(slots=True)
|
13
|
-
class DeviceAws(CallbacksMixin):
|
14
|
-
@classmethod
|
15
|
-
async def create_device(cls, api, uuid, name, mac, type_name, refresh=False):
|
16
|
-
_LOGGER.debug("UUID:"+uuid)
|
17
|
-
device_aws = DeviceAws(
|
18
|
-
api=api,
|
19
|
-
uuid=uuid,
|
20
|
-
name_api=name,
|
21
|
-
mac=mac,
|
22
|
-
type_name=type_name,
|
23
|
-
)
|
24
|
-
if refresh:
|
25
|
-
await device_aws.refresh()
|
26
|
-
_LOGGER.debug(f"create_device blueair device_aws: {device_aws}")
|
27
|
-
return device_aws
|
28
|
-
|
29
|
-
api: HttpAwsBlueair
|
30
|
-
uuid: str = None
|
31
|
-
name: str = None
|
32
|
-
name_api: str = None
|
33
|
-
mac: str = None
|
34
|
-
type_name: str = None
|
35
|
-
|
36
|
-
sku: str = None
|
37
|
-
firmware: str = None
|
38
|
-
mcu_firmware: str = None
|
39
|
-
serial_number: str = None
|
40
|
-
|
41
|
-
brightness: int = None
|
42
|
-
child_lock: bool = None
|
43
|
-
fan_speed: int = None
|
44
|
-
fan_auto_mode: bool = None
|
45
|
-
running: bool = None
|
46
|
-
night_mode: bool = None
|
47
|
-
germ_shield: bool = None
|
48
|
-
|
49
|
-
pm1: int = None
|
50
|
-
pm2_5: int = None
|
51
|
-
pm10: int = None
|
52
|
-
tVOC: int = None
|
53
|
-
temperature: int = None
|
54
|
-
humidity: int = None
|
55
|
-
filter_usage: int = None # percentage
|
56
|
-
wifi_working: bool = None
|
57
|
-
|
58
|
-
# i35
|
59
|
-
wick_usage: int = None # percentage
|
60
|
-
wick_dry_mode: bool = None
|
61
|
-
water_shortage: bool = None
|
62
|
-
auto_regulated_humidity: int = None
|
63
|
-
|
64
|
-
async def refresh(self):
|
65
|
-
_LOGGER.debug(f"refreshing blueair device aws: {self}")
|
66
|
-
info = await self.api.device_info(self.name_api, self.uuid)
|
67
|
-
sensor_data = convert_api_array_to_dict(info["sensordata"])
|
68
|
-
self.pm1 = safely_get_json_value(sensor_data, "pm1", int)
|
69
|
-
self.pm2_5 = safely_get_json_value(sensor_data, "pm2_5", int)
|
70
|
-
self.pm10 = safely_get_json_value(sensor_data, "pm10", int)
|
71
|
-
self.tVOC = safely_get_json_value(sensor_data, "tVOC", int)
|
72
|
-
self.temperature = safely_get_json_value(sensor_data, "t", int)
|
73
|
-
self.humidity = safely_get_json_value(sensor_data, "h", int)
|
74
|
-
|
75
|
-
self.name = safely_get_json_value(info, "configuration.di.name")
|
76
|
-
self.firmware = safely_get_json_value(info, "configuration.di.cfv")
|
77
|
-
self.mcu_firmware = safely_get_json_value(info, "configuration.di.mfv")
|
78
|
-
self.serial_number = safely_get_json_value(info, "configuration.di.ds")
|
79
|
-
self.sku = safely_get_json_value(info, "configuration.di.sku")
|
80
|
-
|
81
|
-
states = convert_api_array_to_dict(info["states"])
|
82
|
-
self.running = safely_get_json_value(states, "standby") is False
|
83
|
-
self.night_mode = safely_get_json_value(states, "nightmode", bool)
|
84
|
-
self.germ_shield = safely_get_json_value(states, "germshield", bool)
|
85
|
-
self.brightness = safely_get_json_value(states, "brightness", int)
|
86
|
-
self.child_lock = safely_get_json_value(states, "childlock", bool)
|
87
|
-
self.fan_speed = safely_get_json_value(states, "fanspeed", int)
|
88
|
-
self.fan_auto_mode = safely_get_json_value(states, "automode", bool)
|
89
|
-
self.filter_usage = safely_get_json_value(states, "filterusage", int)
|
90
|
-
self.wifi_working = safely_get_json_value(states, "online", bool)
|
91
|
-
self.wick_usage = safely_get_json_value(states, "wickusage", int)
|
92
|
-
self.wick_dry_mode = safely_get_json_value(states, "wickdrys", bool)
|
93
|
-
self.auto_regulated_humidity = safely_get_json_value(states, "autorh", int)
|
94
|
-
self.water_shortage = safely_get_json_value(states, "wshortage", bool)
|
95
|
-
|
96
|
-
self.publish_updates()
|
97
|
-
_LOGGER.debug(f"refreshed blueair device aws: {self}")
|
98
|
-
|
99
|
-
async def set_brightness(self, value: int):
|
100
|
-
self.brightness = value
|
101
|
-
await self.api.set_device_info(self.uuid, "brightness", "v", value)
|
102
|
-
self.publish_updates()
|
103
|
-
|
104
|
-
async def set_fan_speed(self, value: int):
|
105
|
-
self.fan_speed = value
|
106
|
-
await self.api.set_device_info(self.uuid, "fanspeed", "v", value)
|
107
|
-
self.publish_updates()
|
108
|
-
|
109
|
-
async def set_running(self, running: bool):
|
110
|
-
self.running = running
|
111
|
-
await self.api.set_device_info(self.uuid, "standby", "vb", not running)
|
112
|
-
self.publish_updates()
|
113
|
-
|
114
|
-
async def set_fan_auto_mode(self, fan_auto_mode: bool):
|
115
|
-
self.fan_auto_mode = fan_auto_mode
|
116
|
-
await self.api.set_device_info(self.uuid, "automode", "vb", fan_auto_mode)
|
117
|
-
self.publish_updates()
|
118
|
-
|
119
|
-
async def set_auto_regulated_humidity(self, value: int):
|
120
|
-
self.auto_regulated_humidity = value
|
121
|
-
await self.api.set_device_info(self.uuid, "autorh", "v", value)
|
122
|
-
self.publish_updates()
|
123
|
-
|
124
|
-
async def set_child_lock(self, child_lock: bool):
|
125
|
-
self.child_lock = child_lock
|
126
|
-
await self.api.set_device_info(self.uuid, "childlock", "vb", child_lock)
|
127
|
-
self.publish_updates()
|
128
|
-
|
129
|
-
async def set_night_mode(self, night_mode: bool):
|
130
|
-
self.night_mode = night_mode
|
131
|
-
await self.api.set_device_info(self.uuid, "nightmode", "vb", night_mode)
|
132
|
-
self.publish_updates()
|
133
|
-
|
134
|
-
async def set_wick_dry_mode(self, value: bool):
|
135
|
-
self.wick_dry_mode = value
|
136
|
-
await self.api.set_device_info(self.uuid, "wickdrys", "vb", value)
|
137
|
-
self.publish_updates()
|
138
|
-
|
139
|
-
@property
|
140
|
-
def model(self) -> ModelEnum:
|
141
|
-
if self.sku == "111633":
|
142
|
-
return ModelEnum.HUMIDIFIER_H35I
|
143
|
-
if self.sku == "105820":
|
144
|
-
return ModelEnum.PROTECT_7440I
|
145
|
-
if self.sku == "105826":
|
146
|
-
return ModelEnum.PROTECT_7470I
|
147
|
-
if self.sku == "110059":
|
148
|
-
return ModelEnum.MAX_211I
|
149
|
-
if self.sku == "110092":
|
150
|
-
return ModelEnum.MAX_311I
|
151
|
-
if self.sku == "110057":
|
152
|
-
return ModelEnum.MAX_411I
|
153
|
-
if self.sku == "112124":
|
154
|
-
return ModelEnum.T10I
|
155
|
-
return ModelEnum.UNKNOWN
|
156
|
-
|
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
|
File without changes
|
File without changes
|