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.
- microgrid-0.1.4/CHANGELOG.md +154 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/PKG-INFO +8 -5
- {microgrid-0.1.2 → microgrid-0.1.4}/README.md +7 -4
- {microgrid-0.1.2 → microgrid-0.1.4}/pyproject.toml +23 -5
- {microgrid-0.1.2 → microgrid-0.1.4}/scripts/ci_local.sh +11 -4
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/__init__.py +2 -3
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/__init__.py +5 -2
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/dataset.py +1 -1
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/history.py +3 -3
- microgrid-0.1.4/src/microgrid/cli/seen.py +114 -0
- microgrid-0.1.4/src/microgrid/cli/signs.py +190 -0
- microgrid-0.1.4/src/microgrid/cli/sources/__init__.py +55 -0
- microgrid-0.1.4/src/microgrid/cli/sources/action.py +66 -0
- microgrid-0.1.4/src/microgrid/cli/sources/amber.py +105 -0
- microgrid-0.1.4/src/microgrid/cli/sources/api.py +54 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/config.py +287 -32
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/emhass.py +2 -2
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/entity.py +86 -62
- microgrid-0.1.4/src/microgrid/cli/sources/entsoe.py +64 -0
- microgrid-0.1.4/src/microgrid/cli/sources/nordpool.py +79 -0
- microgrid-0.1.4/src/microgrid/cli/sources/octopus.py +86 -0
- microgrid-0.1.4/src/microgrid/cli/sources/openei.py +116 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/predbat.py +2 -2
- microgrid-0.1.4/src/microgrid/cli/sources/prices.py +79 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/read.py +79 -18
- microgrid-0.1.4/src/microgrid/cli/sources/rows.py +150 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/solcast.py +3 -3
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/tariffkit.py +3 -3
- microgrid-0.1.4/src/microgrid/cli/sources/tibber.py +69 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/__init__.py +2 -2
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_engine_config.py +2 -2
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/analysis/__init__.py +1 -1
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/analysis/ground_truth.py +2 -2
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/analysis/scoring.py +2 -4
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/extend.py +0 -2
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/ledger.py +0 -2
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/series.py +2 -6
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/testing.py +0 -2
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/units.py +3 -6
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/__init__.py +3 -8
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/examples.py +0 -2
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/flows.py +4 -5
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/port.py +4 -8
- microgrid-0.1.4/tests/cli/test_price_readers.py +566 -0
- microgrid-0.1.4/tests/cli/test_seen.py +91 -0
- microgrid-0.1.4/tests/cli/test_signs.py +80 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_sources.py +6 -2
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_architecture.py +21 -2
- microgrid-0.1.4/tests/test_httpx.py +12 -0
- microgrid-0.1.2/CHANGELOG.md +0 -83
- microgrid-0.1.2/src/microgrid/cli/sources/__init__.py +0 -39
- {microgrid-0.1.2 → microgrid-0.1.4}/.gitignore +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/LICENSE +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/engine.toml +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/expected/ledger-2026-10-21.txt +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/expected/live-2026-10-25.txt +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/expected/replay-2026-10-22.txt +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/expected/value-2026-10-22T13-03.txt +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/expected/value-2026-10-25T02-30.txt +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/forecasts/load_forecast.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/forecasts/prices.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/forecasts/pv_forecast.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/manifest.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/plant.toml +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/battery.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/battery_first.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/export_price.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/grid.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/heartbeat.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/house.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/import_price.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/inverter.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/pv.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/examples/dataset/series/soc.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/scripts/griffe_microgrid.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/scripts/install-hooks.sh +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/scripts/make_example_dataset.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/scripts/make_expected_outputs.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/scripts/mkdocs_hooks.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/_checks.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/check_plant.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/evaluation.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/fit_battery.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/inputs.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/main.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/offline.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/replay.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/fetch.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/sources/home_assistant.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/cli/toml_text.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_api.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_assess.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_inputs.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_live.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_stored.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_supply.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_surplus.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_timeline.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/_value.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/py.typed +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/engine/schemas/valuation-2.schema.json +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/_battery.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/_config.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/_meters.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/_paths.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/plant/_plant.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/src/microgrid/py.typed +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/architecture.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/makeup/engine.toml +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/makeup/live-2026-10-25.txt +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/makeup/replay-2026-10-22.txt +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/makeup/value-2026-10-22T13-03.txt +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/makeup/value-2026-10-25T02-30.txt +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_dataset.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_end_to_end.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_evaluation_properties.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_fit_battery.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_history.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/cli/test_main.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/engine_fixtures.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/legacy_dispatch.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_api.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_assess.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_by_source.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_config.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_dc_loads.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_dispatch_parts.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_engine_config.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_examples.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_ground_truth.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_inputs.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_internals.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_ledger.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_ledger_properties.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_limits.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_live_core.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_meters.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_models.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_outputs.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_paths.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_plant.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_plant_properties.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_port.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_projection.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_pv_per_array.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_rule_on_port.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_scoring.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_series.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_stored_cost.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_supply_properties.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_timeline_properties.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_units.py +0 -0
- {microgrid-0.1.2 → microgrid-0.1.4}/tests/test_value.py +0 -0
- {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.
|
|
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.
|
|
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 (
|
|
80
|
-
|
|
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.
|
|
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 (
|
|
52
|
-
|
|
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.
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
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).
|
|
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
|
|
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
|
|
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
|
|
17
|
-
writes a change late (a sensor updating seconds after the hour)
|
|
18
|
-
|
|
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
|
+
)
|