diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a73f1d..9afb51a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,14 +6,17 @@ repos: # `packages/schema-1/spec/` holds byte copies of the eBus specification, # verified by byte comparison in tests/test_schema_one_conformance.py, and # `tests/fixtures/captures/` holds byte copies of upstream reference captures - # and their LICENSE, pinned by SHA-256 in tests/test_captures_unchanged.py. - # Any hook that rewrites a file must skip them: a "fix" there would - # invalidate the comparison that makes the copies trustworthy. Non-mutating - # checks (check-json) deliberately still run, since a corrupt copy should fail. + # and their LICENSE, pinned by SHA-256 in tests/test_captures_unchanged.py, and + # the two MAIN 32 captures in tests/fixtures/ are byte copies of upstream with + # that repository's LICENSE beside them, pinned by SHA-256 in + # tests/adapter_fidelity/test_main32_fidelity.py. Any hook that rewrites a file + # must skip them: a "fix" there would silently invalidate the comparison that + # makes the copies trustworthy. Non-mutating checks (check-json) deliberately + # still run, since a corrupt copy should fail. - id: trailing-whitespace - exclude: '^src/span_panel_api/generated_client/.*|^packages/schema-1/spec/.*|^tests/fixtures/captures/(.*\.json|LICENSE)$|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*' + exclude: '^src/span_panel_api/generated_client/.*|^packages/schema-1/spec/.*|^tests/fixtures/(captures/(.*\.json|LICENSE)|main32(_r202639)?-tree-v1\.json|distribution-enclosure-simulator\.LICENSE)$|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*' - id: end-of-file-fixer - exclude: '^src/span_panel_api/generated_client/.*|^packages/schema-1/spec/.*|^tests/fixtures/captures/(.*\.json|LICENSE)$|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*' + exclude: '^src/span_panel_api/generated_client/.*|^packages/schema-1/spec/.*|^tests/fixtures/(captures/(.*\.json|LICENSE)|main32(_r202639)?-tree-v1\.json|distribution-enclosure-simulator\.LICENSE)$|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*' - id: check-yaml exclude: '^src/span_panel_api/generated_client/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*' - id: check-toml @@ -27,7 +30,7 @@ repos: exclude: '^src/span_panel_api/generated_client/.*|generate_client\.py|scripts/.*|tests/.*|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*|^examples/.*' - id: mixed-line-ending args: ['--fix=lf'] - exclude: '^src/span_panel_api/generated_client/.*|^packages/schema-1/spec/.*|^tests/fixtures/captures/(.*\.json|LICENSE)$|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*' + exclude: '^src/span_panel_api/generated_client/.*|^packages/schema-1/spec/.*|^tests/fixtures/(captures/(.*\.json|LICENSE)|main32(_r202639)?-tree-v1\.json|distribution-enclosure-simulator\.LICENSE)$|\..*_cache/.*|dist/.*|venv/.*|\.venv/.*' # Ruff for formatting and linting - repo: https://github.com/astral-sh/ruff-pre-commit @@ -105,11 +108,14 @@ repos: # names tests/, which is fine — that flag governs directory discovery, and # a file passed by name is checked regardless. # + # `tests/adapter_fidelity/` is checked for the same reason: its harness and + # rules are the support code every adapter fidelity cell is measured with. + # # `scripts/capture_parent_child_reference.py` is checked for the same reason: # every schema_1 test is written against what it writes, which is not the place # for the one part of the tree nothing type-checks. The hand-run scripts beside # it stay out — see `pyproject.toml`, which names them one by one. - exclude: '^src/span_panel_api/generated_client/.*|scripts/(?!capture_parent_child_reference\.py$).*|tests/(?!reference_payloads/).*|docs/.*|examples/.*|\..*_cache/.*|dist/.*|venv/.*' + exclude: '^src/span_panel_api/generated_client/.*|scripts/(?!capture_parent_child_reference\.py$).*|tests/(?!reference_payloads/|adapter_fidelity/).*|docs/.*|examples/.*|\..*_cache/.*|dist/.*|venv/.*' # Pylint for code quality - repo: https://github.com/pycqa/pylint diff --git a/CHANGELOG.md b/CHANGELOG.md index 7241445..09036d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Snapshot fields that held a fabricated constant can now be unknown, and with `sp - **BREAKING FOR CONSUMERS: `SpanPanelSnapshot.uptime_s` becomes `int | None`**, and with those parsers it is `None` on a parent/child panel and before a flat panel first reports ready, where it was a constant `0`. - **BREAKING FOR CONSUMERS: `SpanCircuitSnapshot.instant_power_update_time_s` and `energy_accum_update_time_s` become `int | None` and default to `None`**, and with those parsers they are `None` until the reading has arrived and always on a parent/child panel, where they were `0`. +- **The `schema-0` and `schema-1` extras require `span-panel-api-schema-0` and `span-panel-api-schema-1` 1.3.0 or newer**, the adapters that report these values as unknown. ### Removed @@ -506,8 +507,7 @@ Everything below is additive. Each field is `None` or empty on a panel that publ `MQTT_FULL_REBUILD_AFTER_FAILURES` (3) consecutive failures, or immediately on any `ssl.SSLError`. The previous behavior pinned the panel's CA certificate into the paho client once at `connect()` time and re-used it across all reconnect attempts; if the panel rotated its private CA — most plausibly during a firmware upgrade — every subsequent reconnect raised `ssl.SSLCertVerificationError` (caught by the broad `OSError` clause and silently retried) and the bridge could not recover without a config-entry reload. The rebuild mirrors what a manual reload does without going through HA's `config_entry` teardown, so entities stay registered and the integration's grace-period logic continues to apply unchanged. The threshold-cadence design (counter reset on - every rebuild attempt, success or fail) keeps the recovery path active throughout extended outages — multi-day disconnections recover whenever the panel becomes usable again, including if the CA rotates a second time mid-outage. See - `SpanPanel_Docs/span-panel-api/2026-05-17-mqtt-ca-refresh-on-reconnect-design.md` for the full design. + every rebuild attempt, success or fail) keeps the recovery path active throughout extended outages — multi-day disconnections recover whenever the panel becomes usable again, including if the CA rotates a second time mid-outage. ### Added diff --git a/packages/schema-0/pyproject.toml b/packages/schema-0/pyproject.toml index 781182d..cae9aed 100644 --- a/packages/schema-0/pyproject.toml +++ b/packages/schema-0/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "span-panel-api-schema-0" -version = "1.2.0" +version = "1.3.0b1" description = "Flat-schema (data-model-version absent) parser for span-panel-api" authors = [ {name = "SpanPanel"} @@ -19,7 +19,9 @@ dependencies = [ # Raised to 3.6.0, the first bootstrap whose snapshot carries `pv_inverters` # and the inverter's `device_id` and `node_id`, which this parser fills. # Against an earlier one, building a snapshot fails on those arguments. - "span-panel-api>=3.6.0,<4.0", + # Raised to 3.7.0b1, whose snapshot fields accept the unknown values this + # parser now reports, where an earlier one types them as never unknown. + "span-panel-api>=3.7.0b1,<4.0", ] [project.urls] diff --git a/packages/schema-1/CHANGELOG.md b/packages/schema-1/CHANGELOG.md index 3372e0b..81849b7 100644 --- a/packages/schema-1/CHANGELOG.md +++ b/packages/schema-1/CHANGELOG.md @@ -24,6 +24,11 @@ Requires `span-panel-api` **3.7.0 or newer**, whose snapshot fields accept the u - **BREAKING: snapshots no longer include synthesised `unmapped_tab_N` entries for unoccupied breaker positions, which carried no measured data**, and `panel.build_unmapped_tabs` and `const.UNMAPPED_TAB_PREFIX` are gone with them. +### Fixed + +- **A circuit whose shed priority is `UNKNOWN` is no longer reported as sheddable.** +- **The grid-forming device name falls back to vendor and model instead of showing a device id.** + ## [1.2.2] Requires `span-panel-api` **3.6.2 or newer**, which waits on this parser's missing labels after a reconnect as well as on connect. diff --git a/packages/schema-1/pyproject.toml b/packages/schema-1/pyproject.toml index 7009099..8f8fac1 100644 --- a/packages/schema-1/pyproject.toml +++ b/packages/schema-1/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "span-panel-api-schema-1" -version = "1.2.2" +version = "1.3.0b1" description = "Parent/child schema (data-model-version 1.x) parser for span-panel-api" authors = [ {name = "SpanPanel"} @@ -28,9 +28,12 @@ dependencies = [ # Raised to 3.6.1, whose SpanPanelSnapshot.pv documents what this parser builds. # Raised to 3.6.2, which waits on this parser's missing labels after a bridge # rebuild as well as on connect; the fix spans both, so the pair moves together. + # Raised to 3.7.0b1, whose snapshot fields accept the unknown values this + # parser now reports: against an earlier one, the update times it leaves + # unset read as a constant 0. # To rise to 3.8.0b1 when that release is cut: this parser fills snapshot # fields new in it and imports `device_id_order` from its models. - "span-panel-api>=3.6.2,<4.0", + "span-panel-api>=3.7.0b1,<4.0", # Only this distribution depends on the eBus SDK. The bootstrap and # schema-0 stay clean, so a flat-panel install never pulls it in — which is # what bounds the release coupling this dependency introduces to panels on diff --git a/packages/schema-1/src/span_panel_api_schema_1/circuits.py b/packages/schema-1/src/span_panel_api_schema_1/circuits.py index c155241..ddfde7a 100644 --- a/packages/schema-1/src/span_panel_api_schema_1/circuits.py +++ b/packages/schema-1/src/span_panel_api_schema_1/circuits.py @@ -10,9 +10,14 @@ ====================== =========================================================== ``always-on`` ``switch/relay-controllable``, inverted, where a relay is declared ``never-backup`` ``$settable`` on ``load-shed/priority``, inverted -``sheddable`` computed: ``priority != NEVER and relay-controllable`` +``sheddable`` computed: a shed priority and ``relay-controllable`` ====================== =========================================================== +A shed priority is ``OFF_GRID`` or ``SOC_THRESHOLD``. SPAN's r202633 changelog +derives sheddable as ``priority != NEVER`` and asks consumers to treat ``UNKNOWN`` +as "not yet known" rather than as sheddable, so the rule names the priorities the +panel sheds by instead of excluding the one it never sheds. + Sign and direction are unchanged from the flat schema, and both are the reverse of what the property names suggest. Values are in the enclosure's reference frame: a normal load reads **negative** ``active-power`` and accumulates @@ -31,7 +36,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final from span_panel_api.models import ( FEEDS_ROLES, @@ -49,7 +54,8 @@ NODE_METER, NODE_PCS, NODE_SWITCH, - PRIORITY_NEVER, + PRIORITY_OFF_GRID, + PRIORITY_SOC_THRESHOLD, PROP_ACTIVE_POWER, PROP_CURRENT, PROP_EXPORTED_ENERGY, @@ -77,6 +83,9 @@ from ebus_sdk.homie import DiscoveredDevice +_SHED_PRIORITIES: Final = frozenset({PRIORITY_OFF_GRID, PRIORITY_SOC_THRESHOLD}) +"""The `load-shed/priority` values the panel sheds a circuit by; see the module docstring.""" + def _text(device: DiscoveredDevice, node: str, prop: str, default: str = "") -> str: value = device.get_property(node, prop) @@ -354,7 +363,7 @@ def build_circuit( # user-controllability from `always-on` — so this is the same answer by # a shorter route. is_user_controllable=relay_controllable, - is_sheddable=priority != PRIORITY_NEVER and relay_controllable, + is_sheddable=priority in _SHED_PRIORITIES and relay_controllable, is_never_backup=not priority_settable, device_type=device_type, relative_position=relative_position, diff --git a/packages/schema-1/src/span_panel_api_schema_1/const.py b/packages/schema-1/src/span_panel_api_schema_1/const.py index a5457db..41b6178 100644 --- a/packages/schema-1/src/span_panel_api_schema_1/const.py +++ b/packages/schema-1/src/span_panel_api_schema_1/const.py @@ -181,6 +181,8 @@ # -- Values ----------------------------------------------------------------- PRIORITY_NEVER = "NEVER" +PRIORITY_OFF_GRID = "OFF_GRID" +PRIORITY_SOC_THRESHOLD = "SOC_THRESHOLD" UNKNOWN = "UNKNOWN" CLOUD_CONNECTED = "CONNECTED" diff --git a/packages/schema-1/src/span_panel_api_schema_1/panel.py b/packages/schema-1/src/span_panel_api_schema_1/panel.py index beb9b00..dab7ce0 100644 --- a/packages/schema-1/src/span_panel_api_schema_1/panel.py +++ b/packages/schema-1/src/span_panel_api_schema_1/panel.py @@ -825,6 +825,29 @@ def resolve_dominant_power_source( return _POWER_SOURCE_BY_TYPE.get(device_types.get(forming, ""), UNKNOWN) +def device_display_name(device: DiscoveredDevice) -> str | None: + """A device's readable name, or `None` when nothing it publishes names it. + + Homie's `$description.name` (`Battery`, `Solar`, `SPAN Drive - Garage`) where it + is set and is not the device's own id. Real r202639 panels publish each non-root + device's id there, and an id shown as a name is the outcome + `resolve_grid_forming_device_name` exists to avoid. Otherwise the name comes from + the device's `info`: vendor and model together (`Tesla Powerwall 2 AC`), or the + model alone. A vendor alone names a maker rather than a device, so it gives `None`. + + An empty value names nothing, exactly as an unpublished one does. + """ + description: dict[str, object] = device.description or {} + name = description.get("name") + if name and str(name) != device.device_id: + return str(name) + vendor = text(device, NODE_INFO, PROP_VENDOR_NAME) + model = text(device, NODE_INFO, PROP_MODEL) + if vendor and model: + return f"{vendor} {model}" + return model or None + + def resolve_grid_forming_device_name( mid: DiscoveredDevice | None, device_names: Mapping[str, str], @@ -833,9 +856,9 @@ def resolve_grid_forming_device_name( The wire value is a Homie device id -- `sim-40t-001-SIM-BESS-40T-001`. That means nothing to someone reading a dashboard: it is not a Home Assistant device id, and an - opaque string is worse than no string. Homie's `$description.name` is the device's - own display name (`Battery`, `Solar`, `SPAN Drive - Garage`), which is what a person - would recognise, so that is what gets surfaced. + opaque string is worse than no string. `device_names` holds each device's + `device_display_name`, which is what a person would recognise, so that is what gets + surfaced. `None` when the grid is forming (there is no device to name), when no MID publishes an answer, or when the id resolves to nothing -- the raw id is still on diff --git a/packages/schema-1/src/span_panel_api_schema_1/snapshot.py b/packages/schema-1/src/span_panel_api_schema_1/snapshot.py index df6f3ba..0e51f31 100644 --- a/packages/schema-1/src/span_panel_api_schema_1/snapshot.py +++ b/packages/schema-1/src/span_panel_api_schema_1/snapshot.py @@ -43,6 +43,7 @@ from span_panel_api_schema_1.panel import ( PanelFields, build_pcs, + device_display_name, find_lugs, panel_positions, panel_size_from_model, @@ -182,12 +183,7 @@ def build_snapshot(panel: DiscoveredDevice, children: list[DiscoveredDevice]) -> # is what makes PANEL_BACKUP distinguishable from PANEL_OFF_GRID authoritatively # instead of guessed from a power source the way flat had to. device_types = {device.device_id: device_type(device) for device in children} - device_names: dict[str, str] = {} - for device in children: - description: dict[str, object] = device.description or {} - name = description.get("name") - if name: - device_names[device.device_id] = str(name) + device_names = {device.device_id: name for device in children if (name := device_display_name(device)) is not None} inverters = [device for device in children if device_type(device) == TYPE_INVERTER] islanding = resolve_islanding_state(roles.mid, panel) diff --git a/pyproject.toml b/pyproject.toml index 9ceb95c..f2af8a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "span-panel-api" -version = "3.6.2" +version = "3.7.0b1" description = "A client library for SPAN Panel API" authors = [ {name = "SpanPanel"} @@ -62,11 +62,14 @@ dependencies = [ # so a 1.1.x adapter is still accepted, but it fills neither `pv_inverters` nor # the inverter identity fields 3.6.0 adds. Upgrading through the extra is what # brings the adapters that do. -schema-0 = ["span-panel-api-schema-0>=1.2.0"] +# Raised to 1.3.0b1 for 3.7.0b1: that adapter reports the unknown values this +# bootstrap's snapshot fields now accept, and stops synthesising unoccupied positions. +schema-0 = ["span-panel-api-schema-0>=1.3.0b1"] # Raised to 1.2.1 for 3.6.1: that adapter builds the `pv` that `SpanPanelSnapshot.pv` now documents. # Raised to 1.2.2 for 3.6.2: the readiness gate every snapshot passes asks that # adapter what is still missing, and 1.2.2 restarts its feed grace correctly. -schema-1 = ["span-panel-api-schema-1>=1.2.2"] +# Raised to 1.3.0b1 for 3.7.0b1, for the same reason as schema-0's. +schema-1 = ["span-panel-api-schema-1>=1.3.0b1"] [project.urls] Homepage = "https://github.com/SpanPanel/span-panel-api" @@ -245,7 +248,11 @@ warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true explicit_package_bases = true -mypy_path = "src" +# `tests` because the suite imports its support packages top-level +# (`from reference_payloads.schema_one import ...`), the way pytest's rootdir puts +# `tests/` on `sys.path`; the support packages the mypy hook checks resolve the same +# way here. +mypy_path = "src:tests" # Excluded by name for the reason `[tool.ruff]` gives above: the capture script is # checked like the library, and the hand-run tools written before that was expected # are not. diff --git a/src/span_panel_api/models.py b/src/span_panel_api/models.py index 7892cf0..faf022e 100644 --- a/src/span_panel_api/models.py +++ b/src/span_panel_api/models.py @@ -278,8 +278,10 @@ class SpanMidSnapshot: The raw value above is a Homie device id, which means nothing on a dashboard — it is not a Home Assistant device id, and an opaque string is worse than none. This is the - device's own `$description.name` (`Battery`, `Solar`, `SPAN Drive - Garage`), which - is the part a person can read. The literal stays available beside it. + device's own `$description.name` (`Battery`, `Solar`, `SPAN Drive - Garage`) where + that is not the id itself, else its vendor and model (`Tesla Powerwall 2 AC`), which + is the part a person can read; `None` when it publishes nothing that names it. The + literal stays available beside it. """ diff --git a/src/span_panel_api/mqtt/connection.py b/src/span_panel_api/mqtt/connection.py index 2ddb761..6846cae 100644 --- a/src/span_panel_api/mqtt/connection.py +++ b/src/span_panel_api/mqtt/connection.py @@ -846,8 +846,7 @@ async def _rebuild_client(self) -> bool: retrying with it. Recovery target: CA rotation (firmware upgrade), stale paho client - internal state, stuck Homie accumulator. See the design doc at - SpanPanel_Docs/span-panel-api/2026-05-17-mqtt-ca-refresh-on-reconnect-design.md. + internal state, stuck Homie accumulator. """ if self._loop is None: return False diff --git a/tests/adapter_fidelity/__init__.py b/tests/adapter_fidelity/__init__.py new file mode 100644 index 0000000..fd10228 --- /dev/null +++ b/tests/adapter_fidelity/__init__.py @@ -0,0 +1,5 @@ +"""Adapter fidelity: captured device trees replayed through the schema-1 adapter. + +`harness` measures one capture and names no panel, model or capture; `rules` +documents why each measured outcome is right; the test modules pick captures. +""" diff --git a/tests/adapter_fidelity/harness.py b/tests/adapter_fidelity/harness.py new file mode 100644 index 0000000..1057231 --- /dev/null +++ b/tests/adapter_fidelity/harness.py @@ -0,0 +1,858 @@ +"""Replay a captured device tree through the schema-1 adapter and account for every property. + +The question is whether `SchemaOneAdapter` turns what a panel publishes into the +snapshot faithfully: every declared property reaches the snapshot or is dropped by +code that says why, every value it carries is the wire value under the field's +documented transform, and no value appears that nothing on the wire produced. + +**Method.** + +1. *Replay.* A ``tree-v1`` capture (``{"devices": {id: {"description", + "properties", "numeric_properties"}}}``) is fed to the real adapter as retained + messages, the path `SpanMqttClient.get_snapshot` takes: the root first, then + breadth-first by declared children, each device's ``$description``, then + ``$state ready``, then its values. +2. *Flatten.* The snapshot, the command targets and the label wait are flattened to + ``leaf path -> value``. +3. *Perturb.* Every declared property, valued or not, is published with values + that differ from the captured one, chosen from its declared datatype, and the + leaves that move are its effect. A valued property is perturbed on one session + and restored by republishing its captured value, as a broker update would; an + unvalued one is perturbed on a replay of its own, since nothing on the wire + returns a property to unvalued. The session must end equal to the baseline. +4. *Classify* each declared property instance by its effect into a `Fate`. +5. *Check values.* Every leaf a property moves, at its captured value and at each + perturbed one, must follow from that wire value under the transform `CellRules` + documents for it. The perturbed values matter: a captured zero reads the same + under identity and negation. +6. *Audit leaves.* Every non-empty leaf that no valued property moves must be + a derivation `CellRules` documents; anything else is fabricated. + +Shape-generic: nothing here names a panel model or a capture. What is right for a +given cell lives in its `CellRules`. +""" + +from __future__ import annotations + +from collections import Counter +from collections.abc import Callable, Iterable, Mapping +from dataclasses import dataclass, field, fields, is_dataclass +from enum import StrEnum +import json +import math +from pathlib import Path +import re +from typing import Final + +from reference_payloads.schema_one import RetainedTopicTree, devices_from_tree +from span_panel_api.models import ( + ADOPTION_IDENTITY_NODE, + ADOPTION_TOPOLOGY_NODE, + AdoptedDevice, + ControlTarget, + ExtensionProperty, + V2HomieSchema, +) +from span_panel_api_schema_1.adapter import SchemaOneAdapter +from span_panel_api_schema_1.const import ( + DEVICE_TYPE_PREFIX, + HOMIE_DOMAIN, + HOMIE_VERSION, + NODE_GRID, + PROP_GRID_FORMING_ENTITY, + TYPE_BESS, + TYPE_CIRCUIT, + TYPE_LUGS, + TYPE_MID, +) +from span_panel_api_schema_1.field_metadata import _CONSUMED_OFF_SNAPSHOT, addressed_rows, is_addressed +from span_panel_api_schema_1.firmware import release_build + +ANY_LEAF: Final = "*" +"""A `CellRules.transforms` family that stands for every leaf a property moves.""" + + +class Fate(StrEnum): + """What became of one declared property.""" + + MAPPED = "mapped" + EXTENSION = "extension" + ADOPTED = "adopted" + CONTROL = "control/readiness only" + INTENTIONAL = "intentionally dropped" + LATENT = "latent" + """Declared and never valued here; the library's tables address it, yet a value given to it moved nothing. + + A drop waiting to happen if the panel starts publishing it, unless the read is + conditional on something this capture does not have. + """ + INERT = "inert" + """Declared and never valued here; nothing addresses it, and a value given to it moved nothing.""" + SILENT = "silently dropped" + """Valued, with no effect anywhere, and no code that says why.""" + + +_REACHES: Final = (Fate.MAPPED, Fate.EXTENSION, Fate.ADOPTED, Fate.CONTROL) +_FATE_ORDER: Final = {fate: index for index, fate in enumerate(Fate)} + + +@dataclass(frozen=True, slots=True, order=True) +class RowKey: + """One row of the coverage matrix: a property on a device role.""" + + role: str # device role, e.g. "circuit", "bess", "panel", "lugs-upstream" + node_property: str # e.g. "meter/active-power" + + +class TransformKind(StrEnum): + """How a snapshot leaf follows from the wire value that moves it.""" + + IDENTITY = "identity" # the number the wire carries + NEGATE = "negate" # its negation, with no -0.0 + INT = "int" # the number as an int + LITERAL = "literal" # the string exactly as published + BOOL = "bool" # True exactly when the wire says "true" + DERIVED = "derived" # a documented derivation, not a one-to-one transform + + +@dataclass(frozen=True, slots=True) +class Transform: + """The documented transform from one wire property to one snapshot leaf family.""" + + kind: TransformKind + cite: str + earlier: tuple[int, TransformKind] | None = None + """``(build, kind)``: before release build ``build`` the wire carries ``kind`` instead.""" + holds: Callable[[str, object], bool] | None = None + """For a derivation simple enough to state: whether ``value`` follows from the wire value ``raw``.""" + + def kind_for(self, firmware: str) -> TransformKind: + """The transform for a panel on ``firmware``. + + Where the transform changed at a release build and the firmware string + names none, the library decides by comparing signs instead, so the value + is then a derivation rather than a fixed transform. + """ + if self.earlier is None: + return self.kind + build = release_build(firmware) + if build is None: + return TransformKind.DERIVED + first_build, before = self.earlier + return self.kind if build >= first_build else before + + +@dataclass(frozen=True, slots=True) +class Derivation: + """A leaf that may hold a value no valued wire property moves on its own.""" + + family: str + cite: str + values: tuple[str | bool, ...] | None = None + """The values the derivation can produce; ``None`` for any.""" + inputs: tuple[str, ...] = () + """``node/property`` on the leaf's own device that must all be valued. + + For a value two valued properties decide together, where perturbing either + one alone cannot move it. + """ + + +class DropReason(StrEnum): + """Why the library reads a declared property into nothing, by design.""" + + OFF_SNAPSHOT = "read by a route no snapshot field shows" + IDENTITY_OR_TOPOLOGY = "an identity or topology property, never a reading" + EXTRA_INSTANCE = "a further instance of a device the snapshot holds one of" + + +@dataclass(frozen=True, slots=True) +class CellRules: + """What is right for a cell, each entry citing the code that makes it so.""" + + transforms: Mapping[RowKey, Mapping[str, Transform]] + """Row -> leaf family (or `ANY_LEAF`) -> the transform from the wire value.""" + derivations: tuple[Derivation, ...] + drops: Mapping[DropReason, str] + """The code behind each kind of intentional drop.""" + deferred: Mapping[RowKey, str] + """Valued rows the library drops today, each with the reason it is deferred.""" + + @property + def justifications(self) -> dict[str, str]: + """Every citation and reason, keyed by what it justifies.""" + cited: dict[str, str] = {} + for key, by_family in self.transforms.items(): + for family_name, transform in by_family.items(): + cited[f"transform {key.role} {key.node_property} -> {family_name}"] = transform.cite + for derivation in self.derivations: + cited[f"derivation {derivation.family}"] = derivation.cite + for reason, cite in self.drops.items(): + cited[f"drop: {reason}"] = cite + for key, reason_text in self.deferred.items(): + cited[f"deferred {key.role} {key.node_property}"] = reason_text + return cited + + def transform_for(self, key: RowKey, family_name: str) -> Transform | None: + by_family = self.transforms.get(key, {}) + return by_family.get(family_name) or by_family.get(ANY_LEAF) + + def derivation_for(self, family_name: str, value: object, device_values: Mapping[str, str] | None) -> Derivation | None: + for derivation in self.derivations: + if derivation.family != family_name: + continue + if derivation.values is not None and not any(_same(value, allowed) for allowed in derivation.values): + continue + if derivation.inputs and ( + device_values is None or any(device_values.get(prop) is None for prop in derivation.inputs) + ): + continue + return derivation + return None + + +@dataclass(frozen=True, slots=True) +class FidelityReport: + """One capture measured against its `CellRules`.""" + + rows: Mapping[RowKey, tuple[Fate, ...]] + silently_dropped: tuple[RowKey, ...] + deferred: tuple[RowKey, ...] + """Rows that would be silently dropped, held back by a reason in `CellRules.deferred`.""" + dropped_with_a_value: tuple[RowKey, ...] + """Rows with a valued instance dropped on purpose, deferrals aside. + + A drop rule covers whole classes of property, so a cell pins this set: a + valued property newly dropped by a rule is reviewed before it passes. + """ + fabricated: tuple[str, ...] # snapshot leaf paths + mismatched: tuple[str, ...] # snapshot leaf paths whose value breaks the transform + declared_property_instances: int + instance_fates: Mapping[Fate, int] + leaf_rows: Mapping[str, frozenset[RowKey]] + """Each fabricated or mismatched leaf -> the rows that own it.""" + notes: Mapping[str, str] + """Each fabricated or mismatched leaf -> what is wrong with it.""" + derivations_used: frozenset[str] + """Families of the `CellRules.derivations` that accounted for a leaf here.""" + grid_forming_names: Mapping[str, str | None] + """Device id -> the name the snapshot shows while the MID names that device as forming the grid.""" + + def failing_rows(self) -> frozenset[RowKey]: + """The silently dropped rows and the rows owning a fabricated or mismatched leaf.""" + owned = (key for leaf in (*self.fabricated, *self.mismatched) for key in self.leaf_rows.get(leaf, frozenset())) + return frozenset((*self.silently_dropped, *owned)) + + def unowned_leaves(self) -> frozenset[str]: + """Fabricated or mismatched leaves no wire row owns (a synthesised entry, a constant field). + + `failing_rows()` cannot see them, so a gate compares them separately. + """ + return frozenset(leaf for leaf in (*self.fabricated, *self.mismatched) if not self.leaf_rows.get(leaf)) + + +# --- the capture ----------------------------------------------------------- + + +@dataclass(frozen=True, slots=True) +class _Device: + device_id: str + device_type: str + description: Mapping[str, object] + values: Mapping[str, str] # "node/property" -> raw wire value + declared: Mapping[str, Mapping[str, object]] # "node/property" -> declaration + children: tuple[str, ...] + parent: str + + +@dataclass(frozen=True, slots=True) +class _Capture: + devices: Mapping[str, _Device] + root_id: str + order: tuple[str, ...] + """The root, then breadth-first by declared children, then any device no parent names.""" + + @property + def firmware(self) -> str: + return self.devices[self.root_id].values.get("info/firmware-version", "") + + def retained(self) -> RetainedTopicTree: + return { + device_id: {"$description": json.dumps(device.description), "$state": "ready", **device.values} + for device_id, device in self.devices.items() + } + + +def _mapping(value: object) -> Mapping[str, object]: + if not isinstance(value, Mapping): + return {} + return {str(key): item for key, item in value.items()} + + +def _text(value: object) -> str: + return value if isinstance(value, str) else "" + + +def _load(tree_path: Path) -> _Capture: + tree = _mapping(json.loads(tree_path.read_text(encoding="utf-8"))) + devices: dict[str, _Device] = {} + for device_id, entry in _mapping(tree.get("devices")).items(): + record = _mapping(entry) + description = _mapping(record.get("description")) + values: dict[str, str] = {} + for bucket in ("properties", "numeric_properties"): + for topic, raw in _mapping(record.get(bucket)).items(): + if not isinstance(raw, str): + raise TypeError(f"{tree_path.name}: {device_id}/{topic} is not a wire string: {raw!r}") + values[topic] = raw + declared = { + f"{node_id}/{property_id}": _mapping(declaration) + for node_id, node in _mapping(description.get("nodes")).items() + for property_id, declaration in _mapping(_mapping(node).get("properties")).items() + } + children = description.get("children") + devices[device_id] = _Device( + device_id=device_id, + device_type=_text(description.get("type")), + description=description, + values=values, + declared=declared, + children=tuple(_text(child) for child in children) if isinstance(children, list) else (), + parent=_text(description.get("parent")), + ) + roots = [device_id for device_id, device in devices.items() if not device.parent] + if len(roots) != 1: + raise ValueError(f"{tree_path.name}: expected one root device, found {roots}") + order: list[str] = [] + queue = [roots[0]] + while queue: + device_id = queue.pop(0) + if device_id in order or device_id not in devices: + continue + order.append(device_id) + queue.extend(devices[device_id].children) + order.extend(sorted(device_id for device_id in devices if device_id not in order)) + return _Capture(devices=devices, root_id=roots[0], order=tuple(order)) + + +# --- replay and flatten ---------------------------------------------------- + + +def _frozen_clock() -> float: + """The feed-grace clock, held still so every replay sees the same label wait.""" + return 1000.0 + + +def _replay(capture: _Capture) -> SchemaOneAdapter: + """A fresh adapter fed the capture as retained messages.""" + schema = V2HomieSchema( + firmware_version=capture.firmware, + types_schema_hash="sha256:capture", + types={}, + data_model_version="1.0", + ) + adapter = SchemaOneAdapter(capture.root_id, schema, clock=_frozen_clock) + for device_id in capture.order: + device = capture.devices[device_id] + prefix = f"{HOMIE_DOMAIN}/{HOMIE_VERSION}/{device_id}" + adapter.handle_message(f"{prefix}/$description", json.dumps(device.description)) + adapter.handle_message(f"{prefix}/$state", "ready") + for topic in sorted(device.values): + adapter.handle_message(f"{prefix}/{topic}", device.values[topic]) + return adapter + + +_DEVICE_MAPS: Final = {"circuits": "circuit", "pv_inverters": "pv_inverter", "evse": "evse"} +"""Snapshot fields that map a key to one device's snapshot, and the leaf family each device takes.""" +_DEVICE_FIELDS: Final = frozenset({"battery", "pv", "mid", "pcs"}) +_INDEXED = re.compile(r"^(?P[a-z_]+)\[(?P[^\]]*)\]\.(?P.+)$") +_INSTANCE = re.compile(r"^[a-z_.]+\[([^\]]*)\]") + + +def _plain(value: object) -> object: + return tuple(value) if isinstance(value, list) else value + + +def _record(leaves: dict[str, object], prefix: str, item: object) -> None: + if item is None: + leaves[prefix] = None + return + if not is_dataclass(item) or isinstance(item, type): + raise TypeError(f"{prefix} is not a device snapshot: {item!r}") + for item_field in fields(item): + leaves[f"{prefix}.{item_field.name}"] = _plain(getattr(item, item_field.name)) + + +def _topic(target: ControlTarget | None) -> str | None: + return None if target is None else target.topic + + +def _extension_leaves(leaves: dict[str, object], properties: Iterable[ExtensionProperty]) -> None: + for prop in properties: + key = f"ext[{prop.subject.kind}|{prop.subject.instance_key or ''}|{prop.node_id}/{prop.property_id}]" + leaves[key] = prop.value + + +def _adopted_leaves(leaves: dict[str, object], devices: Iterable[AdoptedDevice]) -> None: + for device in devices: + base = f"adopted[{device.device_id}]" + for device_field in fields(device): + if device_field.name != "properties": + leaves[f"{base}.{device_field.name}"] = _plain(getattr(device, device_field.name)) + for prop in device.properties: + leaves[f"{base}.prop[{prop.node_id}/{prop.property_id}]"] = prop.value + + +def _flatten(adapter: SchemaOneAdapter) -> dict[str, object]: + """``leaf path -> value`` for the snapshot, the command targets and the label wait.""" + leaves: dict[str, object] = { + "ready.is_ready": adapter.is_ready(), + "ready.missing_names": tuple(sorted(adapter.circuit_nodes_missing_names())), + } + if not adapter.is_ready(): + return leaves + snapshot = adapter.build_snapshot() + for snapshot_field in fields(snapshot): + name = snapshot_field.name + value: object = getattr(snapshot, name) + if name == "extension_properties": + _extension_leaves(leaves, snapshot.extension_properties) + elif name == "adopted_devices": + _adopted_leaves(leaves, snapshot.adopted_devices) + elif name in _DEVICE_MAPS: + if not isinstance(value, Mapping): + raise TypeError(f"snapshot field {name!r} is not a map") + for key, item in value.items(): + _record(leaves, f"{name}[{key}]", item) + elif name in _DEVICE_FIELDS: + _record(leaves, name, value) + elif is_dataclass(value) or isinstance(value, Mapping | list | tuple): + raise TypeError(f"snapshot field {name!r} holds a structure this harness does not flatten") + else: + leaves[f"panel.{name}"] = value + for circuit_id in snapshot.circuits: + leaves[f"ctl.relay_target[{circuit_id}]"] = _topic(adapter.set_circuit_relay_target(circuit_id)) + leaves[f"ctl.priority_target[{circuit_id}]"] = _topic(adapter.set_circuit_priority_target(circuit_id)) + for evse_key in snapshot.evse: + leaves[f"ctl.evse_limit_target[{evse_key}]"] = _topic(adapter.set_evse_charge_limit_target(evse_key)) + leaves["ctl.dps_target"] = _topic(adapter.set_dominant_power_source_target()) + return leaves + + +def family(leaf: str) -> str: + """A leaf path without its instance: ``circuits[abc].instant_power_w`` -> ``circuit.instant_power_w``.""" + if leaf.startswith("ext["): + kind, _key, path = leaf[len("ext[") : -1].split("|", 2) + return f"ext.{kind}:{path}" + if leaf.startswith("adopted["): + return "adopted" + re.sub(r"\[[^\]]*\]", "", leaf.split("]", 1)[1]) + if leaf.startswith("ctl."): + return re.sub(r"\[[^\]]*\]$", "", leaf) + match = _INDEXED.match(leaf) + if match is not None: + return f"{_DEVICE_MAPS[match['container']]}.{match['field']}" + return leaf + + +def _instance(leaf: str) -> str | None: + match = _INSTANCE.match(leaf) + return None if match is None else match.group(1) + + +# --- perturbation ---------------------------------------------------------- + + +def _number(raw: str) -> float | None: + try: + return float(raw) + except ValueError: + return None + + +def _candidates( + declaration: Mapping[str, object], raw: str | None, property_id: str, device_ids: Iterable[str] +) -> list[str]: + """Values that differ from ``raw`` and are legal for the declaration. + + Several per property, so that a derived field with a threshold (``poles >= 2``) + or a reference that must resolve (a ``*-device-id`` naming a device in the + tree) is exercised rather than reported as having no effect. Every member of an + enum, because a derived field may move for only one of them, and one value + outside it. + """ + datatype = _text(declaration.get("datatype")) or "string" + base = None if raw is None else _number(raw) + pool: list[str] + if datatype == "boolean": + pool = ["true", "false"] + elif datatype == "enum": + members = [member.strip() for member in _text(declaration.get("format")).split(",") if member.strip()] + pool = [*members, "PERTURBED_ENUM"] + elif datatype == "integer": + integers = [int(base) + 13] if base is not None else [] + pool = [str(value) for value in (*integers, 1, 2, 0, 13, -7)] + elif datatype == "float": + floats = [base + 13.25, -base] if base else [] + pool = [repr(float(value)) for value in (*floats, 0.0, 13.25, -7.5)] + elif datatype == "json": + pool = ['{"perturbed": true}', "{}"] + else: + pool = [f"{raw}~p" if raw else "perturbed-value"] + if property_id.endswith(("-device-id", "-entity")): + pool.extend((*device_ids, "GRID")) + distinct: list[str] = [] + for value in pool: + if value != raw and value not in distinct: + distinct.append(value) + return distinct[:3] if datatype in ("integer", "float") else distinct + + +_MISSING: Final = object() + + +def _same(left: object, right: object) -> bool: + if isinstance(left, float) and isinstance(right, float) and math.isnan(left) and math.isnan(right): + return True + return type(left) is type(right) and left == right + + +def _moved(baseline: Mapping[str, object], perturbed: Mapping[str, object]) -> set[str]: + return { + leaf + for leaf in baseline.keys() | perturbed.keys() + if not _same(baseline.get(leaf, _MISSING), perturbed.get(leaf, _MISSING)) + } + + +@dataclass(slots=True) +class _Instance: + """One declared property on one device, and what the experiment found.""" + + device_id: str + device_type: str + key: RowKey + declaration: Mapping[str, object] + raw: str | None + addressed: bool + extra: bool + effect: set[str] = field(default_factory=set) + perturbed: list[tuple[str, dict[str, object]]] = field(default_factory=list) + """Each value published, and what every leaf it moved then held.""" + fate: Fate = Fate.SILENT + deferred: bool = False + + +def _role(capture: _Capture, device: _Device) -> str: + if device.device_id == capture.root_id: + return "panel" + short = device.device_type.removeprefix(DEVICE_TYPE_PREFIX) + if device.device_type.startswith(TYPE_LUGS): + direction = device.values.get("info/direction", "").strip().lower() + return f"lugs-{direction}" if direction else "lugs" + return short + + +def _instances(capture: _Capture, adapter: SchemaOneAdapter) -> list[_Instance]: + addressed = addressed_rows(devices_from_tree(capture.retained())) + # The snapshot holds one BESS and one MID, the first of each the adapter finds. + kept = {adapter.find_node_by_type(TYPE_BESS), adapter.find_node_by_type(TYPE_MID)} + found: list[_Instance] = [] + for device_id in capture.order: + device = capture.devices[device_id] + extra = device.device_type in (TYPE_BESS, TYPE_MID) and device_id not in kept + for node_property, declaration in device.declared.items(): + node_id, property_id = node_property.split("/", 1) + found.append( + _Instance( + device_id=device_id, + device_type=device.device_type, + key=RowKey(_role(capture, device), node_property), + declaration=declaration, + raw=device.values.get(node_property), + addressed=is_addressed(addressed, device.device_type, node_id, property_id), + extra=extra, + ) + ) + return found + + +def _reference_ids(capture: _Capture) -> list[str]: + """Devices a ``*-device-id`` or ``*-entity`` property could name: every child but circuits and lugs.""" + return [ + device_id + for device_id in capture.order + if device_id != capture.root_id + and capture.devices[device_id].device_type != TYPE_CIRCUIT + and not capture.devices[device_id].device_type.startswith(TYPE_LUGS) + ] + + +def _measure(capture: _Capture, instances: list[_Instance], baseline: Mapping[str, object]) -> None: + references = _reference_ids(capture) + session = _replay(capture) + for instance in instances: + topic = f"{HOMIE_DOMAIN}/{HOMIE_VERSION}/{instance.device_id}/{instance.key.node_property}" + property_id = instance.key.node_property.split("/", 1)[1] + # An unvalued property cannot be returned to unvalued, so it is perturbed on + # a replay of its own, which is discarded; each candidate replaces the last, + # as a later retained value does. + target = session if instance.raw is not None else _replay(capture) + for candidate in _candidates(instance.declaration, instance.raw, property_id, references): + target.handle_message(topic, candidate) + leaves = _flatten(target) + moved = _moved(baseline, leaves) + instance.effect |= moved + instance.perturbed.append((candidate, {leaf: leaves[leaf] for leaf in moved if leaf in leaves})) + if instance.raw is not None: + session.handle_message(topic, instance.raw) + unrestored = _moved(baseline, _flatten(session)) + if unrestored: + raise AssertionError(f"republishing the captured values did not restore the session: {sorted(unrestored)[:5]}") + + +# --- classification -------------------------------------------------------- + +_OFF_SNAPSHOT: Final = set(_CONSUMED_OFF_SNAPSHOT) + + +def _classify(instance: _Instance, rules: CellRules) -> None: + effect = instance.effect + if any(not leaf.startswith(("ext[", "adopted[", "ctl.", "ready.")) for leaf in effect): + instance.fate = Fate.MAPPED + elif any(leaf.startswith("ext[") for leaf in effect): + instance.fate = Fate.EXTENSION + elif any(leaf.startswith("adopted[") for leaf in effect): + instance.fate = Fate.ADOPTED + elif effect: + instance.fate = Fate.CONTROL + elif _drop_reason(instance) is not None: + instance.fate = Fate.INTENTIONAL + elif instance.raw is not None: + instance.deferred = instance.key in rules.deferred + instance.fate = Fate.INTENTIONAL if instance.deferred else Fate.SILENT + else: + instance.fate = Fate.LATENT if instance.addressed else Fate.INERT + + +def _drop_reason(instance: _Instance) -> DropReason | None: + if instance.extra: + return DropReason.EXTRA_INSTANCE + node_id, property_id = instance.key.node_property.split("/", 1) + if is_addressed(_OFF_SNAPSHOT, instance.device_type, node_id, property_id): + return DropReason.OFF_SNAPSHOT + if node_id in (ADOPTION_IDENTITY_NODE, ADOPTION_TOPOLOGY_NODE) and not instance.addressed: + return DropReason.IDENTITY_OR_TOPOLOGY + return None + + +def _promote_conditional(instances: list[_Instance]) -> None: + """An unvalued instance with no effect, where the same row reaches the snapshot through another. + + Its read is conditional on the instance's own topology or values, such as a + ``feeds-device-status`` on a circuit that feeds nothing, so it takes the + row's fate rather than reading as latent or inert. + """ + groups: dict[RowKey, list[_Instance]] = {} + for instance in instances: + groups.setdefault(instance.key, []).append(instance) + for group in groups.values(): + reached = Counter(instance.fate for instance in group if instance.fate in _REACHES) + if not reached: + continue + fate = max(sorted(reached, key=_FATE_ORDER.__getitem__), key=reached.__getitem__) + for instance in group: + if instance.raw is None and instance.fate in (Fate.LATENT, Fate.INERT): + instance.fate = fate + + +# --- value checks ---------------------------------------------------------- + + +def _observe(raw: str, value: object) -> str: + """Which transform turns ``raw`` into ``value``.""" + if value is None: + return "valued -> None" + if isinstance(value, bool): + return TransformKind.BOOL if value == (raw.strip().lower() == "true") else "bool(other)" + if isinstance(value, int | float): + number = _number(raw) + if number is None: + return "number from an unparseable value" + if isinstance(value, float) and value == 0.0 and math.copysign(1.0, value) < 0: + return "negative zero" + if value == number: + return TransformKind.INT if isinstance(value, int) else TransformKind.IDENTITY + if number != 0.0 and value == -number: + return TransformKind.NEGATE + return "number(other)" + if isinstance(value, str): + return TransformKind.LITERAL if value == raw else "string(other)" + return "other" + + +def _compatible(observed: str, expected: TransformKind, raw: str) -> bool: + if observed == expected: + return True + # A zero reads the same under identity and negation. + if expected == TransformKind.NEGATE and observed == TransformKind.IDENTITY and _number(raw) == 0.0: + return True + return expected == TransformKind.IDENTITY and observed == TransformKind.INT + + +def _check_values( + capture: _Capture, + instances: Iterable[_Instance], + baseline: Mapping[str, object], + rules: CellRules, + failures: dict[str, set[RowKey]], + notes: dict[str, str], +) -> set[str]: + """Every value a property's own wire value set, held to the transform documented for it. + + The captured value and every perturbed one alike. A captured zero reads the + same under identity and negation, so a sign defect on a property the capture + holds at zero shows only in the values the perturbation published. + """ + mismatched: set[str] = set() + for instance in instances: + if instance.fate not in (Fate.MAPPED, Fate.EXTENSION): + continue + observations: list[tuple[str, Mapping[str, object]]] = list(instance.perturbed) + if instance.raw is not None: + # An unvalued source's captured leaves are the leaf audit's to judge. + observations.insert(0, (instance.raw, {leaf: baseline[leaf] for leaf in instance.effect if leaf in baseline})) + for raw, values in observations: + # A property can be the snapshot key of the leaves it moves, as a charger's + # serial is; those leaves are keyed by it, not valued by it. + keys = {raw} if instance.raw is None else {raw, instance.raw} + for leaf, value in sorted(values.items()): + if leaf.startswith(("ctl.", "ready.", "adopted[")) or _instance(leaf) in keys: + continue + problem = _transform_problem(instance.key, leaf, raw, value, rules, capture.firmware) + if problem is not None: + mismatched.add(leaf) + failures.setdefault(leaf, set()).add(instance.key) + notes[leaf] = f"{instance.key.role} {instance.key.node_property}: {problem}" + return mismatched + + +def _transform_problem(key: RowKey, leaf: str, raw: str, value: object, rules: CellRules, firmware: str) -> str | None: + """What is wrong with ``value`` as what ``raw`` on ``key`` set ``leaf`` to, or `None`.""" + if leaf.startswith("ext["): + return None if value == raw else f"extension value {value!r} is not the wire value {raw!r}" + transform = rules.transform_for(key, family(leaf)) + if transform is None: + return f"no documented transform from {key.node_property}" + expected = transform.kind_for(firmware) + if expected == TransformKind.DERIVED: + if transform.holds is None or transform.holds(raw, value): + return None + return f"{raw!r} -> {value!r} breaks the documented derivation ({transform.cite})" + observed = _observe(raw, value) + if _compatible(observed, expected, raw): + return None + return f"{raw!r} -> {value!r} is {observed}, documented {expected} ({transform.cite})" + + +# --- the leaf audit --------------------------------------------------------- + + +def _audit_leaves( + capture: _Capture, + instances: Iterable[_Instance], + baseline: Mapping[str, object], + rules: CellRules, + failures: dict[str, set[RowKey]], + notes: dict[str, str], +) -> tuple[set[str], set[str]]: + """Leaves holding a value no valued wire property moves, and the derivations that account for the rest.""" + sources: dict[str, list[_Instance]] = {} + for instance in instances: + for leaf in instance.effect: + sources.setdefault(leaf, []).append(instance) + fabricated: set[str] = set() + used: set[str] = set() + for leaf, value in baseline.items(): + if leaf.startswith(("ready.", "ext[", "adopted[")) or value is None or value in ((), ""): + continue + owners = sources.get(leaf, []) + if any(owner.raw is not None for owner in owners): + continue + device_id = _instance(leaf) + device = None if device_id is None else capture.devices.get(device_id) + derivation = rules.derivation_for(family(leaf), value, None if device is None else device.values) + if derivation is not None: + used.add(derivation.family) + continue + fabricated.add(leaf) + failures.setdefault(leaf, set()).update(owner.key for owner in owners) + unvalued = ", ".join(sorted({f"{owner.device_id} {owner.key.node_property}" for owner in owners})) + notes[leaf] = f"{value!r} with {'only unvalued sources: ' + unvalued if owners else 'no wire source'}" + return fabricated, used + + +# --- a cell ---------------------------------------------------------------- + + +def _grid_forming_names(capture: _Capture) -> dict[str, str | None]: + """What the snapshot names each device while the MID says that device forms the grid.""" + grid_forming = f"{NODE_GRID}/{PROP_GRID_FORMING_ENTITY}" + mid = next((device_id for device_id in capture.order if capture.devices[device_id].device_type == TYPE_MID), None) + if mid is None or grid_forming not in capture.devices[mid].declared: + return {} + probe = _replay(capture) + topic = f"{HOMIE_DOMAIN}/{HOMIE_VERSION}/{mid}/{grid_forming}" + names: dict[str, str | None] = {} + for device_id in capture.order: + if device_id == capture.root_id: + continue + probe.handle_message(topic, device_id) + snapshot = probe.build_snapshot() + names[device_id] = None if snapshot.mid is None else snapshot.mid.grid_forming_device_name + return names + + +def run_cell(tree_path: Path, rules: CellRules) -> FidelityReport: + """Measure one captured tree against ``rules``.""" + capture = _load(tree_path) + adapter = _replay(capture) + baseline = _flatten(adapter) + if baseline["ready.is_ready"] is not True: + raise AssertionError(f"{tree_path.name}: the replayed tree never became ready") + + instances = _instances(capture, adapter) + _measure(capture, instances, baseline) + for instance in instances: + _classify(instance, rules) + _promote_conditional(instances) + + failures: dict[str, set[RowKey]] = {} + notes: dict[str, str] = {} + mismatched = _check_values(capture, instances, baseline, rules, failures, notes) + fabricated, used = _audit_leaves(capture, instances, baseline, rules, failures, notes) + + fates: dict[RowKey, set[Fate]] = {} + for instance in instances: + fates.setdefault(instance.key, set()).add(instance.fate) + return FidelityReport( + rows={key: tuple(sorted(found, key=_FATE_ORDER.__getitem__)) for key, found in sorted(fates.items())}, + silently_dropped=tuple(sorted({instance.key for instance in instances if instance.fate == Fate.SILENT})), + deferred=tuple(sorted({instance.key for instance in instances if instance.deferred})), + dropped_with_a_value=tuple( + sorted( + { + instance.key + for instance in instances + if instance.fate == Fate.INTENTIONAL and instance.raw is not None and not instance.deferred + } + ) + ), + fabricated=tuple(sorted(fabricated)), + mismatched=tuple(sorted(mismatched)), + declared_property_instances=len(instances), + instance_fates=dict(Counter(instance.fate for instance in instances)), + leaf_rows={leaf: frozenset(keys) for leaf, keys in failures.items()}, + notes=notes, + derivations_used=frozenset(used), + grid_forming_names=_grid_forming_names(capture), + ) diff --git a/tests/adapter_fidelity/rules.py b/tests/adapter_fidelity/rules.py new file mode 100644 index 0000000..2577908 --- /dev/null +++ b/tests/adapter_fidelity/rules.py @@ -0,0 +1,504 @@ +"""What is right for the MAIN 32 cells, and the code that makes each entry so. + +Every citation names a symbol in ``span_panel_api_schema_1`` (or in +``span_panel_api`` where it says so) rather than a line, so a citation outlives +the edit that moves the code. + +The transform table covers every row of the library's own `_PROPERTY_FIELD_MAP` +and the three lugs tables, which `test_every_mapped_field_has_a_documented_transform` +holds it to: a field the library starts mapping cannot reach a snapshot without a +transform documented here. The derivations are only the ones these captures +need, which `test_every_derivation_is_needed` holds them to: a justification no +leaf needs is how a list of reasons turns into an allowlist. +""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Final + +from span_panel_api.models import FEEDS_ROLES, PROTECTION_FUNCTIONS +from span_panel_api_schema_1.const import PANEL_POSITIONS_BY_MODEL + +from .harness import ANY_LEAF, CellRules, Derivation, DropReason, RowKey, Transform, TransformKind + +_IDENTITY: Final = TransformKind.IDENTITY +_NEGATE: Final = TransformKind.NEGATE +_INT: Final = TransformKind.INT +_LITERAL: Final = TransformKind.LITERAL +_BOOL: Final = TransformKind.BOOL +_DERIVED: Final = TransformKind.DERIVED + +BESS_METER_DISCHARGE_POSITIVE_FROM_BUILD: Final = 202639 +"""Mirrors `devices.BESS_METER_DISCHARGE_POSITIVE_FROM_BUILD`, restated so the rule is checked rather than borrowed.""" + + +def _number(raw: str) -> float | None: + try: + return float(raw) + except ValueError: + return None + + +def _two_poles_or_more(raw: str, value: object) -> bool: + poles = _number(raw) + return value is (poles is not None and poles >= 2) + + +def _not_true(raw: str, value: object) -> bool: + return value is (raw.strip().lower() != "true") + + +def _is_connected_cloud(raw: str, value: object) -> bool: + return value is (raw == "CONNECTED") + + +def _is_ok_link(raw: str, value: object) -> bool: + return value is (raw == "OK") + + +def _listed_spaces(raw: str, value: object) -> bool: + """The integers in a comma-separated list, in order; a part that is not one is skipped.""" + spaces: list[int] = [] + for part in raw.split(","): + try: + spaces.append(int(part.strip())) + except ValueError: + continue + return value == tuple(spaces) + + +def _model_or_none(raw: str, value: object) -> bool: + """`UNKNOWN` (or nothing) names no model and reads as None; a named model reads as published.""" + return value == (None if raw.strip().upper() in ("", "UNKNOWN") else raw) + + +def _position(index: int) -> Callable[[str, object], bool]: + """The model's first (0) or last (1) position from the table; for a model it lacks, the occupied range decides.""" + + def holds(raw: str, value: object) -> bool: + known = PANEL_POSITIONS_BY_MODEL.get(raw.strip().upper()) + if known is None: + return value is None or isinstance(value, int) + return value == known[index] + + return holds + + +def _feeds_role(raw: str, value: object) -> bool: + return value == (raw if raw in FEEDS_ROLES else None) + + +def _protection_functions(raw: str, value: object) -> bool: + listed = [part.strip() for part in raw.split(",")] + return value == tuple(part for part in listed if part in PROTECTION_FUNCTIONS) + + +def _transforms() -> dict[RowKey, dict[str, Transform]]: + table: dict[RowKey, dict[str, Transform]] = {} + + def add( + role: str, + node_property: str, + family: str, + kind: TransformKind, + cite: str, + holds: Callable[[str, object], bool] | None = None, + ) -> None: + table.setdefault(RowKey(role, node_property), {})[family] = Transform(kind, cite, holds=holds) + + # --- panel ------------------------------------------------------------- + panel_fields = "panel.PanelFields" + add("panel", "info/firmware-version", "panel.firmware_version", _LITERAL, panel_fields) + add( + "panel", + "info/firmware-version", + "battery.power_w", + _DERIVED, + "devices.build_battery: the release build in the firmware version picks the BESS meter's frame", + ) + add("panel", "info/vendor-name", "panel.vendor_name", _LITERAL, panel_fields) + add( + "panel", + "info/model", + "panel.model", + _DERIVED, + f"{panel_fields}: an UNKNOWN model names no model and reads as None", + _model_or_none, + ) + add("panel", "info/model", "panel.panel_size", _DERIVED, "panel.panel_size_from_model and const.PANEL_SIZE_BY_MODEL") + for index, position in enumerate(("first_position", "last_position")): + add( + "panel", + "info/model", + f"panel.{position}", + _DERIVED, + "panel.panel_positions and const.PANEL_POSITIONS_BY_MODEL; the occupied spaces where the table lacks the model", + _position(index), + ) + add("panel", "info/hardware-version", "panel.hardware_version", _LITERAL, panel_fields) + add("panel", "info/serial-number", "panel.serial_number", _LITERAL, panel_fields) + add("panel", "status/relay", "panel.main_relay_state", _LITERAL, panel_fields) + add("panel", "door/state", "panel.door_state", _LITERAL, panel_fields) + add("panel", "status/ethernet", "panel.eth0_link", _BOOL, panel_fields) + add("panel", "status/wifi", "panel.wlan_link", _BOOL, panel_fields) + add("panel", "status/wifi-ssid", "panel.wifi_ssid", _LITERAL, panel_fields) + add("panel", "status/cloud-connection", "panel.vendor_cloud", _LITERAL, panel_fields) + add( + "panel", + "status/cloud-connection", + "panel.wwan_link", + _DERIVED, + "panel.PanelFields: the cloud link is CONNECTED, as flat reported it", + _is_connected_cloud, + ) + add("panel", "meter/voltage-a", "panel.l1_voltage", _IDENTITY, panel_fields) + add("panel", "meter/voltage-b", "panel.l2_voltage", _IDENTITY, panel_fields) + add("panel", "breaker/rating", "panel.main_breaker_rating_a", _INT, panel_fields) + add("panel", "meter/busbar-current", "panel.busbar_current_a", _IDENTITY, panel_fields) + add("panel", "meter/frequency", "panel.frequency_hz", _IDENTITY, panel_fields) + for flow in ("pv", "battery", "grid", "site"): + add( + "panel", + f"power-flows/{flow}", + f"panel.power_flow_{flow}", + _IDENTITY, + f"{panel_fields}: passed through untouched", + ) + add( + "panel", + "power-flows/battery", + "battery.power_w", + _DERIVED, + "devices.build_battery: compared in sign with the BESS meter where the firmware names no release build", + ) + add( + "panel", + "power-flows/grid", + ANY_LEAF, + _DERIVED, + "panel.resolve_islanding_state: grid power decides on-grid only where no MID is published", + ) + add("panel", "shed/policy", "panel.shed_policy", _LITERAL, panel_fields) + for policy_field in ("shed_policy_algorithm", "shed_soc_threshold_shed_percent", "shed_soc_threshold_release_percent"): + add("panel", "shed/policy", f"panel.{policy_field}", _DERIVED, "panel._shed_policy parses the policy document") + add( + "panel", + "shed/asserted-islanding-state", + ANY_LEAF, + _DERIVED, + "panel.resolve_islanding_state: an assertion in force outranks the MID", + ) + for forecast in ( + "time-to-priority-shed", + "total-time-remaining", + "full-charge-time-to-priority-shed", + "full-charge-total-time-remaining", + ): + add("panel", f"shed-forecast/{forecast}", f"panel.shed_{forecast.replace('-', '_')}_min", _INT, panel_fields) + add("panel", "shed-forecast/confidence", "panel.shed_forecast_confidence", _LITERAL, panel_fields) + pcs = "panel.build_pcs" + add("panel", "pcs/enabled", "pcs.enabled", _BOOL, pcs) + add("panel", "pcs/active", "pcs.active", _BOOL, pcs) + add("panel", "pcs/import-limit", "pcs.import_limit_a", _IDENTITY, pcs) + add("panel", "pcs/binding-constraint", "pcs.binding_constraint", _LITERAL, pcs) + for source in ("feed", "operator", "off-grid", "requested"): + name = source.replace("-", "_") + add( + "panel", f"pcs/{source}-import-limit", f"pcs.{name}_import_limit_a", _IDENTITY, f"panel._limit_triplet via {pcs}" + ) + add( + "panel", + f"pcs/{source}-import-limit-enablement", + f"pcs.{name}_import_limit_enablement", + _LITERAL, + f"panel._limit_triplet via {pcs}", + ) + add( + "panel", + f"pcs/{source}-import-limit-active", + f"pcs.{name}_import_limit_active", + _BOOL, + f"panel._limit_triplet via {pcs}", + ) + + # --- circuit ----------------------------------------------------------- + circuit = "circuits.build_circuit" + add( + "circuit", + "meter/active-power", + "circuit.instant_power_w", + _NEGATE, + f"{circuit}: a load reads negative in the enclosure frame", + ) + add("circuit", "meter/imported-energy", "circuit.produced_energy_wh", _IDENTITY, f"{circuit}: the accumulators swap") + add("circuit", "meter/exported-energy", "circuit.consumed_energy_wh", _IDENTITY, f"{circuit}: the accumulators swap") + add("circuit", "meter/current", "circuit.current_a", _IDENTITY, circuit) + add("circuit", "breaker/rating", "circuit.breaker_rating_a", _IDENTITY, circuit) + add("circuit", "breaker/poles", "circuit.is_240v", _DERIVED, f"{circuit}: two poles or more", _two_poles_or_more) + add("circuit", "info/name", "circuit.name", _LITERAL, circuit) + add("circuit", "info/spaces", "circuit.tabs", _DERIVED, "circuits._tabs parses the list of spaces", _listed_spaces) + for position in ("first_position", "last_position"): + add( + "circuit", + "info/spaces", + f"panel.{position}", + _DERIVED, + "panel.panel_positions: the occupied spaces decide the range where the table lacks the model", + ) + add("circuit", "info/nominal-voltage", "circuit.nominal_voltage_v", _IDENTITY, circuit) + add( + "circuit", + "breaker/protection-functions", + "circuit.protection_functions", + _DERIVED, + "circuits._protection_functions: the listed members of PROTECTION_FUNCTIONS, in published order", + _protection_functions, + ) + add( + "circuit", + "connection/feeds-role", + "circuit.feeds_role", + _DERIVED, + "circuits._feeds_role: a member of FEEDS_ROLES as published, else None", + _feeds_role, + ) + for node, shared in (("meter", "circuit.meter_shared_with"), ("switch", "circuit.relay_shared_with")): + add( + "circuit", + f"{node}/shared-with-device-ids", + shared, + _DERIVED, + "circuits._shared_with: the listed ids naming another circuit of the snapshot, in device_id_order", + ) + add("circuit", "switch/relay", "circuit.relay_state", _LITERAL, circuit) + add("circuit", "switch/relay-requester", "circuit.relay_requester", _LITERAL, circuit) + add("circuit", "switch/relay-controllable", "circuit.is_user_controllable", _BOOL, circuit) + add( + "circuit", + "switch/relay-controllable", + "circuit.always_on", + _DERIVED, + f"{circuit}: not relay-controllable", + _not_true, + ) + add( + "circuit", + "switch/relay-controllable", + "circuit.is_sheddable", + _DERIVED, + f"{circuit}: a shed priority and a controllable relay", + ) + add("circuit", "load-shed/priority", "circuit.priority", _LITERAL, circuit) + add( + "circuit", + "load-shed/priority", + "circuit.is_sheddable", + _DERIVED, + f"{circuit}: a shed priority and a controllable relay", + ) + add("circuit", "pcs/managed", "circuit.pcs_managed", _BOOL, circuit) + add("circuit", "pcs/priority", "circuit.pcs_priority", _INT, circuit) + add( + "circuit", + "connection/feeds-device-id", + ANY_LEAF, + _DERIVED, + "snapshot.build_snapshot and devices.feed_circuit_ids: the circuit's label, and the fed DER's feeding circuit, key and position", + ) + for der in ("battery", "evse", "pv", "pv_inverter"): + add( + "circuit", + "connection/feeds-device-status", + f"{der}.connected", + _DERIVED, + "devices.feed_connection_statuses and devices._connected: OK is a working link " + "(for a battery, where no device claims it through a fed-by record)", + _is_ok_link, + ) + + # --- lugs -------------------------------------------------------------- + lugs_meter = "panel.PanelFields: the lugs read import-positive and pass through" + for role, power, consumed, produced, current_a, current_b in ( + ( + "lugs-upstream", + "panel.instant_grid_power_w", + "panel.main_meter_energy_consumed_wh", + "panel.main_meter_energy_produced_wh", + "panel.upstream_l1_current_a", + "panel.upstream_l2_current_a", + ), + ( + "lugs-downstream", + "panel.feedthrough_power_w", + "panel.feedthrough_energy_consumed_wh", + "panel.feedthrough_energy_produced_wh", + "panel.downstream_l1_current_a", + "panel.downstream_l2_current_a", + ), + ): + add(role, "meter/active-power", power, _IDENTITY, lugs_meter) + add(role, "meter/imported-energy", consumed, _IDENTITY, lugs_meter) + add(role, "meter/exported-energy", produced, _IDENTITY, lugs_meter) + add(role, "meter/current-a", current_a, _IDENTITY, lugs_meter) + add(role, "meter/current-b", current_b, _IDENTITY, lugs_meter) + add(role, "info/direction", ANY_LEAF, _DERIVED, "panel.find_lugs: the direction decides which lugs are which") + add( + role, + "connection/fed-by-device-id", + ANY_LEAF, + _DERIVED, + "panel.PanelFields (lugs_at_service_entrance), devices.connection_status_for and devices.resolve_relative_position", + ) + add( + role, + "connection/fed-by-device-status", + "battery.connected", + _DERIVED, + "devices.connection_status_for and devices._connected: OK is a working link", + _is_ok_link, + ) + add( + role, + "connection/feeds-device-id", + ANY_LEAF, + _DERIVED, + "devices.resolve_relative_position: a DER fed through the feedthrough is in the panel", + ) + add( + "lugs-upstream", + "connection/overcurrent-protection", + "panel.upstream_protection_rating_a", + _INT, + "panel.PanelFields: the upstream lugs' protection rating", + ) + + # --- BESS -------------------------------------------------------------- + battery = "devices.build_battery" + add("bess", "soc/soc", "battery.soe_percentage", _IDENTITY, battery) + add("bess", "soc/soe", "battery.soe_kwh", _IDENTITY, battery) + add("bess", "info/vendor-name", "battery.vendor_name", _LITERAL, battery) + add("bess", "info/model", "battery.model", _LITERAL, battery) + add("bess", "info/part-number", "battery.part_number", _LITERAL, battery) + add("bess", "info/serial-number", "battery.serial_number", _LITERAL, battery) + add("bess", "info/firmware-version", "battery.software_version", _LITERAL, battery) + add("bess", "info/nameplate-capacity", "battery.nameplate_capacity_kwh", _IDENTITY, battery) + add("bess", "status/communication-state", "battery.communication_state", _LITERAL, battery) + table.setdefault(RowKey("bess", "meter/active-power"), {})["battery.power_w"] = Transform( + _IDENTITY, + f"{battery}: discharge-positive, and the wire is too from release build " + f"{BESS_METER_DISCHARGE_POSITIVE_FROM_BUILD}; charge-positive before it, so negated", + earlier=(BESS_METER_DISCHARGE_POSITIVE_FROM_BUILD, _NEGATE), + ) + + # --- MID --------------------------------------------------------------- + mid = "devices.build_mid" + add("mid", "info/vendor-name", "mid.vendor_name", _LITERAL, mid) + add("mid", "info/model", "mid.model", _LITERAL, mid) + add("mid", "info/serial-number", "mid.serial_number", _LITERAL, mid) + add("mid", "info/serial-number", "mid.node_id", _LITERAL, f"{mid}: the serial where one is published") + add("mid", "info/firmware-version", "mid.software_version", _LITERAL, mid) + add("mid", "info/hardware-version", "mid.hardware_version", _LITERAL, mid) + add("mid", "grid/islanding-state", "mid.islanding_state", _LITERAL, mid) + add("mid", "grid/islanding-state", "panel.grid_state", _LITERAL, "panel.PanelFields: islanding-state, not grid-state") + add( + "mid", + "grid/islanding-state", + ANY_LEAF, + _DERIVED, + "panel.resolve_islanding_state, panel.resolve_dsm_state and panel.resolve_run_config", + ) + add("mid", "grid/grid-state", "mid.grid_state", _LITERAL, mid) + add("mid", "grid/grid-forming-entity", "mid.grid_forming_entity", _LITERAL, mid) + add( + "mid", + "grid/grid-forming-entity", + ANY_LEAF, + _DERIVED, + "panel.resolve_run_config, panel.resolve_dominant_power_source and panel.resolve_grid_forming_device_name", + ) + + # --- PV ---------------------------------------------------------------- + for subject in ("pv", "pv_inverter"): + pv = f"devices.build_pv ({'the lone inverter' if subject == 'pv' else 'each inverter'})" + add("pv", "info/vendor-name", f"{subject}.vendor_name", _LITERAL, pv) + add("pv", "info/model", f"{subject}.model", _LITERAL, pv) + add("pv", "info/serial-number", f"{subject}.serial_number", _LITERAL, pv) + add("pv", "info/firmware-version", f"{subject}.software_version", _LITERAL, pv) + add("pv", "info/nominal-power", f"{subject}.nameplate_capacity_w", _IDENTITY, pv) + + # --- EVSE -------------------------------------------------------------- + evse = "devices.build_evse" + add("evse", "status/status", "evse.status", _LITERAL, evse) + add("evse", "switch/lock-state", "evse.lock_state", _LITERAL, evse) + add("evse", "meter/advertised-current", "evse.advertised_current_a", _IDENTITY, evse) + add("evse", "info/vendor-name", "evse.vendor_name", _LITERAL, evse) + add("evse", "info/model", "evse.model", _LITERAL, evse) + add("evse", "info/part-number", "evse.part_number", _LITERAL, evse) + add("evse", "info/serial-number", "evse.serial_number", _LITERAL, evse) + add( + "evse", "info/serial-number", ANY_LEAF, _DERIVED, "snapshot.harmonised_evse_keys: the charger is keyed by its serial" + ) + add("evse", "info/firmware-version", "evse.software_version", _LITERAL, evse) + return table + + +_DERIVATIONS: Final = ( + Derivation("circuit.circuit_id", "circuits.build_circuit: the circuit's device id"), + Derivation( + "circuit.device_type", + "snapshot.build_snapshot labels a circuit by the PV or EVSE its connection/feeds-device-id names, else 'circuit'", + values=("circuit",), + ), + Derivation( + "circuit.is_never_backup", + "circuits.priority_is_settable: `$settable` on the load-shed/priority declaration, not a value", + ), + Derivation( + "circuit.is_sheddable", + "circuits.build_circuit: not sheddable when the published priority and relay-controllable rule it out together", + values=(False,), + inputs=("load-shed/priority", "switch/relay-controllable"), + ), + Derivation( + "ctl.priority_target", + "adapter.SchemaOneAdapter.set_circuit_priority_target: a topic, offered where the declaration is `$settable`", + ), + Derivation( + "ctl.dps_target", + "adapter.SchemaOneAdapter.set_dominant_power_source_target: a topic, offered where the declaration is `$settable`", + ), + Derivation("battery.present", "devices.build_battery: the tree declares a battery, before any of its values"), + Derivation( + "circuit.measures_outside_panel", + "circuits.declares_spaces: whether the description declares info/spaces, not a value", + ), + Derivation( + "panel.publishes_solar_roles", + "snapshot.build_snapshot: whether any circuit's description declares connection/feeds-role, not a value", + ), + Derivation("pv.device_id", "devices.build_pv: the inverter's device id"), + Derivation("pv.node_id", "devices.pv_inverter_key: the feeding circuit's id, else the inverter's device id"), + Derivation("pv_inverter.device_id", "devices.build_pv: the inverter's device id"), + Derivation("pv_inverter.node_id", "devices.pv_inverter_key: the feeding circuit's id, else the inverter's device id"), +) + +_DROPS: Final = { + DropReason.OFF_SNAPSHOT: ( + "field_metadata._CONSUMED_OFF_SNAPSHOT names the code that reads each of these by a route no snapshot field shows" + ), + DropReason.IDENTITY_OR_TOPOLOGY: ( + "extension.build_extension_properties skips the info and connection nodes " + "(ADOPTION_IDENTITY_NODE and ADOPTION_TOPOLOGY_NODE in span_panel_api.models): identity and topology are never readings" + ), + DropReason.EXTRA_INSTANCE: "snapshot.TreeRoles keeps the first BESS and the first MID; the snapshot holds one of each", +} + +_DEFERRED: Final = { + RowKey("lugs-upstream", "connection/fed-by-device-status"): ( + "devices.connection_status_for reads it only for the battery the upstream lugs name as their source; " + "the status of a link to anything else, such as a panel feeding this one, reaches no snapshot field yet" + ), +} + +MAIN32_RULES: Final = CellRules(transforms=_transforms(), derivations=_DERIVATIONS, drops=_DROPS, deferred=_DEFERRED) diff --git a/tests/adapter_fidelity/test_harness.py b/tests/adapter_fidelity/test_harness.py new file mode 100644 index 0000000..b540161 --- /dev/null +++ b/tests/adapter_fidelity/test_harness.py @@ -0,0 +1,194 @@ +"""The harness fails where the adapter is unfaithful, not only passes where it is faithful. + +A gate that cannot fail proves nothing. So each kind of failure is planted in the +adapter's snapshot, over a small cut of the public r202639 capture, and the +harness must name it: a value with no wire behind it, one behind only unvalued +properties, a wrong sign (on a captured zero too), a derivation that breaks its +rule, and a published property that reaches nothing. +""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +from dataclasses import replace +import json +from pathlib import Path + +from ebus_sdk.homie import DiscoveredDevice +import pytest + +from span_panel_api.models import SpanPanelSnapshot +from span_panel_api_schema_1 import adapter as adapter_module +from span_panel_api_schema_1.const import TYPE_CIRCUIT, TYPE_PV +from span_panel_api_schema_1.snapshot import build_snapshot + +from .harness import FidelityReport, RowKey, run_cell +from .rules import MAIN32_RULES + +CAPTURE = Path(__file__).parent.parent / "fixtures" / "main32_r202639-tree-v1.json" + +Plant = Callable[[SpanPanelSnapshot], SpanPanelSnapshot] + + +def _mapping(value: object) -> dict[str, object]: + if not isinstance(value, Mapping): + raise TypeError(f"expected a JSON object, got {value!r}") + return {str(key): item for key, item in value.items()} + + +def _power(record: Mapping[str, object]) -> float | None: + raw = _mapping(record.get("numeric_properties", {})).get("meter/active-power") + return float(raw) if isinstance(raw, str) else None + + +@pytest.fixture +def small_capture(tmp_path: Path) -> Path: + """The capture cut to the panel, its lugs, its battery and MID, and one circuit drawing power. + + A circuit at a non-zero reading, so that a wrong sign shows in the captured + value itself; the battery reads 0 W, so a wrong sign there shows only in the + values the perturbation publishes. + """ + tree = _mapping(json.loads(CAPTURE.read_text(encoding="utf-8"))) + devices = _mapping(tree["devices"]) + circuits = sorted( + device_id + for device_id, entry in devices.items() + if _mapping(_mapping(entry)["description"]).get("type") == TYPE_CIRCUIT + ) + drawing = next(device_id for device_id in circuits if _power(_mapping(devices[device_id])) not in (None, 0.0)) + dropped = {device_id for device_id in circuits if device_id != drawing} | { + device_id for device_id, entry in devices.items() if _mapping(_mapping(entry)["description"]).get("type") == TYPE_PV + } + kept: dict[str, object] = {} + for device_id, entry in devices.items(): + if device_id in dropped: + continue + record = _mapping(entry) + description = _mapping(record["description"]) + children = description.get("children") + if isinstance(children, list): + description["children"] = [child for child in children if child not in dropped] + kept[device_id] = {**record, "description": description} + path = tmp_path / "small-tree-v1.json" + path.write_text(json.dumps({**tree, "devices": kept}), encoding="utf-8") + return path + + +@pytest.fixture +def plant(monkeypatch: pytest.MonkeyPatch) -> Callable[[Plant], None]: + """Change every snapshot the adapter builds.""" + + def install(change: Plant) -> None: + def planted(panel: DiscoveredDevice, children: list[DiscoveredDevice]) -> SpanPanelSnapshot: + return change(build_snapshot(panel, children)) + + monkeypatch.setattr(adapter_module, "build_snapshot", planted) + + return install + + +def _clean(report: FidelityReport) -> bool: + return not (report.silently_dropped or report.fabricated or report.mismatched) + + +def test_the_unchanged_adapter_passes(small_capture: Path) -> None: + """The baseline the planted failures are measured against.""" + assert _clean(run_cell(small_capture, MAIN32_RULES)) + + +def test_a_constant_with_no_wire_source_is_fabricated(small_capture: Path, plant: Callable[[Plant], None]) -> None: + plant(lambda snapshot: replace(snapshot, uptime_s=0)) + report = run_cell(small_capture, MAIN32_RULES) + assert report.fabricated == ("panel.uptime_s",) + assert report.unowned_leaves() == {"panel.uptime_s"} + + +def test_a_value_behind_only_unvalued_properties_is_fabricated(small_capture: Path, plant: Callable[[Plant], None]) -> None: + """The capture declares the MID's model and never values it.""" + + def model_or_placeholder(snapshot: SpanPanelSnapshot) -> SpanPanelSnapshot: + assert snapshot.mid is not None + return replace(snapshot, mid=replace(snapshot.mid, model=snapshot.mid.model or "placeholder")) + + plant(model_or_placeholder) + report = run_cell(small_capture, MAIN32_RULES) + assert report.fabricated == ("mid.model",) + assert report.failing_rows() == {RowKey("mid", "info/model")} + assert report.unowned_leaves() == frozenset() + + +def test_a_wrong_sign_is_mismatched(small_capture: Path, plant: Callable[[Plant], None]) -> None: + """Circuit power left in the enclosure frame, where a load reads negative. + + Flipped as `0.0 - x`, which never yields -0.0, so the sign itself is what fails. + """ + + def unnegated(snapshot: SpanPanelSnapshot) -> SpanPanelSnapshot: + circuits = { + circuit_id: replace( + circuit, instant_power_w=None if circuit.instant_power_w is None else 0.0 - circuit.instant_power_w + ) + for circuit_id, circuit in snapshot.circuits.items() + } + return replace(snapshot, circuits=circuits) + + plant(unnegated) + report = run_cell(small_capture, MAIN32_RULES) + assert [leaf.rsplit(".", 1)[1] for leaf in report.mismatched] == ["instant_power_w"] + assert report.failing_rows() == {RowKey("circuit", "meter/active-power")} + + +def test_a_wrong_sign_on_a_reading_captured_at_zero_is_mismatched( + small_capture: Path, plant: Callable[[Plant], None] +) -> None: + """The battery's frame on r202639, where the capture's battery reads 0 W. + + A captured zero reads the same under either sign, so only the values the + perturbation publishes can show the frame is wrong. + """ + plant( + lambda snapshot: replace( + snapshot, + battery=replace( + snapshot.battery, + power_w=None if snapshot.battery.power_w is None else 0.0 - snapshot.battery.power_w, + ), + ) + ) + report = run_cell(small_capture, MAIN32_RULES) + assert report.mismatched == ("battery.power_w",) + assert report.failing_rows() == {RowKey("bess", "meter/active-power")} + + +def test_a_derivation_that_breaks_its_rule_is_mismatched(small_capture: Path, plant: Callable[[Plant], None]) -> None: + """A breaker read as 240 V exactly when it has fewer than two poles.""" + plant( + lambda snapshot: replace( + snapshot, + circuits={ + circuit_id: replace(circuit, is_240v=not circuit.is_240v) + for circuit_id, circuit in snapshot.circuits.items() + }, + ) + ) + report = run_cell(small_capture, MAIN32_RULES) + assert [leaf.rsplit(".", 1)[1] for leaf in report.mismatched] == ["is_240v"] + assert report.failing_rows() == {RowKey("circuit", "breaker/poles")} + + +def test_a_published_property_that_reaches_nothing_is_silently_dropped( + small_capture: Path, plant: Callable[[Plant], None] +) -> None: + plant(lambda snapshot: replace(snapshot, battery=replace(snapshot.battery, vendor_name=None))) + report = run_cell(small_capture, MAIN32_RULES) + assert report.silently_dropped == (RowKey("bess", "info/vendor-name"),) + assert report.deferred == () + + +def test_a_deferred_row_is_reported_apart_from_the_silent_drops(small_capture: Path, plant: Callable[[Plant], None]) -> None: + plant(lambda snapshot: replace(snapshot, battery=replace(snapshot.battery, vendor_name=None))) + key = RowKey("bess", "info/vendor-name") + report = run_cell(small_capture, replace(MAIN32_RULES, deferred={key: "planted for this test"})) + assert report.silently_dropped == () + assert report.deferred == (key,) diff --git a/tests/adapter_fidelity/test_main32_fidelity.py b/tests/adapter_fidelity/test_main32_fidelity.py new file mode 100644 index 0000000..7302690 --- /dev/null +++ b/tests/adapter_fidelity/test_main32_fidelity.py @@ -0,0 +1,181 @@ +"""Every property the public MAIN 32 captures declare reaches the snapshot, faithfully. + +Each capture is replayed through the real adapter; each declared property is +perturbed to see which snapshot leaves it moves. A property that moves nothing and +is not dropped on purpose by code that says so is silently lost. A value with no +wire behind it, or behind only unvalued properties, is fabricated. + +Two cells: the MAIN 32 capture on spanos3/r202639/03, and the MAIN 32 capture on +spanos3/r202633/02, both published with the eBus panel simulator. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from functools import cache +import hashlib +from pathlib import Path + +import pytest + +from span_panel_api_schema_1.const import DEVICE_TYPE_PREFIX, NODE_CONNECTION, NODE_METER, TYPE_PANEL +from span_panel_api_schema_1.field_metadata import ( + _DOWNSTREAM_LUGS_FIELDS, + _PROPERTY_FIELD_MAP, + _UPSTREAM_LUGS_CONNECTION_FIELDS, + _UPSTREAM_LUGS_FIELDS, +) + +from .harness import DropReason, FidelityReport, RowKey, run_cell +from .rules import MAIN32_RULES + +FIXTURES = Path(__file__).parent.parent / "fixtures" + + +@dataclass(frozen=True, slots=True) +class _Cell: + capture: Path + sha256: str + """Of the file as published at the eBus panel simulator's v0.9.0 tag.""" + deferred: tuple[RowKey, ...] + """The rows this capture values and the library drops today, each deferred in the rules with its reason.""" + dropped_with_a_value: Mapping[RowKey, str] + """The valued rows a drop rule covers here, each reviewed, with why dropping it is right.""" + + +_PEER_TYPE = ( + "the declared type of the device the record names, which the library reads from that device's own " + "$description when it sorts the tree" +) +_DISPATCH = "chooses which adapter parses the tree, so it is consumed before any snapshot exists" + +CELLS = { + "main32_r202639": _Cell( + capture=FIXTURES / "main32_r202639-tree-v1.json", + sha256="bf16d8ce21c2b685496700310286198664c6be1a62ea0fe12b1f15144ebfc086", + deferred=(), + dropped_with_a_value={ + RowKey("circuit", "connection/feeds-device-type"): _PEER_TYPE, + RowKey("lugs-upstream", "connection/fed-by-device-type"): _PEER_TYPE, + RowKey("panel", "info/data-model-version"): _DISPATCH, + }, + ), + "main32_r202633": _Cell( + capture=FIXTURES / "main32-tree-v1.json", + sha256="0e92bbdca40f7d8d471e8edbfe2b49f986f2995512025bc67ffb4fcf20f7dd30", + # The upstream lugs here are fed by another panel, and report that link OK. + deferred=(RowKey("lugs-upstream", "connection/fed-by-device-status"),), + dropped_with_a_value={ + RowKey("lugs-upstream", "connection/fed-by-device-type"): _PEER_TYPE, + RowKey("panel", "info/data-model-version"): _DISPATCH, + }, + ), +} + + +_UPSTREAM_NOTICES = { + "distribution-enclosure-simulator.LICENSE": "6174860cd3289ce0d1a39a186e9bbf907504dbac4ef0e00ab0d490d4a0fc2501", +} +"""Upstream files in `FIXTURES` copied beside the captures, byte for byte from the same tag, and the SHA-256 of each.""" + + +@cache +def _report(cell: str) -> FidelityReport: + return run_cell(CELLS[cell].capture, MAIN32_RULES) + + +def _why(report: FidelityReport, leaves: tuple[str, ...]) -> str: + return "\n".join(f"{leaf}: {report.notes[leaf]}" for leaf in leaves) + + +@pytest.mark.parametrize("cell", sorted(CELLS)) +def test_no_property_is_silently_dropped(cell: str) -> None: + assert _report(cell).silently_dropped == () + + +@pytest.mark.parametrize("cell", sorted(CELLS)) +def test_only_the_recorded_rows_are_deferred(cell: str) -> None: + """A deferral that no longer holds fails as surely as a new silent drop.""" + assert _report(cell).deferred == CELLS[cell].deferred + + +@pytest.mark.parametrize("cell", sorted(CELLS)) +def test_only_the_reviewed_rows_are_dropped_with_a_value(cell: str) -> None: + """A drop rule covers a class of property; a valued one it newly covers waits for review here.""" + assert _report(cell).dropped_with_a_value == tuple(sorted(CELLS[cell].dropped_with_a_value)) + + +@pytest.mark.parametrize("cell", sorted(CELLS)) +def test_no_value_is_fabricated(cell: str) -> None: + report = _report(cell) + assert report.fabricated == (), _why(report, report.fabricated) + + +@pytest.mark.parametrize("cell", sorted(CELLS)) +def test_every_mapped_value_matches_its_documented_transform(cell: str) -> None: + report = _report(cell) + assert report.mismatched == (), _why(report, report.mismatched) + + +@pytest.mark.parametrize("cell", sorted(CELLS)) +def test_the_matrix_covers_every_declared_property(cell: str) -> None: + report = _report(cell) + assert sum(1 for fates in report.rows.values() if fates) == len(report.rows) + assert report.declared_property_instances > 0 + assert sum(report.instance_fates.values()) == report.declared_property_instances + + +@pytest.mark.parametrize("cell", sorted(CELLS)) +def test_no_leaf_is_fabricated_outside_any_row(cell: str) -> None: + assert _report(cell).unowned_leaves() == frozenset() + + +@pytest.mark.parametrize("cell", sorted(CELLS)) +def test_no_device_is_named_by_its_own_id(cell: str) -> None: + """While a device forms the grid, the name shown for it is never its device id. + + Both captures publish each non-root device's `$description.name` as its id. + """ + names = _report(cell).grid_forming_names + assert names + assert sorted(device_id for device_id, name in names.items() if name == device_id) == [] + + +@pytest.mark.parametrize("cell", sorted(CELLS)) +def test_the_capture_is_the_published_one(cell: str) -> None: + assert hashlib.sha256(CELLS[cell].capture.read_bytes()).hexdigest() == CELLS[cell].sha256 + + +@pytest.mark.parametrize("notice", sorted(_UPSTREAM_NOTICES)) +def test_the_upstream_license_is_the_published_one(notice: str) -> None: + """The captures are distributed under the license they were published with, unedited.""" + assert hashlib.sha256((FIXTURES / notice).read_bytes()).hexdigest() == _UPSTREAM_NOTICES[notice] + + +def test_every_intentional_drop_and_derivation_cites_code() -> None: + assert set(MAIN32_RULES.drops) == set(DropReason) + assert all(reason.strip() for reason in MAIN32_RULES.justifications.values()) + + +def _role(device_type: str) -> str: + return "panel" if device_type == TYPE_PANEL else device_type.removeprefix(DEVICE_TYPE_PREFIX) + + +def test_every_mapped_field_has_a_documented_transform() -> None: + """A field the library starts mapping cannot reach a snapshot unchecked.""" + mapped = {(RowKey(_role(device_type), f"{node}/{prop}"), path) for device_type, node, prop, path in _PROPERTY_FIELD_MAP} + for role, node, lugs_fields in ( + ("lugs-upstream", NODE_METER, _UPSTREAM_LUGS_FIELDS), + ("lugs-downstream", NODE_METER, _DOWNSTREAM_LUGS_FIELDS), + ("lugs-upstream", NODE_CONNECTION, _UPSTREAM_LUGS_CONNECTION_FIELDS), + ): + mapped |= {(RowKey(role, f"{node}/{prop}"), path) for prop, path in lugs_fields} + missing = sorted((key, path) for key, path in mapped if path not in MAIN32_RULES.transforms.get(key, {})) + assert missing == [] + + +def test_every_derivation_is_needed() -> None: + """A derivation no leaf needs is a standing excuse for the next fabrication.""" + used = frozenset().union(*(_report(cell).derivations_used for cell in CELLS)) + assert {derivation.family for derivation in MAIN32_RULES.derivations} == used diff --git a/tests/fixtures/distribution-enclosure-simulator.LICENSE b/tests/fixtures/distribution-enclosure-simulator.LICENSE new file mode 100644 index 0000000..1f40454 --- /dev/null +++ b/tests/fixtures/distribution-enclosure-simulator.LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Bill Flood, Clark Communications Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/fixtures/main32-tree-v1.README.md b/tests/fixtures/main32-tree-v1.README.md new file mode 100644 index 0000000..6987ca3 --- /dev/null +++ b/tests/fixtures/main32-tree-v1.README.md @@ -0,0 +1,9 @@ +# main32-tree-v1.json + +The device tree a MAIN 32 panel publishes on firmware `spanos3/r202633/02`, as a `tree-v1` snapshot, with its device ids, serial numbers and postal code masked and its circuit names made generic. It declares `status/wifi-ssid` and publishes no value for +it. + +It is byte-identical to `tests/fixtures/main32-tree-v1.json` in [electrification-bus/distribution-enclosure-simulator](https://github.com/electrification-bus/distribution-enclosure-simulator) at tag `v0.9.0` (commit +`2dbddf7c507776a08e03a64825454972696d23df`), and is distributed under that repository's MIT license. `distribution-enclosure-simulator.LICENSE` beside it is that repository's `LICENSE` at the same tag, copied byte for byte. + +It is never regenerated here. `tests/adapter_fidelity/test_main32_fidelity.py` pins the SHA-256 of the capture and of the license, and replays the capture through the schema-1 adapter. diff --git a/tests/fixtures/main32-tree-v1.json b/tests/fixtures/main32-tree-v1.json new file mode 100644 index 0000000..baaa1d5 --- /dev/null +++ b/tests/fixtures/main32-tree-v1.json @@ -0,0 +1,3748 @@ +{ + "devices": { + "fbaae45daa7c145806eb9c5692fec660": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 8", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662261 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "26593.6", + "meter/imported-energy": "619.4" + }, + "properties": { + "info/name": "Circuit 8", + "info/spaces": "8", + "load-shed/priority": "SOC_THRESHOLD", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "afd57a42ebec7cf6a5083315127698ec": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 15", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662506 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "46113.1", + "meter/imported-energy": "345.6" + }, + "properties": { + "info/name": "Circuit 15", + "info/spaces": "16", + "load-shed/priority": "SOC_THRESHOLD", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "36afaccafd19b2bfbcb6fcb5d6089399": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 2", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662296 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "-15.5", + "meter/current": "0.2", + "meter/exported-energy": "973252.4", + "meter/imported-energy": "0.5" + }, + "properties": { + "info/name": "Circuit 2", + "info/spaces": "2", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "88416e5306e0a2d1e528908967f1f9fa": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 14", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662354 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "-3.4", + "meter/current": "0.1", + "meter/exported-energy": "349678.7", + "meter/imported-energy": "705.5" + }, + "properties": { + "info/name": "Circuit 14", + "info/spaces": "14", + "load-shed/priority": "SOC_THRESHOLD", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "45ac025e77f416de0113e7ec0514925d": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 11", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662412 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "15", + "meter/active-power": "-4.5", + "meter/current": "0.1", + "meter/exported-energy": "425794.3", + "meter/imported-energy": "0.0" + }, + "properties": { + "info/name": "Circuit 11", + "info/spaces": "11", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "e4f6668d42f85d5a0f3300c2a6747290": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 17", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662651 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "-320.3", + "meter/current": "3.6", + "meter/exported-energy": "5762848.5", + "meter/imported-energy": "320.5" + }, + "properties": { + "info/name": "Circuit 17", + "info/spaces": "20", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "f45b824f00dc3829dd2ed6609d6cf17b": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 16", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662678 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "-3.4", + "meter/current": "0.1", + "meter/exported-energy": "51753.1", + "meter/imported-energy": "201.4" + }, + "properties": { + "info/name": "Circuit 16", + "info/spaces": "18", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "1ac7fbc901ecb9d0c785dd3234b21a3c": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 10", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662534 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "-3.8", + "meter/current": "0.1", + "meter/exported-energy": "539141.3", + "meter/imported-energy": "182.0" + }, + "properties": { + "info/name": "Circuit 10", + "info/spaces": "10", + "load-shed/priority": "SOC_THRESHOLD", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "1bc992eaeb96482b1d019edecfcc18c2": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 5", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662233 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "15", + "meter/active-power": "5.1", + "meter/current": "0.1", + "meter/exported-energy": "74400.1", + "meter/imported-energy": "31534.6" + }, + "properties": { + "info/name": "Circuit 5", + "info/spaces": "5", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "9af59a336ebe2cd5c5ae5ff3d8424821": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 3", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662563 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "15", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "43396.0", + "meter/imported-energy": "2.8" + }, + "properties": { + "info/name": "Circuit 3", + "info/spaces": "3", + "load-shed/priority": "OFF_GRID", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "420ee28ea3d7362dad8d341cc93e50b3": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 9", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662733 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "15", + "meter/active-power": "-101.6", + "meter/current": "0.9", + "meter/exported-energy": "274309.6", + "meter/imported-energy": "42.5" + }, + "properties": { + "info/name": "Circuit 9", + "info/spaces": "9", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "76321a44f69235b4702f74baf0d10aea": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 4", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662443 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "36111.2", + "meter/imported-energy": "176.1" + }, + "properties": { + "info/name": "Circuit 4", + "info/spaces": "4", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "2e591e0d0cdc0e8bdbfdccbfc86d14ca": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 18", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662383 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "-10.6", + "meter/current": "0.2", + "meter/exported-energy": "723598.1", + "meter/imported-energy": "1008.9" + }, + "properties": { + "info/name": "Circuit 18", + "info/spaces": "22", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "e437d9b59344dd03bc2775fb238a5692": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 13", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662325 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "20", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "62960.0", + "meter/imported-energy": "28436.1" + }, + "properties": { + "info/name": "Circuit 13", + "info/spaces": "13,15", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "26ae0676844ed69346cccae68c2d516a": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 19", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662592 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "3440.7", + "meter/imported-energy": "2203.9" + }, + "properties": { + "info/name": "Circuit 19", + "info/spaces": "24", + "load-shed/priority": "SOC_THRESHOLD", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "367c30fcffa7ac59fe428d7c426bd19d": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 1", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662708 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "15", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "14525.6", + "meter/imported-energy": "50550.5" + }, + "properties": { + "info/name": "Circuit 1", + "info/spaces": "1", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "d1cad9b5b2be373605cc179c30a74dc3": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 7", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662622 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "15", + "meter/active-power": "-9.2", + "meter/current": "0.1", + "meter/exported-energy": "178558.9", + "meter/imported-energy": "66.5" + }, + "properties": { + "info/name": "Circuit 7", + "info/spaces": "7", + "load-shed/priority": "OFF_GRID", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "15adf6fa36ada1dba2184e9bcaa7fa44": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 12", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662773 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "15", + "meter/active-power": "-73.3", + "meter/current": "0.7", + "meter/exported-energy": "1065194.1", + "meter/imported-energy": "205.6" + }, + "properties": { + "info/name": "Circuit 12", + "info/spaces": "12", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "2835afc8a591cc382c4972a3538b824f": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "Circuit 6", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.circuit", + "version": 1791071662468 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "-5.7", + "meter/current": "0.1", + "meter/exported-energy": "992065.0", + "meter/imported-energy": "309.1" + }, + "properties": { + "info/name": "Circuit 6", + "info/spaces": "6", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "nt-0000-panel": { + "description": { + "children": [ + "nt-0000-panel-lugs-up", + "nt-0000-panel-lugs-dn", + "nt-0000-panel-serial0001", + "nt-0000-panel-iq7plus-72-x-acm-us-y", + "1bc992eaeb96482b1d019edecfcc18c2", + "fbaae45daa7c145806eb9c5692fec660", + "36afaccafd19b2bfbcb6fcb5d6089399", + "e437d9b59344dd03bc2775fb238a5692", + "88416e5306e0a2d1e528908967f1f9fa", + "2e591e0d0cdc0e8bdbfdccbfc86d14ca", + "45ac025e77f416de0113e7ec0514925d", + "76321a44f69235b4702f74baf0d10aea", + "2835afc8a591cc382c4972a3538b824f", + "afd57a42ebec7cf6a5083315127698ec", + "1ac7fbc901ecb9d0c785dd3234b21a3c", + "9af59a336ebe2cd5c5ae5ff3d8424821", + "26ae0676844ed69346cccae68c2d516a", + "d1cad9b5b2be373605cc179c30a74dc3", + "e4f6668d42f85d5a0f3300c2a6747290", + "f45b824f00dc3829dd2ed6609d6cf17b", + "367c30fcffa7ac59fe428d7c426bd19d", + "420ee28ea3d7362dad8d341cc93e50b3", + "15adf6fa36ada1dba2184e9bcaa7fa44" + ], + "extensions": [], + "homie": "5.0", + "name": "SPAN Panel eBus Adapter", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "rating": { + "datatype": "integer", + "name": "Main breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "door": { + "name": "door", + "properties": { + "state": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Door state" + } + }, + "type": "energy.ebus.capability.door" + }, + "info": { + "name": "info", + "properties": { + "data-model-version": { + "datatype": "string", + "name": "eBus data-model version (parent/child schema discriminator)" + }, + "firmware-version": { + "datatype": "string", + "name": "Firmware version" + }, + "hardware-version": { + "datatype": "string", + "name": "Hardware version" + }, + "model": { + "datatype": "enum", + "format": "MAIN_16,MLO_24,MAIN_32,MAIN_40,MLO_48", + "name": "Model" + }, + "serial-number": { + "datatype": "string", + "name": "Serial number" + }, + "vendor-name": { + "datatype": "string", + "name": "Vendor name" + } + }, + "type": "energy.ebus.capability.info" + }, + "meter": { + "name": "meter", + "properties": { + "voltage-a": { + "datatype": "float", + "name": "L1 voltage", + "unit": "V" + }, + "voltage-b": { + "datatype": "float", + "name": "L2 voltage", + "unit": "V" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "active": { + "datatype": "boolean", + "name": "PCS system actively controlling one (or more) loads" + }, + "binding-constraint": { + "datatype": "enum", + "format": "FSR,DOE,VOLTAGE,OFF_GRID,REQUESTED,OPERATOR,NONE,UNKNOWN", + "name": "Which constraint class currently sets the import limit" + }, + "enabled": { + "datatype": "boolean", + "name": "PCS system enabled" + }, + "feed-import-limit": { + "datatype": "float", + "name": "Limit of maximum power feeding the distribution enclosure", + "unit": "A" + }, + "feed-import-limit-active": { + "datatype": "boolean", + "name": "Is feed-import-limit currently being enforced?" + }, + "feed-import-limit-enablement": { + "datatype": "enum", + "format": "UNSPECIFIED,UNCONFIGURED,DISABLED,ENABLED", + "name": "Enablement status of the feed-import-limit" + }, + "import-limit": { + "datatype": "float", + "name": "The power import limit currently being managed to", + "unit": "A" + }, + "off-grid-import-limit": { + "datatype": "float", + "name": "Off-Grid limit maximum import power", + "unit": "A" + }, + "off-grid-import-limit-active": { + "datatype": "boolean", + "name": "Is off-grid-import-limit currently being enforced?" + }, + "off-grid-import-limit-enablement": { + "datatype": "enum", + "format": "UNSPECIFIED,UNCONFIGURED,DISABLED,ENABLED", + "name": "Enablement status of the off-grid-import-limit" + }, + "operator-import-limit": { + "datatype": "float", + "name": "Operator-imposed maximum import limit", + "unit": "A" + }, + "operator-import-limit-active": { + "datatype": "boolean", + "name": "Is operator-import-limit currently being enforced?" + }, + "operator-import-limit-enablement": { + "datatype": "enum", + "format": "UNSPECIFIED,UNCONFIGURED,DISABLED,ENABLED", + "name": "Enablement status of the operator-import-limit" + }, + "requested-import-limit": { + "datatype": "float", + "name": "Requested limit maximum import power", + "unit": "A" + }, + "requested-import-limit-active": { + "datatype": "boolean", + "name": "Is requested-import-limit currently being enforced?" + }, + "requested-import-limit-enablement": { + "datatype": "enum", + "format": "UNSPECIFIED,UNCONFIGURED,DISABLED,ENABLED", + "name": "Enablement status of the requested-import-limit" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "power-flows": { + "name": "power-flows", + "properties": { + "battery": { + "datatype": "float", + "name": "Battery/BESS power flow", + "unit": "W" + }, + "grid": { + "datatype": "float", + "name": "Grid power flow", + "unit": "W" + }, + "pv": { + "datatype": "float", + "name": "PV power flow", + "unit": "W" + }, + "site": { + "datatype": "float", + "name": "Site power flow", + "unit": "W" + } + }, + "type": "energy.ebus.capability.power-flows" + }, + "shed": { + "name": "shed", + "properties": { + "asserted-islanding-state": { + "datatype": "enum", + "format": "NONE,ON_GRID,OFF_GRID", + "name": "Consumer-asserted islanding-state (grid-state override during MID/BESS comm-loss)", + "settable": true + }, + "policy": { + "datatype": "json", + "format": "{\"$id\":\"soc-priority.v1\",\"type\":\"object\",\"required\":[\"algorithm\",\"parameters\"],\"additionalProperties\":false,\"properties\":{\"algorithm\":{\"const\":\"soc-priority.v1\"},\"parameters\":{\"type\":\"object\",\"required\":[\"soc-threshold-shed\",\"soc-threshold-release\"],\"additionalProperties\":false,\"properties\":{\"soc-threshold-shed\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":100,\"description\":\"SoC percent below which SOC_THRESHOLD circuits shed\"},\"soc-threshold-release\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":100,\"description\":\"SoC percent above which shed SOC_THRESHOLD circuits restore\"}}}}}", + "name": "Shed policy (algorithm and parameters)" + } + }, + "type": "energy.ebus.capability.shed" + }, + "shed-forecast": { + "name": "shed-forecast", + "properties": { + "confidence": { + "datatype": "enum", + "format": "LOW,MEDIUM,HIGH", + "name": "Confidence of the shed-forecast estimate" + }, + "full-charge-time-to-priority-shed": { + "datatype": "integer", + "name": "Estimated time to next priority shed assuming BESS starts at full charge", + "unit": "min" + }, + "full-charge-total-time-remaining": { + "datatype": "integer", + "name": "Estimated total time assuming BESS starts at full charge", + "unit": "min" + }, + "time-to-priority-shed": { + "datatype": "integer", + "name": "Estimated time before the next priority tier is shed", + "unit": "min" + }, + "total-time-remaining": { + "datatype": "integer", + "name": "Estimated total time before all sheddable circuits are shed (off-grid runtime)", + "unit": "min" + } + }, + "type": "energy.ebus.capability.shed-forecast" + }, + "status": { + "name": "status", + "properties": { + "cloud-connection": { + "datatype": "enum", + "format": "UNKNOWN,UNCONNECTED,CONNECTED", + "name": "Device connected to vendor cloud?" + }, + "ethernet": { + "datatype": "boolean", + "name": "Is Ethernet network interface operational?" + }, + "postal-code": { + "datatype": "string", + "name": "Postal (Zip) code" + }, + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Main relay" + }, + "time-zone": { + "datatype": "string", + "name": "Time zone" + }, + "wifi": { + "datatype": "boolean", + "name": "Is Wi-Fi network interface operational?" + }, + "wifi-ssid": { + "datatype": "string", + "name": "SSID to which Wi-Fi network interface is connected" + } + }, + "type": "energy.ebus.capability.status" + } + }, + "type": "energy.ebus.device.distribution-enclosure", + "version": 1791071662785 + }, + "numeric_properties": { + "breaker/rating": "200", + "meter/voltage-a": "119.3", + "meter/voltage-b": "120.4", + "pcs/feed-import-limit": "160.0", + "pcs/import-limit": "160.0", + "pcs/requested-import-limit": "200.0", + "power-flows/battery": "0", + "power-flows/grid": "-2655", + "power-flows/pv": "0", + "power-flows/site": "2655.0", + "shed-forecast/full-charge-time-to-priority-shed": "3038", + "shed-forecast/full-charge-total-time-remaining": "4320", + "shed-forecast/time-to-priority-shed": "3037", + "shed-forecast/total-time-remaining": "4320" + }, + "properties": { + "door/state": "UNKNOWN", + "info/data-model-version": "1.0", + "info/firmware-version": "spanos3/r202633/02", + "info/hardware-version": "1.2", + "info/model": "MAIN_32", + "info/serial-number": "nt-0000-panel", + "info/vendor-name": "SPAN", + "pcs/active": "false", + "pcs/binding-constraint": "FSR", + "pcs/enabled": "false", + "pcs/feed-import-limit-active": "true", + "pcs/feed-import-limit-enablement": "ENABLED", + "pcs/requested-import-limit-active": "false", + "pcs/requested-import-limit-enablement": "UNCONFIGURED", + "shed-forecast/confidence": "HIGH", + "shed/asserted-islanding-state": "NONE", + "shed/policy": "{\"algorithm\": \"soc-priority.v1\", \"parameters\": {\"soc-threshold-shed\": 49, \"soc-threshold-release\": 51}}", + "status/cloud-connection": "UNKNOWN", + "status/ethernet": "true", + "status/postal-code": "00000", + "status/relay": "CLOSED", + "status/time-zone": "America/Los_Angeles", + "status/wifi": "false" + } + }, + "nt-0000-panel-iq7plus-72-x-acm-us-y": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "nt-0000-panel-iq7plus-72-x-acm-us-y", + "nodes": { + "info": { + "name": "info", + "properties": { + "firmware-version": { + "datatype": "string", + "name": "Firmware version" + }, + "model": { + "datatype": "string", + "name": "Model" + }, + "nominal-power": { + "datatype": "float", + "name": "Nominal power", + "unit": "W" + }, + "serial-number": { + "datatype": "string", + "name": "Serial number" + }, + "vendor-name": { + "datatype": "string", + "name": "Vendor name" + } + }, + "type": "energy.ebus.capability.info" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.pv", + "version": 1791071661955 + }, + "numeric_properties": { + "info/nominal-power": "4640" + }, + "properties": { + "info/model": "IQ7PLUS-72-x-ACM-US-y", + "info/vendor-name": "Enphase Energy" + } + }, + "nt-0000-panel-lugs-dn": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "nt-0000-panel-lugs-dn", + "nodes": { + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated up/downstream" + }, + "fed-by-device-id": { + "datatype": "string", + "name": "Homie device-id of the upstream device feeding this lugs" + }, + "fed-by-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the upstream device" + }, + "fed-by-device-type": { + "datatype": "string", + "name": "Homie $type of the upstream device" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this lugs" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "direction": { + "datatype": "enum", + "format": "UPSTREAM,DOWNSTREAM", + "name": "Lugs feed direction: upstream or downstream" + } + }, + "type": "energy.ebus.capability.info" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Active power", + "unit": "W" + }, + "current-a": { + "datatype": "float", + "name": "L1 current", + "unit": "A" + }, + "current-b": { + "datatype": "float", + "name": "L2 current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Exported energy", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Imported energy", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.lugs", + "version": 1791071661426 + }, + "numeric_properties": { + "meter/active-power": "1937.2", + "meter/current-a": "27.4", + "meter/current-b": "4.6", + "meter/exported-energy": "11874656.7", + "meter/imported-energy": "34910219.6" + }, + "properties": { + "info/direction": "DOWNSTREAM" + } + }, + "nt-0000-panel-lugs-up": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "nt-0000-panel-lugs-up", + "nodes": { + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated up/downstream" + }, + "fed-by-device-id": { + "datatype": "string", + "name": "Homie device-id of the upstream device feeding this lugs" + }, + "fed-by-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the upstream device" + }, + "fed-by-device-type": { + "datatype": "string", + "name": "Homie $type of the upstream device" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this lugs" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "direction": { + "datatype": "enum", + "format": "UPSTREAM,DOWNSTREAM", + "name": "Lugs feed direction: upstream or downstream" + } + }, + "type": "energy.ebus.capability.info" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Active power", + "unit": "W" + }, + "current-a": { + "datatype": "float", + "name": "L1 current", + "unit": "A" + }, + "current-b": { + "datatype": "float", + "name": "L2 current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Exported energy", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Imported energy", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.lugs", + "version": 1791071661410 + }, + "numeric_properties": { + "meter/active-power": "2490.9", + "meter/current-a": "27.4", + "meter/current-b": "4.6", + "meter/exported-energy": "3.3", + "meter/imported-energy": "35223342.5" + }, + "properties": { + "connection/fed-by-device-id": "nt-0000-other22", + "connection/fed-by-device-status": "OK", + "connection/fed-by-device-type": "energy.ebus.device.distribution-enclosure", + "info/direction": "UPSTREAM" + } + }, + "nt-0000-panel-serial0001": { + "description": { + "children": [ + "nt-0000-panel-serial0002" + ], + "extensions": [], + "homie": "5.0", + "name": "nt-0000-panel-serial0001", + "nodes": { + "info": { + "name": "info", + "properties": { + "firmware-version": { + "datatype": "string", + "name": "Firmware version" + }, + "model": { + "datatype": "string", + "name": "Model" + }, + "nameplate-capacity": { + "datatype": "float", + "name": "Nameplate capacity", + "unit": "kWh" + }, + "part-number": { + "datatype": "string", + "name": "Part number" + }, + "serial-number": { + "datatype": "string", + "name": "Serial number" + }, + "vendor-name": { + "datatype": "string", + "name": "Vendor name" + } + }, + "type": "energy.ebus.capability.info" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Active power", + "unit": "W" + } + }, + "type": "energy.ebus.capability.meter" + }, + "soc": { + "name": "soc", + "properties": { + "soc": { + "datatype": "float", + "name": "State of charge", + "unit": "%" + }, + "soe": { + "datatype": "float", + "name": "State of energy", + "unit": "kWh" + } + }, + "type": "energy.ebus.capability.soc" + }, + "status": { + "name": "status", + "properties": { + "communication-state": { + "datatype": "enum", + "format": "OK,DEGRADED,LOST,UNKNOWN", + "name": "Communication state" + } + }, + "type": "energy.ebus.capability.status" + } + }, + "parent": "nt-0000-panel", + "root": "nt-0000-panel", + "type": "energy.ebus.device.bess", + "version": 1791071661910 + }, + "numeric_properties": { + "info/nameplate-capacity": "81", + "meter/active-power": "0", + "soc/soc": "100.0", + "soc/soe": "81.0" + }, + "properties": { + "info/firmware-version": "26.26.4 68c2bc82", + "info/model": "Powerwall 2 AC", + "info/part-number": "1232100-00-E", + "info/serial-number": "serial0001", + "info/vendor-name": "Tesla", + "status/communication-state": "OK" + } + }, + "nt-0000-panel-serial0002": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "nt-0000-panel-serial0002", + "nodes": { + "grid": { + "name": "grid", + "properties": { + "grid-forming-entity": { + "datatype": "string", + "name": "Identity of the currently grid-forming entity" + }, + "grid-state": { + "datatype": "enum", + "format": "UP,DOWN,DEGRADED,UNKNOWN", + "name": "Sensed grid condition" + }, + "islanding-state": { + "datatype": "enum", + "format": "ON_GRID,OFF_GRID,UNKNOWN", + "name": "Islanding state of the BESS-integrated grid-forming device" + } + }, + "type": "energy.ebus.capability.grid" + }, + "info": { + "name": "info", + "properties": { + "firmware-version": { + "datatype": "string", + "name": "Firmware version" + }, + "hardware-version": { + "datatype": "string", + "name": "Hardware version" + }, + "model": { + "datatype": "string", + "name": "Model" + }, + "serial-number": { + "datatype": "string", + "name": "Serial number" + }, + "vendor-name": { + "datatype": "string", + "name": "Vendor name" + } + }, + "type": "energy.ebus.capability.info" + } + }, + "parent": "nt-0000-panel-serial0001", + "root": "nt-0000-panel", + "type": "energy.ebus.device.mid", + "version": 1791071661924 + }, + "numeric_properties": {}, + "properties": { + "grid/grid-forming-entity": "GRID", + "grid/grid-state": "UNKNOWN", + "grid/islanding-state": "ON_GRID", + "info/serial-number": "serial0002", + "info/vendor-name": "Tesla" + } + } + }, + "metadata": { + "schema": "tree-v1" + } +} diff --git a/tests/fixtures/main32_r202639-tree-v1.README.md b/tests/fixtures/main32_r202639-tree-v1.README.md new file mode 100644 index 0000000..41d9297 --- /dev/null +++ b/tests/fixtures/main32_r202639-tree-v1.README.md @@ -0,0 +1,8 @@ +# main32_r202639-tree-v1.json + +The device tree a MAIN 32 panel publishes on firmware `spanos3/r202639/03`, as a `tree-v1` snapshot, with its device ids, serial numbers and postal code masked. It declares `status/wifi-ssid` and publishes no value for it. + +It is byte-identical to `tests/fixtures/main32_r202639-tree-v1.json` in [electrification-bus/distribution-enclosure-simulator](https://github.com/electrification-bus/distribution-enclosure-simulator) at tag `v0.9.0` (commit +`2dbddf7c507776a08e03a64825454972696d23df`), and is distributed under that repository's MIT license. `distribution-enclosure-simulator.LICENSE` beside it is that repository's `LICENSE` at the same tag, copied byte for byte. + +It is never regenerated here. `tests/adapter_fidelity/test_main32_fidelity.py` pins the SHA-256 of the capture and of the license, and replays the capture through the schema-1 adapter. diff --git a/tests/fixtures/main32_r202639-tree-v1.json b/tests/fixtures/main32_r202639-tree-v1.json new file mode 100644 index 0000000..abfec95 --- /dev/null +++ b/tests/fixtures/main32_r202639-tree-v1.json @@ -0,0 +1,3292 @@ +{ + "devices": { + "390351d21af25d6409aba2b210928f7f": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "390351d21af25d6409aba2b210928f7f", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310949109 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "30", + "meter/active-power": "-4.0", + "meter/current": "0.1", + "meter/exported-energy": "855286.5", + "meter/imported-energy": "3382.6", + "pcs/priority": "6" + }, + "properties": { + "info/name": "Clothes-Dryer", + "info/spaces": "2,4", + "load-shed/priority": "OFF_GRID", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "cb26f4260adebb7b71de48682618db0a": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "cb26f4260adebb7b71de48682618db0a", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310949816 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "-34.4", + "meter/current": "0.4", + "meter/exported-energy": "4176975.5", + "meter/imported-energy": "4928.9", + "pcs/priority": "13" + }, + "properties": { + "info/name": "Wine Refrigerator", + "info/spaces": "7", + "load-shed/priority": "SOC_THRESHOLD", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "9498a44c930374d2ea1e9508c3def44e": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "9498a44c930374d2ea1e9508c3def44e", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310950286 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "50", + "meter/active-power": "-6.6", + "meter/current": "1.1", + "meter/exported-energy": "411437.5", + "meter/imported-energy": "7127.1", + "pcs/priority": "11" + }, + "properties": { + "info/name": "Cooktop/Stove", + "info/spaces": "18,20", + "load-shed/priority": "SOC_THRESHOLD", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "c00a7e2e5d468da627d084071177d865": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "c00a7e2e5d468da627d084071177d865", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310950120 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "30", + "meter/active-power": "-12.6", + "meter/current": "0.1", + "meter/exported-energy": "872104.5", + "meter/imported-energy": "1431.4", + "pcs/priority": "1" + }, + "properties": { + "info/name": "Studio-HVAC", + "info/spaces": "17,19", + "load-shed/priority": "OFF_GRID", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "704b457f76e9451eb44fbc8287e67f9f": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "704b457f76e9451eb44fbc8287e67f9f", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310949200 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "30", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "175305.8", + "meter/imported-energy": "51850.7", + "pcs/priority": "15" + }, + "properties": { + "info/name": "Future-240VAC-1", + "info/spaces": "21,23", + "load-shed/priority": "OFF_GRID", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "a6f39ecc6a8bf3453b9487dba9581b2c": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "a6f39ecc6a8bf3453b9487dba9581b2c", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310949278 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "50", + "meter/active-power": "-4.4", + "meter/current": "0.5", + "meter/exported-energy": "1362032.0", + "meter/imported-energy": "10343.3", + "pcs/priority": "8" + }, + "properties": { + "info/name": "HVAC-Heat-Pump", + "info/spaces": "10,12", + "load-shed/priority": "OFF_GRID", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "266eb903e50116147fb212879e812cd7": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "266eb903e50116147fb212879e812cd7", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310950226 + }, + "numeric_properties": { + "breaker/poles": "1", + "breaker/rating": "20", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "686928.8", + "meter/imported-energy": "33914.1", + "pcs/priority": "14" + }, + "properties": { + "info/name": "Attic-Back-Outlet", + "info/spaces": "5", + "load-shed/priority": "SOC_THRESHOLD", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "f66769009923287a83be6740bd0bbfbd": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "f66769009923287a83be6740bd0bbfbd", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310949916 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "45", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "28532.9", + "meter/imported-energy": "25667.0", + "pcs/priority": "4" + }, + "properties": { + "info/name": "HVAC-Heating-Coil", + "info/spaces": "6,8", + "load-shed/priority": "OFF_GRID", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "c0161381e50e069b9fd063a0ca1df808": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "c0161381e50e069b9fd063a0ca1df808", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310949627 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "30", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "35474.5", + "meter/imported-energy": "45345.3", + "pcs/priority": "7" + }, + "properties": { + "info/name": "Future-240VAC-2", + "info/spaces": "25,27", + "load-shed/priority": "OFF_GRID", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "31f1d56b0926276e04c84360b1a31a6c": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "31f1d56b0926276e04c84360b1a31a6c", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310949154 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "30", + "meter/active-power": "-67.9", + "meter/current": "0.4", + "meter/exported-energy": "1139553.7", + "meter/imported-energy": "2052.6", + "pcs/priority": "2" + }, + "properties": { + "info/name": "HVAC-Air-Handler", + "info/spaces": "13,15", + "load-shed/priority": "OFF_GRID", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "8a562bd10fb843842670080af2bbb983": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "8a562bd10fb843842670080af2bbb983", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)" + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state" + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310950182 + }, + "numeric_properties": { + "breaker/poles": "2", + "meter/active-power": "1565.7", + "meter/current": "6.5", + "meter/exported-energy": "45779.2", + "meter/imported-energy": "17468812.0" + }, + "properties": { + "connection/feeds-device-id": "nt-9874-s7rxt-iq7plus-72-x-us", + "connection/feeds-device-status": "OK", + "connection/feeds-device-type": "energy.ebus.device.pv", + "info/name": "Commissioned PV System", + "info/spaces": "29,31", + "load-shed/priority": "NEVER", + "pcs/managed": "false", + "switch/relay": "CLOSED", + "switch/relay-controllable": "false", + "switch/relay-requester": "PCS" + } + }, + "aa47c5f799def01ce9ed945a4200b544": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "aa47c5f799def01ce9ed945a4200b544", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310949026 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "50", + "meter/active-power": "-11.6", + "meter/current": "0.1", + "meter/exported-energy": "1084195.1", + "meter/imported-energy": "32973.4", + "pcs/priority": "12" + }, + "properties": { + "info/name": "Ovens", + "info/spaces": "14,16", + "load-shed/priority": "OFF_GRID", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "56def60d3bbd177fc8856cca8713c05a": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "56def60d3bbd177fc8856cca8713c05a", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310949522 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "30", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "18919.9", + "meter/imported-energy": "4080.8", + "pcs/priority": "9" + }, + "properties": { + "info/name": "Shed-240VAC-Outlet", + "info/spaces": "26,28", + "load-shed/priority": "OFF_GRID", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "48c572b0051eaddaf85e1c26ef0f52be": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "48c572b0051eaddaf85e1c26ef0f52be", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310949376 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "20", + "meter/active-power": "-3.8", + "meter/current": "0.2", + "meter/exported-energy": "1555161.9", + "meter/imported-energy": "2068.8", + "pcs/priority": "3" + }, + "properties": { + "info/name": "Espresso-Machine", + "info/spaces": "22,24", + "load-shed/priority": "SOC_THRESHOLD", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "f84ad966ee15cec0c359d151af8157e2": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "f84ad966ee15cec0c359d151af8157e2", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310949703 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "30", + "meter/active-power": "0.0", + "meter/current": "0.1", + "meter/exported-energy": "717908.8", + "meter/imported-energy": "38742.9", + "pcs/priority": "5" + }, + "properties": { + "info/name": "Water-Heater", + "info/spaces": "9,11", + "load-shed/priority": "NEVER", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "caff3023f569495b2a36bf9354a8cd38": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "caff3023f569495b2a36bf9354a8cd38", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "poles": { + "datatype": "integer", + "format": "1:4:1", + "name": "Number of breaker poles" + }, + "rating": { + "datatype": "integer", + "name": "Circuit breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated downstream (e.g. microinverters, packs)" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this circuit" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "name": { + "datatype": "string", + "name": "Circuit name" + }, + "spaces": { + "datatype": "string", + "name": "Physical panel position(s) the circuit occupies" + } + }, + "type": "energy.ebus.capability.info" + }, + "load-shed": { + "name": "load-shed", + "properties": { + "priority": { + "datatype": "enum", + "format": "UNKNOWN,OFF_GRID,SOC_THRESHOLD,NEVER", + "name": "Configured priority of circuit shedding when off-grid (dominant-power-source != GRID)", + "settable": true + } + }, + "type": "energy.ebus.capability.load-shed" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Measured active power", + "unit": "W" + }, + "current": { + "datatype": "float", + "name": "Measured current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Measured energy exported", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Measured energy imported", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "managed": { + "datatype": "boolean", + "name": "Is circuit managed by PCS?" + }, + "priority": { + "datatype": "integer", + "name": "Circuit PCS priority ranking" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "switch": { + "name": "switch", + "properties": { + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Circuit relay state", + "settable": true + }, + "relay-controllable": { + "datatype": "boolean", + "name": "Can the circuit's relay be commanded by the user?" + }, + "relay-requester": { + "datatype": "enum", + "format": "UNKNOWN,NONE,LOAD_SHED,USER,PCS,CONFIGURATION,FAULT", + "name": "Actor requesting the relay state" + } + }, + "type": "energy.ebus.capability.switch" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.circuit", + "version": 1791310950034 + }, + "numeric_properties": { + "breaker/poles": "2", + "breaker/rating": "20", + "meter/active-power": "-3.3", + "meter/current": "0.1", + "meter/exported-energy": "129126.5", + "meter/imported-energy": "11355.5", + "pcs/priority": "10" + }, + "properties": { + "info/name": "Kitchen-Outlets-1 & Outlets-2", + "info/spaces": "1,3", + "load-shed/priority": "NEVER", + "pcs/managed": "true", + "switch/relay": "CLOSED", + "switch/relay-controllable": "true", + "switch/relay-requester": "NONE" + } + }, + "nt-9874-s7rxt": { + "description": { + "children": [ + "nt-9874-s7rxt-lugs-up", + "nt-9874-s7rxt-lugs-dn", + "nt-9874-s7rxt-tg681049356k0x", + "nt-9874-s7rxt-iq7plus-72-x-us", + "aa47c5f799def01ce9ed945a4200b544", + "390351d21af25d6409aba2b210928f7f", + "31f1d56b0926276e04c84360b1a31a6c", + "704b457f76e9451eb44fbc8287e67f9f", + "a6f39ecc6a8bf3453b9487dba9581b2c", + "48c572b0051eaddaf85e1c26ef0f52be", + "56def60d3bbd177fc8856cca8713c05a", + "c0161381e50e069b9fd063a0ca1df808", + "f84ad966ee15cec0c359d151af8157e2", + "cb26f4260adebb7b71de48682618db0a", + "f66769009923287a83be6740bd0bbfbd", + "caff3023f569495b2a36bf9354a8cd38", + "c00a7e2e5d468da627d084071177d865", + "8a562bd10fb843842670080af2bbb983", + "266eb903e50116147fb212879e812cd7", + "9498a44c930374d2ea1e9508c3def44e" + ], + "extensions": [], + "homie": "5.0", + "name": "SPAN Panel eBus Adapter", + "nodes": { + "breaker": { + "name": "breaker", + "properties": { + "rating": { + "datatype": "integer", + "name": "Main breaker rating", + "unit": "A" + } + }, + "type": "energy.ebus.capability.breaker" + }, + "door": { + "name": "door", + "properties": { + "state": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Door state" + } + }, + "type": "energy.ebus.capability.door" + }, + "info": { + "name": "info", + "properties": { + "data-model-version": { + "datatype": "string", + "name": "eBus data-model version (parent/child schema discriminator)" + }, + "firmware-version": { + "datatype": "string", + "name": "Firmware version" + }, + "hardware-version": { + "datatype": "string", + "name": "Hardware version" + }, + "model": { + "datatype": "enum", + "format": "MAIN_16,MLO_24,MAIN_32,MAIN_40,MLO_48", + "name": "Model" + }, + "serial-number": { + "datatype": "string", + "name": "Serial number" + }, + "vendor-name": { + "datatype": "string", + "name": "Vendor name" + } + }, + "type": "energy.ebus.capability.info" + }, + "meter": { + "name": "meter", + "properties": { + "voltage-a": { + "datatype": "float", + "name": "L1 voltage", + "unit": "V" + }, + "voltage-b": { + "datatype": "float", + "name": "L2 voltage", + "unit": "V" + } + }, + "type": "energy.ebus.capability.meter" + }, + "pcs": { + "name": "pcs", + "properties": { + "active": { + "datatype": "boolean", + "name": "PCS system actively controlling one (or more) loads" + }, + "binding-constraint": { + "datatype": "enum", + "format": "FSR,DOE,VOLTAGE,OFF_GRID,REQUESTED,OPERATOR,NONE,UNKNOWN", + "name": "Which constraint class currently sets the import limit" + }, + "enabled": { + "datatype": "boolean", + "name": "PCS system enabled" + }, + "feed-import-limit": { + "datatype": "float", + "name": "Limit of maximum power feeding the distribution enclosure", + "unit": "A" + }, + "feed-import-limit-active": { + "datatype": "boolean", + "name": "Is feed-import-limit currently being enforced?" + }, + "feed-import-limit-enablement": { + "datatype": "enum", + "format": "UNSPECIFIED,UNCONFIGURED,DISABLED,ENABLED", + "name": "Enablement status of the feed-import-limit" + }, + "import-limit": { + "datatype": "float", + "name": "The power import limit currently being managed to", + "unit": "A" + }, + "off-grid-import-limit": { + "datatype": "float", + "name": "Off-Grid limit maximum import power", + "unit": "A" + }, + "off-grid-import-limit-active": { + "datatype": "boolean", + "name": "Is off-grid-import-limit currently being enforced?" + }, + "off-grid-import-limit-enablement": { + "datatype": "enum", + "format": "UNSPECIFIED,UNCONFIGURED,DISABLED,ENABLED", + "name": "Enablement status of the off-grid-import-limit" + }, + "operator-import-limit": { + "datatype": "float", + "name": "Operator-imposed maximum import limit", + "unit": "A" + }, + "operator-import-limit-active": { + "datatype": "boolean", + "name": "Is operator-import-limit currently being enforced?" + }, + "operator-import-limit-enablement": { + "datatype": "enum", + "format": "UNSPECIFIED,UNCONFIGURED,DISABLED,ENABLED", + "name": "Enablement status of the operator-import-limit" + }, + "requested-import-limit": { + "datatype": "float", + "name": "Requested limit maximum import power", + "unit": "A" + }, + "requested-import-limit-active": { + "datatype": "boolean", + "name": "Is requested-import-limit currently being enforced?" + }, + "requested-import-limit-enablement": { + "datatype": "enum", + "format": "UNSPECIFIED,UNCONFIGURED,DISABLED,ENABLED", + "name": "Enablement status of the requested-import-limit" + } + }, + "type": "energy.ebus.capability.pcs" + }, + "power-flows": { + "name": "power-flows", + "properties": { + "battery": { + "datatype": "float", + "name": "Battery/BESS power flow", + "unit": "W" + }, + "grid": { + "datatype": "float", + "name": "Grid power flow", + "unit": "W" + }, + "pv": { + "datatype": "float", + "name": "PV power flow", + "unit": "W" + }, + "site": { + "datatype": "float", + "name": "Site power flow", + "unit": "W" + } + }, + "type": "energy.ebus.capability.power-flows" + }, + "shed": { + "name": "shed", + "properties": { + "asserted-islanding-state": { + "datatype": "enum", + "format": "NONE,ON_GRID,OFF_GRID", + "name": "Consumer-asserted islanding-state (grid-state override during MID/BESS comm-loss)", + "settable": true + }, + "policy": { + "datatype": "json", + "format": "{\"$id\":\"soc-priority.v1\",\"type\":\"object\",\"required\":[\"algorithm\",\"parameters\"],\"additionalProperties\":false,\"properties\":{\"algorithm\":{\"const\":\"soc-priority.v1\"},\"parameters\":{\"type\":\"object\",\"required\":[\"soc-threshold-shed\",\"soc-threshold-release\"],\"additionalProperties\":false,\"properties\":{\"soc-threshold-shed\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":100,\"description\":\"SoC percent below which SOC_THRESHOLD circuits shed\"},\"soc-threshold-release\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":100,\"description\":\"SoC percent above which shed SOC_THRESHOLD circuits restore\"}}}}}", + "name": "Shed policy (algorithm and parameters)" + } + }, + "type": "energy.ebus.capability.shed" + }, + "shed-forecast": { + "name": "shed-forecast", + "properties": { + "confidence": { + "datatype": "enum", + "format": "LOW,MEDIUM,HIGH", + "name": "Confidence of the shed-forecast estimate" + }, + "full-charge-time-to-priority-shed": { + "datatype": "integer", + "name": "Estimated time to next priority shed assuming BESS starts at full charge", + "unit": "min" + }, + "full-charge-total-time-remaining": { + "datatype": "integer", + "name": "Estimated total time assuming BESS starts at full charge", + "unit": "min" + }, + "time-to-priority-shed": { + "datatype": "integer", + "name": "Estimated time before the next priority tier is shed", + "unit": "min" + }, + "total-time-remaining": { + "datatype": "integer", + "name": "Estimated total time before all sheddable circuits are shed (off-grid runtime)", + "unit": "min" + } + }, + "type": "energy.ebus.capability.shed-forecast" + }, + "status": { + "name": "status", + "properties": { + "cloud-connection": { + "datatype": "enum", + "format": "UNKNOWN,UNCONNECTED,CONNECTED", + "name": "Device connected to vendor cloud?" + }, + "ethernet": { + "datatype": "boolean", + "name": "Is Ethernet network interface operational?" + }, + "postal-code": { + "datatype": "string", + "name": "Postal (Zip) code" + }, + "relay": { + "datatype": "enum", + "format": "UNKNOWN,OPEN,CLOSED", + "name": "Main relay" + }, + "time-zone": { + "datatype": "string", + "name": "Time zone" + }, + "wifi": { + "datatype": "boolean", + "name": "Is Wi-Fi network interface operational?" + }, + "wifi-ssid": { + "datatype": "string", + "name": "SSID to which Wi-Fi network interface is connected" + } + }, + "type": "energy.ebus.capability.status" + } + }, + "type": "energy.ebus.device.distribution-enclosure", + "version": 1791310950308 + }, + "numeric_properties": { + "breaker/rating": "200", + "meter/voltage-a": "121.8", + "meter/voltage-b": "122.1", + "pcs/feed-import-limit": "160.0", + "pcs/import-limit": "160.0", + "pcs/operator-import-limit": "0.0", + "pcs/requested-import-limit": "200.0", + "power-flows/battery": "0", + "power-flows/grid": "-1257", + "power-flows/pv": "-1622", + "power-flows/site": "2879.0", + "shed-forecast/full-charge-time-to-priority-shed": "3038", + "shed-forecast/full-charge-total-time-remaining": "4320", + "shed-forecast/time-to-priority-shed": "3037", + "shed-forecast/total-time-remaining": "4320" + }, + "properties": { + "door/state": "UNKNOWN", + "info/data-model-version": "1.0", + "info/firmware-version": "spanos3/r202639/03", + "info/hardware-version": "1.2", + "info/model": "MAIN_32", + "info/serial-number": "nt-9874-s7rxt", + "info/vendor-name": "SPAN", + "pcs/active": "false", + "pcs/binding-constraint": "FSR", + "pcs/enabled": "true", + "pcs/feed-import-limit-active": "true", + "pcs/feed-import-limit-enablement": "ENABLED", + "pcs/operator-import-limit-active": "false", + "pcs/operator-import-limit-enablement": "UNCONFIGURED", + "pcs/requested-import-limit-active": "false", + "pcs/requested-import-limit-enablement": "UNCONFIGURED", + "shed-forecast/confidence": "HIGH", + "shed/asserted-islanding-state": "NONE", + "shed/policy": "{\"algorithm\": \"soc-priority.v1\", \"parameters\": {\"soc-threshold-shed\": 49, \"soc-threshold-release\": 51}}", + "status/cloud-connection": "UNKNOWN", + "status/ethernet": "true", + "status/postal-code": "00000", + "status/relay": "CLOSED", + "status/time-zone": "America/Los_Angeles", + "status/wifi": "false" + } + }, + "nt-9874-s7rxt-iq7plus-72-x-us": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "nt-9874-s7rxt-iq7plus-72-x-us", + "nodes": { + "info": { + "name": "info", + "properties": { + "firmware-version": { + "datatype": "string", + "name": "Firmware version" + }, + "model": { + "datatype": "string", + "name": "Model" + }, + "nominal-power": { + "datatype": "float", + "name": "Nominal power", + "unit": "W" + }, + "serial-number": { + "datatype": "string", + "name": "Serial number" + }, + "vendor-name": { + "datatype": "string", + "name": "Vendor name" + } + }, + "type": "energy.ebus.capability.info" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.pv", + "version": 1791310862583 + }, + "numeric_properties": { + "info/nominal-power": "4640" + }, + "properties": { + "info/model": "IQ7PLUS-72-x-US-&", + "info/vendor-name": "Enphase Energy" + } + }, + "nt-9874-s7rxt-lugs-dn": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "nt-9874-s7rxt-lugs-dn", + "nodes": { + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated up/downstream" + }, + "fed-by-device-id": { + "datatype": "string", + "name": "Homie device-id of the upstream device feeding this lugs" + }, + "fed-by-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the upstream device" + }, + "fed-by-device-type": { + "datatype": "string", + "name": "Homie $type of the upstream device" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this lugs" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "direction": { + "datatype": "enum", + "format": "UPSTREAM,DOWNSTREAM", + "name": "Lugs feed direction: upstream or downstream" + } + }, + "type": "energy.ebus.capability.info" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Active power", + "unit": "W" + }, + "current-a": { + "datatype": "float", + "name": "L1 current", + "unit": "A" + }, + "current-b": { + "datatype": "float", + "name": "L2 current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Exported energy", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Imported energy", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.lugs", + "version": 1791310859527 + }, + "numeric_properties": { + "meter/active-power": "2700.6", + "meter/current-a": "26.6", + "meter/current-b": "5.7", + "meter/exported-energy": "9279931.9", + "meter/imported-energy": "9863069.9" + }, + "properties": { + "info/direction": "DOWNSTREAM" + } + }, + "nt-9874-s7rxt-lugs-up": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "nt-9874-s7rxt-lugs-up", + "nodes": { + "connection": { + "name": "connection", + "properties": { + "count": { + "datatype": "integer", + "name": "Number of physical units aggregated up/downstream" + }, + "fed-by-device-id": { + "datatype": "string", + "name": "Homie device-id of the upstream device feeding this lugs" + }, + "fed-by-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the upstream device" + }, + "fed-by-device-type": { + "datatype": "string", + "name": "Homie $type of the upstream device" + }, + "feeds-device-id": { + "datatype": "string", + "name": "Homie device-id of the downstream device fed by this lugs" + }, + "feeds-device-status": { + "datatype": "enum", + "format": "OK,LOST,DEGRADED", + "name": "Panel's view of comm health to the downstream device" + }, + "feeds-device-type": { + "datatype": "string", + "name": "Homie $type of the downstream device" + } + }, + "type": "energy.ebus.capability.connection" + }, + "info": { + "name": "info", + "properties": { + "direction": { + "datatype": "enum", + "format": "UPSTREAM,DOWNSTREAM", + "name": "Lugs feed direction: upstream or downstream" + } + }, + "type": "energy.ebus.capability.info" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Active power", + "unit": "W" + }, + "current-a": { + "datatype": "float", + "name": "L1 current", + "unit": "A" + }, + "current-b": { + "datatype": "float", + "name": "L2 current", + "unit": "A" + }, + "exported-energy": { + "datatype": "float", + "name": "Exported energy", + "unit": "Wh" + }, + "imported-energy": { + "datatype": "float", + "name": "Imported energy", + "unit": "Wh" + } + }, + "type": "energy.ebus.capability.meter" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.lugs", + "version": 1791310859473 + }, + "numeric_properties": { + "meter/active-power": "1283.7", + "meter/current-a": "26.6", + "meter/current-b": "5.7", + "meter/exported-energy": "4055664.2", + "meter/imported-energy": "27610149.5" + }, + "properties": { + "connection/fed-by-device-id": "nt-9874-s7rxt-tg681049356k0x", + "connection/fed-by-device-status": "OK", + "connection/fed-by-device-type": "energy.ebus.device.bess", + "info/direction": "UPSTREAM" + } + }, + "nt-9874-s7rxt-tg681049356k0x": { + "description": { + "children": [ + "nt-9874-s7rxt-tg957432104u3d-djc" + ], + "extensions": [], + "homie": "5.0", + "name": "nt-9874-s7rxt-tg681049356k0x", + "nodes": { + "info": { + "name": "info", + "properties": { + "firmware-version": { + "datatype": "string", + "name": "Firmware version" + }, + "model": { + "datatype": "string", + "name": "Model" + }, + "nameplate-capacity": { + "datatype": "float", + "name": "Nameplate capacity", + "unit": "kWh" + }, + "part-number": { + "datatype": "string", + "name": "Part number" + }, + "serial-number": { + "datatype": "string", + "name": "Serial number" + }, + "vendor-name": { + "datatype": "string", + "name": "Vendor name" + } + }, + "type": "energy.ebus.capability.info" + }, + "meter": { + "name": "meter", + "properties": { + "active-power": { + "datatype": "float", + "name": "Active power", + "unit": "W" + } + }, + "type": "energy.ebus.capability.meter" + }, + "soc": { + "name": "soc", + "properties": { + "soc": { + "datatype": "float", + "name": "State of charge", + "unit": "%" + }, + "soe": { + "datatype": "float", + "name": "State of energy", + "unit": "kWh" + } + }, + "type": "energy.ebus.capability.soc" + }, + "status": { + "name": "status", + "properties": { + "communication-state": { + "datatype": "enum", + "format": "OK,DEGRADED,LOST,UNKNOWN", + "name": "Communication state" + } + }, + "type": "energy.ebus.capability.status" + } + }, + "parent": "nt-9874-s7rxt", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.bess", + "version": 1791310862520 + }, + "numeric_properties": { + "info/nameplate-capacity": "81", + "meter/active-power": "0.0", + "soc/soc": "100.0", + "soc/soe": "81.0" + }, + "properties": { + "info/firmware-version": "26.26.4 68c2bc82", + "info/model": "Powerwall 2 AC", + "info/part-number": "1232100-00-E", + "info/serial-number": "tg681049356k0x", + "info/vendor-name": "Tesla", + "status/communication-state": "OK" + } + }, + "nt-9874-s7rxt-tg957432104u3d-djc": { + "description": { + "children": [], + "extensions": [], + "homie": "5.0", + "name": "nt-9874-s7rxt-tg957432104u3d-djc", + "nodes": { + "grid": { + "name": "grid", + "properties": { + "grid-forming-entity": { + "datatype": "string", + "name": "Identity of the currently grid-forming entity" + }, + "grid-state": { + "datatype": "enum", + "format": "UP,DOWN,DEGRADED,UNKNOWN", + "name": "Sensed grid condition" + }, + "islanding-state": { + "datatype": "enum", + "format": "ON_GRID,OFF_GRID,UNKNOWN", + "name": "Islanding state of the BESS-integrated grid-forming device" + } + }, + "type": "energy.ebus.capability.grid" + }, + "info": { + "name": "info", + "properties": { + "firmware-version": { + "datatype": "string", + "name": "Firmware version" + }, + "hardware-version": { + "datatype": "string", + "name": "Hardware version" + }, + "model": { + "datatype": "string", + "name": "Model" + }, + "serial-number": { + "datatype": "string", + "name": "Serial number" + }, + "vendor-name": { + "datatype": "string", + "name": "Vendor name" + } + }, + "type": "energy.ebus.capability.info" + } + }, + "parent": "nt-9874-s7rxt-tg681049356k0x", + "root": "nt-9874-s7rxt", + "type": "energy.ebus.device.mid", + "version": 1791310862535 + }, + "numeric_properties": {}, + "properties": { + "grid/grid-forming-entity": "GRID", + "grid/grid-state": "UNKNOWN", + "grid/islanding-state": "ON_GRID", + "info/serial-number": "tg957432104u3d-djc", + "info/vendor-name": "Tesla" + } + } + }, + "metadata": { + "schema": "tree-v1" + } +} diff --git a/tests/test_schema_one_circuits.py b/tests/test_schema_one_circuits.py index 425973b..8d0b756 100644 --- a/tests/test_schema_one_circuits.py +++ b/tests/test_schema_one_circuits.py @@ -142,8 +142,8 @@ def test_relay_controllable_defaults_to_controllable_when_absent(kitchen: Discov def test_sheddable_is_computed_not_read(kitchen: DiscoveredDevice, solar: DiscoveredDevice) -> None: - """Retired with no replacement property: the guide defines it as - `priority != NEVER and relay-controllable`.""" + """Retired with no replacement property, so derived: a shed priority + (`OFF_GRID` or `SOC_THRESHOLD`) and a controllable relay.""" # Kitchen: priority SOC_THRESHOLD (not NEVER) and controllable -> sheddable assert build_circuit(kitchen).is_sheddable is True # Solar: priority NEVER and not controllable -> not sheddable @@ -247,16 +247,16 @@ def test_every_catalogued_priority_is_carried_through_rather_than_repaired(kitch representativeness comes from the capture. Sheddability is asserted alongside because it is the derivation that would - hide a repaired value: `priority != NEVER and relay-controllable` makes every - member except `NEVER` sheddable, which is the answer the rule gives and not - one this reader should soften. + hide a repaired value: only a shed priority, `OFF_GRID` or `SOC_THRESHOLD`, + makes a controllable circuit sheddable, and `UNKNOWN` is "not yet known" + rather than a priority the panel sheds by. """ kitchen.update_property("load-shed", "priority", declared) circuit = build_circuit(kitchen) assert circuit.priority == declared - assert circuit.is_sheddable is (declared != "NEVER") + assert circuit.is_sheddable is (declared in {"OFF_GRID", "SOC_THRESHOLD"}) # --------------------------------------------------------------------------- diff --git a/tests/test_schema_one_device_display_name.py b/tests/test_schema_one_device_display_name.py new file mode 100644 index 0000000..c8fd796 --- /dev/null +++ b/tests/test_schema_one_device_display_name.py @@ -0,0 +1,91 @@ +"""A device's display name is never its own id. + +Real r202639 panels publish each non-root device's id as its `$description.name` +(the public MAIN 32 capture shows it for the battery, MID, PV, lugs and circuits). +Surfacing that id as a name is the outcome the attribute's docstring calls worse +than no name. +""" + +from __future__ import annotations + +import json + +from ebus_sdk.homie import DiscoveredDevice + +from reference_payloads.schema_one import device_from_topics, parent_child_tree +from span_panel_api.models import SpanPanelSnapshot +from span_panel_api_schema_1.const import TYPE_BESS, TYPE_MID +from span_panel_api_schema_1.panel import device_display_name +from span_panel_api_schema_1.snapshot import build_snapshot + +_TREE = parent_child_tree() + + +def _device_id(device_type: str) -> str: + return next( + device_id for device_id, topics in _TREE.items() if json.loads(topics["$description"])["type"] == device_type + ) + + +BESS_ID = _device_id(TYPE_BESS) +MID_ID = _device_id(TYPE_MID) +PANEL_ID = next(device_id for device_id, topics in _TREE.items() if not json.loads(topics["$description"]).get("parent")) + + +def _bess_topics(*, name: str, vendor: str | None, model: str | None) -> dict[str, str]: + """The reference BESS's retained topics, named and identified as given; `None` leaves a property unvalued.""" + description = json.loads(_TREE[BESS_ID]["$description"]) + description["name"] = name + topics = {topic: value for topic, value in _TREE[BESS_ID].items() if topic not in ("info/vendor-name", "info/model")} + topics["$description"] = json.dumps(description) + if vendor is not None: + topics["info/vendor-name"] = vendor + if model is not None: + topics["info/model"] = model + return topics + + +def _device(*, name: str, vendor: str | None, model: str | None) -> DiscoveredDevice: + return device_from_topics(BESS_ID, _bess_topics(name=name, vendor=vendor, model=model)) + + +def _reference_snapshot(*, bess_name_is_its_id: bool, grid_forming: str) -> SpanPanelSnapshot: + """The reference tree with the MID naming `grid_forming`, and the BESS identified as the public MAIN 32 capture shows it.""" + tree = {device_id: dict(topics) for device_id, topics in _TREE.items()} + tree[BESS_ID] = _bess_topics(name=BESS_ID if bess_name_is_its_id else "Battery", vendor="Tesla", model="Powerwall 2 AC") + tree[MID_ID]["grid/grid-forming-entity"] = grid_forming + panel = device_from_topics(PANEL_ID, tree[PANEL_ID]) + children = [device_from_topics(device_id, topics) for device_id, topics in tree.items() if device_id != PANEL_ID] + return build_snapshot(panel, children) + + +def test_a_name_equal_to_the_id_falls_back_to_vendor_and_model() -> None: + device = _device(name=BESS_ID, vendor="Tesla", model="Powerwall 2 AC") + assert device_display_name(device) == "Tesla Powerwall 2 AC" + + +def test_model_alone_names_the_device() -> None: + assert device_display_name(_device(name=BESS_ID, vendor=None, model="Powerwall 2 AC")) == "Powerwall 2 AC" + + +def test_a_vendor_alone_does_not_name_a_device() -> None: + assert device_display_name(_device(name=BESS_ID, vendor="Tesla", model=None)) is None + + +def test_an_empty_value_is_no_value() -> None: + """An empty name, vendor or model names nothing, exactly as an unpublished one does.""" + assert device_display_name(_device(name="", vendor="", model="Powerwall 2 AC")) == "Powerwall 2 AC" + assert device_display_name(_device(name=BESS_ID, vendor="Tesla", model="")) is None + + +def test_a_real_name_is_kept() -> None: + assert device_display_name(_device(name="Battery", vendor="Tesla", model="Powerwall 2 AC")) == "Battery" + + +def test_the_grid_forming_attribute_names_the_battery_not_its_id() -> None: + snapshot = _reference_snapshot(bess_name_is_its_id=True, grid_forming=BESS_ID) + assert snapshot.mid is not None + assert snapshot.mid.grid_forming_device_name == "Tesla Powerwall 2 AC" + grid = _reference_snapshot(bess_name_is_its_id=True, grid_forming="GRID") + assert grid.mid is not None + assert grid.mid.grid_forming_device_name is None diff --git a/tests/test_schema_one_sheddable.py b/tests/test_schema_one_sheddable.py new file mode 100644 index 0000000..eb992d1 --- /dev/null +++ b/tests/test_schema_one_sheddable.py @@ -0,0 +1,46 @@ +"""`is_sheddable` means the panel will shed it: a shed priority and a controllable relay. + +The public r202633 changelog says `UNKNOWN` means "not yet known", not sheddable. +""" + +from __future__ import annotations + +import json + +from ebus_sdk.homie import DiscoveredDevice +import pytest + +from reference_payloads.schema_one import device_from_topics, parent_child_tree +from span_panel_api_schema_1.circuits import build_circuit +from span_panel_api_schema_1.const import TYPE_CIRCUIT + + +def _circuit_device(*, priority: str, relay_controllable: bool) -> DiscoveredDevice: + """The reference tree's first circuit, with its shed priority and relay-controllable replaced.""" + tree = parent_child_tree() + circuit_id = next( + device_id for device_id, topics in sorted(tree.items()) if json.loads(topics["$description"])["type"] == TYPE_CIRCUIT + ) + topics = { + **tree[circuit_id], + "load-shed/priority": priority, + "switch/relay-controllable": "true" if relay_controllable else "false", + } + return device_from_topics(circuit_id, topics) + + +@pytest.mark.parametrize( + ("priority", "controllable", "sheddable"), + [ + ("OFF_GRID", True, True), + ("SOC_THRESHOLD", True, True), + ("NEVER", True, False), + ("UNKNOWN", True, False), + ("OFF_GRID", False, False), + ], +) +def test_sheddable_means_a_shed_priority_and_a_controllable_relay( + priority: str, controllable: bool, sheddable: bool +) -> None: + circuit = build_circuit(_circuit_device(priority=priority, relay_controllable=controllable)) + assert circuit.is_sheddable is sheddable diff --git a/uv.lock b/uv.lock index 6e0a858..ea51535 100644 --- a/uv.lock +++ b/uv.lock @@ -974,7 +974,7 @@ wheels = [ [[package]] name = "span-panel-api" -version = "3.6.2" +version = "3.7.0b1" source = { editable = "." } dependencies = [ { name = "httpx" }, @@ -1046,7 +1046,7 @@ dev = [ [[package]] name = "span-panel-api-schema-0" -version = "1.2.0" +version = "1.3.0b1" source = { editable = "packages/schema-0" } dependencies = [ { name = "span-panel-api" }, @@ -1057,7 +1057,7 @@ requires-dist = [{ name = "span-panel-api", editable = "." }] [[package]] name = "span-panel-api-schema-1" -version = "1.2.2" +version = "1.3.0b1" source = { editable = "packages/schema-1" } dependencies = [ { name = "ebus-sdk" },