Skip to content

Commit a5016d9

Browse files
committed
Added test suite for message translation
1 parent 077310f commit a5016d9

File tree

6 files changed

+221
-1
lines changed

6 files changed

+221
-1
lines changed

.github/workflows/checks.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ jobs:
2020

2121
- name: Install dependencies
2222
run: |
23-
pip install mypy
23+
pip install -r requirements.test.txt
2424
2525
- name: Run mypy checks
2626
run: |
2727
python3 -m mypy --show-error-codes --show-column-numbers src
28+
29+
- name: Run pytest checks
30+
run: |
31+
scripts/run-tests.sh

requirements.test.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mypy
2+
pytest
3+
pytest-coverage

scripts/run-tests.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
rootdir="$(realpath "$(dirname "$0")/..")"
4+
testsdir="${rootdir}/tests"
5+
6+
export PYTHONDONTWRITEBYTECODE=1
7+
export PYTHONPATH="${rootdir}/src"
8+
export PYTEST_COVERAGE_DATA_FILE="${testsdir}/.coverage"
9+
10+
extra=
11+
[ -z "$@" ] && extra="--cov-fail-under=100"
12+
13+
echo "Linting tests..."
14+
mypy --ignore-missing-imports "${testsdir}"
15+
16+
pytest \
17+
--cov=powersensor_local.xlatemsg \
18+
--cov-report term-missing \
19+
--cov-config="${testsdir}/.coveragerc" \
20+
--cache-clear \
21+
--capture=no \
22+
"${testsdir}" \
23+
"${extra}" \
24+
"$@"

tests/.coveragerc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[run]
2+
data_file = ${PYTEST_COVERAGE_DATA_FILE}

tests/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.coverage

