ebus-panel-sim 0.3.0__py3-none-any.whl

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 (66) hide show
  1. ebus_panel_sim/__init__.py +119 -0
  2. ebus_panel_sim/conventions/__init__.py +11 -0
  3. ebus_panel_sim/conventions/tab_legs.py +47 -0
  4. ebus_panel_sim/emitter.py +750 -0
  5. ebus_panel_sim/energy_integrator.py +89 -0
  6. ebus_panel_sim/exceptions.py +38 -0
  7. ebus_panel_sim/manifest.py +35 -0
  8. ebus_panel_sim/manifest_physics.py +451 -0
  9. ebus_panel_sim/native_devices/__init__.py +24 -0
  10. ebus_panel_sim/native_devices/bess.py +185 -0
  11. ebus_panel_sim/native_devices/load_shedding.py +60 -0
  12. ebus_panel_sim/native_devices/protocol.py +38 -0
  13. ebus_panel_sim/panel_meter.py +216 -0
  14. ebus_panel_sim/py.typed +0 -0
  15. ebus_panel_sim/relay_resolver.py +113 -0
  16. ebus_panel_sim/snapshot.py +301 -0
  17. ebus_panel_sim/tick_inputs.py +66 -0
  18. ebus_panel_sim/wire/__init__.py +0 -0
  19. ebus_panel_sim/wire/_sdk_seam.py +55 -0
  20. ebus_panel_sim/wire/bag_builder.py +429 -0
  21. ebus_panel_sim/wire/catalogs/breaker.json +52 -0
  22. ebus_panel_sim/wire/catalogs/charge-limit.json +37 -0
  23. ebus_panel_sim/wire/catalogs/connection.json +72 -0
  24. ebus_panel_sim/wire/catalogs/door.json +17 -0
  25. ebus_panel_sim/wire/catalogs/grid.json +38 -0
  26. ebus_panel_sim/wire/catalogs/info.json +52 -0
  27. ebus_panel_sim/wire/catalogs/load-shed.json +18 -0
  28. ebus_panel_sim/wire/catalogs/meter.json +201 -0
  29. ebus_panel_sim/wire/catalogs/pcs.json +111 -0
  30. ebus_panel_sim/wire/catalogs/power-flows.json +35 -0
  31. ebus_panel_sim/wire/catalogs/shed-forecast.json +41 -0
  32. ebus_panel_sim/wire/catalogs/shed.json +24 -0
  33. ebus_panel_sim/wire/catalogs/soc.json +35 -0
  34. ebus_panel_sim/wire/catalogs/status.json +28 -0
  35. ebus_panel_sim/wire/catalogs/switch.json +29 -0
  36. ebus_panel_sim/wire/graph_builder.py +309 -0
  37. ebus_panel_sim/wire/mapping/.gitkeep +0 -0
  38. ebus_panel_sim/wire/mapping/bess.yaml +16 -0
  39. ebus_panel_sim/wire/mapping/circuit.yaml +16 -0
  40. ebus_panel_sim/wire/mapping/evse.yaml +16 -0
  41. ebus_panel_sim/wire/mapping/lugs.yaml +16 -0
  42. ebus_panel_sim/wire/mapping/mid.yaml +16 -0
  43. ebus_panel_sim/wire/mapping/panel.yaml +15 -0
  44. ebus_panel_sim/wire/mapping/pv.yaml +16 -0
  45. ebus_panel_sim/wire/mapping_loader.py +144 -0
  46. ebus_panel_sim/wire/profile_loader.py +215 -0
  47. ebus_panel_sim/wire/profiles/.gitkeep +0 -0
  48. ebus_panel_sim/wire/profiles/bess.json +58 -0
  49. ebus_panel_sim/wire/profiles/circuit.json +88 -0
  50. ebus_panel_sim/wire/profiles/evse.json +26 -0
  51. ebus_panel_sim/wire/profiles/lugs.json +52 -0
  52. ebus_panel_sim/wire/profiles/mid.json +40 -0
  53. ebus_panel_sim/wire/profiles/panel.json +160 -0
  54. ebus_panel_sim/wire/profiles/pv.json +28 -0
  55. ebus_panel_sim/wire/profiles/span/circuit.json +19 -0
  56. ebus_panel_sim/wire/profiles/span/evse.json +52 -0
  57. ebus_panel_sim/wire/profiles/span/lugs.json +16 -0
  58. ebus_panel_sim/wire/profiles/span/panel.json +77 -0
  59. ebus_panel_sim/wire/property_bag.py +56 -0
  60. ebus_panel_sim/wire/publisher.py +36 -0
  61. ebus_panel_sim/wire/set_router.py +122 -0
  62. ebus_panel_sim-0.3.0.dist-info/METADATA +14 -0
  63. ebus_panel_sim-0.3.0.dist-info/RECORD +66 -0
  64. ebus_panel_sim-0.3.0.dist-info/WHEEL +4 -0
  65. ebus_panel_sim-0.3.0.dist-info/licenses/AUTHORS +7 -0
  66. ebus_panel_sim-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,119 @@
