yau-plant-assistant/cube/model/alarms.yml
Claude 939cc2c83c Defer two Phase 5 findings to Phase 4, where imh can settle them
Both were found hand-verifying the measures on fixtures, and both are left
unfixed deliberately: fixing either now means guessing at what imh contains,
and a guess baked into the seed data or the models is harder to find later than
an open finding. Flagged at Phase 4, in the README, in the model files at the
point of use, and as eval cases that fail until they are settled.

(a) The wet well level tag does not join, and fails as "no records found".

    History is keyed PS_STN_WET_WELL_LEVEL, which process_values.yml hardcodes
    in seconds_above_high_level_alarm and seconds_above_lshh. tags.csv carries
    that name only as an ALIAS of LIT-101, so public.tags has no row with that
    tag_id and all 43,201 level rows - a third of the history, on the most
    important tag at this station - are unreachable from a tag-level lookup.
    Resolve "wet well" -> WW-101 -> LIT-101 -> filter history on LIT-101 ->
    zero rows -> "no records found".

    That is the failure mode worth being loudest about: it is the safety
    behaviour, produced by a key mismatch rather than by an absence of data,
    and indistinguishable from the real thing on screen. Filtering by
    equipment_id works, so whether a level question fails depends on which path
    the agent takes.

    The two flow tags use the opposite, self-consistent convention -
    PS_STN_INFLOW is a row in its own right and FIT-201 is marked NOT
    HISTORISED - so applying that to level is the likely fix. It still waits
    for the register map and for imh to say what CI Server historises the point
    as. Seed, hardcoded model names and 002_fixtures.sql change together.

(b) alarms.first_alarm and last_alarm return UTC, not SITE_TIMEZONE.

    Cube converts time dimensions to the query timezone; a min/max measure over
    a timestamp comes back unconverted. The Sydney day bucket for 2026-08-01
    returns 2026-07-31T20:00:35 - the right instant, ten hours and one calendar
    day out, beside a bucket label that IS in site time.

    This breaks "convert to SITE_TIMEZONE exactly once, in Cube", and the fix
    has to stay in Cube - the API compensating with timezone arithmetic is the
    thing that rule exists to prevent. Which fix is right depends on whether
    imh stores UTC or local, which is Phase 4 task 4. Until then these two must
    not be quoted to an operator as a clock time.

Phase 4's gate gains an item for each. Everything else verified in this pass -
alarms, operations, the equipment join, alias resolution and the Sydney/UTC day
boundaries - matched hand-written SQL exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 14:36:47 +10:00

189 lines
6.8 KiB
YAML

