From dd8f7536d3189886c1d5c7cd028cd4489f3a33a2 Mon Sep 17 00:00:00 2001 From: Ignace Date: Sun, 6 Sep 2026 12:25:34 +0200 Subject: [PATCH] with out-temp --- README.md | 11 ++++++----- netatmo_service/data.py | 3 ++- netatmo_service/rrd.py | 31 +++++++++++++++++++++++++------ tests/test_service.py | 2 ++ 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4b5110e..88651d9 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,10 @@ The service creates `netatmo_outdoor.rrd`, `netatmo_pressure.rrd`, ten-minute archives for the last two weeks and six-hour archives covering a year. Day, two-week, and year PNG graphs are rendered on demand. The wind graph splits average wind into 12 colored direction bands and draws gusts as a line. -The rain graph stores Netatmo's cumulative `sum_rain_24` reading and shows the -rain accumulated since midnight in millimetres. Its daily reset is preserved; -MAX consolidation retains daily totals in the longer-term archive. +The rain database stores Netatmo's `sum_rain_1` and cumulative `sum_rain_24` +readings. Day and two-week graphs show rain accumulated per hour; the year graph +shows rain accumulated per day. The midnight reset of `sum_rain_24` is preserved, +and MAX consolidation retains daily totals in the longer-term archive. The Outdoor module does not contain a pressure sensor, so the dedicated pressure graph reads it from the main station. Its Y axis is fixed at 900–1100 mbar. A @@ -143,10 +144,10 @@ curl --output wind.png http://localhost:5000/graph/wind/2weeks Data points are: -- `outdoor`: `min_temp`, `max_temp`, `humidity`, `pressure` +- `outdoor`: `temperature`, `min_temp`, `max_temp`, `humidity`, `pressure` - `pressure`: `pressure` - `wind`: `gust`, `average`, `angle` -- `rain`: `sum_rain_24` +- `rain`: `sum_rain_24`, `sum_rain_1` - `bedroom`, `study`, `living`: `temperature`, `co2`, `humidity` The service creates missing RRDs at startup but deliberately does not alter an diff --git a/netatmo_service/data.py b/netatmo_service/data.py index ae649eb..7b41d49 100644 --- a/netatmo_service/data.py +++ b/netatmo_service/data.py @@ -41,7 +41,7 @@ def extract_samples(payload: dict, names: dict[str, str]) -> dict[str, Sample]: sample("wind", {"gust": "GustStrength", "average": "WindStrength", "angle": "WindAngle"}) # Netatmo resets this cumulative millimetre total at local midnight. Store # it unchanged so historical graphs show the total accumulated each day. - sample("rain", {"sum_rain_24": "sum_rain_24"}) + sample("rain", {"sum_rain_24": "sum_rain_24", "sum_rain_1": "sum_rain_1"}) # Pressure belongs to the main indoor station rather than a separate # module, so give it its own sample and use that station's timestamp. @@ -61,6 +61,7 @@ def extract_samples(payload: dict, names: dict[str, str]) -> dict[str, Sample]: result["outdoor"] = Sample( int(data["time_utc"]), { + "temperature": _number(data.get("Temperature")), "min_temp": _number(data.get("min_temp", data.get("Temperature"))), "max_temp": _number(data.get("max_temp", data.get("Temperature"))), "humidity": _number(data.get("Humidity")), diff --git a/netatmo_service/rrd.py b/netatmo_service/rrd.py index d7e509d..bf48eb7 100644 --- a/netatmo_service/rrd.py +++ b/netatmo_service/rrd.py @@ -18,10 +18,10 @@ HEARTBEAT = 1500 SAFE_NAME = re.compile(r"^[a-z][a-z0-9_]*$") SCHEMAS = { - "outdoor": {"min_temp": "GAUGE:-60:70", "max_temp": "GAUGE:-60:70", "humidity": "GAUGE:0:100", "pressure": "GAUGE:800:1200"}, + "outdoor": {"temperature": "GAUGE:-60:70", "min_temp": "GAUGE:-60:70", "max_temp": "GAUGE:-60:70", "humidity": "GAUGE:0:100", "pressure": "GAUGE:800:1200"}, "pressure": {"pressure": "GAUGE:800:1200"}, "wind": {"gust": "GAUGE:0:300", "average": "GAUGE:0:300", "angle": "GAUGE:0:360"}, - "rain": {"sum_rain_24": "GAUGE:0:1000"}, + "rain": {"sum_rain_24": "GAUGE:0:1000", "sum_rain_1": "GAUGE:0:1000"}, "bedroom": {"temperature": "GAUGE:-20:60", "co2": "GAUGE:0:10000", "humidity": "GAUGE:0:100"}, "study": {"temperature": "GAUGE:-20:60", "co2": "GAUGE:0:10000", "humidity": "GAUGE:0:100"}, "living": {"temperature": "GAUGE:-20:60", "co2": "GAUGE:0:10000", "humidity": "GAUGE:0:100"}, @@ -57,6 +57,7 @@ class RRDStore: for name, schema in SCHEMAS.items(): path = self.path(name) if path.exists(): + self._ensure_data_sources(path, schema) continue # Leave room for a station that has been temporarily offline when the # RRD is first created; RRDtool rejects samples older than --start. @@ -67,6 +68,17 @@ class RRDStore: "RRA:AVERAGE:0.5:36:1464", "RRA:MIN:0.5:36:1464", "RRA:MAX:0.5:36:1464"] rrdtool.create(str(path), *args) + @staticmethod + def _ensure_data_sources(path: Path, schema: dict[str, str]) -> None: + """Add newly introduced data sources without discarding RRD history.""" + info = rrdtool.info(str(path)) + for field, definition in schema.items(): + if f"ds[{field}].index" in info: + continue + data_type, limits = definition.split(":", 1) + LOG.info("Adding %s data source to %s", field, path) + rrdtool.tune(str(path), f"DS:{field}:{data_type}:{HEARTBEAT}:{limits}") + def update(self, name: str, sample: Sample) -> bool: """Store at most one sample per RRD step; return whether it was written.""" path = self.path(name) @@ -139,15 +151,22 @@ class RRDStore: if name == "wind": drawings = self._wind_drawings() elif name == "rain": - drawings = ["AREA:sum_rain_24#3498DB80:Rain today (mm)", - "LINE2:sum_rain_24#2471A3:Rain today (mm)"] + rain_field = "sum_rain_24" if period == "year" else "sum_rain_1" + rain_label = "Rain per day (mm)" if period == "year" else "Rain per hour (mm)" + drawings = [f"AREA:{rain_field}#3498DB80:{rain_label}", + f"LINE2:{rain_field}#2471A3"] elif name == "pressure": drawings = ["AREA:pressure_floor#00000000", "AREA:pressure_above_floor#77787280:Pressure (mbar):STACK", "LINE1:pressure#111111"] elif name == "outdoor": - drawings = ["LINE2:min_temp#3488DB:Minimum temperature (C)", "LINE2:max_temp#E74C3C:Maximum temperature (C)", - "LINE1:humidity_scaled#27AE60:Humidity (% / 2.5)"] + drawings = [] if period == "year" else [ + "AREA:temperature#E74C3C70:Temperature (C)", + "LINE1:temperature#A93226", + ] + drawings += ["LINE2:min_temp#3488DB:Minimum temperature (C)", + "LINE2:max_temp#E74C3C:Maximum temperature (C)", + "LINE1:humidity_scaled#27AE60:Humidity (% / 2.5)"] else: drawings = ["AREA:temperature#E74C3C70:Temperature (C)", "LINE1:temperature#A93226", diff --git a/tests/test_service.py b/tests/test_service.py index 781629b..793dcb3 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -53,9 +53,11 @@ def test_extract_sample_payload(): names = {name: name.title() for name in ("outdoor", "wind", "rain", "bedroom", "study", "living")} samples = extract_samples(namespace["payload"], names) assert samples["outdoor"].values["pressure"] == 1004.8 + assert samples["outdoor"].values["temperature"] == 18.3 assert samples["pressure"].values["pressure"] == 1004.8 assert samples["wind"].values["angle"] == 236.0 assert samples["rain"].values["sum_rain_24"] == 9.8 + assert samples["rain"].values["sum_rain_1"] == 1.9200000000000002 assert "bedroom" not in samples # unreachable module has no dashboard_data