Files
opcda-diagnozer/src/opcda_diagnozer/quality.py
T
2026-10-08 07:26:04 +02:00

90 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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})"