1
+ """ebus-panel-sim — producer-side Homie wire publisher with native-device runtime.
2
+
3
+ Architecture (v0.3.0):
4
+
5
+ - **Wire layer** (``wire/``): vendored Homie 5 device profiles + mapping descriptors,
6
+ graph builder, diff-only publisher, /set setter fan-in, and the property/bag diff
7
+ cache over a thin ebus-sdk seam (ebus-sdk owns $description/$state/LWT/encoding).
8
+ - **Native devices** (``native_devices/``): emitter-resident, configured-and-self-driving
9
+ device runtimes (BESS dispatch, load shedding).
10
+ - **Manifest physics** (``manifest_physics.py``): typed accessor over
11
+ ``DeviceInstance.metadata`` for physics-relevant fields (voltage, breaker rating,
12
+ tabs/legs, placement, default priority, relay behaviour).
13
+ - **Tick pipeline** (``relay_resolver.py`` + ``energy_integrator.py`` + ``panel_meter.py``
14
+ + ``conventions/tab_legs.py``): per-tick state machinery the emitter uses to
15
+ resolve circuit relay state, integrate energy, derive per-leg currents, and
16
+ aggregate panel-level fields.
17
+
18
+ Producer contract (v0.3.0): build a ``DeviceManifest`` once at startup, then call
19
+ ``Emitter.publish_tick(TickInputs)`` each tick with signed circuit/EVSE powers,
20
+ ``current_time``, and ``grid_online``. The emitter does the rest."""
21
+
22
+ from ebus_panel_sim.conventions.tab_legs import Leg, legs_for_tabs
23
+ from ebus_panel_sim.emitter import Emitter
24
+ from ebus_panel_sim.exceptions import (
25
+ EmitterError,
26
+ EmitterStateError,
27
+ ManifestValidationError,
28
+ MissingSetterError,
29
+ ProfileValidationError,
30
+ RuntimeSpecValidationError,
31
+ )
32
+ from ebus_panel_sim.manifest import DeviceInstance, DeviceManifest
33
+ from ebus_panel_sim.manifest_physics import (
34
+ BessPhysics,
35
+ CircuitPhysics,
36
+ EvsePhysics,
37
+ LugsPhysics,
38
+ ManifestPhysicsView,
39
+ PanelPhysics,
40
+ PvPhysics,
41
+ )
42
+ from ebus_panel_sim.native_devices import (
43
+ BESSConfig,
44
+ BESSDevice,
45
+ LoadSheddingConfig,
46
+ LoadSheddingDevice,
47
+ NativeDevice,
48
+ NativeTickContext,
49
+ )
50
+ from ebus_panel_sim.relay_resolver import RelayRequester, RelayResolver, RelayState
51
+ from ebus_panel_sim.snapshot import (
52
+ EbusBatterySnapshot,
53
+ EbusCircuitSnapshot,
54
+ EbusEvseSnapshot,
55
+ EbusLugsSnapshot,
56
+ EbusPanelDoor,
57
+ EbusPanelInfo,
58
+ EbusPanelMeter,
59
+ EbusPanelPcs,
60
+ EbusPanelPowerFlows,
61
+ EbusPanelSnapshot,
62
+ EbusPanelStatus,
63
+ EbusPvSnapshot,
64
+ )
65
+ from ebus_panel_sim.tick_inputs import PanelEnvelopeTick, TickInputs
66
+ from ebus_panel_sim.wire.set_router import SetterHandler, SetterRegistry
67
+
68
+ # Single source of truth for the distribution version: pyproject reads it from here
69
+ # via `[tool.hatch.version]`, and publish.yml refuses to release when the git tag
70
+ # disagrees. Bump it in this one place. Note this is the PACKAGE version and is
71
+ # distinct from the producer-contract version the docstrings above refer to.
72
+ __version__ = "0.3.0"
73
+
74
+ __all__ = [
75
+ "BESSConfig",
76
+ "BESSDevice",
77
+ "BessPhysics",
78
+ "CircuitPhysics",
79
+ "DeviceInstance",
80
+ "DeviceManifest",
81
+ "EbusBatterySnapshot",
82
+ "EbusCircuitSnapshot",
83
+ "EbusEvseSnapshot",
84
+ "EbusLugsSnapshot",
85
+ "EbusPanelDoor",
86
+ "EbusPanelInfo",
87
+ "EbusPanelMeter",
88
+ "EbusPanelPcs",
89
+ "EbusPanelPowerFlows",
90
+ "EbusPanelSnapshot",
91
+ "EbusPanelStatus",
92
+ "EbusPvSnapshot",
93
+ "Emitter",
94
+ "EmitterError",
95
+ "EmitterStateError",
96
+ "EvsePhysics",
97
+ "Leg",
98
+ "LoadSheddingConfig",
99
+ "LoadSheddingDevice",
100
+ "LugsPhysics",
101
+ "ManifestPhysicsView",
102
+ "ManifestValidationError",
103
+ "MissingSetterError",
104
+ "NativeDevice",
105
+ "NativeTickContext",
106
+ "PanelEnvelopeTick",
107
+ "PanelPhysics",
108
+ "ProfileValidationError",
109
+ "PvPhysics",
110
+ "RelayRequester",
111
+ "RelayResolver",
112
+ "RelayState",
113
+ "RuntimeSpecValidationError",
114
+ "SetterHandler",
115
+ "SetterRegistry",
116
+ "TickInputs",
117
+ "__version__",
118
+ "legs_for_tabs",
119
+ ]
@@ -0,0 +1,11 @@
1
+ """Centralized panel-wiring conventions used across the emitter.
2
+
3
+ Each module in this package isolates a single physical-layer convention so the
4
+ rest of the codebase can be convention-agnostic. Convention changes (e.g.,
5
+ supporting European single-phase or 3-phase commercial panels) land here without
6
+ rippling into ``PanelMeter``, ``EnergyIntegrator``, or per-property derivations.
7
+ """
8
+
9
+ from ebus_panel_sim.conventions.tab_legs import Leg, legs_for_tabs
10
+
11
+ __all__ = ["Leg", "legs_for_tabs"]
@@ -0,0 +1,47 @@
1
+ """Tab-to-leg assignment convention for US residential split-phase panels.
2
+
3
+ US residential load centers alternate breaker slots between L1 and L2: odd-numbered
4
+ tabs land on L1, even-numbered tabs land on L2. A 240 V dipole circuit occupies
5
+ two adjacent tabs (e.g. 1 and 2) — one per leg.
6
+
7
+ This module is the single source of truth for that convention. Per-leg current
8
+ calculations in ``PanelMeter`` and the per-circuit ``current_a`` derivation go
9
+ through ``legs_for_tabs`` rather than reaching for ``tab % 2`` directly.
10
+
11
+ Future support for non-US panels (European single-phase, 3-phase commercial) lands
12
+ here as an additional ``Convention`` enum + dispatch, without touching call sites."""
13
+
14
+ from __future__ import annotations
15
+
16
+ from enum import StrEnum
17
+
18
+
19
+ class Leg(StrEnum):
20
+ """A panel power leg. ``L1`` and ``L2`` are the two 120 V legs of a US
21
+ residential split-phase service; line-to-line voltage between them is 240 V."""
22
+
23
+ L1 = "L1"
24
+ L2 = "L2"
25
+
26
+
27
+ def legs_for_tabs(tabs: tuple[int, ...]) -> tuple[Leg, ...]:
28
+ """Return the leg assignment for each tab in ``tabs``, US residential
29
+ convention: odd tabs → L1, even tabs → L2.
30
+
31
+ Examples:
32
+ >>> legs_for_tabs((1,))
33
+ (<Leg.L1: 'L1'>,)
34
+ >>> legs_for_tabs((2,))
35
+ (<Leg.L2: 'L2'>,)
36
+ >>> legs_for_tabs((1, 2)) # standard dipole
37
+ (<Leg.L1: 'L1'>, <Leg.L2: 'L2'>)
38
+ >>> legs_for_tabs((1, 3)) # both on L1 — invalid for a dipole
39
+ (<Leg.L1: 'L1'>, <Leg.L1: 'L1'>)
40
+
41
+ The function does not enforce dipole leg-spanning — that's the validation
42
+ job of ``ManifestPhysicsView``, which sees the ``dipole`` flag.
43
+
44
+ Raises ``ValueError`` if any tab is < 1 (panel tabs are 1-indexed)."""
45
+ if any(t < 1 for t in tabs):
46
+ raise ValueError(f"tab numbers must be >= 1; got {tabs!r}")
47
+ return tuple(Leg.L1 if t % 2 == 1 else Leg.L2 for t in tabs)