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
|
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
|
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.
|
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
|
The rain database stores Netatmo's `sum_rain_1` and cumulative `sum_rain_24`
|
||||||
rain accumulated since midnight in millimetres. Its daily reset is preserved;
|
readings. Day and two-week graphs show rain accumulated per hour; the year graph
|
||||||
MAX consolidation retains daily totals in the longer-term archive.
|
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
|
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
|
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:
|
Data points are:
|
||||||
|
|
||||||
- `outdoor`: `min_temp`, `max_temp`, `humidity`, `pressure`
|
- `outdoor`: `temperature`, `min_temp`, `max_temp`, `humidity`, `pressure`
|
||||||
- `pressure`: `pressure`
|
- `pressure`: `pressure`
|
||||||
- `wind`: `gust`, `average`, `angle`
|
- `wind`: `gust`, `average`, `angle`
|
||||||
- `rain`: `sum_rain_24`
|
- `rain`: `sum_rain_24`, `sum_rain_1`
|
||||||
- `bedroom`, `study`, `living`: `temperature`, `co2`, `humidity`
|
- `bedroom`, `study`, `living`: `temperature`, `co2`, `humidity`
|
||||||
|
|
||||||
The service creates missing RRDs at startup but deliberately does not alter an
|
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"})
|
sample("wind", {"gust": "GustStrength", "average": "WindStrength", "angle": "WindAngle"})
|
||||||
# Netatmo resets this cumulative millimetre total at local midnight. Store
|
# Netatmo resets this cumulative millimetre total at local midnight. Store
|
||||||
# it unchanged so historical graphs show the total accumulated each day.
|
# 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
|
# Pressure belongs to the main indoor station rather than a separate
|
||||||
# module, so give it its own sample and use that station's timestamp.
|
# 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(
|
result["outdoor"] = Sample(
|
||||||
int(data["time_utc"]),
|
int(data["time_utc"]),
|
||||||
{
|
{
|
||||||
|
"temperature": _number(data.get("Temperature")),
|
||||||
"min_temp": _number(data.get("min_temp", data.get("Temperature"))),
|
"min_temp": _number(data.get("min_temp", data.get("Temperature"))),
|
||||||
"max_temp": _number(data.get("max_temp", data.get("Temperature"))),
|
"max_temp": _number(data.get("max_temp", data.get("Temperature"))),
|
||||||
"humidity": _number(data.get("Humidity")),
|
"humidity": _number(data.get("Humidity")),
|
||||||
|
|||||||
+25
-6
@@ -18,10 +18,10 @@ HEARTBEAT = 1500
|
|||||||
SAFE_NAME = re.compile(r"^[a-z][a-z0-9_]*$")
|
SAFE_NAME = re.compile(r"^[a-z][a-z0-9_]*$")
|
||||||
|
|
||||||
SCHEMAS = {
|
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"},
|
"pressure": {"pressure": "GAUGE:800:1200"},
|
||||||
"wind": {"gust": "GAUGE:0:300", "average": "GAUGE:0:300", "angle": "GAUGE:0:360"},
|
"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"},
|
"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"},
|
"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"},
|
"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():
|
for name, schema in SCHEMAS.items():
|
||||||
path = self.path(name)
|
path = self.path(name)
|
||||||
if path.exists():
|
if path.exists():
|
||||||
|
self._ensure_data_sources(path, schema)
|
||||||
continue
|
continue
|
||||||
# Leave room for a station that has been temporarily offline when the
|
# Leave room for a station that has been temporarily offline when the
|
||||||
# RRD is first created; RRDtool rejects samples older than --start.
|
# 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"]
|
"RRA:AVERAGE:0.5:36:1464", "RRA:MIN:0.5:36:1464", "RRA:MAX:0.5:36:1464"]
|
||||||
rrdtool.create(str(path), *args)
|
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:
|
def update(self, name: str, sample: Sample) -> bool:
|
||||||
"""Store at most one sample per RRD step; return whether it was written."""
|
"""Store at most one sample per RRD step; return whether it was written."""
|
||||||
path = self.path(name)
|
path = self.path(name)
|
||||||
@@ -139,15 +151,22 @@ class RRDStore:
|
|||||||
if name == "wind":
|
if name == "wind":
|
||||||
drawings = self._wind_drawings()
|
drawings = self._wind_drawings()
|
||||||
elif name == "rain":
|
elif name == "rain":
|
||||||
drawings = ["AREA:sum_rain_24#3498DB80:Rain today (mm)",
|
rain_field = "sum_rain_24" if period == "year" else "sum_rain_1"
|
||||||
"LINE2:sum_rain_24#2471A3:Rain today (mm)"]
|
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":
|
elif name == "pressure":
|
||||||
drawings = ["AREA:pressure_floor#00000000",
|
drawings = ["AREA:pressure_floor#00000000",
|
||||||
"AREA:pressure_above_floor#77787280:Pressure (mbar):STACK",
|
"AREA:pressure_above_floor#77787280:Pressure (mbar):STACK",
|
||||||
"LINE1:pressure#111111"]
|
"LINE1:pressure#111111"]
|
||||||
elif name == "outdoor":
|
elif name == "outdoor":
|
||||||
drawings = ["LINE2:min_temp#3488DB:Minimum temperature (C)", "LINE2:max_temp#E74C3C:Maximum temperature (C)",
|
drawings = [] if period == "year" else [
|
||||||
"LINE1:humidity_scaled#27AE60:Humidity (% / 2.5)"]
|
"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:
|
else:
|
||||||
drawings = ["AREA:temperature#E74C3C70:Temperature (C)",
|
drawings = ["AREA:temperature#E74C3C70:Temperature (C)",
|
||||||
"LINE1:temperature#A93226",
|
"LINE1:temperature#A93226",
|
||||||
|
|||||||
@@ -53,9 +53,11 @@ def test_extract_sample_payload():
|
|||||||
names = {name: name.title() for name in ("outdoor", "wind", "rain", "bedroom", "study", "living")}
|
names = {name: name.title() for name in ("outdoor", "wind", "rain", "bedroom", "study", "living")}
|
||||||
samples = extract_samples(namespace["payload"], names)
|
samples = extract_samples(namespace["payload"], names)
|
||||||
assert samples["outdoor"].values["pressure"] == 1004.8
|
assert samples["outdoor"].values["pressure"] == 1004.8
|
||||||
|
assert samples["outdoor"].values["temperature"] == 18.3
|
||||||
assert samples["pressure"].values["pressure"] == 1004.8
|
assert samples["pressure"].values["pressure"] == 1004.8
|
||||||
assert samples["wind"].values["angle"] == 236.0
|
assert samples["wind"].values["angle"] == 236.0
|
||||||
assert samples["rain"].values["sum_rain_24"] == 9.8
|
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
|
assert "bedroom" not in samples # unreachable module has no dashboard_data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user