Add REG3 frame parser and fixtures

This commit is contained in:
2026-07-18 16:52:18 +02:00
parent ef7a480180
commit ce4bba673c
9 changed files with 531 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
# Fixtures REG3
Les fichiers binaires de ce dossier sont chacun un exemplaire réel extrait de
`captures/stream_normal.bin` :
- `metadata_95.bin` : 95 octets ;
- `frigo_163.bin` : 163 octets ;
- `regulation_227.bin` : 227 octets.
Ils permettent d'exécuter les tests du parseur sans versionner ni charger la
capture complète. Les limites de lectures TCP ne sont pas représentées par ces
fixtures.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+163
View File
@@ -0,0 +1,163 @@
"""Tests du parseur REG3 sans hypothèse sur les lectures TCP."""
from dataclasses import asdict, replace
from pathlib import Path
import pytest
from custom_components.arkteos.parser import (
BufferBoundsError,
FrameHeaderError,
FrameLengthError,
filter_values,
parse_frame,
parse_frigo_frame,
parse_metadata_frame,
parse_regulation_frame,
read_i8,
read_i16_le,
read_u8,
read_u16_le,
)
FIXTURES_PATH = Path(__file__).with_name("fixtures")
FIXTURE_FILES = {
95: "metadata_95.bin",
163: "frigo_163.bin",
227: "regulation_227.bin",
}
@pytest.fixture(scope="module")
def frames() -> dict[int, bytes]:
"""Charge les exemplaires réels versionnés, indépendamment du flux complet."""
loaded_frames = {
size: (FIXTURES_PATH / filename).read_bytes()
for size, filename in FIXTURE_FILES.items()
}
for expected_size, frame in loaded_frames.items():
assert len(frame) == expected_size
return loaded_frames
def test_read_u8() -> None:
assert read_u8(b"\x12", 0) == 18
def test_read_i8_positive() -> None:
assert read_i8(b"\x7f", 0) == 127
def test_read_i8_negative() -> None:
assert read_i8(b"\x80", 0) == -128
def test_read_u16_little_endian() -> None:
assert read_u16_le(b"\x34\x12", 0) == 0x1234
def test_read_i16_little_endian_positive() -> None:
assert read_i16_le(b"\xff\x7f", 0) == 32767
def test_read_i16_little_endian_negative() -> None:
assert read_i16_le(b"\x00\x80", 0) == -32768
def test_buffer_overrun_and_short_frame_are_controlled() -> None:
with pytest.raises(BufferBoundsError):
read_u16_le(b"\x00", 0)
with pytest.raises(FrameLengthError):
parse_frigo_frame(b"\x55\x00")
def test_bad_signature(frames: dict[int, bytes]) -> None:
frame = bytearray(frames[163])
frame[0] = 0
with pytest.raises(FrameHeaderError, match="signature"):
parse_frame(bytes(frame))
def test_type_and_length_inconsistency(frames: dict[int, bytes]) -> None:
frame = bytearray(frames[163])
frame[8] = 0x0B
with pytest.raises(FrameHeaderError, match="type"):
parse_frame(bytes(frame))
def test_header_length_inconsistency(frames: dict[int, bytes]) -> None:
frame = bytearray(frames[227])
frame[10:12] = (80).to_bytes(2, "little")
with pytest.raises(FrameHeaderError, match="longueur"):
parse_frame(bytes(frame))
def test_unknown_frame_length() -> None:
with pytest.raises(FrameLengthError, match="inconnue"):
parse_frame(b"\x00" * 96)
def test_parse_real_metadata_frame(frames: dict[int, bytes]) -> None:
frame = parse_metadata_frame(frames[95])
assert frame.frame_type == "metadata"
def test_parse_real_frigo_frame(frames: dict[int, bytes]) -> None:
frame = parse_frigo_frame(frames[163])
assert frame.frame_type == "frigo"
assert frame.exterieur_temp == 32.0
def test_parse_real_regulation_frame(frames: dict[int, bytes]) -> None:
frame = parse_regulation_frame(frames[227])
assert frame.frame_type == "regulation"
assert frame.modele_pac_s == "BAGUIO_ZURAN_IV"
def test_values_match_node_red_calculations(frames: dict[int, bytes]) -> None:
frigo = parse_frigo_frame(frames[163])
regulation = parse_regulation_frame(frames[227])
assert frigo.temps_compresseur == 22.4
assert frigo.nb_cycles_compresseur == 8300
assert frigo.dc_voltage == 327
assert regulation.temps_mise_sous_tension == 43
assert regulation.primaire_temp_eau_aller == 54.9
assert regulation.ecs_temp_eau_milieu == 60.5
assert regulation.nb_cycles_compresseur_reg == 7827
def test_filter_keeps_valid_value(frames: dict[int, bytes]) -> None:
result = filter_values(parse_frigo_frame(frames[163]))
assert result.accepted["exterieur_temp"] == 32.0
assert result.rejected == {}
def test_filter_rejects_aberrant_temperature(frames: dict[int, bytes]) -> None:
frame = replace(parse_frigo_frame(frames[163]), exterieur_temp=150.1)
result = filter_values(frame)
assert "exterieur_temp" not in result.accepted
assert result.rejected["exterieur_temp"] == 150.1
def test_filter_rejects_aberrant_pressure(frames: dict[int, bytes]) -> None:
frame = replace(parse_regulation_frame(frames[227]), primaire_pression=10.1)
result = filter_values(frame)
assert "primaire_pression" not in result.accepted
assert result.rejected["primaire_pression"] == 10.1
def test_filter_keeps_text_statuses(frames: dict[int, bytes]) -> None:
frigo_result = filter_values(parse_frigo_frame(frames[163]))
regulation_result = filter_values(parse_regulation_frame(frames[227]))
assert frigo_result.accepted["statut_frigo_s"] == "Arret"
assert regulation_result.accepted["statut_pac_s"] == "Attente"
def test_metadata_has_no_invented_fields(frames: dict[int, bytes]) -> None:
frame = parse_metadata_frame(frames[95])
result = filter_values(frame)
assert asdict(frame) == {"frame_type": "metadata"}
assert result.accepted == {"frame_type": "metadata"}
assert result.rejected == {}