tests/test_xlatemsg.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import json
2+
import pytest
3+
from powersensor_local.xlatemsg import translate_raw_message
4+
5+
RELAY_MAC = '246f280487a4'
6+
7+
ROLE_HOUSENET = 'house-net'
8+
ROLE_SOLAR = 'solar'
9+
ROLE_WATER = 'water'
10+
11+
def test_raw_woh_sensor_msg():
12+
"""Test normal whole-of-house message translation."""
13+
msg = json.loads('{"starttime": 1772164705, "raw_rssi": -91, "device": "sensor", "rssi": -89.0397534995969, "type": "instant_power", "summation_start": 1769972064, "batteryMicrovolt": 3803104, "mac": "bcddc247d1f5", "duration": 30, "role": "house-net", "power": 992, "unit": "w", "summation": -281230401}')
14+
res = translate_raw_message(msg, RELAY_MAC)
15+
16+
average_power = {'mac': 'bcddc247d1f5', 'role': ROLE_HOUSENET, 'starttime_utc': 1772164705, 'watts': 992, 'duration_s': 30, 'via': RELAY_MAC}
17+
battery_level = {'mac': 'bcddc247d1f5', 'role': ROLE_HOUSENET, 'starttime_utc': 1772164705, 'volts': 3.803104, 'via': RELAY_MAC}
18+
radio_signal_quality = {'mac': 'bcddc247d1f5', 'role': ROLE_HOUSENET, 'starttime_utc': 1772164705, 'duration_s': 30, 'average_rssi': -89.0, 'last_rssi': -91, 'via': RELAY_MAC}
19+
summation_energy = {'mac': 'bcddc247d1f5', 'role': ROLE_HOUSENET, 'starttime_utc': 1772164705, 'summation_joules': -281230401, 'summation_resettime_utc': 1769972064, 'via': RELAY_MAC}
20+
21+
assert(len(res) == 4)
22+
assert(res['average_power'] == average_power)
23+
assert(res['battery_level'] == battery_level)
24+
assert(res['radio_signal_quality'] == radio_signal_quality)
25+
assert(res['summation_energy'] == summation_energy)
26+
27+
28+
def test_raw_woh_sensor_msg_no_summation_start():
29+
"""Test legacy firmware without summation_start field."""
30+
msg = json.loads('{"starttime": 1772164705, "raw_rssi": -91, "device": "sensor", "rssi": -89.0397534995969, "type": "instant_power", "batteryMicrovolt": 3803104, "mac": "bcddc247d1f5", "duration": 30, "role": "house-net", "power": 992, "unit": "w", "summation": -281230401}')
31+
res = translate_raw_message(msg, RELAY_MAC)
32+
33+
average_power = {'mac': 'bcddc247d1f5', 'role': ROLE_HOUSENET, 'starttime_utc': 1772164705, 'watts': 992, 'duration_s': 30, 'via': RELAY_MAC}
34+
battery_level = {'mac': 'bcddc247d1f5', 'role': ROLE_HOUSENET, 'starttime_utc': 1772164705, 'volts': 3.803104, 'via': RELAY_MAC}
35+
radio_signal_quality = {'mac': 'bcddc247d1f5', 'role': ROLE_HOUSENET, 'starttime_utc': 1772164705, 'duration_s': 30, 'average_rssi': -89.0, 'last_rssi': -91, 'via': RELAY_MAC}
36+
37+
assert(len(res) == 3)
38+
assert(res['average_power'] == average_power)
39+
assert(res['battery_level'] == battery_level)
40+
assert(res['radio_signal_quality'] == radio_signal_quality)
41+
42+
43+
def test_raw_woh_sensor_msg_malformed():
44+
"""Verify reports with missing field raises an exception."""
45+
msg = json.loads('{"raw_rssi": -91, "device": "sensor", "rssi": -89.0397534995969, "type": "instant_power", "summation_start": 1769972064, "batteryMicrovolt": 3803104, "mac": "bcddc247d1f5", "duration": 30, "role": "house-net", "power": 992, "unit": "w", "summation": -281230401}')
46+
with pytest.raises(KeyError):
47+
translate_raw_message(msg, RELAY_MAC)
48+
49+
50+
def test_raw_solar_sensor_msg():
51+
"""Test regular solar sensor message translation."""
52+
msg = json.loads('{"starttime": 1772164915, "raw_rssi": -91, "device": "sensor", "rssi": -89.14350517179614, "type": "instant_power", "summation_start": 1770530454, "batteryMicrovolt": 4106208, "mac": "bcddc247d289", "duration": 30, "role": "solar", "power": -331, "unit": "w", "summation": -906357844}')
53+
res = translate_raw_message(msg, RELAY_MAC)
54+
55+
average_power = {'mac': 'bcddc247d289', 'role': ROLE_SOLAR, 'starttime_utc': 1772164915, 'watts': -331, 'duration_s': 30, 'via': RELAY_MAC}
56+
battery_level = {'mac': 'bcddc247d289', 'role': ROLE_SOLAR, 'starttime_utc': 1772164915, 'volts': 4.106208, 'via': RELAY_MAC}
57+
radio_signal_quality = {'mac': 'bcddc247d289', 'role': ROLE_SOLAR, 'starttime_utc': 1772164915, 'duration_s': 30, 'average_rssi': -89.1, 'last_rssi': -91, 'via': RELAY_MAC}
58+
summation_energy = {'mac': 'bcddc247d289', 'role': ROLE_SOLAR, 'starttime_utc': 1772164915, 'summation_joules': -906357844, 'summation_resettime_utc': 1770530454, 'via': RELAY_MAC}
59+
60+
assert(len(res) == 4)
61+
assert(res['average_power'] == average_power)
62+
assert(res['battery_level'] == battery_level)
63+
assert(res['radio_signal_quality'] == radio_signal_quality)
64+
assert(res['summation_energy'] == summation_energy)
65+
66+
67+
def test_raw_solar_sensor_msg_uncalibrated():
68+
"""Test uncalibrated solar sensor message translation."""
69+
msg = json.loads('{"starttime": 1772164915, "raw_rssi": -91, "device": "sensor", "rssi": -89.14350517179614, "type": "instant_power", "summation_start": 1770530454, "batteryMicrovolt": 4106208, "mac": "bcddc247d289", "duration": 30, "role": "solar", "power": -331, "unit": "U", "summation": -906357844}')
70+
res = translate_raw_message(msg, RELAY_MAC)
71+
72+
battery_level = {'mac': 'bcddc247d289', 'role': 'solar', 'starttime_utc': 1772164915, 'volts': 4.106208, 'via': '246f280487a4'}
73+
radio_signal_quality = {'mac': 'bcddc247d289', 'role': 'solar', 'starttime_utc': 1772164915, 'duration_s': 30, 'average_rssi': -89.1, 'last_rssi': -91, 'via': '246f280487a4'}
74+
uncalibrated_average_reading = {'mac': 'bcddc247d289', 'role': 'solar', 'starttime_utc': 1772164915, 'value': -331, 'duration_s': 30, 'via': '246f280487a4'}
75+
76+
assert(len(res) == 3)
77+
assert(res['battery_level'] == battery_level)
78+
assert(res['radio_signal_quality'] == radio_signal_quality)
79+
assert(res['uncalibrated_average_reading'] == uncalibrated_average_reading)
80+
81+
82+
def test_raw_plug_power_msg():
83+
"""Test regular plug message translation."""
84+
msg = json.loads('{"reactive_current": 0.006468, "type": "instant_power", "summation_start": 1770501947.113085, "count": 13, "duration": 1.039551, "role": "appliance", "power": 1.033781, "unit": "W", "device": "plug", "source": "BLE", "active_current": 0.004582, "mac": "246f280487a4", "voltage": 232.113508, "starttime": 1772164754.096228, "current": 0.14132, "borrowed_summation": 0.132574, "summation": 5103988.143331}')
85+
res = translate_raw_message(msg, RELAY_MAC)
86+
87+
average_power = {'mac': '246f280487a4', 'role': 'appliance', 'starttime_utc': 1772164754.096, 'watts': 1.0, 'duration_s': 1.04}
88+
average_power_components = {'mac': '246f280487a4', 'role': 'appliance', 'starttime_utc': 1772164754.096, 'apparent_current': 0.141, 'active_current': 0.005, 'reactive_current': 0.006, 'volts': 232.114}
89+
summation_energy = {'mac': '246f280487a4', 'role': 'appliance', 'starttime_utc': 1772164754.096, 'summation_joules': 5103988.0, 'summation_resettime_utc': 1770501947.0}
90+
91+
assert(len(res) == 3)
92+
assert(res['average_power'] == average_power)
93+
assert(res['average_power_components'] == average_power_components)
94+
assert(res['summation_energy'] == summation_energy)
95+
96+
97+
def test_raw_sensor_invalid_sample_msg():
98+
"""Test (rare) invalid sensor sample report."""
99+
msg = json.loads('{"starttime": 1772164705, "raw_rssi": -91, "device": "sensor", "rssi": -89.0397534995969, "type": "instant_power", "batteryMicrovolt": 3803104, "mac": "bcddc247d1f5", "duration": 30, "role": "house-net", "unit": "I"}')
100+
res = translate_raw_message(msg, RELAY_MAC)
101+
102+
battery_level = {'mac': 'bcddc247d1f5', 'role': ROLE_HOUSENET, 'starttime_utc': 1772164705, 'volts': 3.803104, 'via': RELAY_MAC}
103+
radio_signal_quality = {'mac': 'bcddc247d1f5', 'role': ROLE_HOUSENET, 'starttime_utc': 1772164705, 'duration_s': 30, 'average_rssi': -89.0, 'last_rssi': -91, 'via': RELAY_MAC}
104+
105+
assert(len(res) == 2)
106+
assert(res['battery_level'] == battery_level)
107+
assert(res['radio_signal_quality'] == radio_signal_quality)
108+
109+
110+
def test_raw_water_sensor__msg():
111+
"""Test regular water sensor message translation."""
112+
msg = json.loads('{"starttime": 1772164765, "raw_rssi": -89, "device": "sensor", "rssi": -87.03, "type": "instant_power", "summation_start": 1769972151, "batteryMicrovolt": 3612871, "mac": "bcddc247d338", "duration": 30, "role": "water", "power": 92, "unit": "L", "summation": 1321, "average_flow": 9.2, "summation_volume": 132.1}')
113+
res = translate_raw_message(msg, RELAY_MAC)
114+
115+
average_flow = {'mac': 'bcddc247d338', 'role': ROLE_WATER, 'starttime_utc': 1772164765, 'litres_per_minute': 9.2, 'duration_s': 30, 'via': RELAY_MAC}
116+
battery_level = {'mac': 'bcddc247d338', 'role': ROLE_WATER, 'starttime_utc': 1772164765, 'volts': 3.612871, 'via': RELAY_MAC}
117+
radio_signal_quality = {'mac': 'bcddc247d338', 'role': ROLE_WATER, 'starttime_utc': 1772164765, 'duration_s': 30, 'average_rssi': -87.0, 'last_rssi': -89, 'via': RELAY_MAC}
118+
summation_volume = {'mac': 'bcddc247d338', 'role': ROLE_WATER, 'starttime_utc': 1772164765, 'summation_litres': 132.1, 'summation_resettime_utc': 1769972151, 'via': RELAY_MAC}
119+
120+
assert(len(res) == 4)
121+
assert(res['average_flow'] == average_flow)
122+
assert(res['battery_level'] == battery_level)
123+
assert(res['radio_signal_quality'] == radio_signal_quality)
124+
assert(res['summation_volume'] == summation_volume)
125+
126+
127+
def test_raw_water_sensor__msg_legacy():
128+
"""Ensure old, unscaled water messages are suppressed cleanly."""
129+
msg = json.loads('{"starttime": 1772164765, "raw_rssi": -89, "device": "sensor", "rssi": -87.03, "type": "instant_power", "summation_start": 1769972151, "batteryMicrovolt": 3612871, "mac": "bcddc247d338", "duration": 30, "role": "water", "power": 92, "unit": "L", "summation": 1321}')
130+
res = translate_raw_message(msg, RELAY_MAC)
131+
132+
battery_level = {'mac': 'bcddc247d338', 'role': ROLE_WATER, 'starttime_utc': 1772164765, 'volts': 3.612871, 'via': RELAY_MAC}
133+
radio_signal_quality = {'mac': 'bcddc247d338', 'role': ROLE_WATER, 'starttime_utc': 1772164765, 'duration_s': 30, 'average_rssi': -87.0, 'last_rssi': -89, 'via': RELAY_MAC}
134+
135+
assert(len(res) == 2)
136+
assert(res['battery_level'] == battery_level)
137+
assert(res['radio_signal_quality'] == radio_signal_quality)
138+
139+
140+
def test_unused_msg_auxiliary():
141+
"""Test unused message type doesn't propagate."""
142+
msg = json.loads('{"starttime": 1772164705, "device": "plug", "type": "auxiliary", "mac": "bcddc247d1f5", "role": "appliance"}')
143+
res = translate_raw_message(msg, RELAY_MAC)
144+
assert(len(res) == 0)
145+
146+
147+
def test_unused_msg_raw_waveform():
148+
"""Test unused message type doesn't propagate."""
149+
msg = json.loads('{"starttime": 1772164705, "device": "plug", "type": "raw_waveform", "mac": "bcddc247d1f5", "role": "appliance"}')
150+
res = translate_raw_message(msg, RELAY_MAC)
151+
assert(len(res) == 0)
152+
153+
154+
def test_unused_msg_adc():
155+
"""Test unused message type doesn't propagate."""
156+
msg = json.loads('{"starttime": 1772164705, "device": "plug", "type": "adc", "mac": "bcddc247d1f5", "role": "appliance"}')
157+
res = translate_raw_message(msg, RELAY_MAC)
158+
assert(len(res) == 0)
159+
160+
161+
def test_unused_msg_ble_stats():
162+
"""Test unused message type doesn't propagate."""
163+
msg = json.loads('{"starttime": 1772164705, "device": "plug", "type": "ble_stats", "mac": "bcddc247d1f5", "role": "appliance"}')
164+
res = translate_raw_message(msg, RELAY_MAC)
165+
assert(len(res) == 0)
166+
167+
168+
def test_unused_msg_sensor():
169+
"""Test unused message type doesn't propagate."""
170+
msg = json.loads('{"starttime": 1772164705, "device": "plug", "type": "sensor", "mac": "bcddc247d1f5", "role": "appliance"}')
171+
res = translate_raw_message(msg, RELAY_MAC)
172+
assert(len(res) == 0)
173+
174+
175+
def test_unused_msg_lrradio():
176+
"""Test unused message type doesn't propagate."""
177+
msg = json.loads('{"starttime": 1772164705, "device": "plug", "type": "lrradio", "mac": "bcddc247d1f5", "role": "appliance"}')
178+
res = translate_raw_message(msg, RELAY_MAC)
179+
assert(len(res) == 0)
180+
181+
182+
def test_unused_msg_plug_announce():
183+
"""Test unused message type doesn't propagate."""
184+
msg = json.loads('{"starttime": 1772164705, "device": "plug", "type": "plug_announce", "mac": "bcddc247d1f5", "role": "appliance"}')
185+
res = translate_raw_message(msg, RELAY_MAC)
186+
assert(len(res) == 0)

0 commit comments

Comments
 (0)