🚧 WIP
This commit is contained in:
@@ -23,6 +23,10 @@ tags:
|
|||||||
# convention: inverted fault flags -> [false, 0], string states -> ["RUN"].
|
# convention: inverted fault flags -> [false, 0], string states -> ["RUN"].
|
||||||
# Compared by string equality on the rendered value ("True"/"1"/"RUN").
|
# Compared by string equality on the rendered value ("True"/"1"/"RUN").
|
||||||
# Do NOT also list these under tags: - they are polled automatically.
|
# Do NOT also list these under tags: - they are polled automatically.
|
||||||
|
#
|
||||||
|
# tags: and devices: are each optional (devices-only monitors just the links;
|
||||||
|
# tags-only runs classic surveillance) — but both empty is rejected:
|
||||||
|
# the tool would read nothing.
|
||||||
devices:
|
devices:
|
||||||
- { name: "Conveyor 3", tag: "Channel1.Device3.Connected" } # ok: true/1
|
- { name: "Conveyor 3", tag: "Channel1.Device3.Connected" } # ok: true/1
|
||||||
- { name: "Conveyor 4", tag: "Channel1.Device4.Connected", ok_values: [true, 1] }
|
- { name: "Conveyor 4", tag: "Channel1.Device4.Connected", ok_values: [true, 1] }
|
||||||
|
|||||||
@@ -137,6 +137,12 @@ an ok value again. A value that is neither in `ok_values` nor a recognized
|
|||||||
fault shape still logs DOWN — the normal state is explicit, and everything
|
fault shape still logs DOWN — the normal state is explicit, and everything
|
||||||
else is "not normal" by definition.
|
else is "not normal" by definition.
|
||||||
|
|
||||||
|
**Section optionality:** `tags:` and `devices:` are each optional in the
|
||||||
|
config — a devices-only setup monitors just the links, a tags-only setup
|
||||||
|
runs classic quality/latency surveillance. Both empty (or absent) is a
|
||||||
|
validation error: the tool would read nothing and every heartbeat would
|
||||||
|
report `tags_total: 0`.
|
||||||
|
|
||||||
## 7. OPC DA quality codes (reference)
|
## 7. OPC DA quality codes (reference)
|
||||||
|
|
||||||
OPC DA quality is a 16-bit field. Bits 7–6 of the low byte give the class:
|
OPC DA quality is a 16-bit field. Bits 7–6 of the low byte give the class:
|
||||||
|
|||||||
@@ -276,9 +276,11 @@ def _validate(config: Config) -> None:
|
|||||||
)
|
)
|
||||||
if config.opc.connect_timeout_s <= 0:
|
if config.opc.connect_timeout_s <= 0:
|
||||||
raise ConfigError(f"opc.connect_timeout_s: must be > 0, got {config.opc.connect_timeout_s}")
|
raise ConfigError(f"opc.connect_timeout_s: must be > 0, got {config.opc.connect_timeout_s}")
|
||||||
if not config.tags:
|
if not config.tags and not config.devices:
|
||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
"tags: at least one tag is required — without tags there is nothing to monitor"
|
"tags: nothing to monitor - define at least one tag or device "
|
||||||
|
"indicator (devices: entries alone are fine, tags: entries alone "
|
||||||
|
"are fine; both empty means the tool would read nothing)"
|
||||||
)
|
)
|
||||||
names = [t.name for t in config.tags]
|
names = [t.name for t in config.tags]
|
||||||
duplicates = sorted({n for n in names if names.count(n) > 1})
|
duplicates = sorted({n for n in names if names.count(n) > 1})
|
||||||
|
|||||||
@@ -225,6 +225,8 @@ def _cmd_check_config(config_path: str) -> int:
|
|||||||
print(f" devices: {len(config.devices)} link indicator(s):")
|
print(f" devices: {len(config.devices)} link indicator(s):")
|
||||||
for device in config.devices:
|
for device in config.devices:
|
||||||
print(f" {device.name} <- {device.tag}")
|
print(f" {device.name} <- {device.tag}")
|
||||||
|
if not config.tags and config.devices:
|
||||||
|
print(" note: no surveillance tags - monitoring device links only")
|
||||||
print(f" poll: every {config.poll.interval_s} s, hang flagged after {config.poll.timeout_ms} ms")
|
print(f" poll: every {config.poll.interval_s} s, hang flagged after {config.poll.timeout_ms} ms")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
+24
-3
@@ -60,12 +60,33 @@ def test_unknown_top_level_section_rejected(tmp_path: Path) -> None:
|
|||||||
load_config(p)
|
load_config(p)
|
||||||
|
|
||||||
|
|
||||||
def test_empty_tags_rejected(tmp_path: Path) -> None:
|
def test_both_tags_and_devices_empty_rejected(tmp_path: Path) -> None:
|
||||||
p = _write(tmp_path, "opc:\n server: X\ntags: []\n")
|
"""tags: and devices: are each optional, but their union must not be empty."""
|
||||||
with pytest.raises(ConfigError, match="at least one tag"):
|
p = _write(tmp_path, "opc:\n server: X\ntags: []\ndevices: []\n")
|
||||||
|
with pytest.raises(ConfigError, match="nothing to monitor"):
|
||||||
load_config(p)
|
load_config(p)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_tags_section_with_devices_is_allowed(tmp_path: Path) -> None:
|
||||||
|
"""Devices-only config: link monitoring without extra surveillance tags."""
|
||||||
|
p = _write(
|
||||||
|
tmp_path,
|
||||||
|
"opc:\n server: X\n"
|
||||||
|
"devices:\n - { name: 'Pump A', tag: Ind.Tag }\n",
|
||||||
|
)
|
||||||
|
config = load_config(p)
|
||||||
|
assert config.tags == ()
|
||||||
|
assert [d.name for d in config.devices] == ["Pump A"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tags_only_still_allowed(tmp_path: Path) -> None:
|
||||||
|
"""The classic setup (no device indicators) stays valid."""
|
||||||
|
p = _write(tmp_path, "opc:\n server: X\ntags:\n - name: t\n")
|
||||||
|
config = load_config(p)
|
||||||
|
assert len(config.tags) == 1
|
||||||
|
assert config.devices == ()
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate_tags_rejected(tmp_path: Path) -> None:
|
def test_duplicate_tags_rejected(tmp_path: Path) -> None:
|
||||||
p = _write(tmp_path, "opc:\n server: X\ntags:\n - name: a\n - name: a\n")
|
p = _write(tmp_path, "opc:\n server: X\ntags:\n - name: a\n - name: a\n")
|
||||||
with pytest.raises(ConfigError, match="duplicate"):
|
with pytest.raises(ConfigError, match="duplicate"):
|
||||||
|
|||||||
Reference in New Issue
Block a user