microgrid 0.1.2__tar.gz → 0.1.4__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 (154) hide show
  1. microgrid-0.1.4/CHANGELOG.md +154 -0
  2. {microgrid-0.1.2 → microgrid-0.1.4}/PKG-INFO +8 -5
  3. {microgrid-0.1.2 → microgrid-0.1.4}/README.md +7 -4
  4. {microgrid-0.1.2 → microgrid-0.1.4}/pyproject.toml +23 -5
  5. {microgrid-0.1.2 → microgrid-0.1.4}/scripts/ci_local.sh +11 -4
  6. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/__init__.py +2 -3
  7. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/__init__.py +5 -2
  8. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/dataset.py +1 -1
  9. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/history.py +3 -3
  10. microgrid-0.1.4/src/microgrid/cli/seen.py +114 -0
  11. microgrid-0.1.4/src/microgrid/cli/signs.py +190 -0
  12. microgrid-0.1.4/src/microgrid/cli/sources/__init__.py +55 -0
  13. microgrid-0.1.4/src/microgrid/cli/sources/action.py +66 -0
  14. microgrid-0.1.4/src/microgrid/cli/sources/amber.py +105 -0
  15. microgrid-0.1.4/src/microgrid/cli/sources/api.py +54 -0
  16. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/config.py +287 -32
  17. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/emhass.py +2 -2
  18. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/entity.py +86 -62
  19. microgrid-0.1.4/src/microgrid/cli/sources/entsoe.py +64 -0
  20. microgrid-0.1.4/src/microgrid/cli/sources/nordpool.py +79 -0
  21. microgrid-0.1.4/src/microgrid/cli/sources/octopus.py +86 -0
  22. microgrid-0.1.4/src/microgrid/cli/sources/openei.py +116 -0
  23. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/predbat.py +2 -2
  24. microgrid-0.1.4/src/microgrid/cli/sources/prices.py +79 -0
  25. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/read.py +79 -18
  26. microgrid-0.1.4/src/microgrid/cli/sources/rows.py +150 -0
  27. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/solcast.py +3 -3
  28. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/tariffkit.py +3 -3
  29. microgrid-0.1.4/src/microgrid/cli/sources/tibber.py +69 -0
  30. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/__init__.py +2 -2
  31. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_engine_config.py +2 -2
  32. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/analysis/__init__.py +1 -1
  33. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/analysis/ground_truth.py +2 -2
  34. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/analysis/scoring.py +2 -4
  35. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/extend.py +0 -2
  36. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/ledger.py +0 -2
  37. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/series.py +2 -6
  38. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/testing.py +0 -2
  39. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/units.py +3 -6
  40. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/__init__.py +3 -8
  41. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/examples.py +0 -2
  42. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/flows.py +4 -5
  43. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/port.py +4 -8
  44. microgrid-0.1.4/tests/cli/test_price_readers.py +566 -0
  45. microgrid-0.1.4/tests/cli/test_seen.py +91 -0
  46. microgrid-0.1.4/tests/cli/test_signs.py +80 -0
  47. {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_sources.py +6 -2
  48. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_architecture.py +21 -2
  49. microgrid-0.1.4/tests/test_httpx.py +12 -0
  50. microgrid-0.1.2/CHANGELOG.md +0 -83
  51. microgrid-0.1.2/src/microgrid/cli/sources/__init__.py +0 -39
  52. {microgrid-0.1.2 → microgrid-0.1.4}/.gitignore +0 -0
  53. {microgrid-0.1.2 → microgrid-0.1.4}/LICENSE +0 -0
  54. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/engine.toml +0 -0
  55. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/expected/ledger-2026-10-21.txt +0 -0
  56. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/expected/live-2026-10-25.txt +0 -0
  57. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/expected/replay-2026-10-22.txt +0 -0
  58. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/expected/value-2026-10-22T13-03.txt +0 -0
  59. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/expected/value-2026-10-25T02-30.txt +0 -0
  60. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/forecasts/load_forecast.json +0 -0
  61. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/forecasts/prices.json +0 -0
  62. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/forecasts/pv_forecast.json +0 -0
  63. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/manifest.json +0 -0
  64. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/plant.toml +0 -0
  65. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/battery.json +0 -0
  66. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/battery_first.json +0 -0
  67. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/export_price.json +0 -0
  68. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/grid.json +0 -0
  69. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/heartbeat.json +0 -0
  70. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/house.json +0 -0
  71. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/import_price.json +0 -0
  72. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/inverter.json +0 -0
  73. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/pv.json +0 -0
  74. {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/soc.json +0 -0
  75. {microgrid-0.1.2 → microgrid-0.1.4}/scripts/griffe_microgrid.py +0 -0
  76. {microgrid-0.1.2 → microgrid-0.1.4}/scripts/install-hooks.sh +0 -0
  77. {microgrid-0.1.2 → microgrid-0.1.4}/scripts/make_example_dataset.py +0 -0
  78. {microgrid-0.1.2 → microgrid-0.1.4}/scripts/make_expected_outputs.py +0 -0
  79. {microgrid-0.1.2 → microgrid-0.1.4}/scripts/mkdocs_hooks.py +0 -0
  80. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/_checks.py +0 -0
  81. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/check_plant.py +0 -0
  82. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/evaluation.py +0 -0
  83. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/fit_battery.py +0 -0
  84. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/inputs.py +0 -0
  85. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/main.py +0 -0
  86. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/offline.py +0 -0
  87. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/replay.py +0 -0
  88. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/fetch.py +0 -0
  89. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/home_assistant.py +0 -0
  90. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/toml_text.py +0 -0
  91. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_api.py +0 -0
  92. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_assess.py +0 -0
  93. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_inputs.py +0 -0
  94. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_live.py +0 -0
  95. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_stored.py +0 -0
  96. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_supply.py +0 -0
  97. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_surplus.py +0 -0
  98. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_timeline.py +0 -0
  99. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_value.py +0 -0
  100. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/py.typed +0 -0
  101. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/schemas/valuation-2.schema.json +0 -0
  102. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/_battery.py +0 -0
  103. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/_config.py +0 -0
  104. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/_meters.py +0 -0
  105. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/_paths.py +0 -0
  106. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/_plant.py +0 -0
  107. {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/py.typed +0 -0
  108. {microgrid-0.1.2 → microgrid-0.1.4}/tests/architecture.py +0 -0
  109. {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/makeup/engine.toml +0 -0
  110. {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/makeup/live-2026-10-25.txt +0 -0
  111. {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/makeup/replay-2026-10-22.txt +0 -0
  112. {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/makeup/value-2026-10-22T13-03.txt +0 -0
  113. {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/makeup/value-2026-10-25T02-30.txt +0 -0
  114. {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_dataset.py +0 -0
  115. {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_end_to_end.py +0 -0
  116. {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_evaluation_properties.py +0 -0
  117. {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_fit_battery.py +0 -0
  118. {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_history.py +0 -0
  119. {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_main.py +0 -0
  120. {microgrid-0.1.2 → microgrid-0.1.4}/tests/engine_fixtures.py +0 -0
  121. {microgrid-0.1.2 → microgrid-0.1.4}/tests/legacy_dispatch.py +0 -0
  122. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_api.py +0 -0
  123. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_assess.py +0 -0
  124. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_by_source.py +0 -0
  125. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_config.py +0 -0
  126. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_dc_loads.py +0 -0
  127. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_dispatch_parts.py +0 -0
  128. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_engine_config.py +0 -0
  129. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_examples.py +0 -0
  130. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_ground_truth.py +0 -0
  131. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_inputs.py +0 -0
  132. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_internals.py +0 -0
  133. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_ledger.py +0 -0
  134. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_ledger_properties.py +0 -0
  135. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_limits.py +0 -0
  136. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_live_core.py +0 -0
  137. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_meters.py +0 -0
  138. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_models.py +0 -0
  139. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_outputs.py +0 -0
  140. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_paths.py +0 -0
  141. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_plant.py +0 -0
  142. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_plant_properties.py +0 -0
  143. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_port.py +0 -0
  144. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_projection.py +0 -0
  145. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_pv_per_array.py +0 -0
  146. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_rule_on_port.py +0 -0
  147. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_scoring.py +0 -0
  148. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_series.py +0 -0
  149. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_stored_cost.py +0 -0
  150. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_supply_properties.py +0 -0
  151. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_timeline_properties.py +0 -0
  152. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_units.py +0 -0
  153. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_value.py +0 -0
  154. {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_without_battery_or_pv.py +0 -0
@@ -0,0 +1,154 @@
1
+ # Changelog
2
+
3
+ Each user-visible change, under the release it ships in.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.4] - 2026-11-15
8
+
9
+ ### Added
10
+
11
+ - The Home Assistant integration's plant builder asks only what a battery's
12
+ specification states (capacity, limits, reserve, inverter rating) and proposes it
13
+ from the Energy dashboard's battery sensors over the last week; its efficiencies and
14
+ the inverter's draw take the library's defaults.
15
+ - `microgrid.cli.seen`: a battery's rating as its meters show it (the most it charged
16
+ and discharged, its capacity from the energy it took against its SOC).
17
+ - The Home Assistant integration's setup proposes what Home Assistant already knows: the
18
+ price, PV and load forecast integrations installed (TariffKit, Nord Pool, Amber,
19
+ Tibber, Octopus, ENTSO-E, OpenEI, Solcast, EMHASS, Predbat), each read at once where
20
+ it needs nothing more; the measured entities and the battery's capacity the Energy
21
+ dashboard names. Each meter's sign is checked against two days of the recorder's
22
+ history and, where the records say it reads the other way, set on confirmation. A
23
+ plant file and a `sources/1` file can be imported together. A plant is set up once.
24
+ - `microgrid.cli.signs`: each meter's sign, told from what the meters recorded (PV and
25
+ DC-bus loads positive, the battery against its SOC, the grid by the balance).
26
+ - A Home Assistant integration (`custom_components/microgrid`, installed from Home Assistant's
27
+ Marketplace, from a release's `microgrid.zip`; Home Assistant 2026.11 or later): the plant (`plant/2`, pasted or built step by step),
28
+ its measured entities and its sources configured in Home Assistant, each checked by
29
+ the library and each source read once before it is kept. It publishes the valuation
30
+ (its slots in an unrecorded attribute), the live value (marginal cost, surplus,
31
+ regime, values by source), the prices in force and the battery's floor as entities,
32
+ a problem sensor, and `microgrid.get_valuation` (`valuation/2`, the live value); runs
33
+ the battery's ledger over the recorder's history; raises a repair issue for a source
34
+ that keeps failing. *Run it in Home Assistant* documents it.
35
+ - Price readers for more Home Assistant integrations, in `sources/1`: Nord Pool
36
+ (`from = "nordpool"`), Amber Electric (`amber`), Tibber (`tibber`), Octopus Energy
37
+ (`octopus`), ENTSO-E (`entsoe`) and OpenEI (`openei`), each reading what the
38
+ integration publishes. OpenEI publishes no rate past the next change, so its prices,
39
+ and the valuation's horizon, end there.
40
+ - `from = "action"`: any input from an action whose response holds rows, as an entity's
41
+ attribute would.
42
+ - A fixed `export` price per kWh, for prices whose source publishes the import alone
43
+ (an entity's or an action's rows, and the named readers with no export price).
44
+ Import and export prices from separate entities are split at each change of either.
45
+ - A forecast read from a source has each interval that starts or ends inside an engine
46
+ slot split where the slots begin, so quarter-hour prices are priced at 10-minute
47
+ slots, each slot at the mean of its pieces.
48
+ - `microgrid.cli.sources.api.HomeAssistantApi`: what a reader asks of Home Assistant.
49
+ The readers take any client that answers it, and import without an HTTP client.
50
+ `read.read_forecast`, `read.source_name`, `config.forecast_source` and
51
+ `config.measured_source` are public.
52
+
53
+ ### Changed (documentation)
54
+
55
+ - The documentation and the module docstrings no longer rebut what nobody said:
56
+ denials, disclaimers and contrasts with an alternative the text never raised are gone
57
+ ("not the average one", "never guessed", "not one class per topology", "fetches
58
+ none", "Pure: no I/O", ...). A contrast stays where the text argues it, as *The
59
+ marginal price* does the average cost.
60
+ - *Scope* keeps what a client brings and what the library can't model; its *Prices
61
+ energy, models no load* and *No I/O* sections are gone.
62
+ - *The plant model*'s section is *It models the plant's own rules*; *Replay*'s is
63
+ *What a replay reads*; *The make-up rule*'s *What the rule does not decide* is
64
+ *Grid charging*. R-5 is *Marginal*.
65
+ - Nothing is "yours to decide", "yours to add" or "the client's to correct": each
66
+ says who does what. No "your": *Describe a plant*, *Run a first valuation*, *Write
67
+ a plant model*, *Get prices from live sources*, and the text speaks of a plant, the
68
+ meters, the recorder.
69
+
70
+ ## [0.1.3] - 2026-10-18
71
+
72
+ ### Changed (documentation)
73
+
74
+ - The README, the documentation's home page, the install page and the package
75
+ docstrings no longer say that the plant model stands alone; how to use it without the
76
+ prices is its how-to (*Use the plant model on its own*).
77
+
78
+ ## [0.1.2] - 2026-10-18
79
+
80
+ ### Fixed (documentation)
81
+
82
+ - The documentation's home page: *What makes it different* is *How it prices*; the
83
+ parts name `microgrid.plant`, the plant model, apart from `microgrid.engine`; the
84
+ line saying to install from the git repository is gone (it is on PyPI).
85
+
86
+ ## [0.1.1] - 2026-10-18
87
+
88
+ ### Changed (documentation)
89
+
90
+ - The README, the documentation's home page and the package's summary say what the
91
+ library takes and returns, without the "data in, data out" slogan; why it does no I/O
92
+ is in *Scope* (`docs/methodology/scope.md`).
93
+
94
+ ## [0.1.0] - 2026-10-18
95
+
96
+ The first version, under the MIT license.
97
+
98
+ ### Added (library)
99
+
100
+ - **The plant model, `microgrid.plant`.** A plant described as data (`plant/2`): PV
101
+ arrays of either coupling, a battery on its own inverter or on a hybrid's DC bus,
102
+ loads on either bus, the grid's limits, grid-charge and export windows, hold rules
103
+ and the meters. `Plant.project` runs it slot by slot. It stands alone: importing it
104
+ loads none of the price code. Four example plants (`microgrid.plant.examples`), and
105
+ a port for a plant model of your own (`microgrid.plant.port`: `PlantModel`,
106
+ `Storage`), checked by `microgrid.engine.testing.assert_plant_model_contract`.
107
+ - **The price model, `microgrid.engine`.** Marginal prices from inputs given as time
108
+ series (`Inputs`: prices, PV forecast quantiles, the load forecast, the SOC, the
109
+ battery's lots). For each slot it gives:
110
+ - the marginal cost, and the same for a dull day (the PV forecast's p10);
111
+ - the regime, the certainties and the surplus;
112
+ - the supply stack;
113
+ - the value of a PV kWh and of a battery kWh diverted to a flexible load.
114
+
115
+ `Engine.value` gives these as a valuation (`valuation/2`). `Engine.live_session`
116
+ gives a live value between valuations.
117
+ - **A battery kWh** is priced at the running average cost of what the battery holds
118
+ (`Pricing.battery_value = "stored_cost"`), or by the make-up rule (`"replacement"`).
119
+ - **Missing or stale inputs** move values toward the import price, and the valuation
120
+ names them in `degraded`.
121
+ - **Configuration:** `engine/2`.
122
+ - **Series and units** (`microgrid.engine.series`, `microgrid.engine.units`).
123
+ - Forecasts are series of issues, read as a client had them at a moment.
124
+ - Units are converted at the edge.
125
+ - Money is in the prices' currency, never converted, and no name says one.
126
+ - **The battery's lots** (`microgrid.engine.ledger`): what the battery holds by source,
127
+ and what it cost.
128
+ - **Analysis** (`microgrid.engine.analysis`): the plant model scored against its meters,
129
+ and valuations scored against what each kWh realised.
130
+
131
+ ### Added (command)
132
+
133
+ - **`microgrid` on a dataset** (`dataset/1`): `value`, `live`, `replay`, `check-plant`,
134
+ `ledger`, `fit-battery`, `config check` and `schema`. A dataset whose prices name two
135
+ currencies is refused.
136
+ - **`microgrid` on live sources** (`sources/1`, the `sources` extra): `sources`, `now` and
137
+ `fetch`. Each input comes from a provider's Home Assistant integration (TariffKit,
138
+ Solcast, EMHASS, Predbat) or an entity.
139
+ - **The example dataset** (`examples/dataset`): a synthetic week of an example plant in
140
+ Europe/Berlin, across the DST change, with the outputs it is expected to give.
141
+
142
+ ### Added (releases)
143
+
144
+ - **Versions and releases:** semantic versioning, explained beside the formats', the
145
+ API's and the model's versions (`docs/reference/versions.md`); `microgrid --version`
146
+ prints the release with the API (`engine-api/5`) and the model (`pricing/1.3`) it
147
+ carries. Releases are published to
148
+ PyPI from a tag (`vX.Y.Z`).
149
+
150
+ ### Added (documentation)
151
+
152
+ - **The documentation:** the methodology and the library's requirements (R-1 to R-9),
153
+ the concepts, how-to guides and the reference, published with each release at
154
+ `https://eman.github.io/microgrid/`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: microgrid
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A plant model and marginal energy prices for a solar + battery microgrid
5
5
  Project-URL: Homepage, https://github.com/eman/microgrid
6
6
  Project-URL: Documentation, https://eman.github.io/microgrid/
@@ -30,8 +30,7 @@ Description-Content-Type: text/markdown
30
30
 
31
31
  `microgrid` is a library that models a solar + battery plant (`microgrid.plant`) and
32
32
  produces **marginal energy prices** from it (`microgrid.engine`): what one more kWh
33
- costs, slot by slot over a horizon, and the value of energy by source. The plant model
34
- also stands alone, without the price code.
33
+ costs, slot by slot over a horizon, and the value of energy by source.
35
34
 
36
35
  You describe the plant (`plant/2`: PV of either coupling, a battery on its own inverter
37
36
  or on a hybrid's DC bus, loads on either bus, the grid's limits and charge windows) and
@@ -49,6 +48,10 @@ pip install microgrid # or: uv add microgrid
49
48
  pip install "microgrid[sources]" # with the readers of live sources (Home Assistant)
50
49
  ```
51
50
 
51
+ In Home Assistant, the integration (`custom_components/microgrid`, through its Marketplace) installs
52
+ it and runs it on Home Assistant's own entities
53
+ ([Run it in Home Assistant](https://github.com/eman/microgrid/blob/main/docs/how-to/home-assistant.md)).
54
+
52
55
  Python 3.14 or later. Before 1.0 a minor release may change a format, the API or the
53
56
  model: pin the minor release you tested with (`microgrid ~= 0.1.0`,
54
57
  [Versions](https://github.com/eman/microgrid/blob/main/docs/reference/versions.md)).
@@ -76,8 +79,8 @@ uv run microgrid config check --data examples/dataset # its plant/2 and engine
76
79
  uv run microgrid schema # the outputs' schema
77
80
  ```
78
81
 
79
- With a `sources/1` file naming a provider per input (any of several, none assumed; the
80
- `sources` extra, `microgrid[sources]`), it reads live inputs instead of a dataset:
82
+ With a `sources/1` file naming a provider per input (the `sources`
83
+ extra, `microgrid[sources]`), it reads live inputs instead of a dataset:
81
84
 
82
85
  ```sh
83
86
  uv run microgrid sources --sources sources.toml # what each input is read from, or why not
@@ -2,8 +2,7 @@
2
2
 
3
3
  `microgrid` is a library that models a solar + battery plant (`microgrid.plant`) and
4
4
  produces **marginal energy prices** from it (`microgrid.engine`): what one more kWh
5
- costs, slot by slot over a horizon, and the value of energy by source. The plant model
6
- also stands alone, without the price code.
5
+ costs, slot by slot over a horizon, and the value of energy by source.
7
6
 
8
7
  You describe the plant (`plant/2`: PV of either coupling, a battery on its own inverter
9
8
  or on a hybrid's DC bus, loads on either bus, the grid's limits and charge windows) and
@@ -21,6 +20,10 @@ pip install microgrid # or: uv add microgrid
21
20
  pip install "microgrid[sources]" # with the readers of live sources (Home Assistant)
22
21
  ```
23
22
 
23
+ In Home Assistant, the integration (`custom_components/microgrid`, through its Marketplace) installs
24
+ it and runs it on Home Assistant's own entities
25
+ ([Run it in Home Assistant](https://github.com/eman/microgrid/blob/main/docs/how-to/home-assistant.md)).
26
+
24
27
  Python 3.14 or later. Before 1.0 a minor release may change a format, the API or the
25
28
  model: pin the minor release you tested with (`microgrid ~= 0.1.0`,
26
29
  [Versions](https://github.com/eman/microgrid/blob/main/docs/reference/versions.md)).
@@ -48,8 +51,8 @@ uv run microgrid config check --data examples/dataset # its plant/2 and engine
48
51
  uv run microgrid schema # the outputs' schema
49
52
  ```
50
53
 
51
- With a `sources/1` file naming a provider per input (any of several, none assumed; the
52
- `sources` extra, `microgrid[sources]`), it reads live inputs instead of a dataset:
54
+ With a `sources/1` file naming a provider per input (the `sources`
55
+ extra, `microgrid[sources]`), it reads live inputs instead of a dataset:
53
56
 
54
57
  ```sh
55
58
  uv run microgrid sources --sources sources.toml # what each input is read from, or why not
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "microgrid"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "A plant model and marginal energy prices for a solar + battery microgrid"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -55,6 +55,10 @@ dev = [
55
55
  { include-group = "docs" },
56
56
  # The sources extra, so the gate covers the readers.
57
57
  "microgrid[sources]",
58
+ # The Home Assistant integration's tests (custom_components/microgrid).
59
+ # Home Assistant's own harness, released with each Home Assistant version (it pins
60
+ # that version); Home Assistant 2026.11 needs Python 3.14.2.
61
+ "pytest-homeassistant>=2026.11.1; python_full_version >= '3.14.2'",
58
62
  ]
59
63
 
60
64
  [project.scripts]
@@ -86,11 +90,13 @@ include = [
86
90
  "/LICENSE",
87
91
  "/pyproject.toml",
88
92
  ]
93
+ # The Home Assistant integration's tests need the integration, which the sdist leaves out.
94
+ exclude = ["/tests/ha", "/tests/ha_fixtures.py", "/tests/test_manifest.py"]
89
95
 
90
96
  [tool.ruff]
91
97
  line-length = 100
92
98
  target-version = "py314"
93
- src = ["src", "tests"]
99
+ src = ["src", "tests", "custom_components"]
94
100
 
95
101
  [tool.ruff.lint]
96
102
  select = ["E", "F", "W", "I", "N", "UP", "B", "A", "ANN", "C4", "RET", "SIM", "PTH", "RUF"]
@@ -98,18 +104,30 @@ ignore = ["ANN401"]
98
104
 
99
105
  [tool.ruff.lint.per-file-ignores]
100
106
  "tests/**" = ["ANN"]
107
+ # The Home Assistant integration follows Home Assistant's conventions.
108
+ "custom_components/**" = ["A002", "N805"]
101
109
 
102
110
  [tool.mypy]
103
111
  strict = true
104
112
  warn_unreachable = true
105
113
  python_version = "3.14"
106
114
  plugins = ["pydantic.mypy"]
107
- files = ["src", "tests", "scripts"]
115
+ files = ["src", "tests", "scripts", "custom_components/microgrid"]
116
+ mypy_path = ["$MYPY_CONFIG_FILE_DIR/src", "$MYPY_CONFIG_FILE_DIR/tests", "$MYPY_CONFIG_FILE_DIR"]
117
+ explicit_package_bases = true
118
+
119
+ [[tool.mypy.overrides]]
120
+ # Home Assistant's test harness ships no type information.
121
+ module = ["pytest_homeassistant.*"]
122
+ ignore_missing_imports = true
108
123
 
109
124
  [tool.pytest.ini_options]
110
125
  testpaths = ["tests"]
111
- addopts = "-q --strict-markers"
112
- asyncio_mode = "strict"
126
+ # The repository (custom_components) and the tests' shared modules.
127
+ pythonpath = [".", "tests"]
128
+ # respx's plugin imports httpx before Home Assistant aliases it to httpx2.
129
+ addopts = "-q --strict-markers -p no:respx"
130
+ asyncio_mode = "auto"
113
131
  markers = [
114
132
  "live: needs the site's HA, EMQX or InfluxDB; never run by the local gate",
115
133
  ]
@@ -33,17 +33,24 @@ if [ "${1:-}" = "--docs" ]; then
33
33
  fi
34
34
 
35
35
  step "ruff check"
36
- uv run --locked ruff check src tests scripts
36
+ uv run --locked ruff check src tests scripts custom_components
37
37
 
38
38
  step "ruff format --check"
39
- uv run --locked ruff format --check src tests scripts
39
+ uv run --locked ruff format --check src tests scripts custom_components
40
40
 
41
41
  step "mypy (strict)"
42
42
  uv run --locked mypy
43
43
 
44
44
  docs
45
45
 
46
- step "pytest"
47
- uv run --locked pytest -m "not live" --cov --cov-report=term
46
+ step "pytest (the library, without Home Assistant)"
47
+ # Home Assistant's harness loads in every session, and Home Assistant 2026.11 makes
48
+ # `import httpx` resolve to httpx2 process-wide: the library outside Home Assistant runs
49
+ # on httpx itself, so it is tested without the harness.
50
+ uv run --locked pytest -m "not live" --ignore=tests/ha -p no:pytest_homeassistant \
51
+ --cov --cov-report=
52
+
53
+ step "pytest (the Home Assistant integration)"
54
+ uv run --locked pytest -m "not live" tests/ha --cov --cov-append --cov-report=term
48
55
 
49
56
  printf '\nlocal CI: all checks passed\n'
@@ -1,11 +1,10 @@
1
1
  """microgrid: a solar + battery plant modelled, and its marginal energy prices.
2
2
 
3
- - :mod:`microgrid.plant`: the plant model and its simulator, on their own: a plant
3
+ - :mod:`microgrid.plant`: the plant model and its simulator: a plant
4
4
  described as data (``plant/2``), projected slot by slot.
5
5
  - :mod:`microgrid.engine`: the marginal prices, a client of the plant model.
6
6
  - :mod:`microgrid.cli`: the ``microgrid`` command, the library run on a dataset's files
7
- (``dataset/1``) or on a site's live sources (``sources/1``, the ``sources`` extra). A
8
- client of the public names, as any other.
7
+ (``dataset/1``) or on a site's live sources (``sources/1``, the ``sources`` extra).
9
8
 
10
9
  A requirement cited as R-n is the documentation's (``docs/methodology/requirements.md``).
11
10
  """
@@ -9,6 +9,8 @@ files (``dataset/1``), or on a site's live sources (``sources/1``: ``now``, ``so
9
9
  battery's lots.
10
10
  - :mod:`.offline`, :mod:`.replay`, :mod:`.check_plant`, :mod:`.fit_battery`,
11
11
  :mod:`.evaluation`: the commands' work on a dataset.
12
+ - :mod:`.signs`: each meter's sign, told from what it recorded; :mod:`.seen`, a
13
+ battery's rating as its meters show it.
12
14
  - :mod:`.sources`: the readers of the live sources a ``sources/1`` file names (Home
13
15
  Assistant, TariffKit, Solcast, EMHASS, Predbat, any entity), behind the ``sources``
14
16
  extra. A service that runs the library on a site builds on them.
@@ -16,8 +18,7 @@ files (``dataset/1``), or on a site's live sources (``sources/1``: ``now``, ``so
16
18
  **The public surface** is each module's ``__all__``: what another client may import. The
17
19
  library's architecture test holds every module to declaring one.
18
20
 
19
- It reads and writes files and the network; the engine (:mod:`microgrid.engine`) does no
20
- I/O, and this package uses only the engine's public names.
21
+ It reads and writes files and the network.
21
22
  """
22
23
 
23
24
  __all__ = [
@@ -30,6 +31,8 @@ __all__ = [
30
31
  "main",
31
32
  "offline",
32
33
  "replay",
34
+ "seen",
35
+ "signs",
33
36
  "sources",
34
37
  "toml_text",
35
38
  ]
@@ -1,7 +1,7 @@
1
1
  """A dataset: a directory of the library's series (``dataset/1``), so every command
2
2
  runs without the plant it came from.
3
3
 
4
- **The format, ``dataset/1``,** is the library's own terms, not any integration's:
4
+ **The format, ``dataset/1``,** is the library's own terms:
5
5
 
6
6
  - ``manifest.json``: ``{"format": "dataset/1", "entries": {name: {"file": ...,
7
7
  "kind": ..., "forecasts": bool}}}``. An entry's ``kind`` is how its values read:
@@ -13,9 +13,9 @@ battery and a DC-bus load, allocated at every change, only while the path was al
13
13
  within the heartbeat's limit where the bindings name one, else wherever each series is
14
14
  known), the SOC read at each slot boundary, and the prices each slot was charged at as
15
15
  the time-weighted mean of the recorded price in force over it (``series.means``): any
16
- price resolution, on any slot, and no price read before it took effect. A recorder that
17
- writes a change late (a sensor updating seconds after the hour) is the client's to
18
- correct at its edge, as for any series. A PV lot's basis is the export price when it was
16
+ price resolution, on any slot, and no price read before it took effect. A client corrects
17
+ a recorder that writes a change late (a sensor updating seconds after the hour) at
18
+ its edge, as for any series. A PV lot's basis is the export price when it was
19
19
  charged (what the site gave up to store it), a grid lot's the import price
20
20
  (docs/methodology/lots.md). Every function takes the slot it works on
21
21
  (``EngineConfig.slot``; :data:`~microgrid.engine.series.SLOT` by default).
@@ -0,0 +1,114 @@
1
+ """What a battery's meters show of its rating: the most it was seen to charge and
2
+ discharge, and its capacity from the energy it took against its SOC's change.
3
+
4
+ A proposal for a plant's file, where the battery's own figures are not to hand, not a
5
+ fit: the most seen is a floor on the battery's limit (a week may never reach it), and
6
+ the capacity is the energy charged and discharged over the SOC's travel, which leaves
7
+ the conversion's losses out to first order (charging overstates it and discharging
8
+ understates it by about as much). Pure: the caller reads the meters.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import math
14
+ from dataclasses import dataclass
15
+ from datetime import datetime, timedelta
16
+ from typing import Final
17
+
18
+ from microgrid.engine.series import Series, means, value_at
19
+
20
+ __all__ = ["BatterySeen", "battery_seen", "rounded"]
21
+
22
+ #: The span the most seen is taken over: a slot's mean, so a reading's spike is not
23
+ #: taken for the battery's rate.
24
+ PEAK_SLOT: Final = timedelta(minutes=5)
25
+ #: The SOC must travel this far (a fraction, summed) for the capacity to be told.
26
+ _TRAVEL_MIN: Final = 0.3
27
+ #: An hour that moved less than this (kWh) is idle: what the SOC does then (a drift,
28
+ #: a standby draw the meter does not see) is not the meter's energy.
29
+ _IDLE_KWH: Final = 0.05
30
+ #: A SOC this close to the highest or lowest it reached is at its bound: held there, it
31
+ #: cannot step to follow what the meter shows (a battery kept full by PV, still
32
+ #: balancing at full). The bound is what the sensor reached, not 1 or 0: some top out
33
+ #: at 99 %.
34
+ _AT_BOUND: Final = 0.005
35
+ _HOUR: Final = timedelta(hours=1)
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class BatterySeen:
40
+ """The battery's figures its meters show; None where they do not."""
41
+
42
+ #: The most it charged and discharged, kW, as a :data:`PEAK_SLOT` mean.
43
+ charge_kw: float | None
44
+ discharge_kw: float | None
45
+ #: Its capacity, kWh.
46
+ capacity_kwh: float | None
47
+
48
+
49
+ def _slots(start: datetime, end: datetime, step: timedelta) -> list[datetime]:
50
+ return [start + i * step for i in range(int((end - start) / step))]
51
+
52
+
53
+ def battery_seen(
54
+ power: Series[float], soc: Series[float] | None, start: datetime, end: datetime
55
+ ) -> BatterySeen:
56
+ """The figures ``power`` (kW, positive charging) and ``soc`` (a fraction) show
57
+ between ``start`` and ``end`` (whole hours)."""
58
+ peaks = [v for v in means(power, _slots(start, end, PEAK_SLOT), PEAK_SLOT) if v is not None]
59
+ charge = max((v for v in peaks if v > 0), default=None)
60
+ discharge = max((-v for v in peaks if v < 0), default=None)
61
+ return BatterySeen(
62
+ charge, discharge, None if soc is None else _capacity(power, soc, start, end)
63
+ )
64
+
65
+
66
+ def _capacity(
67
+ power: Series[float], soc: Series[float], start: datetime, end: datetime
68
+ ) -> float | None:
69
+ """The energy moved over the SOC's travel. An hour the SOC did not step keeps its
70
+ energy: over a run of hours the SOC's travel adds up to the true change within one
71
+ step, so leaving such hours out would leave energy out and no travel. An hour where
72
+ the meter and the SOC go opposite ways (a step from the hour before, a meter out of
73
+ step) is left out, as is an hour the SOC spends at the highest or lowest it reached,
74
+ where it cannot move however the meter reads."""
75
+ starts = _slots(start, end, _HOUR)
76
+ kw = means(power, starts, _HOUR)
77
+ reached = [s.value for s in soc.samples if s.value is not None and start <= s.at <= end]
78
+ if not reached:
79
+ return None
80
+ top, bottom = max(reached), min(reached)
81
+ moved: list[float] = []
82
+ travelled: list[float] = []
83
+ for hour, mean in zip(starts, kw, strict=True):
84
+ before, after = value_at(soc, hour), value_at(soc, hour + _HOUR)
85
+ if mean is None or before is None or after is None:
86
+ continue
87
+ kwh, change = mean * 1.0, after - before
88
+ if abs(kwh) < _IDLE_KWH or kwh * change < 0 or _held(before, after, top, bottom):
89
+ continue
90
+ moved.append(abs(kwh))
91
+ travelled.append(abs(change))
92
+ total = math.fsum(travelled)
93
+ if total < _TRAVEL_MIN:
94
+ return None
95
+ return math.fsum(moved) / total
96
+
97
+
98
+ def _held(before: float, after: float, top: float, bottom: float) -> bool:
99
+ """Whether the SOC was at the same bound (the highest or lowest it reached) at both
100
+ ends of an hour."""
101
+ return min(before, after) >= top - _AT_BOUND or max(before, after) <= bottom + _AT_BOUND
102
+
103
+
104
+ def _round_up(value: float | None, step: float = 0.1) -> float | None:
105
+ return None if value is None else round(math.ceil(value / step - 1e-9) * step, 1)
106
+
107
+
108
+ def rounded(seen: BatterySeen) -> BatterySeen:
109
+ """``seen`` to a tenth: the limits up, the capacity to the nearest."""
110
+ return BatterySeen(
111
+ charge_kw=_round_up(seen.charge_kw),
112
+ discharge_kw=_round_up(seen.discharge_kw),
113
+ capacity_kwh=None if seen.capacity_kwh is None else round(seen.capacity_kwh, 1),
114
+ )