"""OPC DA quality decode tables (SPECIFICATION §7). Quality is a 16-bit field: bits 7–6 of the low byte give the class (0xC0 GOOD / 0x40 UNCERTAIN / 0x00 BAD); bits 5–0 are the substatus. Every quality event must carry the raw hex code **and** the decoded name. """ from __future__ import annotations from dataclasses import dataclass from enum import Enum class QualityClass(str, Enum): GOOD = "GOOD" UNCERTAIN = "UNCERTAIN" BAD = "BAD" @dataclass(frozen=True) class QualityInfo: """Decoded quality for one read (SPECIFICATION §7).""" raw: int name: str quality_class: QualityClass @property def raw_hex(self) -> str: """Raw code formatted for events, e.g. ``0x18``.""" return f"0x{self.raw & 0xFF:02X}" # Codes from SPECIFICATION §7. # TODO(Phase 1): complete the remaining DA 2.05a UNCERTAIN subcodes in this # table — they decode by the same scheme (spec §7 closing note). _QUALITY_NAMES: dict[int, str] = { 0xC0: "GOOD", 0xD8: "GOOD_LOCAL_OVERRIDE", 0x40: "UNCERTAIN", 0x44: "UNCERTAIN_LAST_USABLE", 0x00: "BAD", 0x04: "BAD_CONFIG_ERROR", 0x08: "BAD_NOT_CONNECTED", 0x0C: "BAD_DEVICE_FAILURE", 0x10: "BAD_SENSOR_FAILURE", 0x14: "BAD_LAST_KNOWN_VALUE", 0x18: "BAD_COMM_FAILURE", 0x1C: "BAD_OUT_OF_SERVICE", } def _class_of(low: int) -> QualityClass: """Class comes from bits 7–6 of the low byte.""" bits = low & 0xC0 if bits == 0xC0: return QualityClass.GOOD if bits == 0x40: return QualityClass.UNCERTAIN return QualityClass.BAD def decode_quality(raw: int) -> QualityInfo: """Decode a raw OPC DA quality word into a named :class:`QualityInfo`.""" low = raw & 0xFF name = _QUALITY_NAMES.get(low) if name is None: name = f"{_class_of(low).value}_0x{low:02X}" return QualityInfo(raw=raw, name=name, quality_class=_class_of(low)) def status_text(raw: int) -> str: """Clear-text status with the raw code in parentheses (SPECIFICATION §7). Every reporter writes quality this way — e.g. ``BAD / COMM FAILURE (0x18)`` — so nobody decodes hex by hand while the raw code stays available for unambiguous correlation with vendor documentation. """ info = decode_quality(raw) classes = {c.value for c in QualityClass} name = info.name for cls in classes: prefix = cls + "_" if name.startswith(prefix): sub = name[len(prefix):].replace("_", " ") name = f"{cls} / {sub}" if sub else cls break else: name = name.replace("_", " ") return f"{name} ({info.raw_hex})"