From 120c260c80e0b9edbc91f10d216fc56464509e9f Mon Sep 17 00:00:00 2001 From: Johannes Ott Date: Tue, 4 Aug 2026 00:16:50 +0200 Subject: [PATCH 1/3] chore: pin dependencies exactly and settle the resolution decision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Phase 0 loose ends, both about making later phases verifiable. Dependencies are now pinned with == across all extras, matching solaredge2mqtt and learninghouse. Updates arrive as individual dependabot pull requests that run the full suite, rather than silently on whatever day a transitive resolve changes. scikit-learn is the only pin that is load-bearing rather than tidy. Verified empirically: the frozen baseline reproduces bit-identically across numpy 2.4.6/2.5.1, pandas 3.0.3/3.0.5 and scipy 1.17.1/1.18.0 as long as scikit-learn stays at 1.9.0. A scikit-learn bump therefore has to regenerate the baseline and say so in the changelog; the others do not. The forecast resolution question from chapter 6 is decided and written up as a new chapter 3.5. The MVP computes hourly; the data model keeps finer resolutions open without implementing them. The interval is a brain property, constrained by both the provider and what the client can push, not a provider property as originally framed. What becomes interval-aware now, because retrofitting it would invalidate every trained model: interval as a required field in brain config and model metadata with invalidation on mismatch, time features on minutes-since-midnight rather than hour, power_period derived from the interval instead of a fixed hour, and aggregation that sums intervals within a period. Why not implement it now: there are no sub-hourly measurements to train on and no baseline for such a path, OpenWeatherMap users are permanently hourly so it would mean two code paths before the first release, and Open-Meteo serves minutely_15 natively only in Central Europe and North America — elsewhere it interpolates hourly data, which yields four times the rows with the same information and strongly autocorrelated neighbours that flatter any naive holdout. Chapter 3.3 also records the Phase 0 measurement that justifies dropping the power model: MAE 620.88 Wh against 624.93 W, R2 0.886 against 0.885. Co-Authored-By: Claude Opus 5 Signed-off-by: Johannes Ott --- pvlearn-umsetzungsplan.md | 60 +++++++++++++++++++++++++++++++++------ pyproject.toml | 47 +++++++++++++++++------------- 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/pvlearn-umsetzungsplan.md b/pvlearn-umsetzungsplan.md index bffdad3..e29f745 100644 --- a/pvlearn-umsetzungsplan.md +++ b/pvlearn-umsetzungsplan.md @@ -81,16 +81,21 @@ Bleiben inhaltlich wie bisher (`TimeEncoder`, `SunEncoder`), aber mit zwei Ände - `SunEncoder` bekommt statt eines `LocationSettings`-Objekts primitive, serialisierbare Parameter: `latitude: float`, `longitude: float`, `timezone: str`. Andernfalls brechen `sklearn.clone()` und Pickling, und Multi-Tenancy ist nicht möglich. - `tzlocal.get_localzone()` auf Modulebene entfällt. Die Zeitzone wird pro Brain explizit gesetzt. Ein Service, der Anlagen in mehreren Zeitzonen bedient, kann sich keine Prozess-globale TZ leisten. +- `TimeEncoder` kodiert die Tageszeit als **Minuten seit Mitternacht**, nicht als Stunde. Bei stündlicher Auflösung ist das Ergebnis identisch; bei feinerer Auflösung wären `hour_sin/cos` für alle Intervalle innerhalb einer Stunde gleich und das Feature damit wertlos. Siehe 3.5. Zusätzlich zu prüfen: `ephem` und `astral` werden derzeit parallel verwendet. Eine der beiden Abhängigkeiten sollte entfallen; `astral` deckt Azimut, Elevation und Sonnenauf-/-untergang vollständig ab. ### 3.3 Zielgröße -**Nur noch ein Modell: Energie pro Stunde in Wh.** +**Nur noch ein Modell: Energie pro Intervall in Wh.** Das bisherige Power-Modell entfällt. Begründung: Beide Modelle trainieren auf identischen Features, und bei Stundenauflösung ist die mittlere Leistung numerisch identisch zur Stundenenergie. Der Wegfall halbiert Trainingszeit, Cache-Bedarf und Wartungsaufwand. -`power_period` wird **weiterhin publiziert**, abgeleitet als `energy_wh / 1h`. Damit ist der Wegfall kein Breaking Change für MQTT-Konsumenten. Was verloren geht, ist die Momentanleistung zum Zeitstempel; das braucht weder das Energy Dashboard noch ein bekannter Automations-Use-Case. Im Changelog als „jetzt Stundenmittel statt Momentanwert" dokumentieren. +Auf dem Referenzdatensatz aus Phase 0 ist das empirisch bestätigt: MAE 620,88 Wh für das Energiemodell gegenüber 624,93 W für das Leistungsmodell, R² 0,886 gegenüber 0,885. Die beiden Modelle liegen unter einem Prozent auseinander. + +`power_period` wird **weiterhin publiziert**, abgeleitet als `energy_wh / interval`. Damit ist der Wegfall kein Breaking Change für MQTT-Konsumenten. Was verloren geht, ist die Momentanleistung zum Zeitstempel; das braucht weder das Energy Dashboard noch ein bekannter Automations-Use-Case. Im Changelog als „jetzt Intervallmittel statt Momentanwert" dokumentieren. + +Die Division durch das Intervall statt fest durch eine Stunde ist der einzige Grund, warum eine spätere Umstellung auf feinere Auflösung kein stiller Faktor-4-Fehler wird. ### 3.4 Modell-Metadaten und Invalidierung @@ -102,6 +107,7 @@ Jedes persistierte Modell trägt: "feature_schema_version": 1, "sklearn_version": "1.5.2", "weather_provider": "open-meteo", + "interval_minutes": 60, "location": {"latitude": 49.45, "longitude": 11.08, "timezone": "Europe/Berlin"}, "trained_at": "2026-08-03T12:20:00+02:00", "training_rows": 1440, @@ -110,10 +116,44 @@ Jedes persistierte Modell trägt: } ``` -Beim Laden gilt hart: **Stimmt `feature_schema_version`, die sklearn-Minor-Version, der Provider oder die Location nicht überein, wird das Modell verworfen und neu trainiert.** Kein Migrationsversuch, kein Best-Effort-Laden. Stumme Fehlprognosen durch ein Modell, das auf einem anderen Feature-Set trainiert wurde, sind praktisch nicht debugbar. +Beim Laden gilt hart: **Stimmt `feature_schema_version`, die sklearn-Minor-Version, der Provider, das Intervall oder die Location nicht überein, wird das Modell verworfen und neu trainiert.** Kein Migrationsversuch, kein Best-Effort-Laden. Stumme Fehlprognosen durch ein Modell, das auf einem anderen Feature-Set trainiert wurde, sind praktisch nicht debugbar. + +Zur sklearn-Version: die Phase-0-Baseline ist nur gegen exakt die Version reproduzierbar, unter der sie entstanden ist. `pvlearn` pinnt scikit-learn deshalb exakt (siehe 6.6); ein Bump verschiebt still jede Prognose und erfordert eine neu erzeugte Baseline. **Persistenzformat:** joblib/Pickle. ONNX wurde geprüft und verworfen — `CyclicalEncoder`, `TimeEncoder`, `SunEncoder` und `PFISelector` sind Custom-Transformer und bräuchten je einen eigenen Shape Calculator plus Converter. Der ursprüngliche Motivator (leichtgewichtige Inferenz in der HA-Integration) entfällt ohnehin, weil die Integration in der Zielarchitektur ein reiner REST-Client ist. + +### 3.5 Prognoseauflösung + +**Entschieden: Das MVP rechnet stündlich. Das Datenmodell hält feinere Auflösungen offen, ohne sie zu implementieren.** + +Das Intervall ist **keine Provider-Eigenschaft**, sondern eine Brain-Eigenschaft, begrenzt von beiden Seiten: + +``` +interval = min(was der Provider liefert, was der Client an Messwerten pusht) +``` + +Open-Meteo kann 15 Minuten, aber wenn der Wechselrichter nur Stundenwerte meldet, nützt das nichts. OpenWeatherMap kann 15 Minuten grundsätzlich nicht. Als Provider-Attribut modelliert entstehen sofort widersprüchliche Zustände. + +**Was jetzt intervall-fähig gebaut wird** — kostet zum jetzigen Zeitpunkt nichts, wäre später eine Migration, die jedes trainierte Modell invalidiert: + +- `interval` als Pflichtfeld in Brain-Konfiguration und Modell-Metadaten, mit Invalidierung bei Abweichung (siehe 3.4) +- Zeit-Features auf Minuten seit Mitternacht (siehe 3.2) +- `power_period` aus dem Intervall abgeleitet (siehe 3.3) +- Aggregationslogik summiert Intervalle innerhalb eines Zeitraums, statt „eine Zeile = eine Stunde" anzunehmen +- Der Prognose-Endpunkt gibt das Intervall in der Antwort mit an + +**Warum nicht sofort implementieren:** + +*Keine Messdaten.* solaredge2mqtt schreibt stündlich. Eine Umstellung erfordert eine Änderung der Datenerfassung und danach Monate Sammelzeit. Referenzdatensatz und Baseline aus Phase 0 sind stündlich — für einen 15-Minuten-Pfad existiert keine Baseline und damit keine Abnahme. + +*Zwei Codepfade ab Tag eins.* OWM-Bestandsnutzer bleiben zwingend stündlich. Jede Aggregation, jede Invalidierungsregel und jeder Test existierte doppelt, bevor überhaupt ein Release draußen ist. + +*Die Interpolationsfalle.* Open-Meteo liefert `minutely_15` nativ nur in Mitteleuropa (ICON-D2, AROME) und Nordamerika (HRRR), dort inklusive Strahlung. Außerhalb dieser Abdeckung — geografisch wie jenseits des Modellhorizonts — gibt die API interpolierte Stundenwerte zurück. Das ergibt viermal so viele Zeilen mit derselben Information, und die künstlich erzeugten Nachbarzeilen sind stark autokorreliert. Ein naives Holdout hält so ein Modell für besser, als es ist. Wer das umsetzt, muss die native Abdeckung prüfen und bei Interpolation ablehnen statt stillschweigend zu trainieren. + +*Rechenaufwand.* Vier Mal so viele Trainingszeilen verschärfen Punkt 6.4 unmittelbar. + +Frühestens Phase 6, konsistent mit der Gegenmaßnahme zum Scope-Creep-Risiko in Kapitel 7. --- ## 4. Phasenplan @@ -174,7 +214,8 @@ Ohne diesen Schritt ist Phase 1a nicht verifizierbar. - Feature-Konstanten auf das kanonische Schema aus Kapitel 3.1 umstellen. - OWM-Adapter in solaredge2mqtt: mappt `OpenWeatherMapForecastData` auf das kanonische Schema. `weather_id` → WMO-Mapping. - `SunEncoder` auf primitive Parameter umstellen, TZ explizit. -- `power_period` aus dem Energiemodell ableiten. +- `power_period` aus dem Energiemodell ableiten, über das konfigurierte Intervall statt fest über eine Stunde. +- `interval` in Konfiguration und Modell-Metadaten einführen, vorerst ausschließlich mit dem Wert 60 Minuten (siehe 3.5). - Metriken beim Training berechnen und in den Metadaten ablegen (MAE, RMSE, R² auf einem `TimeSeriesSplit`-Holdout). - `feature_schema_version = 1` einführen, Invalidierungslogik implementieren. @@ -230,12 +271,12 @@ DELETE /api/v1/brains/{id} POST /api/v1/brains/{id}/measurements [{timestamp, energy_wh}, ...] POST /api/v1/brains/{id}/train Training anstoßen (async, 202) GET /api/v1/brains/{id}/status is_trained, rows, last_training, metrics, features -GET /api/v1/brains/{id}/forecast ?days=2 → Stundenwerte + Aggregate +GET /api/v1/brains/{id}/forecast ?days=2 → Intervallwerte + Aggregate GET /api/v1/providers verfügbare Provider + max. Horizont GET /health ``` -Der Prognosehorizont ist providerabhängig und wird nicht hart kodiert: OpenWeatherMap One Call liefert 48 h stündlich, Open-Meteo bis zu 16 Tage. `GET /forecast` liefert maximal `min(days, provider_horizon)` und meldet den tatsächlichen Horizont im Response mit. +Der Prognosehorizont ist providerabhängig und wird nicht hart kodiert: OpenWeatherMap One Call liefert 48 h stündlich, Open-Meteo bis zu 16 Tage. `GET /forecast` liefert maximal `min(days, provider_horizon)` und meldet den tatsächlichen Horizont im Response mit, zusammen mit dem Intervall der gelieferten Werte. **Auth:** API-Key-Mechanismus aus `learninghouse` übernehmen. @@ -332,13 +373,14 @@ Mindestumfang vor dem ersten öffentlichen Release der Library: ## 6. Offene Entscheidungen -Diese Punkte sind noch nicht entschieden und sollten vor Beginn der jeweiligen Phase geklärt werden: +Diese Punkte sollten vor Beginn der jeweiligen Phase geklärt werden. Entschiedene Punkte bleiben mit Verweis auf die Begründung stehen, statt gelöscht zu werden. -1. **Prognoseintervall:** Bleibt es bei stündlicher Auflösung, oder sollen 15-Minuten-Werte möglich sein? Open-Meteo liefert `minutely_15` inklusive Strahlung. Betrifft das Datenmodell fundamental und sollte vor Phase 1b entschieden werden. +1. ~~**Prognoseintervall**~~ — **entschieden**, siehe 3.5. Das MVP rechnet stündlich, das Datenmodell hält feinere Auflösungen offen. Eine Implementierung kommt frühestens in Phase 6 und setzt voraus, dass die Messdatenerfassung auf der Client-Seite mitzieht. 2. **Unsicherheitsbänder:** `HistGradientBoostingRegressor` kann über `loss="quantile"` Quantilsprognosen liefern. Ein p10/p50/p90-Band wäre für Batteriesteuerung deutlich wertvoller als ein Punktwert — kostet aber drei Modelle statt einem, was der Konsolidierung aus 3.3 entgegenläuft. Kandidat für Phase 6. 3. **Mehrere Strings pro Anlage:** Ost-West-Anlagen könnten von getrennten Modellen je Ausrichtung profitieren. Erfordert, dass der Client getrennte Energiewerte liefert. Als optionales Feature denkbar; erhöht die Komplexität der API spürbar. 4. **Hyperparameter-Tuning im Service:** `GridSearchCV` über neun Kombinationen ist auf einem Raspberry Pi grenzwertig. Entweder deaktivieren, auf gelegentlich (wöchentlich) begrenzen oder auf `HalvingGridSearchCV` wechseln. 5. **Rückwärtsbefüllung:** Soll die HA-Integration beim Setup historische Werte aus dem Recorder nachliefern können? Das würde die Wartezeit bis zur ersten Prognose drastisch verkürzen — allerdings fehlen für die Vergangenheit die passenden Wetter-*Vorhersagen*. Open-Meteo bietet eine Historical-Forecast-API, die genau das liefert (archivierte Vorhersagen statt Reanalyse). Technisch die eleganteste Lösung des Kaltstartproblems, aber nicht trivial. +6. ~~**scikit-learn-Obergrenze**~~ — **entschieden**. Alle Abhängigkeiten sind in `pyproject.toml` exakt gepinnt, wie in `solaredge2mqtt` und `learninghouse`. Empirisch geprüft: die Baseline reproduziert bitidentisch über numpy 2.4.6/2.5.1, pandas 3.0.3/3.0.5 und scipy 1.17.1/1.18.0 hinweg, solange scikit-learn auf 1.9.0 bleibt. Damit ist scikit-learn der einzige Pin, an dem die Reproduzierbarkeit tatsächlich hängt — ein Bump erfordert zwingend eine neu erzeugte Baseline und einen Changelog-Eintrag. --- @@ -373,4 +415,4 @@ P4 HACS-Integration + Energy-Dashboard-Provider P5 Deprecation solaredge2mqtt_forecast ``` -Die drei Entscheidungen, die am schwersten zu revidieren sind und deshalb die meiste Sorgfalt verdienen: das **Feature-Schema** (Kapitel 3.1), die **Trainingsdaten-Semantik** (Phase 2) und die **Prognoseauflösung** (offener Punkt 6.1). +Die drei Entscheidungen, die am schwersten zu revidieren sind und deshalb die meiste Sorgfalt verdienen: das **Feature-Schema** (Kapitel 3.1), die **Trainingsdaten-Semantik** (Phase 2) und die **Prognoseauflösung** (Kapitel 3.5). Die dritte ist inzwischen entschieden; entscheidend bleibt, dass das Intervall von Anfang an ein explizites Feld ist und nirgends implizit als eine Stunde angenommen wird. diff --git a/pyproject.toml b/pyproject.toml index 9ff2836..7dbf74c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,13 +19,22 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] +# Every dependency is pinned exactly, matching solaredge2mqtt and learninghouse. +# Updates arrive as individual dependabot pull requests that run the full test +# suite, rather than silently on whatever day a transitive resolve changes. +# +# scikit-learn is the one pin that is load-bearing rather than merely tidy: the +# frozen Phase 0 baseline is only reproducible against 1.9.0. Verified that the +# baseline reproduces bit-identically across numpy 2.4.6/2.5.1, pandas +# 3.0.3/3.0.5 and scipy 1.17.1/1.18.0 as long as scikit-learn does not move, so +# a scikit-learn bump must regenerate the baseline and say so in its changelog. dependencies = [ - "numpy>=2.4", - "pandas>=2.2", - "scikit-learn>=1.9", - "scipy>=1.17", - "pydantic>=2.13", - "astral>=3.2", + "numpy==2.5.1", + "pandas==3.0.5", + "scikit-learn==1.9.0", + "scipy==1.18.0", + "pydantic==2.13.4", + "astral==3.2", ] [project.urls] @@ -36,23 +45,23 @@ Issues = "https://github.com/LearningHouseService/pvlearn/issues" [project.optional-dependencies] dev = [ - "setuptools-scm[toml]>=8", - "ruff", - "tomli", - "pyright", - "pytest>=9.1", - "pytest-asyncio>=1.4", - "pytest-cov>=7.1", - "pytest-xdist>=3.8", + "setuptools-scm[toml]==10.2.1", + "ruff==0.16.1", + "tomli==2.4.1", + "pyright==1.1.411", + "pytest==9.1.1", + "pytest-asyncio==1.4.0", + "pytest-cov==7.1.0", + "pytest-xdist==3.8.0", # Reading the Parquet reference fixture. The library itself takes DataFrames # from its caller and never touches Parquet, so this stays a test dependency. - "pyarrow>=18", + "pyarrow==25.0.0", ] service = [ - "fastapi>=0.118", - "uvicorn[standard]>=0.38", - "httpx>=0.28", - "pyjwt>=2.13", + "fastapi==0.141.1", + "uvicorn[standard]==0.52.1", + "httpx==0.28.1", + "pyjwt==2.13.0", ] [build-system] From 6b790b8f5ff78f886b30fee1974637433cf9bc6e Mon Sep 17 00:00:00 2001 From: Johannes Ott Date: Tue, 4 Aug 2026 08:39:01 +0200 Subject: [PATCH 2/3] chore: drop Python 3.11 support Pinning numpy and scipy exactly forced the choice: numpy 2.5 and scipy 1.18 both require Python 3.12, so supporting 3.11 would mean holding both a minor version back indefinitely. Supported versions are now 3.12 and 3.13. build-check covers 3.13, the compat matrix covers 3.12, so both remain tested on every run. Co-Authored-By: Claude Opus 5 Signed-off-by: Johannes Ott --- .github/workflows/build_project.yml | 1 - AGENTS.md | 6 +++--- pyproject.toml | 8 +++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build_project.yml b/.github/workflows/build_project.yml index 598ea62..16c2b9e 100644 --- a/.github/workflows/build_project.yml +++ b/.github/workflows/build_project.yml @@ -147,7 +147,6 @@ jobs: strategy: matrix: python-version: - - "3.11" - "3.12" steps: - name: Checkout diff --git a/AGENTS.md b/AGENTS.md index bdefb7e..f8a2bc3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ task — it defines what phase the project is in and which changes are in scope. - **Purpose:** Self-learning PV production forecast library and REST service. Trains on a plant's own historical measurements instead of a generic physical plant model (orientation, tilt, kWp) — that is the differentiator against Forecast.Solar and Solcast. -- **Language:** Python (>=3.11, <4) +- **Language:** Python (>=3.12, <4) - **Package Manager:** pip with `pyproject.toml` - **Origin:** extracted from the forecast module of `DerOetzi/solaredge2mqtt`. See the Umsetzungsplan for the extraction phases and what stays behind in that repository. @@ -79,8 +79,8 @@ To repair a branch where it is missing: `git rebase --signoff` followed b ## Code Conventions -- Use Python >=3.11 syntax and language features; do not rely on 3.12+-only syntax since the - compat matrix in CI tests down to 3.11. +- Use Python >=3.12 syntax and language features; do not rely on 3.13-only syntax since the + compat matrix in CI tests down to 3.12. - All code comments and documentation must be in **English**, independent of the language used in planning documents. - Type hints are mandatory on public functions and methods; `pyright` runs in CI. diff --git a/pyproject.toml b/pyproject.toml index 7dbf74c..349771f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "pvlearn" dynamic = ["version"] description = "Self-learning PV production forecast library and service — teach your home to predict its own solar production." readme = { file = "README.md", content-type = "text/markdown" } -requires-python = ">=3.11,<4" +requires-python = ">=3.12,<4" license = "MIT" authors = [ { name = "Johannes Ott", email = "info@johannes-ott.net" } @@ -15,7 +15,6 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence", "Natural Language :: English", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] @@ -28,6 +27,9 @@ classifiers = [ # baseline reproduces bit-identically across numpy 2.4.6/2.5.1, pandas # 3.0.3/3.0.5 and scipy 1.17.1/1.18.0 as long as scikit-learn does not move, so # a scikit-learn bump must regenerate the baseline and say so in its changelog. +# +# numpy 2.5 and scipy 1.18 require Python 3.12, which is why 3.11 is not +# supported. dependencies = [ "numpy==2.5.1", "pandas==3.0.5", @@ -84,7 +86,7 @@ fallback_version = "0.0.0" [tool.ruff] line-length = 88 -target-version = "py311" +target-version = "py312" [tool.ruff.lint] select = ["E", "F", "I"] From 8b4f46445163705cd53732a84b11f86ec8e7c2d9 Mon Sep 17 00:00:00 2001 From: Johannes Ott Date: Tue, 4 Aug 2026 09:30:10 +0200 Subject: [PATCH 3/3] docs: address review feedback on the resolution decision - power_period was written as energy_wh / interval while the metadata field is interval_minutes, which is exactly the ambiguity the surrounding paragraph warns about. It is energy_wh * 60 / interval_minutes. - The model metadata example still showed sklearn_version 1.5.2 while this branch pins 1.9.0 and calls that pin load-bearing. - The pinning comment claimed every dependency is pinned, but the build-system requirements stay on lower bounds. Narrowed the claim and recorded why they are treated differently. Co-Authored-By: Claude Opus 5 Signed-off-by: Johannes Ott --- pvlearn-umsetzungsplan.md | 8 ++++---- pyproject.toml | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pvlearn-umsetzungsplan.md b/pvlearn-umsetzungsplan.md index e29f745..fa34b44 100644 --- a/pvlearn-umsetzungsplan.md +++ b/pvlearn-umsetzungsplan.md @@ -93,9 +93,9 @@ Das bisherige Power-Modell entfällt. Begründung: Beide Modelle trainieren auf Auf dem Referenzdatensatz aus Phase 0 ist das empirisch bestätigt: MAE 620,88 Wh für das Energiemodell gegenüber 624,93 W für das Leistungsmodell, R² 0,886 gegenüber 0,885. Die beiden Modelle liegen unter einem Prozent auseinander. -`power_period` wird **weiterhin publiziert**, abgeleitet als `energy_wh / interval`. Damit ist der Wegfall kein Breaking Change für MQTT-Konsumenten. Was verloren geht, ist die Momentanleistung zum Zeitstempel; das braucht weder das Energy Dashboard noch ein bekannter Automations-Use-Case. Im Changelog als „jetzt Intervallmittel statt Momentanwert" dokumentieren. +`power_period` wird **weiterhin publiziert**, abgeleitet als `energy_wh * 60 / interval_minutes`. Damit ist der Wegfall kein Breaking Change für MQTT-Konsumenten. Was verloren geht, ist die Momentanleistung zum Zeitstempel; das braucht weder das Energy Dashboard noch ein bekannter Automations-Use-Case. Im Changelog als „jetzt Intervallmittel statt Momentanwert" dokumentieren. -Die Division durch das Intervall statt fest durch eine Stunde ist der einzige Grund, warum eine spätere Umstellung auf feinere Auflösung kein stiller Faktor-4-Fehler wird. +Dass hier durch das konfigurierte Intervall gerechnet wird statt fest durch eine Stunde, ist der einzige Grund, warum eine spätere Umstellung auf feinere Auflösung kein stiller Faktor-4-Fehler wird. Bei 60 Minuten ist der Faktor 1 und die Formel entspricht dem bisherigen Verhalten. ### 3.4 Modell-Metadaten und Invalidierung @@ -105,7 +105,7 @@ Jedes persistierte Modell trägt: { "pvlearn_version": "0.1.0", "feature_schema_version": 1, - "sklearn_version": "1.5.2", + "sklearn_version": "1.9.0", "weather_provider": "open-meteo", "interval_minutes": 60, "location": {"latitude": 49.45, "longitude": 11.08, "timezone": "Europe/Berlin"}, @@ -139,7 +139,7 @@ Open-Meteo kann 15 Minuten, aber wenn der Wechselrichter nur Stundenwerte meldet - `interval` als Pflichtfeld in Brain-Konfiguration und Modell-Metadaten, mit Invalidierung bei Abweichung (siehe 3.4) - Zeit-Features auf Minuten seit Mitternacht (siehe 3.2) -- `power_period` aus dem Intervall abgeleitet (siehe 3.3) +- `power_period` als `energy_wh * 60 / interval_minutes` statt fest über eine Stunde (siehe 3.3) - Aggregationslogik summiert Intervalle innerhalb eines Zeitraums, statt „eine Zeile = eine Stunde" anzunehmen - Der Prognose-Endpunkt gibt das Intervall in der Antwort mit an diff --git a/pyproject.toml b/pyproject.toml index 349771f..40a4327 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,9 +18,12 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] -# Every dependency is pinned exactly, matching solaredge2mqtt and learninghouse. -# Updates arrive as individual dependabot pull requests that run the full test -# suite, rather than silently on whatever day a transitive resolve changes. +# Runtime and optional dependencies are pinned exactly, matching solaredge2mqtt +# and learninghouse. Updates arrive as individual dependabot pull requests that +# run the full test suite, rather than silently on whatever day a transitive +# resolve changes. The build-system requirements below stay on lower bounds: +# they shape how the wheel is built, not how the installed package behaves, and +# pinning them breaks builds on newer setuptools for no reproducibility gain. # # scikit-learn is the one pin that is load-bearing rather than merely tidy: the # frozen Phase 0 baseline is only reproducible against 1.9.0. Verified that the