diff --git a/dcinfra/ha/automations-ups-alerts.yaml b/dcinfra/ha/automations-ups-alerts.yaml new file mode 100644 index 0000000..aa1d1ab --- /dev/null +++ b/dcinfra/ha/automations-ups-alerts.yaml @@ -0,0 +1,134 @@ +# PFV UPS + plant alert automations for Home Assistant [#439] +# +# Paste into automations.yaml (or via UI: paste YAML directly into the +# automation editor). Delivery target: notify.pushover (add the Pushover +# integration first; interim targets that exist today: +# notify.mobile_app_ultix_sidecar / notify.mobile_app_ultix_mini / +# notify.mobile_app_allthes_ipad). +# +# ENTITY IDs: created by the NUT integration when you add the two UPS config +# entries (Settings > Devices & Services > Add Integration > Network UPS +# Tools; hosts pfv-tsys1.knel.net and pfv-tsys9.knel.net, port 3493, user +# homeassistant + the upsd.users password). Suggested device names: +# "tsys1 ups" and "tsys9 ups" -> entities below. If HA names them +# differently, check Developer Tools > States (search "_ups_status") and +# edit the entity_id lines to match. Nothing else needs changing. + +- alias: "PFV plant - UPS on battery" + id: pfv_plant_ups_on_battery + mode: single + triggers: + - trigger: state + entity_id: + - sensor.tsys1_ups_status + - sensor.tsys9_ups_status + conditions: + - condition: template + value_template: >- + {{ 'On Battery' in states(trigger.entity_id) }} + actions: + - action: notify.send_message + target: + entity_id: notify.pushover + data: + title: "POWER OUTAGE" + message: >- + {{ state_attr(trigger.entity_id, 'friendly_name') }} lost utility + power and is ON BATTERY at {{ states('sensor.' ~ trigger.entity_id.split('.')[1] | replace('_status', '_battery_charge')) }}% charge. + +- alias: "PFV plant - UPS battery low" + id: pfv_plant_ups_battery_low + mode: single + triggers: + - trigger: numeric_state + entity_id: + - sensor.tsys1_ups_battery_charge + - sensor.tsys9_ups_battery_charge + below: 40 + conditions: + - condition: template + value_template: >- + {{ 'On Battery' in states(trigger.entity_id | replace('_battery_charge', '_status')) }} + actions: + - action: notify.send_message + target: + entity_id: notify.pushover + data: + title: "UPS BATTERY LOW" + message: >- + {{ trigger.entity_id }} at {{ states(trigger.entity_id) }}% while on + battery. Start graceful shutdown of VMs before hypervisors (#455 ordering). + data: + priority: 1 + +- alias: "PFV plant - UPS communication lost" + id: pfv_plant_ups_comm_lost + mode: single + triggers: + - trigger: state + entity_id: + - sensor.tsys1_ups_status + - sensor.tsys9_ups_status + to: unavailable + for: "00:05:00" + actions: + - action: notify.send_message + target: + entity_id: notify.pushover + data: + title: "UPS COMMS LOST" + message: >- + {{ trigger.entity_id }} unreachable for 5 minutes (nut-server or + network path down; tsys1 also hosts pfv-bms itself). + data: + priority: 1 + +- alias: "PFV plant - utility power restored" + id: pfv_plant_ups_power_restored + mode: single + triggers: + - trigger: state + entity_id: + - sensor.tsys1_ups_status + - sensor.tsys9_ups_status + conditions: + - condition: template + value_template: >- + {{ trigger.from_state is not none and 'On Battery' in trigger.from_state.state + and 'On Battery' not in states(trigger.entity_id) }} + actions: + - action: notify.send_message + target: + entity_id: notify.pushover + data: + title: "Power restored" + message: >- + {{ state_attr(trigger.entity_id, 'friendly_name') }} is back ONLINE. + +- alias: "PFV plant - high temperature" + id: pfv_plant_high_temperature + mode: single + triggers: + - trigger: numeric_state + entity_id: + - sensor.pfv_artroom_temperature + - sensor.pfv_garage_temperature + - sensor.pfv_tsys1_rack_temperature + - sensor.pfv_tsys3_cpu_temperature + - sensor.pfv_tsys4_ambient_temperature + - sensor.pfv_tsys5_ambient_temperature + - sensor.pfv_tsys6_cpu_temperature + - sensor.pfv_tsys7_cpu_temperature + - sensor.pfv_tsys9_cpu_temperature + above: 104 + actions: + - action: notify.send_message + target: + entity_id: notify.pushover + data: + title: "HIGH TEMPERATURE" + message: >- + {{ trigger.entity_id }} is {{ states(trigger.entity_id) }}°F + (threshold 104°F). Check cooling/airflow. + data: + priority: 1 diff --git a/dcinfra/ha/energy-plant-kwh.yaml b/dcinfra/ha/energy-plant-kwh.yaml new file mode 100644 index 0000000..7f66780 --- /dev/null +++ b/dcinfra/ha/energy-plant-kwh.yaml @@ -0,0 +1,62 @@ +# PFV plant energy helpers (kWh) for Home Assistant [#439] +# +# Paste into configuration.yaml. Riemann-sum integral: W -> kWh with the +# `integration` sensor platform (kept across restarts). Feeds the Energy +# dashboard (Settings > Dashboards > Energy > add grid/device consumption +# sources pointing at these kWh sensors). +# +# Node power comes from the ACPI power_meter read through the SNMP extend +# (see sensors-snmp-plant.yaml): sensor.pfv_tsys6_node_power / +# sensor.pfv_tsys7_node_power, in watts. + +sensor: + - platform: integration + source: sensor.pfv_tsys6_node_power + name: "PFV tsys6 node energy" + unique_id: pfv_tsys6_node_energy + unit_prefix: k + round: 2 + method: left + max_sub_interval: + minutes: 5 + + - platform: integration + source: sensor.pfv_tsys7_node_power + name: "PFV tsys7 node energy" + unique_id: pfv_tsys7_node_energy + unit_prefix: k + round: 2 + method: left + max_sub_interval: + minutes: 5 + +# --------------------------------------------------------------------------- +# UPS load -> energy (APC on tsys1, Tripp Lite on tsys9): +# Preferred source is the NUT diagnostic sensor "Real power (W)" — enable it +# on the NUT device page (diagnostics are disabled by default). If your UPS +# does not expose real power, uncomment the template-watts fallback below and +# fill in the nominal wattage from the UPS nameplate. +# +# - platform: integration +# source: sensor.tsys1_ups_watts +# name: "PFV tsys1 UPS energy" +# unique_id: pfv_tsys1_ups_energy +# unit_prefix: k +# round: 2 +# method: left +# max_sub_interval: +# minutes: 5 +# +# Fallback template (only if no realpower diagnostic entity exists): +# +# template: +# - sensor: +# - name: "PFV tsys1 UPS watts" +# unique_id: pfv_tsys1_ups_watts +# unit_of_measurement: "W" +# device_class: power +# state_class: measurement +# state: >- +# {{ (states('sensor.tsys1_ups_load') | float(0) +# * NOMINAL_WATTS / 100) | round(0) }} +# --------------------------------------------------------------------------- diff --git a/dcinfra/ha/sensors-snmp-plant.yaml b/dcinfra/ha/sensors-snmp-plant.yaml new file mode 100644 index 0000000..4788e6f --- /dev/null +++ b/dcinfra/ha/sensors-snmp-plant.yaml @@ -0,0 +1,216 @@ +# PFV physical-plant SNMP sensors for Home Assistant [#439] +# +# Paste the sensor block below into pfv-bms configuration.yaml (or an +# !include). All temperatures are converted C to F at the display layer per +# founder ruling 2026-08-27 (transport stays lm-sensors format, °C). +# +# Source: lmsensors-extend snmpd extend, deployed on all 7 Proxmox hosts + +# stlpc-artroom + stlpc-garage (see dcinfra/sensors/temper/). +# Extend OID: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 +# +# Hosts resolve via knel.net DNS from pfv-bms (verify in Developer Tools > +# States after reload; if a host shows 'unknown' check DNS + the snmpd +# source ACL which already includes the pfv-bms LAN IP 192.168.3.12). +# +# Per founder ruling (#343): LibreNMS does NOT graph temperatures; these HA +# sensors are the canonical environmental record for the plant. + +sensor: + # --- TEMPer USB probe hosts (External probe, Internal fallback) --- + - platform: snmp + name: "PFV artroom temperature" + host: stlpc-artroom.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_artroom_temperature + value_template: >- + {% set ext = value | regex_findall('External:\s*\+?(-?\d+\.?\d*)') %} + {% set int = value | regex_findall('Internal:\s*\+?(-?\d+\.?\d*)') %} + {% set c = (ext | first) if ext else ((int | first) if int else none) %} + {{ ((c | float * 9 / 5) + 32) | round(1) if c is not none else 'unknown' }} + + - platform: snmp + name: "PFV garage temperature" + host: stlpc-garage.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_garage_temperature + value_template: >- + {% set ext = value | regex_findall('External:\s*\+?(-?\d+\.?\d*)') %} + {% set int = value | regex_findall('Internal:\s*\+?(-?\d+\.?\d*)') %} + {% set c = (ext | first) if ext else ((int | first) if int else none) %} + {{ ((c | float * 9 / 5) + 32) | round(1) if c is not none else 'unknown' }} + + - platform: snmp + name: "PFV tsys1 rack temperature" + host: pfv-tsys1.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_tsys1_rack_temperature + value_template: >- + {% set ext = value | regex_findall('External:\s*\+?(-?\d+\.?\d*)') %} + {% set int = value | regex_findall('Internal:\s*\+?(-?\d+\.?\d*)') %} + {% set c = (ext | first) if ext else ((int | first) if int else none) %} + {{ ((c | float * 9 / 5) + 32) | round(1) if c is not none else 'unknown' }} + + # --- Native lm-sensors hosts (label picked per host ground truth 2026-08-27) --- + # tsys3 Precision 7510: CPU package + - platform: snmp + name: "PFV tsys3 CPU temperature" + host: pfv-tsys3.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_tsys3_cpu_temperature + value_template: >- + {% set m = value | regex_findall('Package id 0:\s*\+?(-?\d+\.?\d*)') %} + {{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }} + + # tsys4 Precision T1700: Dell SMM ambient (board inlet) + - platform: snmp + name: "PFV tsys4 ambient temperature" + host: pfv-tsys4.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_tsys4_ambient_temperature + value_template: >- + {% set m = value | regex_findall('Ambient:\s*\+?(-?\d+\.?\d*)') %} + {{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }} + + # tsys5 Precision T7500: Dell SMM ambient (board inlet) + - platform: snmp + name: "PFV tsys5 ambient temperature" + host: pfv-tsys5.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_tsys5_ambient_temperature + value_template: >- + {% set m = value | regex_findall('Ambient:\s*\+?(-?\d+\.?\d*)') %} + {{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }} + + # tsys6 PowerEdge R610: socket-0 core temp (no Package label on Westmere) + - platform: snmp + name: "PFV tsys6 CPU temperature" + host: pfv-tsys6.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_tsys6_cpu_temperature + value_template: >- + {% set m = value | regex_findall('Core 0:\s*\+?(-?\d+\.?\d*)') %} + {{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }} + + # tsys6 ACPI power meter (node draw, watts) + - platform: snmp + name: "PFV tsys6 node power" + host: pfv-tsys6.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: power + state_class: measurement + unit_of_measurement: "W" + unique_id: pfv_tsys6_node_power + value_template: >- + {% set m = value | regex_findall('power1:\s*(\d+\.?\d*)\s*W') %} + {{ m | first | float if m else 'unknown' }} + + # tsys7 PowerEdge R620: package temp + - platform: snmp + name: "PFV tsys7 CPU temperature" + host: pfv-tsys7.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_tsys7_cpu_temperature + value_template: >- + {% set m = value | regex_findall('Package id 0:\s*\+?(-?\d+\.?\d*)') %} + {{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }} + + # tsys7 ACPI power meter (node draw, watts) + - platform: snmp + name: "PFV tsys7 node power" + host: pfv-tsys7.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: power + state_class: measurement + unit_of_measurement: "W" + unique_id: pfv_tsys7_node_power + value_template: >- + {% set m = value | regex_findall('power1:\s*(\d+\.?\d*)\s*W') %} + {{ m | first | float if m else 'unknown' }} + + # tsys9 OptiPlex 7080: package temp + - platform: snmp + name: "PFV tsys9 CPU temperature" + host: pfv-tsys9.knel.net + version: 2c + community: kn3lmgmt + baseoid: .1.3.6.1.4.1.8072.1.3.2.3.1.2.9.108.109.115.101.110.115.111.114.115 + accept_errors: true + device_class: temperature + state_class: measurement + unit_of_measurement: "°F" + unique_id: pfv_tsys9_cpu_temperature + value_template: >- + {% set m = value | regex_findall('Package id 0:\s*\+?(-?\d+\.?\d*)') %} + {{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }} + +# --------------------------------------------------------------------------- +# PLACEHOLDERS — enable after dependencies land (do not invent OIDs live): +# +# Garage PDU (AP7830, SNMPv1, HA owns PDU power graphing per #343 ruling): +# Needs the custom v1 community from LibreNMS (founder: supply value as a +# HA secret) + OID discovery via snmpwalk of .1.3.6.1.4.1.318 subtree. +# - platform: snmp +# name: "PFV garage PDU current" +# host: +# version: 1 +# community: !secret pdu_community +# baseoid: +# +# iDRAC6 feeds (pfv-tsys6-oob / pfv-tsys7-oob, community kn3lmgmt) once the +# OOB ports are cabled (#460): inlet temp + system power via Dell PowerNet +# MIB. OIDs to be walked and pinned post-cabling. +# ---------------------------------------------------------------------------