Files
steveandClaude Opus 5 ba0c3b58b6 Initial commit: PT1000 probe emulation bridge
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]>
2026-08-16 22:46:14 -04:00

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")