Feeds third-party BBQ probes into a pellet grill's wired probe jacks by presenting the resistance a real PT1000 RTD would show at that temperature. The grill sees an ordinary wired probe, so its display, app, target-temp alarms and Keep Warm all work with no protocol reversing involved. Pluggable probe sources: ThermoWorks RFX (via ThermoWorks Cloud), MEATER (community-derived BLE decode), a synthetic simulator for hardware-free development, and a Combustion stub. Two safety invariants are load-bearing: - Stale, missing or implausible readings drive the channel cold, never hot. A stuck-hot channel could convince the grill a cook finished early. - Unimplemented sources raise rather than returning plausible numbers, since the grill acts on these values. Cloud sources are dated by the cloud's own timestamp rather than by fetch time, because ThermoWorks serves a dead probe's last value with a fresh 200. Co-Authored-By: Claude Opus 5 <[email protected]>
62 lines
1.7 KiB
Python
62 lines
1.7 KiB
Python
"""MEATER decode tests.
|
|
|
|
These pin the community-derived decode so a refactor cannot silently change
|
|
it. They do NOT prove the decode is correct -- only bench comparison against
|
|
the MEATER app can do that. See docs/PROTOCOL_NOTES.md.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from smokescreen.sources.meater import decode_battery, decode_temperature
|
|
|
|
|
|
def payload(tip: int, ambient_raw: int, ambient_offset: int) -> bytes:
|
|
return (
|
|
tip.to_bytes(2, "little")
|
|
+ ambient_raw.to_bytes(2, "little")
|
|
+ ambient_offset.to_bytes(2, "little")
|
|
+ b"\x00\x00"
|
|
)
|
|
|
|
|
|
def test_tip_decodes_to_sixteenths_of_a_degree():
|
|
# 400 counts -> (400 + 8) / 16 = 25.5 C
|
|
tip_c, _ = decode_temperature(payload(400, 0, 0))
|
|
assert tip_c == pytest.approx(25.5)
|
|
|
|
|
|
def test_zero_counts_is_just_above_freezing():
|
|
tip_c, _ = decode_temperature(payload(0, 0, 0))
|
|
assert tip_c == pytest.approx(0.5)
|
|
|
|
|
|
def test_ambient_never_reads_below_tip():
|
|
"""The max(0, ...) term means ambient floors at the tip temperature."""
|
|
tip_c, ambient_c = decode_temperature(payload(1000, 0, 200))
|
|
assert ambient_c == pytest.approx(tip_c)
|
|
|
|
|
|
def test_ambient_rises_above_tip_when_raw_exceeds_offset():
|
|
tip_c, ambient_c = decode_temperature(payload(1000, 500, 48))
|
|
assert ambient_c > tip_c
|
|
|
|
|
|
def test_short_payload_is_rejected_not_guessed():
|
|
with pytest.raises(ValueError):
|
|
decode_temperature(b"\x01\x02")
|
|
|
|
|
|
def test_battery_scales_by_ten():
|
|
assert decode_battery((7).to_bytes(2, "little")) == 70
|
|
|
|
|
|
def test_battery_clamps_to_100():
|
|
assert decode_battery((25).to_bytes(2, "little")) == 100
|
|
|
|
|
|
def test_short_battery_payload_is_rejected():
|
|
with pytest.raises(ValueError):
|
|
decode_battery(b"\x01")
|