span-panel-api-schema-1 1.2.2__tar.gz → 1.3.0b1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/CHANGELOG.md +20 -0
  2. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/PKG-INFO +2 -2
  3. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/pyproject.toml +5 -2
  4. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/adapter.py +4 -4
  5. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/circuits.py +13 -4
  6. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/const.py +5 -8
  7. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/panel.py +34 -47
  8. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/snapshot.py +10 -18
  9. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/.gitignore +0 -0
  10. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/README.md +0 -0
  11. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/breaker.json +0 -0
  12. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/charge-limit.json +0 -0
  13. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/connection.json +0 -0
  14. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/door.json +0 -0
  15. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/grid-forming.json +0 -0
  16. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/grid.json +0 -0
  17. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/info.json +0 -0
  18. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/load-shed.json +0 -0
  19. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/meter.json +0 -0
  20. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/pcs.json +0 -0
  21. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/power-flows.json +0 -0
  22. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/shed-forecast.json +0 -0
  23. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/shed.json +0 -0
  24. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/soc.json +0 -0
  25. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/status.json +0 -0
  26. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/catalogs/switch.json +0 -0
  27. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/spec/registries/device-types.md +0 -0
  28. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/__init__.py +0 -0
  29. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/adoption.py +0 -0
  30. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/catalog.py +0 -0
  31. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/charge_limit.py +0 -0
  32. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/description.py +0 -0
  33. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/devices.py +0 -0
  34. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/extension.py +0 -0
  35. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/field_metadata.py +0 -0
  36. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/firmware.py +0 -0
  37. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/py.typed +0 -0
  38. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/reference/parent_child_tree.json +0 -0
  39. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/spec_lock.json +0 -0
  40. {span_panel_api_schema_1-1.2.2 → span_panel_api_schema_1-1.3.0b1}/src/span_panel_api_schema_1/transport.py +0 -0
@@ -9,6 +9,26 @@ number. A release here means this parser changed, never that the panel did.
9
9
 
10
10
  Pre-releases are not listed separately. A beta is a step towards the next public version, so its changes are folded into that version's entry as they land and are described against the last public release, never against the beta before it.
11
11
 
12
+ ## [Unreleased]
13
+
14
+ Requires `span-panel-api` **3.7.0 or newer**, whose snapshot fields accept the unknown values this parser now reports.
15
+
16
+ ### Changed
17
+
18
+ - **`proximity_proven` is `None`**, where it was a hard-coded `True`, because the tree publishes no proximity property.
19
+ - **`uptime_s` is `None`**, where it was always `0`, because no device publishes an uptime and this parser keeps no readiness clock to measure one.
20
+
21
+ ### Removed
22
+
23
+ - **BREAKING FOR CONSUMERS: `snapshot.build_snapshot()` no longer takes the `ready_since` argument**, which no caller in this repository passed, so a call that passes it by keyword or position now raises `TypeError`.
24
+
25
+ - **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.
26
+
27
+ ### Fixed
28
+
29
+ - **A circuit whose shed priority is `UNKNOWN` is no longer reported as sheddable.**
30
+ - **The grid-forming device name falls back to vendor and model instead of showing a device id.**
31
+
12
32
  ## [1.2.2]
13
33
 
14
34
  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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: span-panel-api-schema-1
3
- Version: 1.2.2
3
+ Version: 1.3.0b1
4
4
  Summary: Parent/child schema (data-model-version 1.x) parser for span-panel-api
5
5
  Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
6
6
  Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
@@ -8,7 +8,7 @@ Author: SpanPanel
8
8
  License-Expression: MIT
9
9
  Requires-Python: <4.0,>=3.14
10
10
  Requires-Dist: ebus-sdk<0.24,>=0.19.0
11
- Requires-Dist: span-panel-api<4.0,>=3.6.2
11
+ Requires-Dist: span-panel-api<4.0,>=3.7.0b1
12
12
  Description-Content-Type: text/markdown
13
13
 
14
14
  # span-panel-api-schema-1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "span-panel-api-schema-1"
3
- version = "1.2.2"
3
+ version = "1.3.0b1"
4
4
  description = "Parent/child schema (data-model-version 1.x) parser for span-panel-api"
