added rain, updated graph styles

This commit is contained in:
2026-09-03 22:26:40 +02:00
parent 67d8b50333
commit 77bf334fce
9 changed files with 128 additions and 35 deletions
+63 -11
View File
@@ -19,7 +19,9 @@ 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"},
"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"},
"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"},
@@ -29,8 +31,18 @@ PERIODS = {"day": ("1 day", "-1d"), "2weeks": ("2 weeks", "-14d"), "year": ("1 y
ALIASES = {"1day": "day", "14days": "2weeks", "2week": "2weeks", "1year": "year"}
def rainbow(angle: float) -> tuple[int, int, int]:
"""Map a compass angle to an RGB rainbow color."""
hue = (angle - 180) / 360
hue %= 1.0
return tuple(
round(channel * 255)
for channel in colorsys.hsv_to_rgb(hue, 1.0, 1.0)
)
class RRDStore:
def __init__(self, folder: Path, width: int = 900, height: int = 240):
def __init__(self, folder: Path, width: int = 900, height: int = 420):
self.folder = Path(folder)
self.width = width
self.height = height
@@ -95,15 +107,52 @@ class RRDStore:
title, start = PERIODS[period]
common = ["--imgformat", "PNG", "--start", start, "--end", "now",
"--width", str(self.width), "--height", str(self.height), "--title", f"{name.title()} - {title}",
"--slope-mode", "--watermark", "GetNetatmoData v2"]
definitions = [f"DEF:{field}={self.path(name)}:{field}:AVERAGE" for field in SCHEMAS[name]]
"--slope-mode", "--watermark", "GetNetatmoData v2",
"--color", "BACK#F4F1E8", "--color", "CANVAS#FAF8F2",
"--color", "FONT#111111", "--color", "AXIS#111111",
"--color", "FRAME#111111", "--color", "ARROW#111111",
"--color", "GRID#D8D3C7", "--color", "MGRID#AAA397"]
if name == "pressure":
common += ["--lower-limit", "900", "--upper-limit", "1100", "--rigid",
"--vertical-label", "Pressure (mbar)", "--units-exponent", "0"]
elif name in {"outdoor", "bedroom", "study", "living"}:
common += ["--lower-limit", "0", "--upper-limit", "40", "--rigid",
"--vertical-label", "Temperature (C) / humidity (% / 2.5)"]
if name != "outdoor":
# CO2 is divided by 50 onto the 0-40 plotting range; the right
# axis converts those positions back to their 0-2000 ppm labels.
common += ["--right-axis", "50:0", "--right-axis-label", "CO2 (ppm)",
"--right-axis-format", "%.0lf",
"--y-grid", "4:1",
"--units-exponent", "0"]
# MAX retains each day's highest cumulative rain reading in the coarser
# archive, rather than averaging away the total when the counter resets.
consolidation = "MAX" if name == "rain" else "AVERAGE"
definitions = [f"DEF:{field}={self.path(name)}:{field}:{consolidation}" for field in SCHEMAS[name]]
if "humidity" in SCHEMAS[name]:
definitions.append("CDEF:humidity_scaled=humidity,2.5,/")
if "co2" in SCHEMAS[name]:
definitions.append("CDEF:co2_scaled=co2,50,/")
if name == "pressure":
definitions += ["CDEF:pressure_floor=pressure,UN,UNKN,900,IF",
"CDEF:pressure_above_floor=pressure,900,-"]
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)"]
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#27AE60:Humidity (%)", "LINE1:pressure#8E44AD:Pressure (hPa):dashes"]
"LINE1:humidity_scaled#27AE60:Humidity (% / 2.5)"]
else:
drawings = ["LINE2:temperature#E74C3C:Temperature (C)", "LINE1:co2#8E44AD:CO2 (ppm)", "LINE1:humidity#27AE60:Humidity (%):dashes"]
drawings = ["AREA:temperature#E74C3C70:Temperature (C)",
"LINE1:temperature#A93226",
"LINE2:co2_scaled#8E44AD:CO2 (ppm, right axis)",
"LINE2:humidity_scaled#27AE60:Humidity (% / 2.5):dashes"]
# The Python binding returns graph metadata, not the encoded image, so
# render to an isolated temporary file and return its contents.
with tempfile.TemporaryDirectory(prefix="netatmo-graph-") as folder:
@@ -114,11 +163,14 @@ class RRDStore:
@staticmethod
def _wind_drawings() -> list[str]:
result: list[str] = []
# Split average wind into 12 angle bands. Each AREA starts at zero and
# only exists for its direction, producing rainbow-colored bars/areas.
for index in range(12):
low, high = index * 30, (index + 1) * 30
color = "#%02X%02X%02X80" % tuple(round(channel * 255) for channel in colorsys.hsv_to_rgb(index / 12, .85, .9))
result += [f"CDEF:dir{index}=angle,{low},GE,angle,{high},LT,*,average,UNKN,IF", f"AREA:dir{index}{color}:{low:03d}-{high:03d} degrees"]
# Give every compass degree its own color. The AREA entries deliberately
# have no labels, keeping the 360-color key out of the graph legend.
for angle in range(360):
color = "#%02X%02X%02X80" % rainbow(angle)
upper_comparison = "LE" if angle == 359 else "LT"
result += [
f"CDEF:dir{angle}=angle,{angle},GE,angle,{angle + 1},{upper_comparison},*,average,UNKN,IF",
f"AREA:dir{angle}{color}",
]
result += ["LINE2:gust#111111:Gust", "LINE1:average#555555:Average"]
return result