Skip to content

Commit 822d4e3

Browse files
sidey79claude
andcommitted
feat(decoders): add the enablers for the sensor decoding layer
Groundwork for ADR-006. No decoding happens yet, but everything stage 2 needs is now in place: * the demodulated bit string is carried in metadata["bits"] for MS, MU and MC messages. It was computed already but only its length was kept, and the FHEM decoding logic this layer mirrors works on bits, not on hex. * SensorEvent holds the interpreted measurements, and DecodedMessage gains a single optional `sensor` field, so no existing signature changes. The MQTT serializer drops that field, keeping state/messages byte identical for current consumers. * tools/fhem_testdata_import.py vendors the FHEM test vectors into tests/data/fhem/. They carry the raw telegram, the expected stage 1 string and the expected stage 2 readings, which makes them the parity reference for both stages. Vendoring keeps the suite independent of a RFFHEM checkout sitting next to this one. The new baseline test pins stage 1 at 86 of 99 reproduced vectors and requires the five protocols with hardware behind them here (27, 50, 85, 125, 126) to match completely, so a stage 2 change cannot quietly damage stage 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBqRw2DPXbSzhGBqB55cFZ
1 parent 7f0b7c3 commit 822d4e3

9 files changed

Lines changed: 3331 additions & 1 deletion

File tree

‎sd_protocols/manchester.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ def _demodulate_mc_data(self, name: str, protocol_id: int, clock: int, raw_hex:
135135

136136
metadata = {
137137
"protocol_id": protocol_id,
138+
"bits": bit_data,
139+
"bit_length": len(bit_data),
138140
"rssi": None,
139141
"freq_afc": None,
140142
}

‎sd_protocols/message_synced.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ def demodulate_ms(self, msg_data: Dict[str, Any], msg_type: str = "MS") -> List[
234234
"protocol_id": pid,
235235
"payload": final_payload,
236236
"meta": {
237+
"bits": bit_str,
237238
"bit_length": len(bit_str),
238239
"rssi": msg_data.get('R'),
239240
"clock": clock_abs

‎sd_protocols/message_unsynced.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ def demodulate_mu(self, msg_data: Dict[str, Any], msg_type: str = "MU") -> List[
283283
"protocol_id": pid,
284284
"payload": final_payload,
285285
"meta": {
286+
"bits": bit_str,
286287
"bit_length": len(bit_str),
287288
"rssi": msg_data.get('R'),
288289
"clock": clock_abs

‎signalduino/mqtt.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,10 @@ def _raw_frame_to_dict(raw_frame: RawFrame) -> dict:
241241

242242
# Remove empty or non-useful fields for publication
243243
message_dict.pop("raw", None) # Do not publish raw frame data by default
244-
244+
# Stage 2 results have their own topics (sensors/, rtl433/, fhem/), so this
245+
# topic keeps the exact shape consumers already rely on. See ADR-006.
246+
message_dict.pop("sensor", None)
247+
245248
return json.dumps(message_dict, indent=4)
246249

247250
async def publish_simple(self, subtopic: str, payload: str, retain: bool = False) -> None:

‎signalduino/types.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,32 @@ class RawFrame:
2121
message_type: Optional[str] = None
2222

2323

24+
@dataclass(slots=True)
25+
class SensorEvent:
26+
"""Interpreted measurements of a single received frame (decoding stage 2).
27+
28+
While DecodedMessage carries the demodulated payload, this carries what the
29+
payload means: temperature, humidity, battery state and so on. The keys used
30+
in ``values`` are the FHEM reading names, because those are the de facto
31+
standard across the ported protocols and keep the parity tests against the
32+
FHEM test vectors a plain comparison. Output adapters map them to their own
33+
naming.
34+
"""
35+
36+
protocol_id: str
37+
model: str
38+
sensor_type: str
39+
device_id: str
40+
sensor_id: str
41+
values: dict[str, Any] = field(default_factory=dict)
42+
units: dict[str, str] = field(default_factory=dict)
43+
channel: Optional[int] = None
44+
raw_hex: str = ""
45+
dmsg: str = ""
46+
rssi: Optional[float] = None
47+
timestamp: datetime = field(default_factory=datetime.utcnow)
48+
49+
2450
@dataclass(slots=True)
2551
class DecodedMessage:
2652
"""Higher-level frame after running through the parser."""
@@ -29,6 +55,7 @@ class DecodedMessage:
2955
payload: str
3056
raw: RawFrame
3157
metadata: dict = field(default_factory=dict)
58+
sensor: Optional[SensorEvent] = None
3259

3360

3461
@dataclass(slots=True)

0 commit comments

Comments
 (0)