yau-plant-assistant/cube/model/equipment.yml
Claude 8aba1f7f5c Rebuild the stand-in historian on CI Server item names
The three open Phase 5 findings were one defect: the stand-in was keyed on
CI Server POINT names (PS_STN_WET_WELL_LEVEL) when the historian is keyed on
CI Server ITEM names (AID.WRPS.STN.LEVEL). Modbus carries register numbers,
not names, so those two layers are free to differ - and do. Reconciling
against the register map, as planned, would only have proved the first three
namespaces agreed with each other.

Rebuilt from WRPS/05-scada/modbus, so item names, sample rates, retention and
timestamp semantics come from the machine rather than from a guess.

(a) Level tag does not join. PS_STN_WET_WELL_LEVEL becomes a tag row in its
    own right; LIT-101 is marked NOT HISTORISED - a field input on %IW0 that
    never reaches SCADA. It was the only seed row carrying two addresses.
    public.historian_items holds the item-to-tag mapping, generated by
    scripts/gen_historian_items.py and enforced non-empty at generate, at
    deploy and at verify.

(b) first_alarm/last_alarm returned UTC. Converted inside the measure, so it
    stays in Cube and happens once. Aggregate first, convert after - the other
    order picks the wrong row across a DST fall-back. Returned as a formatted
    string with a companion site_timezone measure. Storage being UTC is now
    confirmed, not assumed: all 49 points carry TIME_ZONE "Date+time GMT" and
    every history group CORRECT_DAYLIGHT=0. This answers Phase 4 task 4.

(c) High level alarm filed against the wrong equipment. Both sides were right
    about different things; the defect was asserting equipment twice. The
    history now carries no equipment column at all - faithful, since CI
    Server's section tree stops at the station and three pumps. Equipment is
    reached bit -> tag -> equipment via public.alarm_bits.

Alarms are derived, not stored: CI Server's ALARM_HISTORY group is empty
because every item imports with alarming off. Decomposing the alarm word needs
no configuration that does not exist.

Three things the SCADA config changed that were never filed as faults:
  - retention is 7 days, not 30. The advisory path was reporting a month of
    evidence drawn from a week of data
  - the analogue rate is 5 s, not 60. Two measures multiplied sample counts by
    a hardcoded 60 - a twelvefold overstatement that read as plausible
  - the deadband warning in process_values.yml was wrong and was steering
    people away from the correct measure

db/002_fixtures.sql now asserts its own counts at load and cross-checks the
alarm derivation against two independent signals. Those prove the pipeline,
not the plant.

db/README-standin-historian.md documents removal: the seam between generation
and contract, and twelve assumptions about imh that are NOT confirmed. Two of
them fail silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 11:42:02 +10:00

306 lines
9.4 KiB
YAML

