From c7d2b1133fcbc4bd3ae861c19cc142ac2038f3b3 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Fri, 20 Feb 2026 10:44:25 +0400 Subject: [PATCH 01/23] fix: make battery sign conventions consistent Both battery_current and battery_power now use generator reference frame: positive = discharging (energy out), negative = charging (energy in). Previously current used the opposite convention, making P = V * I inconsistent. Also documents sign and naming conventions in README. --- README.md | 28 ++++++++++++++++++++++++++++ lib/energy/battery/electrical.yml | 4 ++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d28e599..64c8e0e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,34 @@ Profiles are used in Enapter Blueprints to specify the capabilities and the inte 2. **Extend with Components**: Add additional library components to the device blueprint if needed. 3. **Submit a Proposal**: If a profile for your device doesn't exist, create a new one and submit a pull request. +## Conventions + +### Sign Conventions + +All power and current measurements follow the **generator reference frame** (positive = energy flowing out of the device into the system): + +| Measurement Point | Positive Value | Negative Value | +|---|---|---| +| Battery current/power | Discharging (energy out) | Charging (energy in) | +| Inverter AC power | Delivering to loads/grid | Consuming (e.g. standby) | +| Grid power | Importing from grid | Exporting to grid | +| Load power | Consumed by loads | Fed back | +| PV power | Always positive (generation) | N/A | + +This convention ensures consistent energy balance calculations: `pv_power + battery_power + grid_power = load_power` (signs will balance naturally). + +### Naming Conventions + +- All field names use `snake_case` +- Battery fields use `battery_` prefix +- Inverter AC fields use `ac_` prefix (DC side is either battery or PV and has its own prefix) +- PV fields use `pv_` prefix with `s1`/`s2`/etc. for individual strings +- Grid fields use `grid_` prefix +- Load fields use `load_` prefix +- Power meter total fields (`total_power`, `total_current`, `energy_total`) omit the `ac_` prefix because power meters only measure one type of current, so the prefix is redundant. Per-phase fields keep the `ac_` prefix since they follow the same naming as inverter per-phase measurements. +- Three-phase measurements use `l1`/`l2`/`l3` suffixes (IEC convention) +- Units follow [UCUM](https://ucum.org/) notation: `W`, `Wh`, `V`, `A`, `Hz`, `VA`, `VAR`, `Cel`, `%` + ## Development ### Device Profile diff --git a/lib/energy/battery/electrical.yml b/lib/energy/battery/electrical.yml index 296b977..5493deb 100644 --- a/lib/energy/battery/electrical.yml +++ b/lib/energy/battery/electrical.yml @@ -14,10 +14,10 @@ telemetry: display_name: Battery Current type: float unit: A - description: Current DC current flow to/from the battery bank (positive for charge, negative for discharge). + description: DC current flow to/from the battery bank. Positive values indicate discharging (current flowing out of battery), negative values indicate charging (current flowing into battery). battery_power: display_name: Battery Power type: float unit: W - description: Current DC power (positive for discharge, negative for charge). + description: DC power to/from the battery bank. Positive values indicate discharging (power delivered to system), negative values indicate charging (power absorbed from system). From 365a1b266337a39e18e51000fa78a6d1c7c3df24 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 12:04:22 +0400 Subject: [PATCH 02/23] fix: add throttled status to inverter status enum Add 'throttled' state between 'operating' and 'shutting_down' for inverters operating at reduced power due to temperature derating, grid requirements, or power limits. Common in SMA, Fronius, and SolarEdge inverters. --- lib/energy/inverter/status.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/energy/inverter/status.yml b/lib/energy/inverter/status.yml index 61c3d97..e99c2f2 100644 --- a/lib/energy/inverter/status.yml +++ b/lib/energy/inverter/status.yml @@ -26,6 +26,9 @@ telemetry: operating: display_name: Operating description: Inverter is actively producing power + throttled: + display_name: Throttled + description: Inverter is operating at reduced power output due to external conditions such as high temperature, grid requirements, or power limits. shutting_down: display_name: Shutting Down description: Inverter is going through shutdown procedure From 9f4910246013252addf956034740888b394a98e7 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 16:39:52 +0400 Subject: [PATCH 03/23] fix: add convention about `status` telemetry use --- README.md | 1 + lib/energy/inverter/status.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 64c8e0e..ff33a03 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ This convention ensures consistent energy balance calculations: `pv_power + batt - Power meter total fields (`total_power`, `total_current`, `energy_total`) omit the `ac_` prefix because power meters only measure one type of current, so the prefix is redundant. Per-phase fields keep the `ac_` prefix since they follow the same naming as inverter per-phase measurements. - Three-phase measurements use `l1`/`l2`/`l3` suffixes (IEC convention) - Units follow [UCUM](https://ucum.org/) notation: `W`, `Wh`, `V`, `A`, `Hz`, `VA`, `VAR`, `Cel`, `%` +- Status fields must use a context-appropriate prefix instead of the bare `status` name (e.g. `inverter_status`, `charger_status`, `relay_state`). The bare `status` is a reserved field name in the Enapter platform with [special meaning](https://developers.enapter.com/docs/reference/manifest#device-status) and is intentionally left for the Blueprint developer to expose the native device status. Profiles define a separate prefixed field with a unified set of operational states (e.g. `off`, `operating`, `fault`) so that UIs and automation can treat all devices of the same type consistently, while the native status remains available for device-specific diagnostics. ## Development diff --git a/lib/energy/inverter/status.yml b/lib/energy/inverter/status.yml index e99c2f2..26811e8 100644 --- a/lib/energy/inverter/status.yml +++ b/lib/energy/inverter/status.yml @@ -4,7 +4,7 @@ display_name: Inverter Status Profile description: Status profile for battery/solar/hybrid inverters telemetry: - status: + inverter_status: display_name: Status type: string description: Current operational status of the inverter From 5d4db3bf93f3550ff809524eae5f1b872f09f7df Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 17:29:49 +0400 Subject: [PATCH 04/23] docs: add /sensor directory to README repository structure --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ff33a03..60fe111 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ The profiles are organized hierarchically to allow for flexible composition base - Components are modular and focused on a specific functionality - Components can be composed to create complete device profiles -2. **Device Implementations (`/energy`)** - Complete device profiles organized by type: +2. **Device Implementations (`/energy`, `/sensor`)** - Complete device profiles organized by domain: - Each implementation combines multiple library profiles - - Implementations are organized by device type - - Ready-to-use profiles for common energy devices + - Implementations are organized by device type within each domain directory + - Ready-to-use profiles for common energy and sensor devices ## Usage From 9d3b6525569da56bae6b023bdc78b656ab7b8237 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 17:29:52 +0400 Subject: [PATCH 05/23] fix: correct typo "Configutaion" in grid charge enabled display_name --- lib/energy/inverter/grid/charge/enabled.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/energy/inverter/grid/charge/enabled.yml b/lib/energy/inverter/grid/charge/enabled.yml index 6cf67f9..71d6cc4 100644 --- a/lib/energy/inverter/grid/charge/enabled.yml +++ b/lib/energy/inverter/grid/charge/enabled.yml @@ -1,6 +1,6 @@ blueprint_spec: profile/1.0 -display_name: Grid Charging/Feed-in Configutaion Status +display_name: Grid Charging/Feed-in Configuration Status description: Information about grid charging/feeding-in configuration status. properties: From 809ddffa7bca7488c43ec1e2e45da9bc1af437e0 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 17:29:54 +0400 Subject: [PATCH 06/23] fix: revise gas_lel description to accurately describe explosive concentration range --- lib/sensor/gas/lel.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sensor/gas/lel.yml b/lib/sensor/gas/lel.yml index a785b6e..efd6292 100644 --- a/lib/sensor/gas/lel.yml +++ b/lib/sensor/gas/lel.yml @@ -8,4 +8,4 @@ telemetry: display_name: Gas Percent of Lower Explosive Limit (%LEL) type: float unit: '%LEL' - description: Lower explosive limit (LEL) is the lowest concentration of gas that can ignite in air. 100% LEL indicates the concentration of gas is explosive, while 0% LEL indicates no gas is present. + description: Gas concentration as a percentage of the Lower Explosive Limit (LEL) - the minimum concentration at which the gas can ignite in air. 0% LEL indicates no gas is present. At 100% LEL the gas has reached the ignition threshold; concentrations above this are within the explosive range up to the Upper Explosive Limit (UEL). From 38ea6ba03b316b6661f1636c09f3324a24a73803 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 17:29:59 +0400 Subject: [PATCH 07/23] fix: remove redundant "Profile" suffix from inverter, firmware, and location display_names --- lib/energy/inverter/nameplate.yml | 2 +- lib/energy/inverter/status.yml | 2 +- lib/energy/inverter/temperature.yml | 2 +- lib/energy/power_meter/energy/today.yml | 3 ++- lib/energy/power_meter/energy/total.yml | 3 ++- lib/energy/pv/power.yml | 2 +- lib/firmware/version.yml | 2 +- 7 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/energy/inverter/nameplate.yml b/lib/energy/inverter/nameplate.yml index 56f340c..f79c2f7 100644 --- a/lib/energy/inverter/nameplate.yml +++ b/lib/energy/inverter/nameplate.yml @@ -1,6 +1,6 @@ blueprint_spec: profile/1.0 -display_name: Inverter Nameplate Profile +display_name: Inverter Nameplate description: Nameplate profile for battery/solar/hybrid inverters covering core attributes properties: diff --git a/lib/energy/inverter/status.yml b/lib/energy/inverter/status.yml index 26811e8..a02d2ca 100644 --- a/lib/energy/inverter/status.yml +++ b/lib/energy/inverter/status.yml @@ -1,6 +1,6 @@ blueprint_spec: profile/1.0 -display_name: Inverter Status Profile +display_name: Inverter Status description: Status profile for battery/solar/hybrid inverters telemetry: diff --git a/lib/energy/inverter/temperature.yml b/lib/energy/inverter/temperature.yml index 4ef15a7..672a20f 100644 --- a/lib/energy/inverter/temperature.yml +++ b/lib/energy/inverter/temperature.yml @@ -1,6 +1,6 @@ blueprint_spec: profile/1.0 -display_name: Inverter Temperature Profile +display_name: Inverter Temperature description: Provides temperature measurements for different parts of the inverter telemetry: diff --git a/lib/energy/power_meter/energy/today.yml b/lib/energy/power_meter/energy/today.yml index 2451dc1..7933597 100644 --- a/lib/energy/power_meter/energy/today.yml +++ b/lib/energy/power_meter/energy/today.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 -display_name: Energy Statistics Profile - Today +display_name: Energy Statistics - Today +description: Energy consumption since the start of the current day. telemetry: energy_today: diff --git a/lib/energy/power_meter/energy/total.yml b/lib/energy/power_meter/energy/total.yml index 070fb78..99c32cf 100644 --- a/lib/energy/power_meter/energy/total.yml +++ b/lib/energy/power_meter/energy/total.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 -display_name: Energy Statistics Profile - Total +display_name: Energy Statistics - Total +description: Cumulative energy consumption since installation. telemetry: energy_total: diff --git a/lib/energy/pv/power.yml b/lib/energy/pv/power.yml index d3a7b29..159f91e 100644 --- a/lib/energy/pv/power.yml +++ b/lib/energy/pv/power.yml @@ -1,6 +1,6 @@ blueprint_spec: profile/1.0 -display_name: Total PV Power Profile +display_name: Total PV Power description: Total PV power measurement across all strings. telemetry: diff --git a/lib/firmware/version.yml b/lib/firmware/version.yml index 9e6207c..acd2bf3 100644 --- a/lib/firmware/version.yml +++ b/lib/firmware/version.yml @@ -1,6 +1,6 @@ blueprint_spec: profile/1.0 -display_name: Firmware Version Profile +display_name: Firmware Version properties: firmware_version: From fc7ca07a4cd50956801584490beef899bdf2c6f2 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 17:30:16 +0400 Subject: [PATCH 08/23] fix: change residual_current unit to mA and add UCUM deg unit to coordinates --- lib/energy/inverter/residual_current.yml | 4 ++-- lib/location/coordinates.yml | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/energy/inverter/residual_current.yml b/lib/energy/inverter/residual_current.yml index 1e5f795..e88f1dd 100644 --- a/lib/energy/inverter/residual_current.yml +++ b/lib/energy/inverter/residual_current.yml @@ -1,10 +1,10 @@ blueprint_spec: profile/1.0 -display_name: Inverter Residual Current Profile +display_name: Inverter Residual Current telemetry: residual_current: display_name: Residual Current type: float - unit: A + unit: mA description: Residual current detected by the inverter. diff --git a/lib/location/coordinates.yml b/lib/location/coordinates.yml index 375c0ef..094fe8d 100644 --- a/lib/location/coordinates.yml +++ b/lib/location/coordinates.yml @@ -1,14 +1,16 @@ blueprint_spec: profile/1.0 -display_name: Location Coordinates Profile +display_name: Location Coordinates properties: latitude: display_name: Latitude type: float + unit: deg description: Latitude coordinate in decimal degrees longitude: display_name: Longitude type: float + unit: deg description: Longitude coordinate in decimal degrees From 98830235d72dad6aaec057930d4e6991db53ba8f Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 17:41:05 +0400 Subject: [PATCH 09/23] docs: clarify sign convention for bidirectional vs unidirectional power --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 60fe111..a9e2219 100644 --- a/README.md +++ b/README.md @@ -30,17 +30,19 @@ Profiles are used in Enapter Blueprints to specify the capabilities and the inte ### Sign Conventions -All power and current measurements follow the **generator reference frame** (positive = energy flowing out of the device into the system): +Power and current signs are defined so that the energy balance `pv_power + battery_power + grid_power = load_power` holds naturally: -| Measurement Point | Positive Value | Negative Value | +| Measurement Point | Positive (+) | Negative (-) | |---|---|---| -| Battery current/power | Discharging (energy out) | Charging (energy in) | +| Battery current/power | Discharging | Charging | | Inverter AC power | Delivering to loads/grid | Consuming (e.g. standby) | | Grid power | Importing from grid | Exporting to grid | -| Load power | Consumed by loads | Fed back | -| PV power | Always positive (generation) | N/A | +| Load power | Consumed by loads | N/A (typically) | +| PV power | Generating | N/A | -This convention ensures consistent energy balance calculations: `pv_power + battery_power + grid_power = load_power` (signs will balance naturally). +**Rule of thumb:** if the measurement represents energy being delivered to the system, it is positive. If it represents energy being absorbed, it is negative. + +For unidirectional measurements where the direction is clear from context (e.g. `power_consumption` for a device that only consumes), positive values representing the natural physical quantity are acceptable. ### Naming Conventions @@ -52,7 +54,7 @@ This convention ensures consistent energy balance calculations: `pv_power + batt - Load fields use `load_` prefix - Power meter total fields (`total_power`, `total_current`, `energy_total`) omit the `ac_` prefix because power meters only measure one type of current, so the prefix is redundant. Per-phase fields keep the `ac_` prefix since they follow the same naming as inverter per-phase measurements. - Three-phase measurements use `l1`/`l2`/`l3` suffixes (IEC convention) -- Units follow [UCUM](https://ucum.org/) notation: `W`, `Wh`, `V`, `A`, `Hz`, `VA`, `VAR`, `Cel`, `%` +- Units follow [UCUM](https://ucum.org/) notation. See the [units introduction](https://v3.developers.enapter.com/docs/units/introduction) and [frequently used units](https://v3.developers.enapter.com/docs/units/frequently-used) for the full reference. - Status fields must use a context-appropriate prefix instead of the bare `status` name (e.g. `inverter_status`, `charger_status`, `relay_state`). The bare `status` is a reserved field name in the Enapter platform with [special meaning](https://developers.enapter.com/docs/reference/manifest#device-status) and is intentionally left for the Blueprint developer to expose the native device status. Profiles define a separate prefixed field with a unified set of operational states (e.g. `off`, `operating`, `fault`) so that UIs and automation can treat all devices of the same type consistently, while the native status remains available for device-specific diagnostics. ## Development From c4e038fe687a0c5bc26380e60acfb08a905b3298 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 18:14:39 +0400 Subject: [PATCH 10/23] fix: use accurate ac_frequency description for inverters and power meters --- lib/energy/inverter/ac/1_phase.yml | 2 +- lib/energy/inverter/ac/3_phase.yml | 2 +- lib/energy/power_meter/ac/1_phase.yml | 2 +- lib/energy/power_meter/ac/3_phase.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/energy/inverter/ac/1_phase.yml b/lib/energy/inverter/ac/1_phase.yml index 8c04df9..0d1b672 100644 --- a/lib/energy/inverter/ac/1_phase.yml +++ b/lib/energy/inverter/ac/1_phase.yml @@ -8,7 +8,7 @@ telemetry: display_name: AC Frequency type: float unit: Hz - description: AC grid frequency. + description: AC output frequency. ac_l1_power: display_name: AC Power diff --git a/lib/energy/inverter/ac/3_phase.yml b/lib/energy/inverter/ac/3_phase.yml index 7fd7c62..76aba5c 100644 --- a/lib/energy/inverter/ac/3_phase.yml +++ b/lib/energy/inverter/ac/3_phase.yml @@ -8,7 +8,7 @@ telemetry: display_name: AC Frequency type: float unit: Hz - description: AC grid frequency. + description: AC output frequency. # Phase L1 ac_l1_voltage: diff --git a/lib/energy/power_meter/ac/1_phase.yml b/lib/energy/power_meter/ac/1_phase.yml index 7b7e87c..574634a 100644 --- a/lib/energy/power_meter/ac/1_phase.yml +++ b/lib/energy/power_meter/ac/1_phase.yml @@ -8,7 +8,7 @@ telemetry: display_name: AC Frequency type: float unit: Hz - description: AC grid frequency. + description: AC frequency at the measurement point. ac_l1_power: display_name: AC Power diff --git a/lib/energy/power_meter/ac/3_phase.yml b/lib/energy/power_meter/ac/3_phase.yml index c7e6150..6cec878 100644 --- a/lib/energy/power_meter/ac/3_phase.yml +++ b/lib/energy/power_meter/ac/3_phase.yml @@ -8,7 +8,7 @@ telemetry: display_name: AC Frequency type: float unit: Hz - description: AC grid frequency. + description: AC frequency at the measurement point. # Phase L1 ac_l1_voltage: From 4e357f6c6b46bdd980aa336360907ae3c306b8b9 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 18:14:43 +0400 Subject: [PATCH 11/23] fix: clarify inverter_nameplate_capacity is apparent power in VA --- lib/energy/inverter/nameplate.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/energy/inverter/nameplate.yml b/lib/energy/inverter/nameplate.yml index f79c2f7..8c6eae6 100644 --- a/lib/energy/inverter/nameplate.yml +++ b/lib/energy/inverter/nameplate.yml @@ -1,11 +1,11 @@ blueprint_spec: profile/1.0 display_name: Inverter Nameplate -description: Nameplate profile for battery/solar/hybrid inverters covering core attributes +description: Nameplate attributes for battery, solar, and hybrid inverters. properties: inverter_nameplate_capacity: display_name: Nameplate Capacity type: integer unit: VA - description: Maximum power rating of the inverter + description: Maximum apparent power rating of the inverter in VA (volt-amperes). From b0ab209103c04586de0ef3215dc4405b45ad6894 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 18:14:46 +0400 Subject: [PATCH 12/23] fix: align pv_energy_total description to say "since installation" --- lib/energy/pv/stats/energy/total.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/energy/pv/stats/energy/total.yml b/lib/energy/pv/stats/energy/total.yml index cdf8666..0f1e101 100644 --- a/lib/energy/pv/stats/energy/total.yml +++ b/lib/energy/pv/stats/energy/total.yml @@ -7,4 +7,4 @@ telemetry: display_name: Total PV Energy Produced type: float unit: Wh - description: Cumulative energy produced by all PV strings since commissioning. + description: Cumulative energy produced by all PV strings since installation. From f198050b448b6bed12d9982251dc8d2f4787de7f Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 18:14:50 +0400 Subject: [PATCH 13/23] fix: improve vague device profile descriptions for battery and pv_charge_controller --- energy/battery.yml | 2 +- energy/battery_inverter/1_phase.yml | 2 +- energy/battery_inverter/3_phase.yml | 2 +- energy/hybrid_inverter/1_phase.yml | 2 +- energy/hybrid_inverter/3_phase.yml | 2 +- energy/power_meter/ac/1_phase.yml | 2 +- energy/power_meter/ac/3_phase.yml | 2 +- energy/pv_charge_controller.yml | 2 +- energy/pv_inverter/1_phase.yml | 2 +- energy/pv_inverter/3_phase.yml | 2 +- sensor/ambient_temperature.yml | 2 +- sensor/hydrogen.yml | 2 +- sensor/solar_irradiance.yml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/energy/battery.yml b/energy/battery.yml index 615fa5d..e53392c 100644 --- a/energy/battery.yml +++ b/energy/battery.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Battery/BMS Profile -description: A profile for battery and battery management systems +description: A profile for battery systems and battery management systems (BMS). Covers state of charge, charge/discharge power and current, and nameplate attributes. Suitable for standalone battery banks and integrated BESS units. implements: - lib.device.nameplate diff --git a/energy/battery_inverter/1_phase.yml b/energy/battery_inverter/1_phase.yml index ddf51b8..e98d2e3 100644 --- a/energy/battery_inverter/1_phase.yml +++ b/energy/battery_inverter/1_phase.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Single-Phase Battery Inverter -description: A single-phase battery inverter profile combining all relevant capabilities +description: A profile for single-phase battery inverters (off-grid and grid-tied). Covers battery state of charge and charge/discharge power, AC output measurements, grid import/export and load power, operational status, and nameplate attributes. implements: - lib.device.nameplate diff --git a/energy/battery_inverter/3_phase.yml b/energy/battery_inverter/3_phase.yml index cdb232e..5d9df4d 100644 --- a/energy/battery_inverter/3_phase.yml +++ b/energy/battery_inverter/3_phase.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Three-Phase Battery Inverter -description: A three-phase battery inverter profile combining all relevant capabilities +description: A profile for three-phase battery inverters (off-grid and grid-tied). Covers battery state of charge and charge/discharge power, AC output measurements, grid import/export and load power, operational status, and nameplate attributes. implements: - lib.device.nameplate diff --git a/energy/hybrid_inverter/1_phase.yml b/energy/hybrid_inverter/1_phase.yml index 41cd01f..4a3ada0 100644 --- a/energy/hybrid_inverter/1_phase.yml +++ b/energy/hybrid_inverter/1_phase.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Single-Phase Hybrid Inverter -description: A single-phase hybrid inverter profile combining battery and PV capabilities +description: A profile for single-phase hybrid inverters with integrated PV input and battery management. Covers PV input power, battery state of charge and power, AC output measurements, grid and load power, operational status, and nameplate attributes. implements: - lib.device.nameplate diff --git a/energy/hybrid_inverter/3_phase.yml b/energy/hybrid_inverter/3_phase.yml index 9575aab..9b8fb84 100644 --- a/energy/hybrid_inverter/3_phase.yml +++ b/energy/hybrid_inverter/3_phase.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Three-Phase Hybrid Inverter -description: A three-phase hybrid inverter profile combining battery and PV capabilities +description: A profile for three-phase hybrid inverters with integrated PV input and battery management. Covers PV input power, battery state of charge and power, AC output measurements, grid and load power, operational status, and nameplate attributes. implements: - lib.device.nameplate diff --git a/energy/power_meter/ac/1_phase.yml b/energy/power_meter/ac/1_phase.yml index 6536612..efc77f4 100644 --- a/energy/power_meter/ac/1_phase.yml +++ b/energy/power_meter/ac/1_phase.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Single-Phase Power Meter -description: A single-phase power meter profile +description: A profile for single-phase AC power meters. Covers voltage, current, power, and cumulative energy consumption. implements: - lib.device.nameplate diff --git a/energy/power_meter/ac/3_phase.yml b/energy/power_meter/ac/3_phase.yml index d5cea9c..472606d 100644 --- a/energy/power_meter/ac/3_phase.yml +++ b/energy/power_meter/ac/3_phase.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Three-Phase Power Meter -description: A three-phase power meter profile +description: A profile for three-phase AC power meters. Covers per-phase and total voltage, current, power, and cumulative energy consumption. implements: - lib.device.nameplate diff --git a/energy/pv_charge_controller.yml b/energy/pv_charge_controller.yml index 2943eea..cc22adf 100644 --- a/energy/pv_charge_controller.yml +++ b/energy/pv_charge_controller.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: PV Charge Controller -description: A profile for PV charge controllers +description: A profile for solar charge controllers (MPPT and PWM). Covers PV input power, battery state of charge, charge/discharge current, and battery nameplate attributes. implements: - lib.device.nameplate diff --git a/energy/pv_inverter/1_phase.yml b/energy/pv_inverter/1_phase.yml index a98b3a7..706c1e6 100644 --- a/energy/pv_inverter/1_phase.yml +++ b/energy/pv_inverter/1_phase.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Single-Phase PV Inverter -description: A single-phase PV inverter profile combining all relevant capabilities +description: A profile for single-phase grid-tied PV inverters. Covers PV input power, AC output voltage/current/power, total AC power, operational status, and nameplate capacity. implements: - lib.device.nameplate diff --git a/energy/pv_inverter/3_phase.yml b/energy/pv_inverter/3_phase.yml index c349caf..86a74b2 100644 --- a/energy/pv_inverter/3_phase.yml +++ b/energy/pv_inverter/3_phase.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Three-Phase PV Inverter -description: A three-phase PV inverter profile combining all relevant capabilities +description: A profile for three-phase grid-tied PV inverters. Covers PV input power, per-phase and total AC output power, operational status, and nameplate capacity. implements: - lib.device.nameplate diff --git a/sensor/ambient_temperature.yml b/sensor/ambient_temperature.yml index 5292159..75e2c66 100644 --- a/sensor/ambient_temperature.yml +++ b/sensor/ambient_temperature.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Ambient Temperature Profile -description: A profile for devices that can measure ambient temperature. +description: A profile for ambient temperature sensors. Reports air temperature in degrees Celsius. implements: - lib.device.nameplate diff --git a/sensor/hydrogen.yml b/sensor/hydrogen.yml index 24060c6..4a15572 100644 --- a/sensor/hydrogen.yml +++ b/sensor/hydrogen.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Hydrogen Concentration Sensor Profile -description: A profile for hydrogen concentration and safety sensors. +description: A profile for hydrogen gas detectors. Reports gas concentration as a percentage of the Lower Explosive Limit (%LEL). implements: - lib.device.nameplate diff --git a/sensor/solar_irradiance.yml b/sensor/solar_irradiance.yml index 47da601..e9ec4da 100644 --- a/sensor/solar_irradiance.yml +++ b/sensor/solar_irradiance.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Solar Irradiance Profile -description: A profile for devices that can measure solar irradiance. +description: A profile for solar irradiance sensors (pyranometers). Reports irradiance in W/m². implements: - lib.device.nameplate From 784cf9fa24ea4dff9a5d96b9f0a36041dd6dd139 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 18:14:53 +0400 Subject: [PATCH 14/23] fix: note that negative load_total_power is possible but uncommon --- lib/energy/inverter/load/power.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/energy/inverter/load/power.yml b/lib/energy/inverter/load/power.yml index 2b6cf92..fe254c3 100644 --- a/lib/energy/inverter/load/power.yml +++ b/lib/energy/inverter/load/power.yml @@ -1,11 +1,11 @@ blueprint_spec: profile/1.0 display_name: Inverter Load Power -description: Power consumed by local loads on inverter AC output or fed back to grid/battery. +description: Power consumed by local loads on the inverter AC output. telemetry: load_total_power: display_name: Load Power type: float unit: W - description: Total power consumed by local loads on inverter AC output (positive) or fed back to grid/battery (negative). + description: Total power consumed by local loads on the inverter AC output. Typically positive; negative values are possible but uncommon when a bidirectional load or generator is connected to the load port. From fcf600dc2b515df14289c402850d1671fe010394 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Mon, 23 Feb 2026 18:15:27 +0400 Subject: [PATCH 15/23] docs: remove battery chemistry reference --- lib/energy/battery/nameplate.yml | 112 +------------------------------ 1 file changed, 1 insertion(+), 111 deletions(-) diff --git a/lib/energy/battery/nameplate.yml b/lib/energy/battery/nameplate.yml index 331aa8c..a3361ec 100644 --- a/lib/energy/battery/nameplate.yml +++ b/lib/energy/battery/nameplate.yml @@ -17,127 +17,17 @@ properties: description: Main type of the battery system. type: string enum: - # Charging: 3-stage (Bulk/Absorption/Float) - # - Bulk: Constant current (0.1-0.2C) until ~80% charge - # - Absorption: Constant voltage (14.2-14.4V per 12V) until current drops - # - Float: Lower voltage (13.2-13.4V per 12V) maintenance - # - # Typical cell voltage: 2.0V - # System voltages: 12V, 24V, 48V - # Charge voltage limits: 2.30-2.45V per cell - # Discharge voltage limits: 1.75-1.90V per cell - # - # Cycle life: 200-300 cycles (to 80% DoD) - # Calendar life: 3-5 years - # Recommended DoD: 50% max - # Self-discharge: 3-20% per month - # Temperature range: -20°C to 50°C - # Energy density: 30-50 Wh/kg - # - # Special considerations: - # - Requires regular equalization charging - # - Temperature compensation required - # - Susceptible to sulfation - # - Regular watering needed (flooded type) + # See .context/battery-types.md for per-type charging parameters and considerations. lead_based: display_name: Lead-based - - # Charging: 2-stage (CC/CV) - # - Constant Current until reaching max voltage - # - Constant Voltage until current drops (typically 0.05C) - # - # Typical cell voltage: 3.2V (LFP), 3.6-3.7V (NMC/NCA) - # System voltages: 12V, 24V, 48V, 400V, 800V - # Charge voltage limits: 3.65V (LFP), 4.2V (NMC) per cell - # Discharge voltage limits: 2.5V-3.0V per cell - # - # Cycle life: 2000-8000 cycles (to 80% DoD) - # Calendar life: 10-20 years - # Recommended DoD: 80-90% - # Self-discharge: 1-3% per month - # Temperature range: 0°C to 45°C - # Energy density: 100-265 Wh/kg - # - # Special considerations: - # - Cell balancing required - # - Temperature monitoring critical - # - No trickle charging - # - BMS required for safety lithium_based: display_name: Lithium-based - - # Charging: Multi-stage with negative delta V detection - # - Constant current until -ΔV detection - # - Trickle charge for maintenance - # - # Typical cell voltage: 1.2V - # System voltages: 12V, 24V, 48V - # Charge voltage: 1.45V per cell - # Discharge voltage limit: 1.0V per cell - # - # Cycle life: NiCd 1500 cycles, NiMH 500-1000 cycles - # Calendar life: 5-10 years - # Recommended DoD: 80% - # Self-discharge: 15-20% per month - # Temperature range: -20°C to 45°C - # Energy density: 40-80 Wh/kg - # - # Special considerations: - # - Memory effect (especially NiCd) - # - Periodic full discharge recommended - # - Higher self-discharge rate - # - Environmentally hazardous (NiCd) nickel_based: display_name: Nickel-based - - # Charging: Single-stage - # - Constant current charging - # - Power proportional to electrolyte flow rate - # - # Typical cell voltage: 1.4-1.6V - # System voltages: Flexible, typically high voltage - # Operating voltage range: 1.2-1.6V per cell - # - # Cycle life: 12000-14000 cycles - # Calendar life: 20+ years - # Recommended DoD: 100% - # Self-discharge: Minimal if electrolytes separated - # Temperature range: 10°C to 40°C - # Energy density: 20-40 Wh/kg - # - # Special considerations: - # - Separate power/energy scaling - # - Pump power consumption - # - Regular maintenance of fluid systems - # - Temperature management critical - # - Electrolyte monitoring required flow: display_name: Flow - - # Charging: Temperature-dependent charging - # - Must maintain high operating temperature - # - Constant current followed by constant voltage - # - # Typical cell voltage: 2.1V - # System voltages: Usually high voltage (500V+) - # Charge voltage limits: 2.3V per cell - # Discharge voltage limits: 1.9V per cell - # - # Cycle life: 4500+ cycles - # Calendar life: 10-15 years - # Recommended DoD: 80% - # Self-discharge: Dependent on thermal insulation - # Temperature range: 300-350°C operating - # Energy density: 100-150 Wh/kg - # - # Special considerations: - # - High temperature operation - # - Thermal management critical - # - Internal heater power consumption - # - Extended startup time from cold sodium_based: display_name: Sodium-based - other: display_name: Other description: Other battery type, not covered by the standard types. From ddc02fefd608ace6ce22064757cbdf9274842df4 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Tue, 24 Feb 2026 11:07:20 +0400 Subject: [PATCH 16/23] feat: clarify Wh/Ah and W/A unit conventions for battery profiles --- README.md | 7 ++++++ lib/energy/battery/charge.yml | 34 ++++++++++++++++++++++++++ lib/energy/battery/energy.yml | 19 ++++++++++++--- lib/energy/battery/limits.yml | 41 +++++++++++++++++++------------- lib/energy/battery/nameplate.yml | 15 +++++++++--- 5 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 lib/energy/battery/charge.yml diff --git a/README.md b/README.md index a9e2219..e0f41f8 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,13 @@ For unidirectional measurements where the direction is clear from context (e.g. - Units follow [UCUM](https://ucum.org/) notation. See the [units introduction](https://v3.developers.enapter.com/docs/units/introduction) and [frequently used units](https://v3.developers.enapter.com/docs/units/frequently-used) for the full reference. - Status fields must use a context-appropriate prefix instead of the bare `status` name (e.g. `inverter_status`, `charger_status`, `relay_state`). The bare `status` is a reserved field name in the Enapter platform with [special meaning](https://developers.enapter.com/docs/reference/manifest#device-status) and is intentionally left for the Blueprint developer to expose the native device status. Profiles define a separate prefixed field with a unified set of operational states (e.g. `off`, `operating`, `fault`) so that UIs and automation can treat all devices of the same type consistently, while the native status remains available for device-specific diagnostics. +### Unit Conventions + +Some physical quantities appear in different units across device types. There are two ways to handle this, depending on whether the units are interchangeable: + +- **Separate profiles per unit**: when units measure different physical quantities and silent conversion would hide precision loss. Blueprint authors implement whichever profile their device supports natively; consumers must handle both explicitly. See `lib.energy.battery.energy` (Wh) and `lib.energy.battery.charge` (Ah) as an example. +- **Single canonical unit with required conversion**: when the same quantity can be meaningfully expressed in either unit and one is clearly more appropriate. Blueprint authors must convert to the canonical unit. See `lib.energy.battery.limits` (W, with a documented formula for devices that only report in A) as an example. + ## Development ### Device Profile diff --git a/lib/energy/battery/charge.yml b/lib/energy/battery/charge.yml new file mode 100644 index 0000000..e7243da --- /dev/null +++ b/lib/energy/battery/charge.yml @@ -0,0 +1,34 @@ +blueprint_spec: profile/1.0 + +display_name: Battery Cumulative Charge Statistics +description: | + Cumulative charge in Ah. Use for solar charge controllers and BMS devices that meter + in ampere-hours natively. + + If the device meters in watt-hours, use lib.energy.battery.energy instead. + + Ah and Wh are not interchangeable: Ah measures charge (coulombs), Wh measures energy + (joules). To convert, use battery_nominal_voltage from lib.energy.battery.nameplate: + + battery_charged_energy_total = battery_charge_in_total * battery_nominal_voltage + battery_discharged_energy_total = battery_charge_out_total * battery_nominal_voltage + + Approximation error: below 3% for LFP, up to ~12% for lead-acid. + + Automation: prefer lib.energy.battery.energy (Wh) for fleet-wide aggregation. When a + device only provides Ah, apply the conversion above and document the assumptions. + + UI: prefer Wh from lib.energy.battery.energy, fall back to Ah. + +telemetry: + battery_charge_in_total: + display_name: Total Charge In + type: float + unit: Ah + description: Cumulative charge transferred into batteries since installation. + + battery_charge_out_total: + display_name: Total Charge Out + type: float + unit: Ah + description: Cumulative charge transferred out of batteries since installation. diff --git a/lib/energy/battery/energy.yml b/lib/energy/battery/energy.yml index de3c8de..26b2f78 100644 --- a/lib/energy/battery/energy.yml +++ b/lib/energy/battery/energy.yml @@ -1,10 +1,23 @@ blueprint_spec: profile/1.0 -display_name: Battery Energy Statistics -description: Cumulative energy statistics for batteries. +display_name: Battery Cumulative Energy Statistics +description: | + Cumulative energy in Wh. Use for battery inverters, hybrid inverters, and AC-coupled systems. + + If the device meters in ampere-hours, use lib.energy.battery.charge instead. Do not + convert Ah to Wh silently; consumers need to know what unit they are working with. + + Automation: prefer Wh. For devices implementing lib.energy.battery.charge instead, + convert explicitly using battery_nominal_voltage from lib.energy.battery.nameplate: + + battery_charged_energy_total = battery_charge_in_total * battery_nominal_voltage + battery_discharged_energy_total = battery_charge_out_total * battery_nominal_voltage + + Approximation error: below 3% for LFP, up to ~12% for lead-acid. + + UI: prefer Wh, fall back to Ah from lib.energy.battery.charge. telemetry: - # XXX: Wh vs Ah battery_charged_energy_total: display_name: Total Energy Charged type: float diff --git a/lib/energy/battery/limits.yml b/lib/energy/battery/limits.yml index b11cd62..e9f90ec 100644 --- a/lib/energy/battery/limits.yml +++ b/lib/energy/battery/limits.yml @@ -1,23 +1,32 @@ blueprint_spec: profile/1.0 -display_name: Battery Charge/Discharge Limits -description: Defines the current limits for battery charging and discharging operations. +display_name: Battery Charge/Discharge Power Limits +description: | + Maximum charge and discharge power limits in watts (W), consistent with inverter + control, grid codes, and VPP setpoints. + + For devices (typically a BMS) that only report current limits in amperes, convert + using battery_nominal_voltage from lib.energy.battery.nameplate: + + battery_max_charge_power (W) = max_charge_current_A * battery_nominal_voltage (V) + battery_max_discharge_power (W) = max_discharge_current_A * battery_nominal_voltage (V) + + Use battery_nominal_voltage, not the instantaneous battery_voltage telemetry. + battery_voltage fluctuates continuously and would make the limit value noisy even + when the BMS has not changed the underlying current limit. + Approximation error: ±5% for LFP, ±10-15% for lead-acid. + + Automation: treat these as operational guidance. The BMS enforces the actual limit. telemetry: - # XXX: A vs W - # NOTE: Some devices may report this in power (W) instead of current (A). - # The integration should convert between units if necessary using the formula I = P / V. - # where I is current in amperes, P is power in watts, and V is voltage in volts. - # One can either use the battery nominal voltage or the actual voltage to convert - # between power and current. - battery_max_charge_current: - display_name: Maximum Charge Current + battery_max_charge_power: + display_name: Maximum Charge Power type: float - unit: A - description: Maximum allowed charging current. + unit: W + description: Maximum allowed charging power as permitted by the battery system. - battery_max_discharge_current: - display_name: Maximum Discharge Current + battery_max_discharge_power: + display_name: Maximum Discharge Power type: float - unit: A - description: Maximum allowed discharging current. + unit: W + description: Maximum allowed discharging power as permitted by the battery system. diff --git a/lib/energy/battery/nameplate.yml b/lib/energy/battery/nameplate.yml index a3361ec..d7d054a 100644 --- a/lib/energy/battery/nameplate.yml +++ b/lib/energy/battery/nameplate.yml @@ -4,8 +4,18 @@ display_name: Battery Nameplate description: Battery nameplate attributes for battery inverters, charge controllers, and battery management systems. properties: - # XXX: Wh vs Ah - # NOTE: If the battery system is reporting capacity in Ah, device implementation should convert it to Wh. + # NOTE: Proper implementation is strongly recommended. + # It is used by lib.energy.battery.charge and lib.energy.battery.limits to convert + # Ah -> Wh and A -> W respectively. Leaving it unset breaks those conversions. + # Consumers of those profiles should handle the case when this property is not set. + battery_nominal_voltage: + display_name: Battery Nominal Voltage + description: Nominal DC bus voltage of the battery system as specified by the manufacturer. Used to convert between charge (Ah) and energy (Wh) when the device only reports one of the two. + type: float + unit: V + + # NOTE: If the battery system reports capacity in Ah, convert using: + # battery_nameplate_capacity (Wh) = capacity_ah * battery_nominal_voltage battery_nameplate_capacity: display_name: Battery Nameplate Capacity description: Nameplate energy capacity of connected batteries according to manufacturer specifications. @@ -17,7 +27,6 @@ properties: description: Main type of the battery system. type: string enum: - # See .context/battery-types.md for per-type charging parameters and considerations. lead_based: display_name: Lead-based lithium_based: From 1af338a5b8f55651407625f812b09ff7ef8b3f40 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Tue, 24 Feb 2026 12:44:46 +0400 Subject: [PATCH 17/23] docs: add versioning section to README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index e0f41f8..51e57fa 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,17 @@ Some physical quantities appear in different units across device types. There ar - **Separate profiles per unit**: when units measure different physical quantities and silent conversion would hide precision loss. Blueprint authors implement whichever profile their device supports natively; consumers must handle both explicitly. See `lib.energy.battery.energy` (Wh) and `lib.energy.battery.charge` (Ah) as an example. - **Single canonical unit with required conversion**: when the same quantity can be meaningfully expressed in either unit and one is clearly more appropriate. Blueprint authors must convert to the canonical unit. See `lib.energy.battery.limits` (W, with a documented formula for devices that only report in A) as an example. +## Versioning + +Profiles use **append-only immutability with composition-based extension**: + +- **Published profiles are immutable.** Once a profile is published (non-draft), its YAML cannot change. This guarantees that existing Blueprints on deployed Gateways never become invalid. +- **No field removal.** If a profile needs incompatible changes, create a new profile version (e.g. `energy.battery_v2`) that implements the old one plus new library components. +- **Field addition via composition.** New capabilities are added by creating new library components and composing them into a new device profile that `implements` the previous version. +- **Draft mode.** Profiles start as drafts and can be iterated freely. Once promoted to non-draft, they become immutable. A non-draft profile cannot implement a draft profile, preventing unstable dependencies from leaking into production. + +In practice, keeping profiles minimal reduces the need for breaking changes. When extension is needed, the composition model allows new profiles to build on existing ones without invalidating older Blueprints. + ## Development ### Device Profile From f0accfa0c753f41dcc40df2048b96d46bc358034 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Tue, 24 Feb 2026 12:44:50 +0400 Subject: [PATCH 18/23] fix: correct grammar in power meter descriptions --- lib/energy/power_meter/current.yml | 2 +- lib/energy/power_meter/power.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/energy/power_meter/current.yml b/lib/energy/power_meter/current.yml index 419f5e2..a6d4184 100644 --- a/lib/energy/power_meter/current.yml +++ b/lib/energy/power_meter/current.yml @@ -8,4 +8,4 @@ telemetry: display_name: Total Current type: float unit: A - description: Total current across all connected loads. Positive values typically corresponds to current flowing out to loads. + description: Total current across all connected loads. Positive values typically correspond to current flowing out to loads. diff --git a/lib/energy/power_meter/power.yml b/lib/energy/power_meter/power.yml index 72f4098..692d1cb 100644 --- a/lib/energy/power_meter/power.yml +++ b/lib/energy/power_meter/power.yml @@ -8,4 +8,4 @@ telemetry: display_name: Total Power type: float unit: W - description: Total power across all connected loads. Positive values typically corresponds to power being consumed by loads. + description: Total power across all connected loads. Positive values typically correspond to power being consumed by loads. From 8bcdba8030a8f089625704eaa8c8b1e70a108fde Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Tue, 24 Feb 2026 12:44:55 +0400 Subject: [PATCH 19/23] fix: add missing descriptions to library profiles --- lib/energy/inverter/ac/energy/today.yml | 1 + lib/energy/inverter/ac/energy/total.yml | 1 + lib/energy/inverter/grid/mode.yml | 1 + lib/energy/inverter/grid/power.yml | 1 + lib/energy/inverter/grid/status.yml | 1 + lib/energy/inverter/residual_current.yml | 1 + lib/energy/pv/stats/energy/today.yml | 1 + lib/energy/pv/stats/energy/total.yml | 1 + lib/firmware/version.yml | 1 + lib/location/coordinates.yml | 1 + lib/sensor/gas/ppm.yml | 1 + 11 files changed, 11 insertions(+) diff --git a/lib/energy/inverter/ac/energy/today.yml b/lib/energy/inverter/ac/energy/today.yml index 993eecd..af39699 100644 --- a/lib/energy/inverter/ac/energy/today.yml +++ b/lib/energy/inverter/ac/energy/today.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 display_name: Inverter Energy Statistics - Today +description: AC energy production since the start of the current day. telemetry: ac_energy_today: diff --git a/lib/energy/inverter/ac/energy/total.yml b/lib/energy/inverter/ac/energy/total.yml index 7fcf1e7..94da5da 100644 --- a/lib/energy/inverter/ac/energy/total.yml +++ b/lib/energy/inverter/ac/energy/total.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 display_name: Inverter Energy Statistics - Total +description: Cumulative AC energy production since installation. telemetry: ac_energy_total: diff --git a/lib/energy/inverter/grid/mode.yml b/lib/energy/inverter/grid/mode.yml index a0e80ae..e3820a7 100644 --- a/lib/energy/inverter/grid/mode.yml +++ b/lib/energy/inverter/grid/mode.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 display_name: Grid Connection Mode +description: Configured grid connection mode for the inverter. properties: grid_mode: diff --git a/lib/energy/inverter/grid/power.yml b/lib/energy/inverter/grid/power.yml index df7f560..d5c916f 100644 --- a/lib/energy/inverter/grid/power.yml +++ b/lib/energy/inverter/grid/power.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 display_name: Grid Power +description: Power exchange with the utility grid. telemetry: grid_total_power: diff --git a/lib/energy/inverter/grid/status.yml b/lib/energy/inverter/grid/status.yml index c3a7a65..b26b68b 100644 --- a/lib/energy/inverter/grid/status.yml +++ b/lib/energy/inverter/grid/status.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 display_name: Grid Connection Status +description: Grid connection state for grid-tied and hybrid inverters. telemetry: grid_status: diff --git a/lib/energy/inverter/residual_current.yml b/lib/energy/inverter/residual_current.yml index e88f1dd..db48718 100644 --- a/lib/energy/inverter/residual_current.yml +++ b/lib/energy/inverter/residual_current.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 display_name: Inverter Residual Current +description: Residual current monitoring for inverter ground fault detection. telemetry: residual_current: diff --git a/lib/energy/pv/stats/energy/today.yml b/lib/energy/pv/stats/energy/today.yml index c728dac..9a52d88 100644 --- a/lib/energy/pv/stats/energy/today.yml +++ b/lib/energy/pv/stats/energy/today.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 display_name: PV Energy Statistics - Today +description: PV energy production since the start of the current day. telemetry: pv_energy_today: diff --git a/lib/energy/pv/stats/energy/total.yml b/lib/energy/pv/stats/energy/total.yml index 0f1e101..f26e444 100644 --- a/lib/energy/pv/stats/energy/total.yml +++ b/lib/energy/pv/stats/energy/total.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 display_name: PV Energy Statistics - Total +description: Cumulative PV energy production since installation. telemetry: pv_energy_total: diff --git a/lib/firmware/version.yml b/lib/firmware/version.yml index acd2bf3..3237e5a 100644 --- a/lib/firmware/version.yml +++ b/lib/firmware/version.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 display_name: Firmware Version +description: Device firmware version information. properties: firmware_version: diff --git a/lib/location/coordinates.yml b/lib/location/coordinates.yml index 094fe8d..43ffd13 100644 --- a/lib/location/coordinates.yml +++ b/lib/location/coordinates.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 display_name: Location Coordinates +description: Geographic coordinates in decimal degrees. properties: latitude: diff --git a/lib/sensor/gas/ppm.yml b/lib/sensor/gas/ppm.yml index c573315..fa85744 100644 --- a/lib/sensor/gas/ppm.yml +++ b/lib/sensor/gas/ppm.yml @@ -1,6 +1,7 @@ blueprint_spec: profile/1.0 display_name: Gas Concentration in ppm +description: Gas concentration measurement in parts per million. telemetry: gas_ppm: From acb80ae12336822de10cb2e8d992010d77b27454 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Tue, 24 Feb 2026 13:41:58 +0400 Subject: [PATCH 20/23] fix: add missing trailing periods to descriptions --- lib/energy/inverter/status.yml | 2 +- lib/energy/inverter/temperature.yml | 2 +- lib/location/coordinates.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/energy/inverter/status.yml b/lib/energy/inverter/status.yml index a02d2ca..a88dd92 100644 --- a/lib/energy/inverter/status.yml +++ b/lib/energy/inverter/status.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Inverter Status -description: Status profile for battery/solar/hybrid inverters +description: Status profile for battery/solar/hybrid inverters. telemetry: inverter_status: diff --git a/lib/energy/inverter/temperature.yml b/lib/energy/inverter/temperature.yml index 672a20f..f646557 100644 --- a/lib/energy/inverter/temperature.yml +++ b/lib/energy/inverter/temperature.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Inverter Temperature -description: Provides temperature measurements for different parts of the inverter +description: Provides temperature measurements for different parts of the inverter. telemetry: inverter_heatsink_temperature: diff --git a/lib/location/coordinates.yml b/lib/location/coordinates.yml index 43ffd13..acd567f 100644 --- a/lib/location/coordinates.yml +++ b/lib/location/coordinates.yml @@ -8,10 +8,10 @@ properties: display_name: Latitude type: float unit: deg - description: Latitude coordinate in decimal degrees + description: Latitude coordinate in decimal degrees. longitude: display_name: Longitude type: float unit: deg - description: Longitude coordinate in decimal degrees + description: Longitude coordinate in decimal degrees. From 877549cb76d3a4f2c4fa0977919f2fd0bf94841b Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Tue, 24 Feb 2026 14:02:45 +0400 Subject: [PATCH 21/23] fix: add missing unit to power factor telemetry attributes Power factor is a dimensionless ratio; use UCUM unit "1" for consistency with all other numeric telemetry attributes that specify a unit. --- lib/energy/power_quality/power_factor/1_phase.yml | 1 + lib/energy/power_quality/power_factor/3_phase.yml | 3 +++ lib/energy/power_quality/power_factor/totals.yml | 1 + 3 files changed, 5 insertions(+) diff --git a/lib/energy/power_quality/power_factor/1_phase.yml b/lib/energy/power_quality/power_factor/1_phase.yml index 45fd370..9c57d71 100644 --- a/lib/energy/power_quality/power_factor/1_phase.yml +++ b/lib/energy/power_quality/power_factor/1_phase.yml @@ -19,6 +19,7 @@ telemetry: ac_l1_power_factor: display_name: Power Factor type: float + unit: 1 description: | Ratio of active power to apparent power, range from -1 to 1. Positive values indicate lagging (inductive) power factor. diff --git a/lib/energy/power_quality/power_factor/3_phase.yml b/lib/energy/power_quality/power_factor/3_phase.yml index edf9ffe..65819b1 100644 --- a/lib/energy/power_quality/power_factor/3_phase.yml +++ b/lib/energy/power_quality/power_factor/3_phase.yml @@ -20,6 +20,7 @@ telemetry: ac_l1_power_factor: display_name: L1 Power Factor type: float + unit: 1 description: | Ratio of L1 active power to L1 apparent power, range from -1 to 1. Positive values indicate lagging (inductive) power factor. @@ -41,6 +42,7 @@ telemetry: ac_l2_power_factor: display_name: L2 Power Factor type: float + unit: 1 description: | Ratio of L2 active power to L2 apparent power, range from -1 to 1. Positive values indicate lagging (inductive) power factor. @@ -62,6 +64,7 @@ telemetry: ac_l3_power_factor: display_name: L3 Power Factor type: float + unit: 1 description: | Ratio of L3 active power to L3 apparent power, range from -1 to 1. Positive values indicate lagging (inductive) power factor. diff --git a/lib/energy/power_quality/power_factor/totals.yml b/lib/energy/power_quality/power_factor/totals.yml index e218af0..e558d81 100644 --- a/lib/energy/power_quality/power_factor/totals.yml +++ b/lib/energy/power_quality/power_factor/totals.yml @@ -12,6 +12,7 @@ telemetry: ac_power_factor: display_name: Power Factor type: float + unit: 1 description: | Ratio of total active power to total apparent power (Ptotal/Stotal), range from -1 to 1 Positive values indicate lagging (inductive) power factor. From fa3e558f2b9b0ad1892715d94af5945c75d78f70 Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Tue, 24 Feb 2026 16:51:58 +0400 Subject: [PATCH 22/23] fix: remove "profile" word from sensor profile descriptions Sensor profile descriptions now describe what the device IS rather than saying "A profile for...". --- sensor/ambient_temperature.yml | 2 +- sensor/hydrogen.yml | 2 +- sensor/solar_irradiance.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sensor/ambient_temperature.yml b/sensor/ambient_temperature.yml index 75e2c66..873198c 100644 --- a/sensor/ambient_temperature.yml +++ b/sensor/ambient_temperature.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Ambient Temperature Profile -description: A profile for ambient temperature sensors. Reports air temperature in degrees Celsius. +description: Ambient temperature sensor reporting air temperature in degrees Celsius. implements: - lib.device.nameplate diff --git a/sensor/hydrogen.yml b/sensor/hydrogen.yml index 4a15572..2bc7e50 100644 --- a/sensor/hydrogen.yml +++ b/sensor/hydrogen.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Hydrogen Concentration Sensor Profile -description: A profile for hydrogen gas detectors. Reports gas concentration as a percentage of the Lower Explosive Limit (%LEL). +description: Hydrogen gas detector reporting gas concentration as a percentage of the Lower Explosive Limit (%LEL). implements: - lib.device.nameplate diff --git a/sensor/solar_irradiance.yml b/sensor/solar_irradiance.yml index e9ec4da..a738a9b 100644 --- a/sensor/solar_irradiance.yml +++ b/sensor/solar_irradiance.yml @@ -1,7 +1,7 @@ blueprint_spec: profile/1.0 display_name: Solar Irradiance Profile -description: A profile for solar irradiance sensors (pyranometers). Reports irradiance in W/m². +description: Solar irradiance sensor (pyranometer) reporting irradiance in W/m². implements: - lib.device.nameplate From 416b0dd8a38869b8e8db4e0bd647b0cdf77b40aa Mon Sep 17 00:00:00 2001 From: Nikita Afanasenko Date: Tue, 3 Mar 2026 18:47:24 +0400 Subject: [PATCH 23/23] feat: add deviec status guide --- guides/status-framework.md | 637 +++++++++++++++++++++++++++++++++++++ 1 file changed, 637 insertions(+) create mode 100644 guides/status-framework.md diff --git a/guides/status-framework.md b/guides/status-framework.md new file mode 100644 index 0000000..89d4d90 --- /dev/null +++ b/guides/status-framework.md @@ -0,0 +1,637 @@ +# Enapter Profile Status Framework + +This document defines how device operational status should be modelled in Enapter profiles and mapped from blueprints. It covers the reasoning behind the framework, the canonical status vocabulary, rules for extending it, and practical guidance for profile and blueprint authors. + +--- + +## How to Use This Document + +This document serves two audiences with different concerns. + +**Profile authors** define YAML files in `lib/` that declare what status values a device type exposes. They need: [The Backbone](#the-backbone), [Naming the Active Status](#naming-the-active-status), [Adding Statuses Beyond the Backbone](#adding-statuses-beyond-the-backbone), [What Never Belongs in a Profile Operational Status Field](#what-never-belongs-in-a-profile-operational-status-field), and the YAML examples in [How Profiles Use This Framework](#how-profiles-use-this-framework). + +**Blueprint authors** write Lua code that maps a vendor device's raw status values to the normalized profile vocabulary. They need [How Blueprints Map to Profiles](#how-blueprints-map-to-profiles), including the translation pattern, mapping rules, and the Hydrogenics worked example. The [Quick Reference](#quick-reference) at the end is a concise lookup table for both audiences. + +--- + +## Background: Why Status Modelling Matters + +A profile's operational status field is the primary signal used by Rule Engine rules to make decisions about a device. Rules written against a profile must work across all devices that implement it. This means the status values must: + +- Be **consistent in meaning** across different manufacturers +- Be **coarse enough** to be universally applicable +- Be **precise enough** to drive automation decisions + +A hydrogen electrolyser from Enapter and one from Nel must both report `producing` when they are making hydrogen, even though their internal state machines may have dozens of vendor-specific sub-statuses. The profile provides the normalized interface; the blueprint handles the translation. + +--- + +## Scope: What This Framework Governs + +Operational status in Enapter profiles operates on two layers. Each layer has a distinct owner, field name, and purpose. + +### Native layer – `status`: belongs to the blueprint + +The Enapter platform reserves the bare `status` telemetry field for the blueprint developer. It is intentionally left unrestricted so the developer can expose the device's native status directly – whatever the manufacturer calls it, in whatever form is most useful for diagnostics. + +```lua +-- Blueprint Lua: send the native vendor status verbatim +enapter.send_telemetry({ + status = vendor_status, -- "STARTUP_WARMUP", "FAULT_STACK_OVERPRESSURE", etc. +}) +``` + +This field is available in the UI and API for diagnostics. Automation rules and third-party integrations cannot rely on it having consistent values across different devices. + +### Profile layer – `{type}_status`: the device-type profile field + +Device-type profiles such as `lib/energy/electrolyser/status` define a prefixed field like `electrolyser_status`. The profile extends the backbone vocabulary with a device-appropriate active status name (`producing`, `running`, `operating`) and optional type-specific values (`purging`, `throttled`, `derated`). + +This is the layer automation rules use – distinguishing `producing` from `purging`, or `running` from `derated`. + +### Blueprint sends both layers + +A blueprint implementing a profile sends both fields: + +```lua +enapter.send_telemetry({ + status = vendor_status, -- native layer: vendor status for diagnostics + electrolyser_status = to_el_status(vendor_status), -- profile layer: for automation rules +}) +``` + +### Sub-system status fields are outside this framework's scope + +A device profile may also define status fields for specific sub-systems: `grid_status`, `battery_charge_status`, `relay_status`. These are named after what they represent and carry their own enumeration. They follow the same naming convention (prefixed, never bare `status`) but are not governed by this framework, which concerns only the top-level device operational status. + +While sub-system status fields define their own enumerations, profile authors are encouraged to follow similar conventions where applicable: use clear state names that describe what the sub-system is doing, avoid connectivity indicators and health severity levels, and apply the same two-question test before adding values beyond the obvious ones. + +--- + +## What Belongs in the Profile Operational Status Field + +The profile operational status field answers one question: **what mode is the device currently in?** Three other things are frequently confused with this, and all three are wrong answers to that question. + +### What it is: operational status + +```yaml +# lib/energy/electrolyser/status.yml +telemetry: + electrolyser_status: + type: string + enum: + idle: ... + starting: ... + producing: ... + stopping: ... + standby: ... + fault: ... +``` + +This is what profiles define. It answers the question a Rule Engine rule needs to ask: *what is this device doing right now?* + +### What it is not: connectivity indicator + +Does the blueprint Lua code successfully communicate with the device right now? + +```lua +-- This is internal blueprint state, not a profile field +if not ok then + enapter.send_telemetry({ status = "read_error" }) + return +end +``` + +Values like `ok` / `read_error` / `conn_error` / `no_data` describe the communication channel, not the device. The platform already handles device reachability at the infrastructure level – if a device stops reporting, it appears as offline. Connectivity indicators belong inside the blueprint's Lua logic and alert definitions, never in a profile interface. + +### What it is not: health severity + +Is anything wrong, and how serious is it? + +Values like `ok` / `warning` / `alarm` / `critical` are health levels, not operational statuses. Health information belongs in Enapter alerts, which carry severity, description, and troubleshooting steps. A device can be `producing` and simultaneously have an active `warning`-severity alert for an elevated temperature. A single combined field cannot represent both at once. + +### What it is not: sub-system status + +Fields like `grid_status`, `battery_charge_status`, or `fan_state` describe the condition of a specific sub-system. They are valid profile fields but governed by the context of that sub-system, not by this framework. Do not conflate them with the top-level device operational status. + +--- + +## The Backbone + +Every device profile that exposes an operational status field should build on this set of seven statuses. They form a state machine that applies to any energy conversion or control device. + +``` + ┌─────────────── fault ──────────────────┐ + │ (from any status) │ + │ ▼ +idle ──► starting ──► [ running ] ──► stopping ──► idle + ↕ │ (condition) +maint. standby +(operator) │ (condition clears) + └──► starting ───► +``` + +### `idle` + +The device is energized and all systems are operational, but it is not performing its primary function. It is waiting for an explicit command – from a user, from a Rule Engine rule, or from a schedule – before it will do anything. + +> *"The device is ready. Nothing will happen until someone asks it to start."* + +A blueprint author maps to `idle` when the device reports statuses like: `inactive`, `powered`, `stopped`, `ready`, or any vendor status that means "on but not running, waiting for a command." + +**Example situations:** +- An electrolyser that has been manually stopped and is ready to start producing again +- A fuel cell whose start sequence has not been triggered yet +- An inverter that has been manually placed on hold + +### `standby` + +The device is energized and not performing its primary function, but it will autonomously resume operation when the appropriate conditions are met. No command from a user or rule is needed. + +> *"The device is paused. It will restart itself when it's ready."* + +The key test: *if this status persists, will the device restart itself automatically?* If yes, it is `standby`. If a human or rule must issue a command to restart, it is `idle`. + +**Example situations:** +- An electrolyser that has automatically paused because its outlet pressure reached the maximum limit – it will resume producing once the downstream pressure drops +- A PV inverter at night – it will automatically start operating at dawn when irradiance is sufficient + +### `starting` + +The device is executing its startup sequence. It has received a command to operate (or conditions have triggered an autonomous start from `standby`) but is not yet performing its primary function. + +`starting` covers everything that needs to happen before nominal operation: warmup, thermal conditioning, purging, pressurization, grid synchronization, self-tests. Blueprint authors do not need separate status values for these sub-phases – they all map to `starting`. + +> *"The device is on its way to operating. Wait for it."* + +**Example situations:** +- An electrolyser warming its stack to operating temperature and purging the gas lines before hydrogen production begins +- A fuel cell executing its startup sequence: anode purge, stack pressurization, temperature ramp +- A solar inverter checking grid conditions before connecting + +### `[running]` + +The device is performing its primary function in nominal conditions. The name of this status is **device type-specific** – chosen to reflect what the device actually does: `producing` for an electrolyser, `operating` for an inverter, `charging` for an EV charger. See [Naming the Active Status](#naming-the-active-status) below. + +### `stopping` + +The device is executing a controlled stop sequence. It has received a command to stop (or a condition has triggered an autonomous stop) but has not yet reached `idle` or `standby`. + +`stopping` covers everything in the wind-down: cooldown, depressurization, purging, grid disconnection. Like `starting`, blueprint authors do not need separate status values for sub-phases – they all map to `stopping`. + +> *"The device is on its way to idle or standby. Let it finish."* + +**Example situations:** +- An electrolyser cooling its stack and purging hydrogen after a stop command +- A fuel cell completing its cooldown and anode purge before entering standby +- An inverter disconnecting from the grid in a controlled sequence + +### `fault` + +The device has detected a condition that prevents normal operation. Operator attention is required to diagnose and resolve the issue. + +Some fault conditions clear automatically when the triggering event resolves – for example, a PV inverter that trips on an isolation fault and resets once the grid stabilizes – and the device will return to `idle` or `starting` on its own in those cases. Others require an explicit operator reset. From the profile's perspective the device is in `fault` in both cases until it resumes normal operation. + +`fault` covers all error conditions: hardware faults, safety trips, persistent alarms, unrecoverable errors. Blueprint authors should map all vendor fault statuses to `fault` regardless of their severity label (`error`, `fatal`, `alarm`, `fail_safe`, `locked_out`). To distinguish self-clearing faults from those requiring manual intervention, use Enapter alerts with appropriate severity and resolution tracking alongside the `fault` status. + +> *"Something is wrong. A person needs to look at this."* + +### `maintenance` + +The device is in a maintenance or configuration mode. Normal operation has been suspended by an operator and will not resume until the operator explicitly exits this mode. + +`maintenance` differs from `fault` in that it is intentional: an operator placed the device here to perform service. It differs from `idle` in that automation rules must not attempt to start or dispatch a device in this status. + +> *"The device is under service. Do not attempt to start or dispatch it."* + +**Mapping guidance:** In automation rules, treat `maintenance` the same as `fault` when deciding whether to dispatch a device – skip both. + +**Example situations:** +- An electrolyser placed in maintenance mode for a periodic stack inspection +- An inverter taken offline for a firmware update +- Any device temporarily withdrawn from automated control + +--- + +## Naming the Active Status + +The active status name should reflect the primary function of the device. Choose a word that completes the sentence: *"The device is [word]."* + +| Device type | Active status | Reasoning | +|------------|--------------|-----------| +| Hydrogen electrolyser | `producing` | The product is hydrogen | +| Fuel cell | `running` | The system is running under load | +| Solar inverter | `operating` | Converting PV power to AC | +| Battery / hybrid inverter | `operating` | Actively converting power | +| EV charger | `charging` | Delivering energy to the vehicle | + +Do not unify these into a single word across all device-specific profiles. The different status names are intentional – they convey what the device is actually doing and make device-specific rules more readable. + +```lua +-- Good: the device-specific status name makes intent self-evident +if device.telemetry.now("electrolyser_status") == "producing" then + enapter.log("Electrolyser is producing hydrogen, dryer can be enabled") +end +``` + +### Why `starting` and `stopping` are not customizable + +The active status name carries meaning specific to what the device produces or converts: `producing` for an electrolyser tells a rule author what the device is making; `charging` for an EV charger tells it what energy direction is active. This specificity is valuable. + +Transition states carry no output-specific information. A device that is `starting` is doing pre-operational work regardless of whether it will produce hydrogen, generate electricity, or charge a vehicle. Renaming `starting` to `warming` or `hydrogen-starting` adds no information that a rule author needs – rules care only that the device is not yet available. The backbone transition names are therefore fixed across all device types. + +### Devices with multiple active modes + +When a device can perform more than one distinct primary function, define a separate active status for each function. A vehicle-to-grid (V2G) EV charger, for example, operates in two active modes: + +| Active status | What the device is doing | +|--------------|--------------------------| +| `charging` | Delivering energy from the grid to the vehicle | +| `discharging` | Exporting energy from the vehicle battery to the grid | + +Rules that manage energy direction can check the specific status; rules that only need to know "is this charger active?" can check for either value. + +--- + +## Adding Statuses Beyond the Backbone + +The backbone covers the vast majority of automation needs. Before adding a status, apply this test: the status is worth adding only if **both** of the following are true. + +**Question 1: Is it long enough to matter?** +Will a device plausibly spend more than a few minutes in this status during normal operation? Status values that last seconds are too granular – they will appear as brief flickers in the telemetry chart and are not useful for automation. + +**Question 2: Would a rule author write `if status == "X"`?** +Is there a realistic automation scenario where knowing the device is in this specific status changes what the rule should do? If you cannot think of a concrete example, the status is not earning its place. One way to think about this: would a rule behave differently during this status compared to `starting`, `stopping`, or the active status? If the answer is "it's basically just a kind of starting," then map it to `starting`. + +### Approved Optional Statuses + +The following status values have passed both questions and may be used in profiles where applicable. + +#### `throttled` / `derated` + +The device is performing its primary function but at reduced output due to an external constraint or an internal condition that limits capacity. + +Use `throttled` for inverters (power curtailment, grid frequency response, temperature derating). Use `derated` for fuel cells (stack health, hydrogen pressure, temperature limits). The active status name stays device-specific; these are sub-statuses of active operation. + +**Note:** `throttled` and `derated` only make sense for devices with a single active status. For devices with multiple active modes -- such as a V2G charger with both `charging` and `discharging` -- a single `throttled` value is ambiguous because the rule author cannot tell which mode the device is throttled in. In such cases, prefer reporting the specific active status and exposing the capacity limitation through a separate telemetry field (e.g. `max_available_power`). + +**Why it passes the test:** Rules managing energy dispatch need to know whether a device is at full capacity or limited. A rule that assumes an inverter can provide 10 kW should behave differently when the inverter is throttled. + +```lua +-- Example: suppress a high-power command if the inverter is not at full capacity +local status = device.telemetry.now("inverter_status") +if status == "operating" then + device.commands.execute("set_power", { watts = 10000 }) +elseif status == "throttled" then + enapter.log("Inverter is throttled, skipping high-power command") +end +``` + +#### `purging` + +The device is executing a gas or fluid purge cycle. It is not performing its primary function and is not yet ready to do so. + +This status is appropriate for electrolysers performing a safety purge cycle where hydrogen lines are cleared. It differs from `stopping` in that purging may be a recurring maintenance cycle the device enters and exits without stopping -- the device will return to `producing` when the purge completes, without any command. + +**Note:** `purging` only makes sense as a separate status when the purge cycle lasts long enough to matter for automation (minutes, not seconds). Some electrolysers complete a purge in a few seconds -- in that case, the purge is a sub-phase of `starting` or `stopping` and should be mapped accordingly. Expose `purging` as a distinct status only when the cycle is long enough that a downstream consumer (compressor, dryer, buffer tank) needs to actively pause during it. + +**Why it passes the test:** A downstream hydrogen consumer must not draw gas during a purge. A rule managing a hydrogen buffer tank needs to suppress consumption requests while the electrolyser is purging. + +```lua +-- Example: hold downstream consumer while electrolyser purges +local el_status = device.telemetry.now("electrolyser_status") +if el_status == "purging" then + device.commands.execute("compressor", "stop") +end +``` + +#### `preheating` + +The device is executing an operator-commanded pre-heating sequence: thermal conditioning systems such as pumps and circulation heaters are active, but the primary conversion process has not started and the stack has not been energized. + +This status is appropriate for electrolysers that support a manual preheat mode, where the operator brings the system to operating temperature ahead of a planned production run. When the preheat sequence completes, the device transitions to `keeping_warm` (see below), not to `standby` – a start command is required before production can begin. + +**Why it passes the test:** Rules managing a production schedule need to distinguish a cold-idle electrolyser from one that is actively conditioning and will be ready to produce sooner. A warming electrolyser must not be issued a redundant preheat command. + +```lua +-- Example: avoid issuing a redundant preheat command +local el_status = device.telemetry.now("electrolyser_status") +if el_status == "idle" then + device.commands.execute("preheat") +elseif el_status == "preheating" then + enapter.log("Electrolyser is preheating, waiting for keeping_warm before dispatching") +end +``` + +#### `keeping_warm` + +The stack is at operating temperature and the device is maintaining thermal conditioning. The device is not producing hydrogen and will not start automatically – a start command is required. + +This status follows `preheating` when the preheat sequence completes. It may also be entered briefly after a production stop if the stack is still hot. When the device eventually cools below the threshold without a start command, it transitions to `idle`. + +**Why it passes the test:** A rule dispatching an electrolyser needs to know whether preheat is still required. If `electrolyser_status == "keeping_warm"`, the device can start immediately without a warmup penalty. If `electrolyser_status == "idle"`, the rule should schedule preheat first or accept a longer startup. + +```lua +-- Example: dispatch strategy based on thermal state +local el_status = device.telemetry.now("electrolyser_status") +if el_status == "keeping_warm" then + device.commands.execute("start") -- fast start, stack already conditioned +elseif el_status == "idle" then + device.commands.execute("preheat") -- cold start: preheat first, then start +end +``` + +### Status Values That Do Not Pass the Test + +| Candidate | Verdict | Reason | +|-----------|---------|--------| +| `warmup` | Reject | Sub-phase of `starting`. Rules do not need to act differently during electrolyser or fuel cell thermal warmup vs. other startup steps. Map to `starting`. | +| `cooldown` | Reject | Sub-phase of `stopping`. Rules do not need to differentiate cooldown from other stopping sub-phases. Map to `stopping`. | +| `anode_purge` | Reject | Vendor sub-status during startup or shutdown. Map to `starting` or `stopping` based on context. | +| `leak_check` | Reject | Vendor startup sub-status. Map to `starting`. | +| `freeze_prep` | Reject | Vendor shutdown variant. Map to `stopping`. | +| `cooldown_complete` | Reject | Transient; device should immediately transition to `idle` or `standby`. The final status is what matters. | + +--- + +## What Never Belongs in a Profile Operational Status Field + +### `off` + +Do not define an `off` value in any profile status field. + +If a device is completely de-energized, the blueprint Lua code cannot communicate with it and therefore cannot report any status at all. The platform handles this at the connectivity layer: the device appears as offline. If the Lua code *can* report a status, the device's control electronics are powered – and "powered but not running" is `idle`. + +Some vendor devices report an internal `off`, `halted`, or `powered-off` status over the protocol. This means the device has a software-controlled shutdown of its main function while the controller is still running. Map these to `idle`: the device is communicating, so it is energized, and it is waiting for a command to start. + +### Connectivity indicators + +Values like `ok` / `read_error` / `conn_error` / `modbus_error` / `no_data` describe the communication channel, not the device. Keep these internal to the blueprint's Lua code and alert logic. Never publish them as a profile field. + +### Health severity levels + +Values like `ok` / `warning` / `alarm` / `critical` are health levels, not operational statuses. A device can be `producing` and simultaneously have an active `warning` alert. Use Enapter alerts with appropriate severity for health conditions. + +### Platform-managed connectivity statuses + +Values like `Online` / `Offline` are managed by the platform. Do not surface them as telemetry in a profile. + +--- + +## How Profiles Use This Framework + +A device-type profile defines: + +1. The backbone statuses: `idle`, `starting`, `stopping`, `standby`, `fault`, and `maintenance` +2. A device-appropriate active status name (`producing`, `running`, `operating`, `charging`) that reflects what the device does +3. Any approved optional statuses or those that pass the two-question test for this device type +4. Clear descriptions for each value answering: what triggered entry, what the device is doing, what causes exit + +**Example: `lib/energy/electrolyser/status.yml`** + +```yaml +blueprint_spec: profile/1.0 + +display_name: Electrolyser Status +description: Operational status for electrolyser systems. + +telemetry: + electrolyser_status: + display_name: Electrolyser Status + description: Current operational status of the electrolyser. + type: string + enum: + idle: + display_name: Idle + description: > + Electrolyser is powered and ready to operate but is not producing + hydrogen. It will not start until commanded by a user or a rule. + starting: + display_name: Starting + description: > + Electrolyser is executing its startup sequence, which includes stack + warmup, gas line purging, and pressurization. Hydrogen production + has not yet begun. + producing: + display_name: Producing + description: > + Electrolyser is actively producing hydrogen at its operating setpoint. + stopping: + display_name: Stopping + description: > + Electrolyser is executing a controlled stop sequence, including stack + cooldown and line depressurization. It will reach idle or standby + when the sequence completes. + standby: + display_name: Standby + description: > + Electrolyser has automatically paused hydrogen production because the + outlet pressure reached the maximum limit. It will resume producing + autonomously once the downstream pressure drops below the threshold. + No command is required to restart. + purging: + display_name: Purging + description: > + Electrolyser is performing a gas purge cycle. Hydrogen is not being + produced and should not be drawn from the outlet during this status. + preheating: + display_name: Preheating + description: > + Electrolyser is executing an operator-commanded pre-heating sequence. + Pumps and heaters are active to condition the system to operating + temperature, but the stack has not been energized and hydrogen + production has not begun. The device transitions to keeping_warm when + the sequence completes; a start command is required to begin production. + keeping_warm: + display_name: Keeping Warm + description: > + Stack is at operating temperature and the electrolyser is maintaining + thermal conditioning. The device is not producing hydrogen and will + not start automatically. A start command will begin production + immediately without a warmup penalty. Transitions to idle if the + stack cools below the threshold without a start command being issued. + maintenance: + display_name: Maintenance + description: > + Electrolyser is in a maintenance or configuration mode. Normal + operation has been suspended by an operator and will not resume + until the operator exits this mode. + fault: + display_name: Fault + description: > + Electrolyser has detected a condition that prevents normal operation. + Operator attention is required before the device can resume. +``` + +--- + +## How Blueprints Map to Profiles + +A blueprint's `implements` declaration tells the platform that the device satisfies a profile's interface. The blueprint's Lua code is responsible for: + +1. Sending the native device status in the bare `status` field (for diagnostics and display) +2. Translating the native status into the profile's normalized vocabulary + +### The Translation Pattern + +Vendor devices expose raw status values – often dozens of granular sub-statuses reflecting the manufacturer's internal state machine. The Lua code maps these to the profile's coarser vocabulary. + +```lua +local VENDOR_TO_FC_STATUS = { + -- Vendor status fuel_cell_status + ["POWER_OFF"] = "idle", + ["STANDBY_READY"] = "idle", + ["STARTUP_PURGE"] = "starting", + ["STARTUP_PRESSURIZE"] = "starting", + ["STARTUP_WARMUP"] = "starting", + ["RUNNING_FULL"] = "running", + ["RUNNING_PARTIAL"] = "derated", + ["COOLDOWN_PHASE_1"] = "stopping", + ["COOLDOWN_PHASE_2"] = "stopping", + ["ANODE_PURGE"] = "stopping", + ["HOT_STANDBY"] = "standby", + ["FAULT_RECOVERABLE"] = "fault", + ["FAULT_CRITICAL"] = "fault", + ["MAINTENANCE_MODE"] = "maintenance", +} + +local function to_fc_status(vendor_status) + return VENDOR_TO_FC_STATUS[vendor_status] +end + +-- In the telemetry loop: +enapter.send_telemetry({ + status = vendor_status, -- native, for diagnostics + fuel_cell_status = to_fc_status(vendor_status), -- profile, for automation rules +}) +``` + +The mapping table must be exhaustive: every known vendor status should have an entry. See [Handling Hard-to-Map Vendor Statuses](#handling-hard-to-map-vendor-statuses) below for what to do when a vendor status does not obviously fit. + +### Rules for Mapping + +**Many vendor statuses → one profile status is expected and correct.** A fuel cell may have `STARTUP_PURGE`, `STARTUP_PRESSURIZE`, and `STARTUP_WARMUP` – all map to `starting`. This is the point of the profile: to normalize. + +**When a vendor uses a familiar word with a different meaning, map by meaning, not by word.** Several manufacturers use `idle` to mean different things: + +| Vendor | Their `idle` means | Profile mapping | +|--------|-------------------|----------------| +| Powercell PS5 | Energized, ready, waiting for start command | `idle` ✓ | +| H2sys ACS 1000 | Running at low/no load | `running` | +| LG RESU | Connected, not charging or discharging | `idle` | + +Map the *behavior*, not the label. + +**`fault` absorbs all error variants.** Vendor values like `error`, `fatal`, `fail_safe`, `locked_out`, `alarm`, `trip` all map to `fault`. If the distinction matters (some faults auto-clear, others require a reset), use Enapter alerts with appropriate severity and description alongside the `fault` status. + +**Never map a communication failure to a profile status.** If the Lua code cannot read the device status, do not send `fuel_cell_status = "fault"` – that misrepresents a communication issue as a device fault. Stop sending the profile field and let the platform detect the absence of data as an offline condition. + +### Handling Hard-to-Map Vendor Statuses + +A blueprint's mapping table should cover every known vendor status. Most vendor statuses map straightforwardly to a backbone status, but some require careful analysis. When a vendor status does not obviously correspond to a profile status, follow this decision process. + +**Step 1: Identify what the device is actually doing.** Ignore the vendor's label and observe the behavior. A vendor's `idle` may mean the device is running at low load (see the H2sys example above). A vendor's `standby` may mean it is waiting for a command, which is `idle` in the profile vocabulary. + +**Step 2: Ask what a rule should do.** This is the key question. It tells you which profile status the vendor status behaves like: + +- Device is waiting for a command to start → `idle` +- Device will auto-resume when a condition clears → `standby` +- Device is working toward becoming active → `starting` +- Device is performing its primary function → the active status (`producing`, `running`, etc.) +- Device is winding down toward inactive → `stopping` +- Device shouldn't be dispatched, operator put it there → `maintenance` +- Device has a problem that prevents operation → `fault` + +**Step 3: Map to the closest profile status.** In most cases, Steps 1 and 2 lead to a clear answer. Map the vendor status there, even if the vendor's label suggests something different. + +**Examples of hard-to-map statuses:** + +| Vendor status | Situation | Profile mapping | Reasoning | +|---------------|-----------|-----------------|-----------| +| `SWITCHING_MODE` | V2G charger transitioning from charging to discharging | `starting` | Device is working toward a new active mode; rules should wait | +| `CALIBRATING` | Automatic periodic calibration, device resumes on its own | `standby` | Device will auto-resume; no command needed | +| `FIELD_SERVICE` | Operator-initiated diagnostic mode | `maintenance` | Operator placed it here; do not dispatch | +| `LOW_OUTPUT_WARMUP` | Producing at reduced capacity during warmup | `starting` | Not yet at nominal operation | + +#### When mapping returns nil: do not fall back + +If a vendor status has no entry in the mapping table, do not send the profile status field. + +```lua +local function to_fc_status(vendor_status) + local mapped = VENDOR_TO_FC_STATUS[vendor_status] + if not mapped then + enapter.log("unmapped vendor status: "..tostring(vendor_status), "warn") + end + return mapped -- nil if unmapped; profile field will not be sent +end +``` + +When `to_fc_status` returns `nil`, the profile status field is absent from the telemetry payload. Automation rules that call `device.telemetry.now()` will receive `nil`, which is the same result they get when the device is offline or communication has failed. Both cases mean the same thing from the rule's perspective: the device's current status is unknown. The rule author chooses their own strategy for handling this -- wait, attempt recovery, or use `relevance_interval` to control staleness tolerance. + +#### When no mapping is possible + +If a vendor status cannot be meaningfully mapped to any profile status using the three-step process above, the blueprint developer should raise an issue in the [profiles GitHub repository](https://github.com/Enapter/profiles) explaining the vendor status, why it does not fit the existing vocabulary, and what automation behavior they believe is correct. This may lead to an approved extension of the profile or a revision of the mapping approach. + +### Example: Mapping the Hydrogenics HYPM HD-8 + +The Hydrogenics fuel cell exposes a `state` field with 15 granular values. The native status goes into `status`; the normalized status goes into `fuel_cell_status`. + +| Hydrogenics `state` | `status` (native) | `fuel_cell_status` (profile) | Notes | +|--------------------|------------------|------------------------------|-------| +| `standby` | `standby` | `idle` | Their "standby" means "ready, awaiting command" | +| `startup` | `startup` | `starting` | | +| `run` | `run` | `running` | | +| `shutdown` | `shutdown` | `stopping` | | +| `fault` | `fault` | `fault` | | +| `cooldown` | `cooldown` | `stopping` | Post-shutdown cooldown is part of stopping | +| `cooldown_complete` | `cooldown_complete` | `idle` | Ready for next start | +| `freeze_prep` | `freeze_prep` | `stopping` | Safety shutdown variant | +| `freeze_prep_complete` | `freeze_prep_complete` | `idle` | Ready after freeze protection | +| `anoge_purge` | `anoge_purge` | `starting` | Startup anode purge | +| `anode_purge_complete` | `anode_purge_complete` | `starting` | Still in startup sequence | +| `leak_check` | `leak_check` | `starting` | Pre-run safety check | +| `leak_check_complete` | `leak_check_complete` | `starting` | Still in startup sequence | +| `prime` | `prime` | `starting` | Fuel priming | +| `prime_complete` | `prime_complete` | `starting` | Still starting | + +> **Note on `cooldown_complete`:** The mapping above assumes the Hydrogenics controller requires an explicit start command after cooldown finishes. Verify this against your hardware documentation. If the device transitions to hot standby autonomously after cooldown – resuming operation on its own when conditions allow – map `cooldown_complete` to `standby` instead of `idle`. + +--- + +## Quick Reference + +### Field naming + +| Layer | Field | Owner | Values | Purpose | +|-------|-------|-------|--------|---------| +| Native | `status` | Blueprint developer | Any vendor-specific values | Native device status for diagnostics and display | +| Profile | `{type}_status` | Device-type profile | Backbone + type-specific extensions | Normalized status for automation rules | +| – | `{subsystem}_status` | Profile | Sub-system-specific enum | Component status (grid, battery charge, etc.) – out of scope for this framework | + +### Status backbone + +| Status | Meaning | Exits to | +|--------|---------|---------| +| `idle` | Energized, not operating, awaiting command | `starting`, `fault`, `maintenance` | +| `standby` | Energized, not operating, will auto-resume | `starting`, `fault` | +| `starting` | Startup sequence in progress | `[running]`, `fault` | +| `[running]` | Nominal operation (device-specific name: `producing`, `operating`, `charging`, etc.) | `stopping`, `standby`, `fault` | +| `stopping` | Controlled stop in progress | `idle`, `standby`, `fault` | +| `maintenance` | Under service, operator-suspended | `idle` (when operator exits) | +| `fault` | Requires operator attention | `idle` (after recovery) | + +### Decision: `idle` vs `standby` + +> Will the device restart itself automatically, without any command? **Yes** → `standby`. **No** → `idle`. + +### Decision: add a new status? + +> Is the status long enough to matter? Would a rule author write `if {type}_status == "X"`? Both **yes** → add it. Either **no** → map to the nearest backbone status. + +### Never in a profile operational status field + +- `off` – de-energized devices appear offline on the platform; powered-but-not-running is `idle` +- `ok` / `read_error` / `conn_error` – communication indicators; keep inside blueprint Lua +- `ok` / `warning` / `alarm` – health severity; use Enapter alerts +- `Online` / `Offline` – platform-managed connectivity