5
5
  authors = [
6
6
  {name = "SpanPanel"}
@@ -28,7 +28,10 @@ dependencies = [
28
28
  # Raised to 3.6.1, whose SpanPanelSnapshot.pv documents what this parser builds.
29
29
  # Raised to 3.6.2, which waits on this parser's missing labels after a bridge
30
30
  # rebuild as well as on connect; the fix spans both, so the pair moves together.
31
- "span-panel-api>=3.6.2,<4.0",
31
+ # Raised to 3.7.0b1, whose snapshot fields accept the unknown values this
32
+ # parser now reports: against an earlier one, the update times it leaves
33
+ # unset read as a constant 0.
34
+ "span-panel-api>=3.7.0b1,<4.0",
32
35
  # Only this distribution depends on the eBus SDK. The bootstrap and
33
36
  # schema-0 stay clean, so a flat-panel install never pulls it in — which is
34
37
  # what bounds the release coupling this dependency introduces to panels on
@@ -167,10 +167,10 @@ class SchemaOneAdapter:
167
167
 
168
168
  The model is required only when the root's description declares it: the
169
169
  panel's size comes from nowhere else, and a snapshot built a moment too
170
- early reports zero spaces, which erases every unmapped position rather
171
- than merely mis-stating a number. Asking only for what the panel itself
172
- promised keeps a firmware that omits the property connectable — it
173
- falls back to the drift warning in `panel_size_from_model`.
170
+ early reports zero spaces, which leaves a consumer laying out the
171
+ panel's positions with none to lay out. Asking only for what the panel
172
+ itself promised keeps a firmware that omits the property connectable —
173
+ it falls back to the drift warning in `panel_size_from_model`.
174
174
  """
175
175
  root = self._controller.get_root(self._serial_number)
176
176
  if root is None or root.state != STATE_READY or not root.description:
@@ -10,9 +10,14 @@ Flat property v1.0 source
10
10
  ====================== ===========================================================
11
11
  ``always-on`` ``switch/relay-controllable``, inverted
12
12
  ``never-backup`` ``$settable`` on ``load-shed/priority``, inverted
13
- ``sheddable`` computed: ``priority != NEVER and relay-controllable``
13
+ ``sheddable`` computed: a shed priority and ``relay-controllable``
14
14
  ====================== ===========================================================
15
15
 
16
+ A shed priority is ``OFF_GRID`` or ``SOC_THRESHOLD``. SPAN's r202633 changelog
17
+ derives sheddable as ``priority != NEVER`` and asks consumers to treat ``UNKNOWN``
18
+ as "not yet known" rather than as sheddable, so the rule names the priorities the
19
+ panel sheds by instead of excluding the one it never sheds.
20
+
16
21
  Sign and direction are unchanged from the flat schema, and both are the reverse
17
22
  of what the property names suggest. Values are in the enclosure's reference
18
23
  frame: a normal load reads **negative** ``active-power`` and accumulates
@@ -23,7 +28,7 @@ accumulators are swapped.
23
28
 
24
29
  from __future__ import annotations
25
30
 
26
- from typing import TYPE_CHECKING
31
+ from typing import TYPE_CHECKING, Final
27
32
 
28
33
  from span_panel_api.models import SpanCircuitSnapshot
29
34
  from span_panel_api_schema_1.const import (
@@ -33,7 +38,8 @@ from span_panel_api_schema_1.const import (
33
38
  NODE_METER,
34
39
  NODE_PCS,
35
40
  NODE_SWITCH,
36
- PRIORITY_NEVER,
41
+ PRIORITY_OFF_GRID,
42
+ PRIORITY_SOC_THRESHOLD,
37
43
  PROP_ACTIVE_POWER,
38
44
  PROP_CURRENT,
39
45
  PROP_EXPORTED_ENERGY,
@@ -54,6 +60,9 @@ from span_panel_api_schema_1.description import declared_settable, node_properti
54
60
  if TYPE_CHECKING:
55
61
  from ebus_sdk.homie import DiscoveredDevice
56
62
 
63
+ _SHED_PRIORITIES: Final = frozenset({PRIORITY_OFF_GRID, PRIORITY_SOC_THRESHOLD})
64
+ """The `load-shed/priority` values the panel sheds a circuit by; see the module docstring."""
65
+
57
66
 
58
67
  def _text(device: DiscoveredDevice, node: str, prop: str, default: str = "") -> str:
59
68
  value = device.get_property(node, prop)
@@ -260,7 +269,7 @@ def build_circuit(
260
269
  # user-controllability from `always-on` — so this is the same answer by
261
270
  # a shorter route.
262
271
  is_user_controllable=relay_controllable,
263
- is_sheddable=priority != PRIORITY_NEVER and relay_controllable,
272
+ is_sheddable=priority in _SHED_PRIORITIES and relay_controllable,
264
273
  is_never_backup=not priority_settable,
265
274
  device_type=device_type,
266
275
  relative_position=relative_position,
@@ -152,6 +152,8 @@ PROP_COMMUNICATION_STATE = "communication-state"
152
152
  # -- Values -----------------------------------------------------------------
153
153
 
154
154
  PRIORITY_NEVER = "NEVER"
155
+ PRIORITY_OFF_GRID = "OFF_GRID"
156
+ PRIORITY_SOC_THRESHOLD = "SOC_THRESHOLD"
155
157
  UNKNOWN = "UNKNOWN"
156
158
  CLOUD_CONNECTED = "CONNECTED"
157
159
 
@@ -181,9 +183,9 @@ STATE_READY = "ready"
181
183
  # exists because of that split — the panel can tell us a model we have no size
182
184
  # for, and we would rather say so than guess.
183
185
  #
184
- # Total size matters beyond a display field: unoccupied positions are only
185
- # knowable as `total - occupied`, and synthesising them is what gives the
186
- # integration its unmapped-circuit sensors.
186
+ # Total size matters beyond a display field: the tree lists only occupied
187
+ # positions, so the total is the only way a consumer knows how many positions
188
+ # to lay out, empty ones included.
187
189
  PANEL_SIZE_BY_MODEL: dict[str, int] = {
188
190
  "MAIN_16": 16,
189
191
  "MLO_24": 24,
@@ -192,11 +194,6 @@ PANEL_SIZE_BY_MODEL: dict[str, int] = {
192
194
  "MLO_48": 48,
193
195
  }
194
196
 
195
- # Prefix for synthesised unoccupied-position entries. Must match the flat
196
- # adapter's, because the integration keys entities off it and a rename would
197
- # strand every existing unmapped-tab entity.
198
- UNMAPPED_TAB_PREFIX = "unmapped_tab_"
199
-
200
197
  # The Homie attribute that carries what the flat schema published as the
201
198
  # `never-backup` boolean. v1.0 retires the property and expresses it as
202
199
  # mutability: a circuit commissioned never-backup has its priority locked, and
@@ -35,7 +35,7 @@ import json
35
35
  import logging
36
36
  from typing import TYPE_CHECKING, NamedTuple
37
37
 
38
- from span_panel_api.models import SpanCircuitSnapshot, SpanPcsSnapshot
38
+ from span_panel_api.models import SpanPcsSnapshot
39
39
  from span_panel_api_schema_1.const import (
40
40
  CLOUD_CONNECTED,
41
41
  NODE_BREAKER,
@@ -89,7 +89,6 @@ from span_panel_api_schema_1.const import (
89
89
  TYPE_BESS,
90
90
  TYPE_PV,
91
91
  UNKNOWN,
92
- UNMAPPED_TAB_PREFIX,
93
92
  )
94
93
 
95
94
  if TYPE_CHECKING:
@@ -201,20 +200,19 @@ def panel_size_from_model(model: str) -> int:
201
200
  device publishes no size property.
202
201
 
203
202
  The highest *occupied* space is not a substitute: it is a lower bound, so a
204
- 40-space panel whose highest occupied slot is 36 would report 36 and every
205
- position above it would silently cease to exist. Since unoccupied positions
206
- are exactly `total - occupied`, that would delete the integration's
207
- unmapped-circuit sensors rather than merely miscount a display value.
203
+ panel whose highest occupied slot is below its size would report that slot,
204
+ and a consumer laying out positions `1..panel_size` would silently lose
205
+ every position above it.
208
206
 
209
207
  Unknown models return 0 and log, because inventing a size is worse than
210
- reporting none: a wrong total fabricates unmapped positions that are not
211
- there, or hides real ones.
208
+ reporting none: a wrong total lays out positions that are not there, or
209
+ hides real ones.
212
210
  """
213
211
  size = PANEL_SIZE_BY_MODEL.get(model.strip().upper())
214
212
  if size is None:
215
213
  if model:
216
214
  _LOGGER.warning(
217
- "Unknown panel model %r; panel size unavailable and unmapped positions cannot be derived. Known models: %s",
215
+ "Unknown panel model %r; panel size unavailable. Known models: %s",
218
216
  model,
219
217
  ", ".join(sorted(PANEL_SIZE_BY_MODEL)),
220
218
  )
@@ -248,47 +246,13 @@ def panel_model_drift(panel: DiscoveredDevice) -> tuple[str, ...]:
248
246
  ]
249
247
  if unknown:
250
248
  _LOGGER.warning(
251
- "Panel advertises model(s) %s that this adapter cannot size; "
252
- "unmapped positions would be wrong for such a panel. Known: %s",
249
+ "Panel advertises model(s) %s that this adapter cannot size; its panel size would be reported as 0. Known: %s",
253
250
  ", ".join(unknown),
254
251
  ", ".join(sorted(PANEL_SIZE_BY_MODEL)),
255
252
  )
256
253
  return tuple(unknown)
257
254
 
258
255
 
259
- def build_unmapped_tabs(panel_size: int, occupied: set[int]) -> dict[str, SpanCircuitSnapshot]:
260
- """Synthesise a zero-power entry for every unoccupied breaker position.
261
-
262
- The integration surfaces these as unmapped-circuit sensors, gated by its
263
- own `enable_unmapped_circuit_sensors` option, and builds entity ids from
264
- the circuit id — so the `unmapped_tab_<n>` naming is a compatibility
265
- contract with entities that already exist, not an internal detail.
266
-
267
- Reproducible under v1.0 only because the model gives a true total: the tree
268
- itself lists occupied positions and says nothing about the rest. A panel
269
- whose model is unrecognised yields nothing rather than a guess.
270
- """
271
- unmapped: dict[str, SpanCircuitSnapshot] = {}
272
- for tab in range(1, panel_size + 1):
273
- if tab in occupied:
274
- continue
275
- circuit_id = f"{UNMAPPED_TAB_PREFIX}{tab}"
276
- unmapped[circuit_id] = SpanCircuitSnapshot(
277
- circuit_id=circuit_id,
278
- name=f"Unmapped Tab {tab}",
279
- relay_state="CLOSED",
280
- instant_power_w=0.0,
281
- produced_energy_wh=0.0,
282
- consumed_energy_wh=0.0,
283
- tabs=[tab],
284
- priority=UNKNOWN,
285
- is_user_controllable=False,
286
- is_sheddable=False,
287
- is_never_backup=False,
288
- )
289
- return unmapped
290
-
291
-
292
256
  def find_lugs(devices: list[DiscoveredDevice], upstream: bool) -> DiscoveredDevice | None:
293
257
  """Locate a lugs device by its declared direction.
294
258
 
@@ -795,6 +759,29 @@ def resolve_dominant_power_source(
795
759
  return _POWER_SOURCE_BY_TYPE.get(device_types.get(forming, ""), UNKNOWN)
796
760
 
797
761
 
762
+ def device_display_name(device: DiscoveredDevice) -> str | None:
763
+ """A device's readable name, or `None` when nothing it publishes names it.
764
+
765
+ Homie's `$description.name` (`Battery`, `Solar`, `SPAN Drive - Garage`) where it
766
+ is set and is not the device's own id. Real r202639 panels publish each non-root
767
+ device's id there, and an id shown as a name is the outcome
768
+ `resolve_grid_forming_device_name` exists to avoid. Otherwise the name comes from
769
+ the device's `info`: vendor and model together (`Tesla Powerwall 2 AC`), or the
770
+ model alone. A vendor alone names a maker rather than a device, so it gives `None`.
771
+
772
+ An empty value names nothing, exactly as an unpublished one does.
773
+ """
774
+ description: dict[str, object] = device.description or {}
775
+ name = description.get("name")
776
+ if name and str(name) != device.device_id:
777
+ return str(name)
778
+ vendor = text(device, NODE_INFO, PROP_VENDOR_NAME)
779
+ model = text(device, NODE_INFO, PROP_MODEL)
780
+ if vendor and model:
781
+ return f"{vendor} {model}"
782
+ return model or None
783
+
784
+
798
785
  def resolve_grid_forming_device_name(
799
786
  mid: DiscoveredDevice | None,
800
787
  device_names: Mapping[str, str],
@@ -803,9 +790,9 @@ def resolve_grid_forming_device_name(
803
790
 
804
791
  The wire value is a Homie device id -- `sim-40t-001-SIM-BESS-40T-001`. That means
805
792
  nothing to someone reading a dashboard: it is not a Home Assistant device id, and an
806
- opaque string is worse than no string. Homie's `$description.name` is the device's
807
- own display name (`Battery`, `Solar`, `SPAN Drive - Garage`), which is what a person
808
- would recognise, so that is what gets surfaced.
793
+ opaque string is worse than no string. `device_names` holds each device's
794
+ `device_display_name`, which is what a person would recognise, so that is what gets
795
+ surfaced.
809
796
 
810
797
  `None` when the grid is forming (there is no device to name), when no MID publishes
811
798
  an answer, or when the id resolves to nothing -- the raw id is still on
@@ -8,7 +8,6 @@ likes, and the type string is what the schema defines.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import time
12
11
  from typing import TYPE_CHECKING
13
12
 
14
13
  from span_panel_api.models import ExtensionSubject, SpanPanelSnapshot, SpanPVSnapshot
@@ -43,7 +42,7 @@ from span_panel_api_schema_1.field_metadata import addressed_rows
43
42
  from span_panel_api_schema_1.panel import (
44
43
  PanelFields,
45
44
  build_pcs,
46
- build_unmapped_tabs,
45
+ device_display_name,
47
46
  find_lugs,
48
47
  panel_size_from_model,
49
48
  resolve_dominant_power_source,
@@ -128,7 +127,7 @@ def _pv_for(inverters: Sequence[SpanPVSnapshot]) -> SpanPVSnapshot:
128
127
  )
129
128
 
130
129
 
131
- def build_snapshot(panel: DiscoveredDevice, children: list[DiscoveredDevice], ready_since: float = 0.0) -> SpanPanelSnapshot:
130
+ def build_snapshot(panel: DiscoveredDevice, children: list[DiscoveredDevice]) -> SpanPanelSnapshot:
132
131
  """Build a full snapshot from the panel and its descendants."""
133
132
  roles = TreeRoles(children)
134
133
  upstream = find_lugs(roles.lugs, upstream=True)
@@ -165,12 +164,8 @@ def build_snapshot(panel: DiscoveredDevice, children: list[DiscoveredDevice], re
165
164
  circuits[snapshot.circuit_id] = snapshot
166
165
  circuit_subjects.append((circuit, ExtensionSubject(kind="circuit", instance_key=snapshot.circuit_id)))
167
166
 
168
- occupied = {tab for circuit in circuits.values() for tab in circuit.tabs}
169
- # Unoccupied positions are `total - occupied`, so this is only meaningful
170
- # when the model gave a real total. An unknown model yields size 0 and no
171
- # unmapped entries rather than a fabricated set.
167
+ # An unknown model yields size 0 rather than a guessed total.
172
168
  panel_size = panel_size_from_model(text(panel, NODE_INFO, PROP_MODEL))
173
- circuits.update(build_unmapped_tabs(panel_size, occupied))
174
169
 
175
170
  # Owners are every device that can claim a DER through a `connection` node.
176
171
  owners = [*roles.lugs, *roles.circuits, panel]
@@ -180,12 +175,7 @@ def build_snapshot(panel: DiscoveredDevice, children: list[DiscoveredDevice], re
180
175
  # is what makes PANEL_BACKUP distinguishable from PANEL_OFF_GRID authoritatively
181
176
  # instead of guessed from a power source the way flat had to.
182
177
  device_types = {device.device_id: device_type(device) for device in children}
183
- device_names: dict[str, str] = {}
184
- for device in children:
185
- description: dict[str, object] = device.description or {}
186
- name = description.get("name")
187
- if name:
188
- device_names[device.device_id] = str(name)
178
+ device_names = {device.device_id: name for device in children if (name := device_display_name(device)) is not None}
189
179
  inverters = [device for device in children if device_type(device) == TYPE_INVERTER]
190
180
  islanding = resolve_islanding_state(roles.mid, panel)
191
181
 
@@ -259,10 +249,12 @@ def build_snapshot(panel: DiscoveredDevice, children: list[DiscoveredDevice], re
259
249
  dsm_state=resolve_dsm_state(islanding),
260
250
  current_run_config=resolve_run_config(roles.mid, islanding, device_types),
261
251
  door_state=fields.door_state,
262
- # The panel has no proximity sensor property; the flat adapter reports
263
- # authenticated-and-ready, and the same holds here.
264
- proximity_proven=True,
265
- uptime_s=int(time.monotonic() - ready_since) if ready_since > 0.0 else 0,
252
+ # The panel publishes no proximity property, and being connected and
253
+ # ready is not one, so the snapshot does not know.
254
+ proximity_proven=None,
255
+ # No device publishes an uptime, and this adapter keeps no readiness
256
+ # clock of its own to measure one from.
257
+ uptime_s=None,
266
258
  eth0_link=fields.eth0_link,
267
259
  wlan_link=fields.wlan_link,
268
260
  wwan_link=fields.wwan_link,