# =============================================================================
# equipment.yml — alias resolution at equipment and tag level.
#
# SOURCE: pg-ai public.equipment and public.tags. These live in pg-ai whatever
# happens to imh, so this file does not change at Phase 4.
#
# WHY IT IS IN CUBE AT ALL: so that a question about "Pump 02" can be answered
# without the caller knowing that its data is on PS_PU302_RUNNING,
# PS_PU302_TRIPPED and PS_PU302_RUN_HOURS. Without equipment-level joins every
# equipment question fails, because no historian point is called "Pump 02".
#
# Alias matching itself is done in the API (api/tools/equipment.py) against the
# GIN-indexed alias arrays, not here - Cube is the aggregation layer, not the
# entity resolver. What Cube provides is the join, so that once the API has
# resolved "Pump 02" to PU-302 the measures in the other cubes can be filtered
# by equipment rather than by a list of tags the model would have to hardcode.
# =============================================================================
cubes:
- name: equipment
sql: >
SELECT
equipment_id,
display_name,
equipment_type,
unit_name,
description,
array_to_string(aliases, ' | ') AS alias_list
FROM public.equipment
description: >
Plant equipment at the Waterloo Road Pump Station - the station itself,
the wet well, three pumps, the discharge manifold, the spill weir and the
switchboard.
dimensions:
- name: equipment_id
sql: equipment_id
type: string
primary_key: true
description: Canonical identifier - PU-302, WW-101, STN-001.
- name: display_name
sql: display_name
type: string
description: What to call it in an answer - "Pump 02", not "PU-302".
- name: equipment_type
sql: equipment_type
type: string
- name: unit_name
sql: unit_name
type: string
- name: alias_list
sql: alias_list
type: string
description: >
Pipe-separated aliases, for display and debugging. Resolution happens
in the API against the array column with a GIN index; this string is
for showing an operator why "pump2" was understood as PU-302.
- name: description
sql: description
type: string
measures:
- name: count
type: count
- name: tags
sql_table: public.tags
description: >
Historian points and field instruments, their units, ranges and
setpoints. Read the description column before interpreting any value -
it records the historian-versus-PLC unit conversion and, critically,
whether the tag is historised at all.
joins:
- name: equipment
sql: "{CUBE}.equipment_id = {equipment}.equipment_id"
relationship: many_to_one
dimensions:
- name: tag_id
sql: tag_id
type: string
primary_key: true
- name: equipment_id
sql: equipment_id
type: string
- name: display_name
sql: display_name
type: string
- name: signal_type
sql: signal_type
type: string
description: level, flow, pressure, vibration, status, state, speed, hours.
- name: engineering_unit
sql: engineering_unit
type: string
description: >
The unit the HISTORIAN stores, which is not always the unit the PLC
works in. Wet well level is historised as percent of the spill weir
crest (raw mm / 60). Always report the unit with the number.
- name: range_low
sql: range_low
type: number
- name: range_high
sql: range_high
type: number
- name: alarm_setpoint_hi
sql: alarm_setpoint_hi
type: number
description: >
The configured alarm setpoint at the time this reference data was
loaded. Setpoints are writable from SCADA - if a question compares
two periods, check PS_STN_HIGH_LEVEL_ALARM_SP for a change before
attributing a difference in alarm counts to the process.
- name: alarm_setpoint_lo
sql: alarm_setpoint_lo
type: number
- name: trip_setpoint
sql: trip_setpoint
type: number
- name: description
sql: description
type: string
measures:
- name: count
type: count
# ---------------------------------------------------------------------------
# historian_items — the CI Server item dictionary.
#
# WHY IT IS A CUBE AT ALL: so "what is AID.WRPS.STN.LEVEL" and "which item
# holds the wet well level" are both answerable, and so an engineer checking
# a number can walk from the answer back through item -> point -> PLC address
# without leaving the assistant. Four namespaces name the same measurement
# and this table is the only place all four appear together.
#
# It is also where an item's SAMPLE RATE and RETENTION come from, both taken
# from the live CI Server historisation groups rather than assumed.
# ---------------------------------------------------------------------------
- name: historian_items
sql_table: public.historian_items
description: >
CI Server items - what the historian is actually keyed on - and how each
maps to a tag, a SCADA point and a PLC register.
joins:
- name: tags
sql: "{CUBE}.tag_id = {tags}.tag_id"
relationship: many_to_one
dimensions:
- name: item_name
sql: item_name
type: string
primary_key: true
description: AID.WRPS.STN.LEVEL. The historian's own key.
- name: tag_id
sql: tag_id
type: string
description: >
NULL only for items deliberately excluded from answering - see
exclusion_reason. A NULL here with no reason is a build failure.
- name: exclusion_reason
sql: exclusion_reason
type: string
description: >
Why this item cannot be asked about. Present it verbatim rather than
reporting "no records found" - the two mean very different things.
- name: section
sql: section
type: string
description: >
CI Server's own grouping - STN, PU301, PU302, PU303, SP, SIM. It is
COARSER than plant equipment: there is no wet well, weir, manifold or
switchboard section. Use equipment for the plant view, not this.
- name: attribute
sql: attribute
type: string
- name: description
sql: description
type: string
- name: eng_unit
sql: eng_unit
type: string
description: The unit CI Server presents, which is not the PLC's unit.
- name: raw_to_eng
sql: raw_to_eng
type: string
description: >
How the raw register becomes the engineering value, as written in the
SCADA point list - "value / 60" for level. Quote this when explaining
why a level reads 70.3 and not 4217.
- name: his_group
sql: his_group
type: string
description: WRPS_ONE_SEC (5 s), WRPS_THIRTY_SEC (30 s), WRPS_EVENT (on change).
- name: scan_interval_seconds
sql: scan_interval_seconds
type: number
- name: life_time
sql: life_time
type: string
description: >
Retention on the item's history group - "1 weeks" on every WRPS
group. This is the answer to "why can it not tell me about last
month".
- name: scada_point
sql: scada_point
type: string
- name: iec_address
sql: iec_address
type: string
description: The PLC address, %QW0. Field inputs (%IW/%IX) are not historised.
measures:
- name: count
type: count
views:
- name: item_reference
description: >
The CI Server item dictionary joined to the tag and equipment each item
belongs to. This is the lookup behind "which item holds the wet well
level", "how often is it sampled" and "how far back does it go".
cubes:
- join_path: historian_items
includes:
- item_name
- tag_id
- exclusion_reason
- section
- attribute
- description
- eng_unit
- raw_to_eng
- his_group
- scan_interval_seconds
- life_time
- scada_point
- iec_address
- count
- join_path: historian_items.tags
prefix: true
includes:
- display_name
- signal_type
- engineering_unit
- join_path: historian_items.tags.equipment
prefix: true
includes:
- equipment_id
- display_name
- equipment_type
- name: equipment_reference
description: >
Equipment joined to its tags - the lookup behind "what does the PVHI
alarm on the wet well mean" and "which pump is PU-302".
cubes:
- join_path: tags
includes:
- tag_id
- display_name
- signal_type
- engineering_unit
- range_low
- range_high
- alarm_setpoint_hi
- alarm_setpoint_lo
- trip_setpoint
- description
- join_path: tags.equipment
prefix: true
includes:
- equipment_id
- display_name
- equipment_type
- alias_list