docs: showpiece README — mermaid architecture, sensor/alert tables [#762]
Also absorbs PFVCluster operator tooling: tools/ha-ws-call.py and docs/reference-packs/ (historical, NOT loaded by HA — clearly marked). No live-config changes in this commit. Detail: https://projects.knownelement.com/issues/762
This commit is contained in:
@@ -1,45 +1,164 @@
|
||||
# pfv-bms — Home Assistant configuration (Turnsys PFV)
|
||||
# 🏠 pfv-bms — the house brain
|
||||
|
||||
Production Home Assistant configuration for a private residence +
|
||||
server room: physical-plant environmental monitoring (temperatures,
|
||||
power, UPS), server-room badge access control, and alerting.
|
||||
> **Datacenter-grade monitoring and access control for a private residence + server room** —
|
||||
> 372 entities, ~260 sensors, and a badge-actuated door, all deployed by `git push`.
|
||||
|
||||
Licensed under the [GNU AGPLv3](LICENSE) — see [AGPL compliance
|
||||
notes](#license) below.
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## What lives here
|
||||
Production Home Assistant configuration for the Turnsys PFV —
|
||||
environmental monitoring (temperatures, power, UPS) across a seven-node
|
||||
Proxmox fleet plus single-board computers, server-room badge access
|
||||
control, and tiered alerting to mobile devices.
|
||||
|
||||
- `configuration.yaml` + `packages/` — the full running config,
|
||||
deployed to the HAOS box via a git-webhook pipeline
|
||||
- `packages/doorman.yaml` — badge-scan ingest + whitelist decisions
|
||||
(server-room door access)
|
||||
- `packages/plant_snmp.yaml` — fleet temperature/power sensors over
|
||||
SNMP (lm-sensors extends, iDRAC, UPS, PDU relay)
|
||||
- `automations.yaml` — tiered thermal alerts + sensor watchdogs
|
||||
- `dashboards/` — Lovelace dashboards
|
||||
Work tracking: [Redmine](https://projects.knownelement.com)
|
||||
([#344](https://projects.knownelement.com/issues/344) umbrella) ·
|
||||
Docs: [VP TechOps on Discourse](https://community.turnsys.com/c/vp-techops)
|
||||
|
||||
## Security model (read this before forking)
|
||||
---
|
||||
|
||||
## 🗺️ System architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph FIELD[📡 The field]
|
||||
TS[🖥️ 7× Proxmox hosts<br/>lm-sensors + iDRAC extends]
|
||||
BOARDS[🥧 SBCs<br/>pfvsvrpi · jetson · 3× subopi]
|
||||
UPS[🔋 UPS on tsys1<br/>NUT server :3493]
|
||||
PDU[🔌 Garage PDU<br/>SNMPv1 relay]
|
||||
METER[⚡ Utility meter<br/>Smart Meter Texas]
|
||||
READER[🪪 Badge readers<br/>pfvsvrpi · ultix-field]
|
||||
end
|
||||
|
||||
subgraph HA[🏠 pfv-bms — this repo]
|
||||
INGEST[📦 packages/plant_snmp.yaml<br/>SNMP pollers → raw sensors]
|
||||
TEMPLATE[🧮 template sensors<br/>raw → °F/units + thresholds]
|
||||
DOOR[🚪 packages/doorman.yaml<br/>scan ingest + decision engine]
|
||||
ROSTER[🔒 roster sensors<br/>REST poll of private access-roster]
|
||||
ENERGY[📊 utility_meter +<br/>Riemann-sum energy]
|
||||
AUTO[🚨 automations.yaml<br/>tiered thermal + UPS alerts]
|
||||
end
|
||||
|
||||
subgraph OUT[📲 Outcomes]
|
||||
ALERT[📱 push alerts<br/>companion apps]
|
||||
LOG[📓 logbook + history<br/>every scan, every degree]
|
||||
DASH[🖥️ dashboards<br/>server-room + plant]
|
||||
RELAY[⚡ doorctl → usbrelay<br/>→ door strike]
|
||||
end
|
||||
|
||||
READER -->|webhook| DOOR
|
||||
DOOR -->|whitelist| ROSTER
|
||||
DOOR -->|armed + valid| RELAY
|
||||
TS & BOARDS -->|SNMP| INGEST
|
||||
UPS -->|NUT| INGEST
|
||||
PDU -->|SNMP relay| INGEST
|
||||
METER -->|REST| INGEST
|
||||
INGEST --> TEMPLATE --> AUTO
|
||||
INGEST --> ENERGY
|
||||
AUTO --> ALERT
|
||||
TEMPLATE & DOOR --> LOG & DASH
|
||||
```
|
||||
|
||||
### 🛡️ Badge access lane (server-room door)
|
||||
|
||||
| Component | Role | Lives |
|
||||
|-----------|------|-------|
|
||||
| USB HID readers | 13.56 MHz badge readers (keyboard-emulating) | door + dev bench |
|
||||
| [`KNEL/doorman`](https://git.knownelement.com/KNEL/doorman) | pure-bash listener, decode, webhook, doorctl | `pfvsvrpi` (prod), `ultix-field` (dev) |
|
||||
| `packages/doorman.yaml` | scan ingest, whitelist decision, alerts, unlock dispatch | **this repo** |
|
||||
| [`KNEL/access-roster`](https://git.knownelement.com/KNEL/access-roster) 🔒 | badge IDs + holders — **private**, two-human PR gate | gitea (private) |
|
||||
| `doorctl` + usbrelay | socket-activated, token + source-IP ACL, EXIT-trap secured | `pfvsvrpi` |
|
||||
|
||||
Fail-closed end to end: HA unreachable → the door does not open.
|
||||
Break-glass badges fire the relay locally (works with HA down) and
|
||||
report `local_unlock` so HA never double-fires. Arm switch
|
||||
(`input_boolean.doorman_unlock_enabled`) is founder-level control.
|
||||
Verified end-to-end 2026-09-03: real armed scan → dispatch → relay.
|
||||
|
||||
---
|
||||
|
||||
## 🌡️ Environmental monitoring
|
||||
|
||||
Raw SNMP pollers (`packages/plant_snmp.yaml`) → unit-normalized
|
||||
template sensors → tiered automations. Every host contributes
|
||||
lm-sensors extends; iDRAC hosts add inlet/exhaust/CPU; NUT feeds UPS
|
||||
electricals.
|
||||
|
||||
| Family | Coverage |
|
||||
|--------|----------|
|
||||
| 🖥️ Host silicon | CPU/drive/NVMe/PCH temps on 7 Proxmox hosts |
|
||||
| 🌡️ Thermal zones | rack ambient, DIMM banks, GPU, tsys7 iDRAC inlet→exhaust ΔT |
|
||||
| 🥧 Boards | pfvsvrpi, jetson, 3× subopi |
|
||||
| 🔋 UPS | tsys1 battery charge/voltage, on-battery + runtime alerts |
|
||||
| ⚡ Power | whole-house kWh (utility), tsys6/7 node watts + kWh integrals, garage PDU amps |
|
||||
| 🏠 Rooms | temp/humidity/battery sensors (Govee, cloud-fed) |
|
||||
|
||||
### 🚨 Alert tiers (automations.yaml)
|
||||
|
||||
| Trigger | Threshold |
|
||||
|---------|-----------|
|
||||
| Drive / DIMM temps | > 140 °F |
|
||||
| NVMe | > 150 °F |
|
||||
| PCH | > 160 °F |
|
||||
| GPU | > 185 °F |
|
||||
| Ambient | > 104 °F |
|
||||
| UPS | on-battery transitions + runtime |
|
||||
| Watchdog | plant sensor freshness sweep every 30 min |
|
||||
|
||||
### 📊 Snapshot (2026-09-03)
|
||||
|
||||
372 entities · ~260 sensors · 45 temperature sensors · 21
|
||||
electrical/energy sensors · 8 todo lists · 2-way alerting to companion
|
||||
apps.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security model (read this before forking)
|
||||
|
||||
- **No secrets in git, ever.** Everything sensitive is `!secret`
|
||||
indirection into an on-box `secrets.yaml` (gitignored). Verified by
|
||||
full-history scans.
|
||||
- **Badge IDs are NOT in this repo.** The badge roster lives in a
|
||||
separate *private* repository (`access-roster`, two-human PR gate)
|
||||
and is fetched live at runtime via the gitea API with a secret auth
|
||||
header. This repo contains only the decision logic.
|
||||
- Battery-powered room sensors and lock integrations are cloud-fed;
|
||||
their credentials also live in `secrets.yaml`.
|
||||
- **Badge IDs are NOT in this repo.** The roster lives in the *private*
|
||||
[`KNEL/access-roster`](https://git.knownelement.com/KNEL/access-roster)
|
||||
repo (two-human PR gate) and is fetched live at runtime.
|
||||
- **Fail-closed access control** — see the badge lane above.
|
||||
- Battery room sensors and locks are cloud-fed; credentials also live
|
||||
in `secrets.yaml`.
|
||||
|
||||
## Related
|
||||
## 🚀 Deployment pipeline
|
||||
|
||||
- Work tracking: [Redmine project 55](https://projects.knownelement.com)
|
||||
([#344](https://projects.knownelement.com/issues/344) HA integration,
|
||||
[#345](https://projects.knownelement.com/issues/345) badge access,
|
||||
[#762](https://projects.knownelement.com/issues/762) publication)
|
||||
- Documentation:
|
||||
[VP TechOps on Discourse](https://community.turnsys.com/c/vp-techops)
|
||||
- Badge listener/relay code: [KNEL/doorman](https://git.knownelement.com/KNEL/doorman)
|
||||
- Wider fleet (Proxmox, DNS, monitoring): [KNEL/PFVCluster](https://git.knownelement.com/KNEL/PFVCluster)
|
||||
```mermaid
|
||||
flowchart LR
|
||||
PUSH[📝 git push] --> HOOK[gitea webhook<br/>fast path]
|
||||
PUSH --> SHA[sha-watch<br/>safety net · 300 s poll]
|
||||
HOOK & SHA --> CHECK{changed files?}
|
||||
CHECK -->|packages/**| RESTART[♻️ core restart<br/>webhook triggers need it]
|
||||
CHECK -->|other YAML| RELOAD[⚡ reload only<br/>~5 min to live]
|
||||
```
|
||||
|
||||
Gate before pushing `packages/**`: `ha core check` must pass on-box.
|
||||
Measured end-to-end: 4 m 52 s commit → live.
|
||||
|
||||
## 🗺️ Repo map
|
||||
|
||||
| Path | What |
|
||||
|------|------|
|
||||
| `configuration.yaml` | entrypoint + package includes |
|
||||
| `packages/doorman.yaml` | 🚪 badge scan ingest + decisions |
|
||||
| `packages/plant_snmp.yaml` | 🌡️ SNMP pollers + templates |
|
||||
| `automations.yaml` | 🚨 tiered alerts + watchdogs |
|
||||
| `dashboards/` | Lovelace dashboards |
|
||||
| `tools/` | operator tooling (websocket CLI) |
|
||||
| `docs/reference-packs/` | historical YAML references — NOT loaded |
|
||||
|
||||
## 🔗 Related
|
||||
|
||||
- Badge listener + relay code: [`KNEL/doorman`](https://git.knownelement.com/KNEL/doorman)
|
||||
- Wider fleet (Proxmox, DNS, monitoring): [`KNEL/PFVCluster`](https://git.knownelement.com/KNEL/PFVCluster)
|
||||
- Badge roster (private): `KNEL/access-roster` 🔒
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# REFERENCE ONLY — historical packs, NOT loaded by HA (superseded by packages/; kept for lineage). Statuses in the repo README.
|
||||
# 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
|
||||
- sensor.pfv_jetson_temperature
|
||||
- sensor.pfv_svrpi_temperature
|
||||
- sensor.pfv_subopi3_temperature
|
||||
- sensor.pfv_subopi_dev_3_temperature
|
||||
- sensor.pfv_subopi_dev_4_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
|
||||
@@ -0,0 +1,63 @@
|
||||
# REFERENCE ONLY — historical packs, NOT loaded by HA (superseded by packages/; kept for lineage). Statuses in the repo README.
|
||||
# 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) }}
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -0,0 +1,293 @@
|
||||
# REFERENCE ONLY — historical packs, NOT loaded by HA (superseded by packages/; kept for lineage). Statuses in the repo README.
|
||||
# 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' }}
|
||||
|
||||
# --- SBC fleet (sysfs thermal fallback in lmsensors-extend v2; temp1 = zone0/SoC) ---
|
||||
- platform: snmp
|
||||
name: "PFV jetson temperature"
|
||||
host: pfv-jetson-nano-1.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_jetson_temperature
|
||||
value_template: >-
|
||||
{% set m = value | regex_findall('temp1:\s*\+?(-?\d+\.?\d*)') %}
|
||||
{{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }}
|
||||
|
||||
- platform: snmp
|
||||
name: "PFV svrpi temperature"
|
||||
host: pfvsvrpi.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_svrpi_temperature
|
||||
value_template: >-
|
||||
{% set m = value | regex_findall('temp1:\s*\+?(-?\d+\.?\d*)') %}
|
||||
{{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }}
|
||||
|
||||
- platform: snmp
|
||||
name: "PFV subopi3 temperature"
|
||||
host: subopi3.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_subopi3_temperature
|
||||
value_template: >-
|
||||
{% set m = value | regex_findall('temp1:\s*\+?(-?\d+\.?\d*)') %}
|
||||
{{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }}
|
||||
|
||||
- platform: snmp
|
||||
name: "PFV subopi-dev-3 temperature"
|
||||
host: subopi-dev-3.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_subopi_dev_3_temperature
|
||||
value_template: >-
|
||||
{% set m = value | regex_findall('temp1:\s*\+?(-?\d+\.?\d*)') %}
|
||||
{{ ((m | first | float * 9 / 5) + 32) | round(1) if m else 'unknown' }}
|
||||
|
||||
- platform: snmp
|
||||
name: "PFV subopi-dev-4 temperature"
|
||||
host: subopi-dev-4.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_subopi_dev_4_temperature
|
||||
value_template: >-
|
||||
{% set m = value | regex_findall('temp1:\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.
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ha-ws-call: send websocket commands to Home Assistant, print JSON replies.
|
||||
|
||||
Env: HASS_URL (http(s)://host[:port]), HASS_TOKEN (long-lived token).
|
||||
Args: one or more JSON command objects — argv literals or file paths.
|
||||
Replies printed one-per-command, JSON indented. Stdlib + websockets only.
|
||||
Usage example (docker, from repo root):
|
||||
docker run --rm --network host --env-file ~/.creds/homeassistant.env \
|
||||
-v "$PWD/HomeAssistant:/w" python:3.12-alpine \
|
||||
sh -c 'pip install -q websockets && python /w/ha-ws-call.py "{...json...}"'
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
import websockets
|
||||
except ImportError:
|
||||
sys.exit("missing dependency: pip install websockets")
|
||||
|
||||
|
||||
def load_cmd(arg):
|
||||
if arg.startswith("{"):
|
||||
return json.loads(arg)
|
||||
with open(arg, encoding="latin1") as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
async def main():
|
||||
base = os.environ["HASS_URL"]
|
||||
url = base.replace("http", "ws", 1).rstrip("/") + "/api/websocket"
|
||||
token = os.environ["HASS_TOKEN"]
|
||||
cmds = [load_cmd(arg) for arg in sys.argv[1:]]
|
||||
async with websockets.connect(url, max_size=16 * 1024 * 1024) as ws:
|
||||
hello = json.loads(await ws.recv())
|
||||
if hello.get("type") != "auth_required":
|
||||
sys.exit(f"unexpected hello: {hello}")
|
||||
await ws.send(json.dumps({"type": "auth", "access_token": token}))
|
||||
authed = json.loads(await ws.recv())
|
||||
if authed.get("type") != "auth_ok":
|
||||
sys.exit(f"auth failed: {authed}")
|
||||
for message_id, cmd in enumerate(cmds, start=1):
|
||||
cmd = dict(cmd)
|
||||
cmd["id"] = message_id
|
||||
await ws.send(json.dumps(cmd))
|
||||
reply = json.loads(await ws.recv())
|
||||
print(json.dumps(reply, indent=1))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user