with out-temp
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")),
|
||||
|
||||
+24
-5
@@ -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,14 +151,21 @@ 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)",
|
||||
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)",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user