Historia temperatur z całego domu w InfluxDB i Grafanie#
Termometry 1-Wire z Grentona i czujnik Zigbee co minutę trafiają do InfluxDB, a Grafana rysuje z nich wykresy z dowolnego okresu: wczorajszą noc, ostatni tydzień albo cały sezon grzewczy. Po stronie vCLU to jeden moduł Lua z listą czujników.
vCLU ma własne wykresy na dashboardach, ale trzyma je w pamięci, na podgląd ostatnich godzin. Historia na lata, porównanie sezonów i ładne panele to robota dla bazy szeregów czasowych. Ten tutorial pokazuje, jak robi to dom autora vCLU.
Co zbudujemy#
- InfluxDB 2 z bucketem
domi Grafana, w Dockerze - moduł
temperaturyw vCLU: lista czujników i wysyłka odczytów co minutę - w Grafanie wykres temperatur na parterze i piętrze, aktualne odczyty i dzienne minimum i maksimum w ogrodzie
Czego potrzebujesz#
| Co | Uwagi |
|---|---|
| Czujniki temperatury w vCLU | tu: 1-Wire z Grentona i czujnik Zigbee z tutorialu 7 |
| Komputer na InfluxDB i Grafanę | Raspberry Pi 4/5 z 64-bitowym systemem albo inny serwer w domu; InfluxDB 2 nie ma wersji na 32-bitowy ARM |
| Plugin InfluxDB Metrics | w wersji 1.1.2 lub nowszej (panel vCLU → Pluginy) |
InfluxDB i Grafana razem zajmują kilkaset MB RAM. Nie muszą stać na tym samym urządzeniu co vCLU: w domu autora vCLU działa na małym komputerze, a baza na osobnym serwerze.
Krok 1: InfluxDB i Grafana#
# docker-compose.yml
services:
influxdb:
image: influxdb:2
restart: unless-stopped
ports:
- "8086:8086"
volumes:
- influxdb-data:/var/lib/influxdb2
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: admin
DOCKER_INFLUXDB_INIT_PASSWORD: zmien-to-haslo
DOCKER_INFLUXDB_INIT_ORG: dom
DOCKER_INFLUXDB_INIT_BUCKET: dom
DOCKER_INFLUXDB_INIT_RETENTION: 0s # bez limitu: historia zostaje na zawsze
grafana:
image: grafana/grafana:latest
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
volumes:
influxdb-data:
grafana-data:docker compose up -dInfluxDB jest pod http://ADRES_SERWERA:8086, Grafana pod http://ADRES_SERWERA:3000 (pierwsze logowanie: admin/admin).
Krok 2: dwa tokeny#
W InfluxDB (Load Data → API Tokens → Generate API Token → Custom API Token) utwórz dwa tokeny:
| Token | Uprawnienia | Dla kogo |
|---|---|---|
| zapis | Write do bucketa dom | vCLU |
| odczyt | Read z bucketa dom | Grafana |
Osobne tokeny ograniczają szkody: token z vCLU może tylko dopisywać punkty do jednego bucketa, nie przeczyta ani nie skasuje niczego. Gdy później dojdzie np. bucket energia, dostanie własny token zapisu.
Krok 3: plugin w vCLU#
W panelu vCLU: Pluginy → Dostępne → InfluxDB Metrics → Zainstaluj. Plugin nie ma formularza konfiguracji; połączenie tworzysz w kodzie, w kroku 4.
Krok 4: moduł z czujnikami#
Cała lista czujników jest w jednym miejscu. Wysyłka co minutę, każdy odczyt z tagami: pomieszczenie, kondygnacja i czy to wewnątrz, czy na zewnątrz.
-- modules/temperatury/init.lua
local metrics = Plugin.getPlugin("@vclu/influx-metrics")
local influx = metrics:create({
url = "http://ADRES_SERWERA:8086/api/v2/write?org=dom&bucket=dom&precision=s",
token = "TOKEN_ZAPISU",
interval = 60, -- wysyłka paczką co minutę
})
-- Ścieżki z panelu vCLU → Urządzenia
local CZUJNIKI = {
{ id = "salon", kondygnacja = "parter", path = "CLU220000001.ONE6305" },
{ id = "salon_zigbee", kondygnacja = "parter", path = "zigbee.salon.temperatura" },
{ id = "biuro", kondygnacja = "parter", path = "CLU220000001.ONE2545" },
{ id = "lazienka_dol", kondygnacja = "parter", path = "CLU220000001.ONE7200" },
{ id = "sypialnia", kondygnacja = "pietro", path = "CLU220000001.ONE2071" },
{ id = "garderoba", kondygnacja = "pietro", path = "CLU220000001.ONE9011" },
{ id = "pokoj_1", kondygnacja = "pietro", path = "CLU220000001.ONE3544" },
{ id = "pokoj_2", kondygnacja = "pietro", path = "CLU220000001.ONE9444" },
{ id = "lazienka_gora", kondygnacja = "pietro", path = "CLU220000001.ONE0993" },
{ id = "ogrod", kondygnacja = "zewnatrz", path = "CLU220000001.ONE2546" },
}
local function wyslij()
for _, c in ipairs(CZUJNIKI) do
local obj = _:get(c.path)
local v = obj and obj:getValue()
-- Brak odczytu (CLU offline, czujnik odpięty) to dziura na wykresie, nie zero
if type(v) == "number" then
influx:gauge("temperatura", v, {
pomieszczenie = c.id,
kondygnacja = c.kondygnacja,
typ = c.kondygnacja == "zewnatrz" and "zewnetrzna" or "wewnetrzna",
})
end
end
end
setInterval(wyslij, 60 * 1000)
wyslij()
return { stats = function() return influx:stats() end }I w modules/init.lua:
require("temperatury")W InfluxDB trafiają wtedy linie takie jak:
temperatura,kondygnacja=parter,pomieszczenie=salon,typ=wewnetrzna value=21.4 1792051200Zapisuj surowe odczyty, nie średnie. Średnią domu, minimum dnia czy różnicę między piętrami Grafana policzy z surowych serii w chwili rysowania. Gdyby moduł zapisywał gotowe średnie, dzisiejszy sposób liczenia zostałby na zawsze w danych historycznych.
Pro tip: czujniki prosto z
expose(). Termometry, które już wystawiasz do Home Assistant i Apple Home, vCLU zna:ExposedObjects:list()zwraca wszystkie wystawione obiekty (ścieżka → uchwyt z typem, opcjami igetValue()). Zamiast tablicy możesz wysyłać wszystkie obiekty typutemperature, a nowy czujnik poexpose()sam trafi do Influxa:local function wyslij() for path, h in pairs(ExposedObjects:list()) do local v = h.type == "temperature" and h:getValue() if type(v) == "number" then influx:gauge("temperatura", v, { czujnik = path, pomieszczenie = h.options.area }) end end end
areato nazwa do wyświetlania i może się zmienić; w InfluxDB każda zmiana tagu zaczyna nową serię. Dlatego w Grafanie grupuj poczujnik(ścieżce), który się nie zmienia, wtedy historia sprzed i po zmianie nazwy pomieszczenia jest jedną linią. Kondygnacjiexpose()nie zna, więc filtrowanie po piętrach zostaje przy tablicy. Lista jest tylko do odczytu, nie zmieniaj jej w pętli. Opislist()jako oficjalnego API, z gwarancją tych pól, jest w toku: #40.
Krok 5: sprawdzenie#
Nieudaną wysyłkę plugin zapisuje w logach vCLU (panel → Logi) jako Flush failed: .... Licznik wysłanych punktów da się też zobaczyć wprost, np. prosząc agenta o wynik require("temperatury").stats(): po minucie sent powinno rosnąć, a lastError być puste.
| Błąd | Przyczyna |
|---|---|
HTTP 401 | zły token |
HTTP 404 | literówka w org albo bucket w adresie, albo token bez prawa zapisu do dom |
| błąd połączenia | zły adres serwera albo zapora na porcie 8086 |
W InfluxDB dane widać w Data Explorer: bucket dom, pomiar temperatura.
Krok 6: Grafana#
Źródło danych. W Grafanie: Connections → Data sources → Add → InfluxDB:
- Query language: Flux
- URL:
http://influxdb:8086(Grafana i InfluxDB w tym samymdocker-compose.yml), inaczejhttp://ADRES_SERWERA:8086 - Organization:
dom, Token: token odczytu, Default bucket:dom
Wykres pięter. Nowy dashboard → panel Time series → zapytanie:
from(bucket: "dom")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r._measurement == "temperatura" and r._field == "value")
|> filter(fn: (r) => r.kondygnacja == "pietro")
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|> keep(columns: ["_time", "_value", "pomieszczenie"])W opcjach panelu ustaw Standard options → Display name na ${__field.labels.pomieszczenie}, a jednostkę na Temperature → Celsius. Drugi panel dla parteru to ten sam kod z "parter".
aggregateWindow z v.windowPeriod sprawia, że przy widoku całego roku Grafana nie ciągnie pół miliona punktów, tylko średnie z okien dopasowanych do szerokości wykresu.
Aktualne odczyty. Panel Stat:
from(bucket: "dom")
|> range(start: -15m)
|> filter(fn: (r) => r._measurement == "temperatura" and r._field == "value")
|> last()
|> keep(columns: ["_value", "pomieszczenie"])Tu też ustaw Display name na ${__field.labels.pomieszczenie}.
Ogród: minimum i maksimum dnia. Panel Time series albo Bar chart, zakres np. ostatnie 30 dni:
import "timezone"
option location = timezone.location(name: "Europe/Warsaw")
dane = from(bucket: "dom")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r._measurement == "temperatura" and r.pomieszczenie == "ogrod")
dane |> aggregateWindow(every: 1d, fn: min, createEmpty: false) |> set(key: "_field", value: "min") |> yield(name: "min")
dane |> aggregateWindow(every: 1d, fn: max, createEmpty: false) |> set(key: "_field", value: "max") |> yield(name: "max")location liczy doby od polskiej północy, a nie od północy UTC.
Albo: niech zrobi to agent#
Z agentem podłączonym przez MCP wystarczy:
Wysyłaj co minutę wszystkie termometry z domu do InfluxDB pod http://ADRES_SERWERA:8086, org dom, bucket dom, przez plugin influx-metrics. Token zapisu: … Taguj pomieszczeniem i kondygnacją.
Agent znajdzie czujniki w rejestrze (vclu_registry), napisze moduł, sprawdzi go w sandboxie i wdroży. Zapytania do Grafany też może napisać, ale panele klikasz już sam.
Co dalej#
- Ten sam moduł może wysyłać wilgotność, stan ogrzewania albo pozycję rolet: kolejny pomiar, te same tagi.
- Liczniki energii trafiają do osobnego bucketa (
energia) z własnym tokenem; porządek w bucketach ułatwia retencję i uprawnienia. - Alerty w Grafanie (np. temperatura w pokoju poniżej 17 °C nocą) działają niezależnie od vCLU i dom ich nie odczuje, gdy Grafana leży.