feat(ha): pfv-bms plant monitoring YAML pack [#439]

SNMP sensors for all 9 instrumented hosts (temps in °F per founder
ruling, °C converted at display layer), ACPI node-wattage feeds for
tsys6/7, UPS alert automations (on-battery, battery-low, comm-lost,
power-restored, high-temp) targeting Pushover, and Riemann-sum kWh
helpers for the Energy dashboard. PDU and iDRAC blocks left as pinned
placeholders pending PDU community and #462 OMSA work.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
2026-08-27 11:35:58 -05:00
parent d2f4214b8c
commit 2b6fc2b388
3 changed files with 412 additions and 0 deletions
+134
View File
@@ -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
+62
View File
@@ -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) }}
# ---------------------------------------------------------------------------
+216
View File
@@ -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: <pdu-dns-name>
# version: 1
# community: !secret pdu_community
# baseoid: <discover: bank current OID>
#
# 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.
# ---------------------------------------------------------------------------