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>
306 lines
9.4 KiB
YAML
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
|