feat: vendor the flat emitter, fixing an add-on image that could never start - #37
Merged
Conversation
…r start ebus_emitter is a hard, unconditional import (app.py -> emitter_adapter/ runtime.py), but the add-on Dockerfile installs only `pip install .` and the package was never a declared dependency — it is not on PyPI and was installed editable from EBUS_EMITTER_PATH by scripts/dev-setup.sh. The image therefore built successfully and died at container start with ModuleNotFoundError. The same applied to anyone who cloned this repo and ran `uv sync` without dev-setup.sh. Only developers who had run dev-setup.sh ever had a working install. Vendor the emitter at src/span_panel_simulator/flat_emitter, copied from ebus-emitter 0.2.1 (commit 5b84de8) — MIT, same copyright holders. Upstream has permanently diverged onto the parent/child v1.0 Homie data model while this simulator continues to publish the flat schema, so the dependency delivered no upstream changes while costing path configuration, stale editable metadata, and an unsolvable distribution problem for the add-on. It also closes a correctness hazard. clone.py seeds energy accumulators against what this code publishes; while the two lived in separate repos each side could look locally correct while jointly inverting circuit energy, which is exactly what happened and what no single test suite could see. Both ends now sit in one repo under one test run. The emitter's tests come with it (tests/flat_emitter/, 154 tests), including the circuit energy reference-frame regression tests. Suite is now 395 tests. Supporting changes, each a consequence of the move: - ebus-sdk pinned to ==0.1.5 rather than the range upstream declared, so vendoring is behaviour-neutral. 0.1.5 is what the emitter's lockfile resolved and what this code was tested against; floating within <0.2 resolves 0.1.10, which drops the module-level setLevel(INFO) on the homie logger that tests/test_main_logging.py guards. The add-on installs from pyproject rather than the lockfile, so the declared bound is the operative control in production. Raising it is a deliberate follow-up. - [tool.ruff.lint] now declares ignore = TC001/TC002/TC003. The existing comment already described this ignore but the key was never present — none of the simulator's own modules triggered the rules, so the omission was invisible. The vendored code was authored under an identical select list plus this ignore. - ChargeMode is exported from the vendored package and annotates the charge_mode derivation in engine.py and emitter_adapter/runtime.py. Both sites already produced only valid values; mypy could not see it while ebus_emitter was an ignore_missing_imports module and BESSConfig was therefore Any. - ebus_sdk mypy overrides move here from the emitter's pyproject. - dev-setup.sh becomes a thin uv sync wrapper; .env.example no longer defines EBUS_EMITTER_PATH. No force-include is added for the vendored wire/profiles and wire/mapping data: packages = ["src/span_panel_simulator"] already ships them, and re-declaring them would reintroduce the duplicate-path collision fixed in 1.0.12. Verified by installing into a clean venv the way the Dockerfile does and confirming the entry point imports and all twelve data files are packaged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug this fixes
ebus_emitteris a hard, unconditional import (app.py:36→emitter_adapter/runtime.py:20), but the add-on Dockerfile installs onlypip install --no-cache-dir .and the package was never a declared dependency — it is not on PyPI, and was installed editable fromEBUS_EMITTER_PATHbyscripts/dev-setup.sh.So the image built fine and died at container start:
The same applied to anyone who cloned this repo and ran
uv syncwithoutdev-setup.sh. Only developers who had rundev-setup.shever had a working install. Verified by blocking the module with an import hook and importing the entry point.The change
Vendor the emitter at
src/span_panel_simulator/flat_emitter, fromebus-emitter0.2.1 (commit5b84de8) — MIT, same copyright holders. Provenance and rationale are in the package docstring.Upstream has permanently diverged onto the parent/child v1.0 Homie data model while this simulator continues to publish the flat schema. A dependency earns its keep by delivering upstream changes; once the fork is permanent it delivers nothing while costing path configuration, stale editable metadata, and an unsolvable distribution problem for the add-on.
It also closes a correctness hazard.
clone.pyseeds energy accumulators against what this code publishes. While the two lived in separate repos, each side could look locally correct while jointly inverting circuit energy — which is exactly what happened (#36), and what no single test suite could see. Both ends now sit in one repo under one test run.The emitter's tests come with it (
tests/flat_emitter/, 154 tests), including the circuit energy reference-frame regression tests. Suite is now 395 tests.Supporting changes, each a consequence of the move
ebus-sdkpinned to==0.1.5rather than the range upstream declared, so vendoring is behaviour-neutral. That is what the emitter's lockfile resolved and what this code was tested against. Floating within<0.2resolves 0.1.10, which drops the module-levelsetLevel(INFO)on thehomielogger thattests/test_main_logging.pyguards — the guard fired exactly as designed. The add-on installs frompyproject.toml, not the lockfile, so the declared bound is the operative control in production. Raising it should be its own PR with its own testing.[tool.ruff.lint]now declaresignore = ["TC001", "TC002", "TC003"]. The existing comment already described this ignore but the key was never present — none of the simulator's own modules happened to trigger the rules, so the omission was invisible. The vendored code was authored under an identicalselectlist plus this ignore, so this restores documented intent rather than relaxing the bar for vendored code.ChargeModeexported and used to annotate thecharge_modederivation inengine.pyandemitter_adapter/runtime.py. Both sites already produced only valid values; mypy could not see it whileebus_emitterwas anignore_missing_importsmodule andBESSConfigwas thereforeAny. Typing-only, no behaviour change.ebus_sdkmypy overrides move here from the emitter's pyproject.dev-setup.shbecomes a thinuv syncwrapper;.env.exampleno longer definesEBUS_EMITTER_PATH.Packaging note
No
force-includeis added for the vendoredwire/profilesandwire/mappingdata.packages = ["src/span_panel_simulator"]already ships them, and re-declaring them would reintroduce the duplicate-path collision fixed in 1.0.12. Upstream neededforce-includeonly because its package root was elsewhere.Verification
ruff checkclean,mypy --strictclean across 85 source files, no new ignores beyond the two that moved with the codepip install --no-cache-dir .), then confirmed the entry point imports, the vendored emitter imports, and all twelve data files are packagedsync-versionacross all four version referencesRelationship to #36
Independent — this branches from
mainand touchesdev-setup.sh/.env.example, which #36 no longer does. They will conflict only onCHANGELOG.mdand the version, both trivial. #36 carries theclone.pyenergy fix; this carries the dependency story.