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 dom i Grafana, w Dockerze
  • moduł temperatury w 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#

CoUwagi
Czujniki temperatury w vCLUtu: 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 Metricsw 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 -d

InfluxDB 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:

TokenUprawnieniaDla kogo
zapisWrite do bucketa domvCLU
odczytRead z bucketa domGrafana

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 1792051200

Zapisuj 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 i getValue()). Zamiast tablicy możesz wysyłać wszystkie obiekty typu temperature, a nowy czujnik po expose() 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

area to nazwa do wyświetlania i może się zmienić; w InfluxDB każda zmiana tagu zaczyna nową serię. Dlatego w Grafanie grupuj po czujnik (ścieżce), który się nie zmienia, wtedy historia sprzed i po zmianie nazwy pomieszczenia jest jedną linią. Kondygnacji expose() nie zna, więc filtrowanie po piętrach zostaje przy tablicy. Lista jest tylko do odczytu, nie zmieniaj jej w pętli. Opis list() 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łądPrzyczyna
HTTP 401zły token
HTTP 404literówka w org albo bucket w adresie, albo token bez prawa zapisu do dom
błąd połączeniazł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 samym docker-compose.yml), inaczej http://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.