@camstack/addon-provider-bosch 0.1.50 → 0.1.52
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.
- package/CHANGELOG.md +13 -0
- package/dist/{account-integration-offline-7CH3r5jI.js → account-integration-offline-BEzGeJcl.js} +1 -1
- package/dist/{account-integration-offline-B1Wgr_rT.mjs → account-integration-offline-D5vjmfia.mjs} +1 -1
- package/dist/{bosch-instances-CdvcBYye.mjs → bosch-instances-DSE6IXdU.mjs} +775 -213
- package/dist/{bosch-instances-CYBnIcHg.js → bosch-instances-LWbVnsaD.js} +780 -212
- package/dist/home-connect/addon.js +2 -2
- package/dist/home-connect/addon.mjs +2 -2
- package/dist/homecom/addon.js +789 -556
- package/dist/homecom/addon.mjs +789 -556
- package/dist/shc/addon.js +163 -133
- package/dist/shc/addon.mjs +165 -135
- package/package.json +6 -2
- package/python/homecom_normalize.py +300 -0
- package/python/homecom_worker.py +250 -0
- package/python/requirements.txt +2 -0
- package/python/test_homecom_worker.py +258 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""The worker's normalization of a `homecom_alt` K40 reading, and its stdin contract.
|
|
2
|
+
|
|
3
|
+
The fixture is shaped like the library's `BHCDeviceK40` (as `dataclasses.asdict` gives it),
|
|
4
|
+
with the values a live K40 air/water heat pump answered. Runs under pytest, or plainly:
|
|
5
|
+
`python3 test_homecom_worker.py`. Neither needs `homecom_alt` installed.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
HERE = Path(__file__).resolve().parent
|
|
13
|
+
sys.path.insert(0, str(HERE))
|
|
14
|
+
|
|
15
|
+
from homecom_normalize import normalize_reading, probe_outcome # noqa: E402
|
|
16
|
+
from homecom_worker import WorkerError, parse_request # noqa: E402
|
|
17
|
+
|
|
18
|
+
FORBIDDEN = {} # the library turns a 403/404 into an empty dict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def env(value, **extra):
|
|
22
|
+
return {"id": "/x", "type": "stringValue", "value": value, **extra}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def k40_device():
|
|
26
|
+
return {
|
|
27
|
+
"device": "101000000",
|
|
28
|
+
"notifications": [],
|
|
29
|
+
"holiday_mode": {"id": "/holidayMode/activeModes", "values": [], "allowedValues": []},
|
|
30
|
+
"away_mode": env("off", allowedValues=["on", "off"], writeable=1),
|
|
31
|
+
"power_limitation": FORBIDDEN,
|
|
32
|
+
"outdoor_temp": env(7.5, type="floatValue", unitOfMeasure="C"),
|
|
33
|
+
"heat_sources": {
|
|
34
|
+
"pumpType": env("air_water"),
|
|
35
|
+
"starts": {"id": "/heatSources/hs1/numberOfStarts", "type": "emonValue",
|
|
36
|
+
"values": [{"ch": 400}, {"total": 516}]},
|
|
37
|
+
"returnTemperature": env(47.8, type="floatValue"),
|
|
38
|
+
"actualSupplyTemperature": env(52.2, type="floatValue"),
|
|
39
|
+
"actualModulation": env(89, type="floatValue"),
|
|
40
|
+
"systemPressure": env(1.7, type="floatValue"),
|
|
41
|
+
"consumption": FORBIDDEN,
|
|
42
|
+
"totalWorkingTime": FORBIDDEN,
|
|
43
|
+
},
|
|
44
|
+
"dhw_circuits": [
|
|
45
|
+
{
|
|
46
|
+
"id": "/dhwCircuits/dhw1",
|
|
47
|
+
"operationMode": env("eco", allowedValues=["Off", "low", "high", "ownprogram", "eco"],
|
|
48
|
+
writeable=1),
|
|
49
|
+
"actualTemp": env(48.0, type="floatValue"),
|
|
50
|
+
"charge": env("stop", allowedValues=["start", "stop"], writeable=1),
|
|
51
|
+
"chargeRemainingTime": env(0, type="floatValue"),
|
|
52
|
+
"currentTemperatureLevel": env("eco", allowedValues=["off", "low", "high", "eco"],
|
|
53
|
+
writeable=0),
|
|
54
|
+
"singleChargeSetpoint": env(60, type="floatValue", minValue=50, maxValue=70),
|
|
55
|
+
"tempLevel": {"eco": env(51, type="floatValue", minValue=30, maxValue=60),
|
|
56
|
+
"low": FORBIDDEN},
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"heating_circuits": [
|
|
60
|
+
{
|
|
61
|
+
"id": "/heatingCircuits/hc1",
|
|
62
|
+
"operationMode": env("manual", allowedValues=["off", "manual", "auto"], writeable=1),
|
|
63
|
+
"currentSuWiMode": env("forced"),
|
|
64
|
+
"suWiSwitchMode": FORBIDDEN,
|
|
65
|
+
"heatCoolMode": env("heat", allowedValues=["heat", "cool", "heatCool"], writeable=0),
|
|
66
|
+
"roomTemp": env(-3276.8, type="floatValue", state=[{"open": -3276.8}, {"short": 3276.7}]),
|
|
67
|
+
"actualHumidity": FORBIDDEN,
|
|
68
|
+
"manualRoomSetpoint": env(30, type="floatValue", minValue=5, maxValue=30, writeable=1),
|
|
69
|
+
"currentRoomSetpoint": env(30, type="floatValue"),
|
|
70
|
+
"maxFlowTemp": env(60, type="floatValue", minValue=30, maxValue=60, writeable=1),
|
|
71
|
+
"temperatureLevelComfort2": None,
|
|
72
|
+
},
|
|
73
|
+
{"id": "/heatingCircuits/hc2"},
|
|
74
|
+
],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def extras():
|
|
79
|
+
return {
|
|
80
|
+
"silentMode": FORBIDDEN,
|
|
81
|
+
"additionalHeaterMode": FORBIDDEN,
|
|
82
|
+
"aggregateStarts": env(516, type="floatValue"),
|
|
83
|
+
"chargeDuration": {"dhw1": env(60, type="floatValue", minValue=60, maxValue=2880)},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_heat_source_values_come_from_the_k40_keys():
|
|
88
|
+
hs = normalize_reading(k40_device(), extras())["heatSource"]
|
|
89
|
+
assert hs["heatPumpType"] == "air_water"
|
|
90
|
+
assert hs["supplyTemp"] == 52.2
|
|
91
|
+
assert hs["returnTemp"] == 47.8
|
|
92
|
+
assert hs["modulationPct"] == 89
|
|
93
|
+
assert hs["pressureBar"] == 1.7
|
|
94
|
+
assert hs["starts"] == 516
|
|
95
|
+
assert hs["outdoorTemp"] == 7.5
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_a_forbidden_resource_is_null_never_zero():
|
|
99
|
+
reading = normalize_reading(k40_device(), extras())
|
|
100
|
+
assert reading["heatSource"]["thermalKwh"] is None
|
|
101
|
+
assert reading["heatSource"]["electricalKwh"] is None
|
|
102
|
+
assert reading["system"]["silent"] is None
|
|
103
|
+
assert reading["system"]["additionalHeater"] is None
|
|
104
|
+
hc1 = reading["heatingCircuits"][0]
|
|
105
|
+
assert hc1["suWiSwitchMode"] is None
|
|
106
|
+
assert hc1["humidity"] is None
|
|
107
|
+
assert hc1["comfortSetpoint"] is None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_a_sensor_fault_marker_is_null():
|
|
111
|
+
device = k40_device()
|
|
112
|
+
device["heat_sources"]["returnTemperature"] = env(
|
|
113
|
+
3276.7, type="floatValue", state=[{"open": -3276.8}, {"short": 3276.7}]
|
|
114
|
+
)
|
|
115
|
+
reading = normalize_reading(device, extras())
|
|
116
|
+
assert reading["heatSource"]["returnTemp"] is None
|
|
117
|
+
assert reading["heatingCircuits"][0]["roomTemp"] is None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_energy_is_split_from_the_emon_parts_and_electrical_needs_both_parts():
|
|
121
|
+
device = k40_device()
|
|
122
|
+
device["heat_sources"]["consumption"] = {
|
|
123
|
+
"type": "emonValue", "values": [{"outputProduced": 9000.5}, {"eheater": 12}, {"compressor": 2500}],
|
|
124
|
+
}
|
|
125
|
+
hs = normalize_reading(device, extras())["heatSource"]
|
|
126
|
+
assert hs["thermalKwh"] == 9000.5
|
|
127
|
+
assert hs["electricalKwh"] == 2512
|
|
128
|
+
device["heat_sources"]["consumption"] = {"type": "emonValue", "values": [{"compressor": 2500}]}
|
|
129
|
+
assert normalize_reading(device, extras())["heatSource"]["electricalKwh"] is None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_starts_fall_back_to_the_aggregate_when_hs1_has_no_total():
|
|
133
|
+
device = k40_device()
|
|
134
|
+
device["heat_sources"]["starts"] = FORBIDDEN
|
|
135
|
+
assert normalize_reading(device, extras())["heatSource"]["starts"] == 516
|
|
136
|
+
assert normalize_reading(device, {})["heatSource"]["starts"] is None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_circuits_keep_their_settings_with_limits_and_allowed_values():
|
|
140
|
+
reading = normalize_reading(k40_device(), extras())
|
|
141
|
+
assert [c["id"] for c in reading["heatingCircuits"]] == ["hc1", "hc2"]
|
|
142
|
+
hc1 = reading["heatingCircuits"][0]
|
|
143
|
+
assert hc1["operationMode"] == {"value": "manual", "min": None, "max": None,
|
|
144
|
+
"allowed": ["off", "manual", "auto"], "writeable": True}
|
|
145
|
+
assert hc1["heatCoolMode"]["writeable"] is False
|
|
146
|
+
assert hc1["manualRoomSetpoint"]["min"] == 5 and hc1["manualRoomSetpoint"]["max"] == 30
|
|
147
|
+
assert hc1["maxFlowTemp"]["value"] == 60
|
|
148
|
+
assert reading["heatingCircuits"][1]["operationMode"] is None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_a_heating_circuit_keeps_the_name_the_gateway_gave():
|
|
152
|
+
device = k40_device()
|
|
153
|
+
device["heating_circuits"][0]["name"] = "Living room"
|
|
154
|
+
circuits = normalize_reading(device, extras())["heatingCircuits"]
|
|
155
|
+
assert circuits[0]["name"] == "Living room"
|
|
156
|
+
assert circuits[1]["name"] is None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_a_heat_source_probe_separates_refused_from_absent_and_transient():
|
|
160
|
+
assert [probe_outcome(s) for s in (200, 403, 500, 404, 429, 503, 502)] == [
|
|
161
|
+
"answered", "refused", "refused", "absent", "transient", "transient", "transient"
|
|
162
|
+
]
|
|
163
|
+
probe = {"/heatSources/hs1/heatPumpType": "refused", "/x": "bogus", 3: "absent"}
|
|
164
|
+
reading = normalize_reading(k40_device(), {**extras(), "heatSourceProbe": probe})
|
|
165
|
+
assert reading["heatSourceProbe"] == {"/heatSources/hs1/heatPumpType": "refused"}
|
|
166
|
+
assert normalize_reading(k40_device(), extras())["heatSourceProbe"] == {}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_setpoint_limits_come_only_from_the_gateway_answer():
|
|
170
|
+
device = k40_device()
|
|
171
|
+
hc1 = device["heating_circuits"][0]
|
|
172
|
+
hc1["manualRoomSetpoint"] = env(30, type="floatValue", writeable=1)
|
|
173
|
+
hc1["temperatureLevelComfort2"] = env(22, type="floatValue", minValue=15, maxValue=28, writeable=1)
|
|
174
|
+
hc1["temperatureLevelEco"] = env(19, type="floatValue", minValue=10, maxValue=25, writeable=1)
|
|
175
|
+
out = normalize_reading(device, extras())["heatingCircuits"][0]
|
|
176
|
+
assert out["manualRoomSetpoint"]["min"] is None and out["manualRoomSetpoint"]["max"] is None
|
|
177
|
+
assert (out["comfortSetpoint"]["value"], out["comfortSetpoint"]["min"], out["comfortSetpoint"]["max"]) == (22, 15, 28)
|
|
178
|
+
assert (out["ecoSetpoint"]["value"], out["ecoSetpoint"]["min"], out["ecoSetpoint"]["max"]) == (19, 10, 25)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_dhw_carries_levels_charge_and_the_separately_read_duration():
|
|
182
|
+
dhw1 = normalize_reading(k40_device(), extras())["dhwCircuits"][0]
|
|
183
|
+
assert dhw1["id"] == "dhw1"
|
|
184
|
+
assert dhw1["actualTemp"] == 48.0
|
|
185
|
+
assert dhw1["charge"]["value"] == "stop"
|
|
186
|
+
assert dhw1["chargeDuration"]["max"] == 2880
|
|
187
|
+
assert dhw1["currentTemperatureLevel"]["value"] == "eco"
|
|
188
|
+
assert dhw1["currentSetpoint"] is None
|
|
189
|
+
assert list(dhw1["temperatureLevels"]) == ["eco"]
|
|
190
|
+
assert dhw1["temperatureLevels"]["eco"]["value"] == 51
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_holiday_answered_with_no_active_mode_is_an_empty_value_not_null():
|
|
194
|
+
reading = normalize_reading(k40_device(), extras())
|
|
195
|
+
assert reading["system"]["holiday"]["value"] == ""
|
|
196
|
+
assert reading["system"]["away"]["value"] == "off"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_icom_aliases_are_read():
|
|
200
|
+
device = {
|
|
201
|
+
"heat_sources": {
|
|
202
|
+
"type": env("Heatpump"),
|
|
203
|
+
"supplyTemperature": env(40.0, type="floatValue"),
|
|
204
|
+
"modulation": env(50, type="floatValue"),
|
|
205
|
+
"numberOfStarts": env(12, type="floatValue"),
|
|
206
|
+
"outdoorTemp": env(3.0, type="floatValue"),
|
|
207
|
+
},
|
|
208
|
+
"heating_circuits": [{"id": "/heatingCircuits/hc1", "roomtemperature": env(20.5, type="floatValue")}],
|
|
209
|
+
"dhw_circuits": [{"id": "/dhwCircuits/dhw1", "chargeDuration": env(90, type="floatValue"),
|
|
210
|
+
"currentSetpoint": env(52, type="floatValue")}],
|
|
211
|
+
}
|
|
212
|
+
reading = normalize_reading(device, {})
|
|
213
|
+
assert reading["heatSource"]["heatPumpType"] == "Heatpump"
|
|
214
|
+
assert reading["heatSource"]["supplyTemp"] == 40.0
|
|
215
|
+
assert reading["heatSource"]["modulationPct"] == 50
|
|
216
|
+
assert reading["heatSource"]["starts"] == 12
|
|
217
|
+
assert reading["heatSource"]["outdoorTemp"] == 3.0
|
|
218
|
+
assert reading["heatingCircuits"][0]["roomTemp"] == 20.5
|
|
219
|
+
assert reading["dhwCircuits"][0]["chargeDuration"]["value"] == 90
|
|
220
|
+
assert reading["dhwCircuits"][0]["currentSetpoint"]["value"] == 52
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_the_request_is_validated_before_the_library_loads():
|
|
224
|
+
good = {"token": "t", "gatewayId": "g", "deviceType": "k40", "action": "read"}
|
|
225
|
+
assert parse_request(json.dumps(good))["action"] == "read"
|
|
226
|
+
for bad in (
|
|
227
|
+
"not json",
|
|
228
|
+
json.dumps({**good, "token": ""}),
|
|
229
|
+
json.dumps({**good, "deviceType": "rac"}),
|
|
230
|
+
json.dumps({**good, "action": "write", "write": {"op": "rm -rf"}}),
|
|
231
|
+
):
|
|
232
|
+
try:
|
|
233
|
+
parse_request(bad)
|
|
234
|
+
except WorkerError as err:
|
|
235
|
+
assert err.kind == "bad-request"
|
|
236
|
+
else:
|
|
237
|
+
raise AssertionError(f"accepted {bad}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_a_bad_request_answers_one_json_line_without_echoing_the_token():
|
|
241
|
+
secret = "secret-token-value"
|
|
242
|
+
proc = subprocess.run(
|
|
243
|
+
[sys.executable, str(HERE / "homecom_worker.py")],
|
|
244
|
+
input=json.dumps({"token": secret, "gatewayId": "g", "deviceType": "rac", "action": "read"}),
|
|
245
|
+
capture_output=True, text=True, timeout=30,
|
|
246
|
+
)
|
|
247
|
+
assert proc.returncode == 1
|
|
248
|
+
answer = json.loads(proc.stdout.strip())
|
|
249
|
+
assert answer["ok"] is False and answer["kind"] == "bad-request"
|
|
250
|
+
assert secret not in proc.stdout and secret not in proc.stderr
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
if __name__ == "__main__":
|
|
254
|
+
tests = [(name, fn) for name, fn in sorted(globals().items()) if name.startswith("test_")]
|
|
255
|
+
for name, fn in tests:
|
|
256
|
+
fn()
|
|
257
|
+
print(f"ok {name}")
|
|
258
|
+
print(f"{len(tests)} passed")
|