# =============================================================================
# alarms.yml — alarm and event history.
#
# SOURCE: fixture.alarm_history while USE_FIXTURES=true. When imh is live this
# becomes the agreed imh alarm table and the column names below change with it.
# Nothing else in this file should need to change; that is the point of it.
#
# THREE DEFINITIONS THAT DECIDE WHETHER THE ANSWERS ARE RIGHT. They are here in
# comments because the person checking the number needs to read them, and they
# are not obvious from the measure names.
#
# 1. AN ALARM IS A TRANSITION INTO THE ACTIVE STATE.
# state = 'ACTIVE' only. RTN is the return-to-normal of the activation that
# preceded it, and ACK is an operator acknowledging one. Counting every row
# roughly doubles every answer. "6 times last week" must mean six
# activations.
#
# 2. "LAST WEEK" IS A ROLLING 7 x 24 h WINDOW IN SITE_TIMEZONE.
# Not the previous calendar week, not 7 calendar days. Storage is UTC and
# the conversion happens here, once. If someone means the calendar week they
# have to say so, and the answer must state the window it used.
#
# 3. CHATTERING IS 3 OR MORE ACTIVATIONS OF THE SAME TAG WITHIN 60 MINUTES.
# An arbitrary threshold, chosen to match the site's alarm rationalisation
# convention. It is stated in the answer whenever chattering is reported,
# because a different threshold gives a different story.
# =============================================================================
cubes:
- name: alarms
sql_table: fixture.alarm_history # -> imh alarm table at Phase 4
description: >
Alarm and event history for the Waterloo Road Pump Station. One row per
state transition. Activations only are counted as alarms.
joins:
- name: equipment
sql: "{CUBE}.equipment_id = {equipment}.equipment_id"
relationship: many_to_one
dimensions:
- name: alarm_id
sql: alarm_id
type: number
primary_key: true
- name: event_time
sql: event_time
type: time
description: Transition time. Stored UTC, presented in SITE_TIMEZONE.
- name: tag_id
sql: tag_id
type: string
- name: equipment_id
sql: equipment_id
type: string
- name: alarm_type
sql: alarm_type
type: string
description: >
HIGH_LEVEL, HIGH_HIGH_LEVEL, LOW_LOW_LEVEL, SPILL, PUMP_TRIP,
SEAL_LEAK, HIGH_VIBRATION, LEVEL_SIGNAL_FAULT, MAINS_FAILURE,
SETPOINT_REJECTED. These correspond to the bits of the PLC alarm
bitmask %QW17 - see db/seed/tags.csv, PS_STN_ALARM_BITMASK.
- name: state
sql: state
type: string
description: ACTIVE, RTN or ACK. Only ACTIVE counts as an alarm.
- name: priority
sql: priority
type: number
description: >
1 highest, 3 lowest. Priority 1 is SPILL, PUMP_TRIP and
LEVEL_SIGNAL_FAULT - losing the level signal on a well that can spill
is a priority 1 condition, and the fixtures already treat it as one.
This comment previously named only SPILL and PUMP_TRIP and disagreed
with the data, which matters because this is the line an engineer
reads when checking a priority_1_count.
- name: value
sql: value
type: number
description: Process value at the transition, in engineering_unit.
- name: is_fixture
sql: is_fixture
type: boolean
description: >
TRUE means this row came from db/002_fixtures.sql and is generated
test data, not plant history. The API surfaces this to the operator.
measures:
- name: alarm_count
type: count
filters:
- sql: "{CUBE}.state = 'ACTIVE'"
description: >
Number of alarm ACTIVATIONS. Definition 1 above. This is the measure
behind "how many times did X alarm come up".
- name: transition_count
type: count
description: >
Every row including RTN and ACK. Diagnostics only - do not answer an
operator question with this.
- name: distinct_tags
sql: tag_id
type: count_distinct
filters:
- sql: "{CUBE}.state = 'ACTIVE'"
description: How many different tags alarmed in the window.
# DEFERRED DEFECT - these two return UTC, not SITE_TIMEZONE. Cube converts
# time DIMENSIONS to the query timezone; a min/max MEASURE over a
# timestamp comes back unconverted. In a Sydney day bucket for
# 2026-08-01, first_alarm returns 2026-07-31T20:00:35 - the right
# instant, ten hours and one calendar day out, next to a bucket label
# that IS in site time.
#
# Left unfixed on purpose until imh is connected: the fix must keep the
# conversion inside Cube, and which fix is right depends on whether imh
# stores UTC or local time (Phase 4, task 4). Until then, do NOT quote
# either of these to an operator as a clock time. See Phase 4,
# "Deferred from Phase 5", finding (b) in BUILD-AI-CONTAINERS.md.
- name: first_alarm
sql: event_time
type: min
filters:
- sql: "{CUBE}.state = 'ACTIVE'"
- name: last_alarm
sql: event_time
type: max
filters:
- sql: "{CUBE}.state = 'ACTIVE'"
- name: priority_1_count
type: count
filters:
- sql: "{CUBE}.state = 'ACTIVE' AND {CUBE}.priority = 1"
description: Priority 1 activations - trips and spills.
pre_aggregations:
# Keeps "count alarms last week" fast without repeatedly scanning imh.
# Materialised into pg-ai schema cube_preagg. Watch its growth on
# /datadisk; the retention policy is the refresh_key plus manual pruning.
- name: alarms_by_hour
measures: [alarm_count, distinct_tags, priority_1_count]
dimensions: [alarm_type, equipment_id, tag_id]
time_dimension: event_time
granularity: hour
partition_granularity: month
refresh_key:
every: 10 minutes
build_range_start:
sql: "SELECT now() - interval '180 days'"
build_range_end:
sql: "SELECT now()"
views:
- name: alarm_activity
description: >
Alarm activations joined to equipment, so a question about "Pump 02" can
be answered without the caller knowing which tags belong to it.
cubes:
- join_path: alarms
includes:
- event_time
- alarm_type
- tag_id
- state
- priority
- value
- is_fixture
- alarm_count
- distinct_tags
- priority_1_count
- join_path: alarms.equipment
prefix: true
includes:
- equipment_id
- display_name
- equipment_type