Dashboard v reálném čase pro Farm IoT s Angular a Grafana
Je 3.47 ráno červencové noci. Senzor na vinici Chianti zaznamenává tuto teplotu Počtvrté překročila půda na úrovni kořenů kritický práh 28 stupňů Celsia po sobě jdoucí hodina. Bez monitorovacího systému v reálném čase zjistí agronom problém ráno následující při ruční kontrole. K poškození již došlo: akutní vodní stres, zrání zrychlil nerovnoměrně, odhadovaná ztráta 12 % na výtěžku šarže.
S řídicím panelem v reálném čase propojeným s upozorněním generuje stejná událost upozornění Telegram ve 3:48. Automatický zavlažovací systém se spustí v 03:49. V 06:00 když se agronom probudí, najde zprávu dokumentující zásah, který proběhl, křivku správná teplota a aktuální stav: vše normální. Tento rozdíl mezi reaktivitou a proaktivita, stojí v průměru za to 15-30 % ztrát z vodního stresu v plodinách jemné středomořské.
Informační panely farmy nejsou nástrojem pro podávání zpráv – jsou to systémy pro včasné rozhodování skutečný. Kombinování Grafana pro provozní displej, InfluxDB pro ukládání časových řad, např Hranatý pro vlastní mobilní rozhraní, např Je možné vybudovat platformu farmářského zpravodajství, která přemění nezpracovaná data ze senzorů na konkrétní agronomické akce. Tento článek pokrývá celou architekturu, od brokera MQTT až po panel ovládání na telefonu agronoma v terénu.
Co se dozvíte v tomto článku
- Kompletní architektura systému dashboard pro IoT farmy: senzory, InfluxDB, Grafana, Angular
- Návrh schématu InfluxDB optimalizovaný pro zemědělská data pomocí Flux a InfluxQL
- Konfigurace Grafany: zdroj dat, panely, proměnné šablony, zajišťování YAML
- Upozornění Grafany pomocí kontaktních míst (e-mail, Telegram, Slack), pravidel upozornění a zásad oznamování
- Kdy použít Angular custom vs čistá Grafana: případy použití a hybridní architektura
- Služba Angular IoT s WebSockets, signály a grafy v reálném čase s grafy ngx
- Docker Compose full stack: Mosquitto + InfluxDB + Grafana + Angular
- Případová studie: vinařství s 50 senzory a 6 pozemky
FoodTech Series – všechny články
| # | Položka | Úroveň | Stát |
|---|---|---|---|
| 1 | IoT Pipeline pro přesné zemědělství s Pythonem a MQTT | Moderní | K dispozici |
| 2 | ML Edge pro monitorování plodin: Počítačové vidění v polích | Moderní | K dispozici |
| 3 | Satelitní API a vegetační indexy: NDVI s Pythonem a Sentinel-2 | Střední | K dispozici |
| 4 | Sledovatelnost blockchainu v potravinách: od pole až po supermarket | Střední | K dispozici |
| 5 | Počítačová vize pro kontrolu kvality v potravinářském průmyslu | Moderní | K dispozici |
| 6 | FSMA a Digital Compliance: Automatizace regulačních procesů | Střední | K dispozici |
| 7 | Vertikální zemědělství: Kontrola životního prostředí s IoT a ML | Moderní | K dispozici |
| 8 | Prognóza poptávky pro maloobchod s potravinami s Prophet a LightGBM | Střední | K dispozici |
| 9 | Dashboard v reálném čase pro Farm IoT s Angular a Grafana (jste zde) | Moderní | Proud |
| 10 | Optimalizace potravin v dodavatelském řetězci: ML pro snížení odpadu | Střední | K dispozici |
proč panely v reálném čase mění zemědělství
Tradiční zemědělství funguje na dlouhých pozorovacích cyklech: agronom navštíví pole jednou až dvakrát týdně, zjišťuje zrakové parametry, jedná na základě zkušeností. Tento model funguje dobře, když se proměnné prostředí mění pomalu. Ale krize klima zrychlilo nestálost: vlny veder, náhlá sucha, pozdní mrazy a bleskové povodně vyžadují dobu odezvy, kterou týdenní návštěvy nemohou zaručit.
Ekonomické údaje kvantifikují cenu této latence rozhodování. Podle studie Wageningen University v roce 2024, ztráty přičitatelné žádné zavlažování optimální představují 15 % až 30 % potenciálního výnosu plodin intenzivní ovoce a zelenina. Pro kvalitní vinohradnictví, vodní stres v období Veraison může způsobit trvalé poškození hroznů, což má za následek pokles o 20-35%. komerční hodnotu pro tuto položku. V Itálii, kde je odvětví vína důležité přes 15 miliard eur ročně, rozdíl mezi pasivním sledováním a reaktivní představuje stovky milionů eur v hodnotě ohrožené každou sezónu.
Ekonomický dopad monitorování v reálném čase: srovnání
| Scénář | Bez reálného času | S Real-Time | Delta |
|---|---|---|---|
| Roční spotřeba vody | Základní 100 % | -25 % až -40 % | Významné úspory |
| Ztráty vodního stresu | 15-30% výtěžek | 3-7% výtěžek | +8-23 % zpětného výtěžku |
| Náklady na hnojiva | Základní 100 % | -20 % až -35 % | Variabilní optimalizace |
| Pozdní ztráty mrazem | Vysoká, částečně se lze vyhnout | Sníženo s včasným varováním | -60% poškození, kterému lze předejít |
| Sledování pracovní doby | 8-12 hodin/týden | 1-2 hodiny/týden | -80% provozní doby |
| ROI systému IoT + dashboard | N/A | Návratnost 18-36 měsíců | Pozitivní za 2-3 sezóny |
Platformu Grafana používá AgriTech, společnost specializující se na průmyslové monitorování konopí pro správu dat z více než 70 měřicích bodů prostředí na farmu s obnovou každých 60 sekund. Zdokumentovaný výsledek byl a 4x zvýšení produkce ve srovnání se základní linií předběžného monitorování, s a snížení vstupních nákladů o 28 %. Případy jako tento tuto technologii dokazují Řídicí panely v reálném čase nejsou náklady na IT: je to agronomická investice s měřitelnou návratností.
Architektura systému Dashboard Farm IoT
Architektonický návrh je prvním kritickým krokem. V tom je špatná volba úroveň se šíří celým systémem a její pozdější oprava je nákladná. Architektura, kterou zde představujeme, byla ověřena v reálných produkčních prostředích a měřítku od 10 do 10 000 senzorů bez architektonických změn.
End-to-End architektura: Senzory na řídicím panelu
┌──────────────────────────────────────────────────────────────────────────┐
│ LAYER 1: FIELD (Campo) │
│ │
│ [Sensore Suolo] [Stazione Meteo] [Sensore pH] [Sensore PAR/DLI] │
│ T/U/CE/N suolo T aria, RH, mm pH 0-14 Lux, umol/m2/s │
│ │ │ │ │ │
│ └────────────────┴─────────────────┴─────────────────┘ │
│ LoRaWAN / Zigbee / RS-485 / 4G │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ LAYER 2: EDGE GATEWAY │
│ │
│ [Gateway Raspberry Pi 4 / Industrial IoT Gateway] │
│ - Aggregazione dati multi-sensore (polling 30s) │
│ - Filtro outlier e validazione locale │
│ - Buffer SQLite offline (7 giorni autonomia) │
│ - Timestamp normalizzazione UTC │
│ - MQTT publish: farm/campo1/sensore01/soil │
└─────────────────────────────────┬────────────────────────────────────────┘
│ MQTT over TLS 8883
┌─────────────────────────────────▼────────────────────────────────────────┐
│ LAYER 3: BROKER MQTT │
│ │
│ [Eclipse Mosquitto / EMQX Enterprise] │
│ - Topic ACL per appezzamento e sensore │
│ - mTLS autenticazione dispositivi │
│ - QoS 1 per dati critici, QoS 0 per telemetria │
│ - Last Will Testament per offline detection │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ LAYER 4: INGESTION & STORAGE │
│ │
│ [Telegraf MQTT Consumer] ───► [InfluxDB v2.7 / v3] │
│ - Parsing JSON payload - Measurement: farm_sensors │
│ - Tag extraction - Retention: 90 giorni raw │
│ - Field mapping - Downsampling: 1y compressed │
│ - Batch write 1000 punti - Continuous queries 5m/1h/1d │
└───────────┬─────────────────────────┬────────────────────────────────────┘
│ │
┌───────────▼───────────┐ ┌────────▼────────────────────────────────────┐
│ LAYER 5: GRAFANA │ │ LAYER 5: ANGULAR FRONTEND │
│ │ │ │
│ - Datasource InfluxDB│ │ - Dashboard custom mobile-first │
│ - 12 panel types │ │ - WebSocket real-time (1s refresh) │
│ - Alerting engine │ │ - Angular Signals state management │
│ - Provisioning YAML │ │ - ngx-charts: line, gauge, geo │
│ - Embed in Angular │ │ - PWA offline-first │
└───────────────────────┘ └─────────────────────────────────────────────┘
│ │
└────────────┬────────────┘
│
┌────────────────────────▼────────────────────────────────────────────────┐
│ LAYER 6: ALERTING & NOTIFICATION │
│ Email, Telegram Bot, Slack, Webhook, SMS Gateway │
│ Alert rules: stress idrico, gelo, pH anomalo, batteria bassa │
└─────────────────────────────────────────────────────────────────────────┘
Klíčovým bodem této architektury je oddělení odpovědnosti: Grafana se stará o provozní vizualizaci a varování pro technické operátory, zatímco Angular slouží jako mobilní rozhraní pro agronomy a terénní manažery, kteří to potřebují lépe vedeného a personalizovaného zážitku. Data vždy proudí z InfluxDB as jediný zdroj pravdy, vyhýbat se nesrovnalostem.
InfluxDB pro zemědělská data: Návrh schématu a dotaz
InfluxDB je referenční databáze časových řad pro zemědělské aplikace IoT díky jeho schopnost zpracovat vysoce propustné ingestování, efektivní kompresi a dotazy nativní bouřky. Volba mezi InfluxDB v2 (open source), InfluxDB v3 (nová architektura OSS založené na Apache Arrow/Parquet) a InfluxDB Cloud závisí na velikosti nasazení. Pro farmu s až 200 senzory postačuje vlastní hostování InfluxDB v2 OSS.
Návrh schématu pro zemědělská data
Základní pravidlo InfluxDB: i značky jsou indexovány (použijte je pro filtr), i pole nejsou (použijte je pro číselné hodnoty). Zlý chlap návrh schématu je hlavní příčinou snížení výkonu ve výrobě.
# Schema InfluxDB per Farm IoT
# Measurement: farm_sensors
# Tags (indicizzati - alta cardinalita moderata):
# farm_id: "az_rossi_chianti"
# field_id: "appezzamento_a1"
# sensor_id: "soil_01"
# sensor_type: "soil" | "weather" | "ph" | "par"
# zone: "zona_nord" | "zona_sud"
#
# Fields (valori numerici):
# soil_temp_c: float (temperatura suolo gradi Celsius)
# soil_moisture_pct: float (umidita suolo %)
# soil_ec_ds: float (conducibilita elettrica dS/m)
# soil_ph: float (pH 0-14)
# air_temp_c: float (temperatura aria gradi Celsius)
# air_humidity_pct: float (umidita relativa aria %)
# rainfall_mm: float (precipitazioni mm)
# wind_speed_ms: float (velocità vento m/s)
# par_umol: float (radiazione PAR umol/m2/s)
# dli_mol: float (DLI giornaliero mol/m2/d)
# water_level_cm: float (livello acqua cisterna cm)
# battery_pct: float (livello batteria sensore %)
# Esempio line protocol per Telegraf/scrittura diretta:
farm_sensors,farm_id=az_rossi_chianti,field_id=appezzamento_a1,sensor_id=soil_01,sensor_type=soil \
soil_temp_c=24.7,soil_moisture_pct=38.2,soil_ec_ds=0.45,battery_pct=87.0 1709289600000000000
Upozornění: Vysoká kardinalita značek
Nikdy do značek nevkládejte vysoké hodnoty mohutnosti: časová razítka, náhodné UUID, GPS souřadnice přesné nebo spojité číselné hodnoty. Ty vytvářejí „výbušnou mohutnost“, která degraduje InfluxDB výkon exponenciálně. Místo toho použijte pole pro vysoké hodnoty variabilita, jako jsou GPS souřadnice (které musí být zaokrouhleny nebo uloženy jako značky s diskrétními hodnotami pro zóny).
Konfigurace Telegraf pro spotřebitele MQTT
Telegraf a datový kolektor InfluxData: připojuje se k brokerovi MQTT, parsa i Payload JSON a zapíše je do InfluxDB se správnou konfigurací měření, značek a polí.
# telegraf.conf - MQTT Consumer per Farm IoT
[agent]
interval = "10s"
round_interval = true
metric_batch_size = 1000
metric_buffer_limit = 10000
flush_interval = "10s"
# Input: MQTT Broker
[[inputs.mqtt_consumer]]
servers = ["tcp://mosquitto:1883"]
topics = [
"farm/+/+/soil",
"farm/+/+/weather",
"farm/+/+/ph",
"farm/+/+/par"
]
username = "telegraf"
password = "{{ TELEGRAF_MQTT_PASSWORD }}"
qos = 1
client_id = "telegraf_farm_consumer"
data_format = "json"
json_name_key = ""
tag_keys = ["farm_id", "field_id", "sensor_id", "sensor_type"]
# Processore: aggiungi zone da sensor_id
[[processors.regex]]
[[processors.regex.tags]]
key = "sensor_id"
pattern = "soil_0[1-3]"
replacement = "zona_nord"
result_key = "zone"
# Output: InfluxDB v2
[[outputs.influxdb_v2]]
urls = ["http://influxdb:8086"]
token = "{{ INFLUXDB_TOKEN }}"
organization = "azienda_rossi"
bucket = "farm_raw"
timeout = "5s"
Flux dotazy pro řídicí panely
Flux a dotazovací jazyk InfluxDB v2/v3. Je výraznější než InfluxQL a podporuje spojuje měření, pokročilé statistické funkce a deklarativní podvzorkování.
// Query 1: Temperatura suolo ultima ora per tutti i sensori di un appezzamento
from(bucket: "farm_raw")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "farm_sensors")
|> filter(fn: (r) => r.farm_id == "az_rossi_chianti")
|> filter(fn: (r) => r.field_id == "appezzamento_a1")
|> filter(fn: (r) => r._field == "soil_temp_c")
|> aggregateWindow(every: 5m, fn: mean, createEmpty: false)
|> yield(name: "soil_temp_5m_avg")
// Query 2: Stress idrico - soglia umidita sotto 30%
from(bucket: "farm_raw")
|> range(start: -24h)
|> filter(fn: (r) => r._measurement == "farm_sensors")
|> filter(fn: (r) => r._field == "soil_moisture_pct")
|> filter(fn: (r) => r._value < 30.0)
|> group(columns: ["sensor_id", "field_id"])
|> count()
|> yield(name: "stress_idrico_events")
// Query 3: DLI giornaliero accumulato per ottimizzare esposizione
from(bucket: "farm_raw")
|> range(start: today())
|> filter(fn: (r) => r._measurement == "farm_sensors")
|> filter(fn: (r) => r._field == "par_umol")
|> filter(fn: (r) => r.farm_id == "az_rossi_chianti")
|> aggregateWindow(every: 1d, fn: integral, unit: 1s, createEmpty: false)
|> map(fn: (r) => ({ r with _value: r._value * 0.0000864 }))
// Converti da umol/m2/s * secondi a mol/m2/d (DLI)
|> yield(name: "dli_daily")
// Query 4: Downsampling per retention policy - dati orari
option task = {name: "downsample_to_hourly", every: 1h}
from(bucket: "farm_raw")
|> range(start: -task.every)
|> filter(fn: (r) => r._measurement == "farm_sensors")
|> aggregateWindow(
every: 1h,
fn: (tables=<>, column) => tables
|> mean(column: column),
createEmpty: false
)
|> to(bucket: "farm_1h", org: "azienda_rossi")
// Query 5: Alert check - batteria sensori sotto 20%
from(bucket: "farm_raw")
|> range(start: -5m)
|> filter(fn: (r) => r._measurement == "farm_sensors")
|> filter(fn: (r) => r._field == "battery_pct")
|> last()
|> filter(fn: (r) => r._value < 20.0)
|> yield(name: "batteria_critica")
Zásady uchovávání a převzorkování
# Struttura bucket InfluxDB per retention multi-tier:
#
# Bucket "farm_raw" → retention 90 giorni (dati raw ogni 30s)
# Bucket "farm_1h" → retention 2 anni (media oraria)
# Bucket "farm_1d" → retention 10 anni (media giornaliera)
# Bucket "farm_alerts" → retention 5 anni (eventi alert)
#
# Task Flux per downsampling automatico (eseguito da InfluxDB Tasks):
# influxdb-tasks/downsample-daily.flux
option task = {
name: "downsample_farm_to_daily",
every: 1d,
offset: 30m
}
data = from(bucket: "farm_1h")
|> range(start: -task.every)
|> filter(fn: (r) => r._measurement == "farm_sensors")
data
|> aggregateWindow(every: 1d, fn: mean, createEmpty: false)
|> set(key: "_measurement", value: "farm_sensors_daily")
|> to(bucket: "farm_1d", org: "azienda_rossi")
Grafana: Nastavení a konfigurace pro Farm IoT
Grafana je nejoblíbenější platforma pro vizualizaci a upozornění pro průmyslová prostředí IoT. Verze 11.x (2025) zavedla významná vylepšení v jednotném upozorňování, v geografických panelech (Geomap) a v poskytování jako kód. Pro instalaci farmy IoT self-hosted, verze OSS je dostačující: verze Enterprise přidává SSO, auditování pokročilé a proprietární zásuvné moduly užitečné pouze v podnikových prostředích s více farmami.
Poskytování zdroje dat přes YAML
# grafana/provisioning/datasources/influxdb.yaml
apiVersion: 1
datasources:
- name: InfluxDB-FarmRaw
type: influxdb
access: proxy
url: http://influxdb:8086
jsonData:
version: Flux
organization: azienda_rossi
defaultBucket: farm_raw
tlsSkipVerify: false
secureJsonData:
token: ${INFLUXDB_GRAFANA_TOKEN}
isDefault: true
editable: false
- name: InfluxDB-Farm1h
type: influxdb
access: proxy
url: http://influxdb:8086
jsonData:
version: Flux
organization: azienda_rossi
defaultBucket: farm_1h
secureJsonData:
token: ${INFLUXDB_GRAFANA_TOKEN}
editable: false
Proměnné šablony řídicího panelu
Proměnné šablony v Grafaně umožňují vytvářet interaktivní dashboardy, kde je uživatel vyberte farmu, pozemek a časový rozsah z rozbalovacích nabídek v horní části stránky.
# grafana/provisioning/dashboards/farm-overview.json (estratto variabili)
{
"templating": {
"list": [
{
"name": "farm_id",
"type": "query",
"label": "Azienda",
"datasource": "InfluxDB-FarmRaw",
"query": "import \"influxdata/influxdb/schema\"\nschema.tagValues(bucket: \"farm_raw\", tag: \"farm_id\")",
"refresh": 2,
"includeAll": false,
"multi": false,
"current": {}
},
{
"name": "field_id",
"type": "query",
"label": "Appezzamento",
"datasource": "InfluxDB-FarmRaw",
"query": "import \"influxdata/influxdb/schema\"\nschema.tagValues(bucket: \"farm_raw\", tag: \"field_id\", predicate: (r) => r.farm_id == \"${farm_id}\")",
"refresh": 2,
"includeAll": true,
"multi": true
},
{
"name": "sensor_id",
"type": "query",
"label": "Sensore",
"datasource": "InfluxDB-FarmRaw",
"query": "import \"influxdata/influxdb/schema\"\nschema.tagValues(bucket: \"farm_raw\", tag: \"sensor_id\", predicate: (r) => r.field_id == \"${field_id}\")",
"refresh": 2,
"includeAll": true,
"multi": true
}
]
}
}
Panely přístrojové desky pro zemědělství: Kompletní průvodce
Výběr správného typu panelu pro každou metriku je zásadní pro čitelnost přístrojová deska. Panel typu "Time Series" pro úroveň baterie a méně okamžitý "Měřidlo" nebo "Stat". Zde je mapa optimálních panelů pro zemědělské metriky:
Typy panelů pro zemědělské metriky
| Metrický | Typ panelu | Proč | Barevné prahy |
|---|---|---|---|
| Teplota půdy/vzduchu | Časové řady + statistiky | Časový trend + aktuální hodnota | Zelená <25, oranžová 25-30, červená >30 |
| Půdní vlhkost | Měřidlo + časové řady | Měřidlo ukazuje % versus HR a PWP | Červená <25, oranžová 25-35, zelená 35-70 |
| pH půdy | Měřidlo | Přesná hodnota, mění se pomalu | Červená <5,5 nebo >7,5, Zelená 6,0-7,0 |
| PAR/DLI | Časová řada | Denní variabilita, sluneční vzor | Bez prahu, informativní |
| Srážky | Sloupcový graf | Denní diskrétní data | Bez prahu, informativní |
| Hladina vody | Měřidlo + Stat | Procento nádrže + dostupné litry | Červená <20 %, oranžová 20–40 % |
| Baterie senzoru | Tabulka | Zobrazit všechny senzory, filtr kritický | Červená <20 %, oranžová 20–30 % |
| Vykreslit mapu | Geomapa | Geografické zobrazení stavu farmy | Barva podle stavu agregátu |
Konfigurace panelu půdní vlhkosti s prahovými hodnotami
# Estratto JSON panel Gauge umidita suolo
{
"id": 3,
"title": "Umidita Suolo - ${sensor_id}",
"type": "gauge",
"datasource": "InfluxDB-FarmRaw",
"targets": [
{
"refId": "A",
"query": "from(bucket: \"farm_raw\")\n |> range(start: -5m)\n |> filter(fn: (r) => r._measurement == \"farm_sensors\")\n |> filter(fn: (r) => r.farm_id == \"${farm_id}\")\n |> filter(fn: (r) => r._field == \"soil_moisture_pct\")\n |> last()\n |> group(columns: [\"sensor_id\"])"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "red", "value": 0 },
{ "color": "orange", "value": 25 },
{ "color": "green", "value": 35 },
{ "color": "orange", "value": 70 },
{ "color": "red", "value": 85 }
]
},
"mappings": [],
"custom": {
"neutralColors": false
}
}
},
"options": {
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"showThresholdLabels": true,
"showThresholdMarkers": true
}
}
Panel geomap pro vizualizaci plotu
# Panel Geomap - stato aggregato per appezzamento
{
"id": 10,
"title": "Mappa Farm - Stato Sensori",
"type": "geomap",
"datasource": "InfluxDB-FarmRaw",
"targets": [
{
"refId": "A",
"query": "// Query che restituisce lat/lon e stato aggregato\nfrom(bucket: \"farm_raw\")\n |> range(start: -10m)\n |> filter(fn: (r) => r._measurement == \"farm_sensors\")\n |> filter(fn: (r) => r.farm_id == \"${farm_id}\")\n |> filter(fn: (r) => r._field == \"soil_moisture_pct\")\n |> last()\n |> group(columns: [\"field_id\"])\n |> mean()"
}
],
"options": {
"view": {
"id": "coords",
"lat": 43.4,
"lon": 11.1,
"zoom": 13
},
"layers": [
{
"type": "markers",
"config": {
"size": { "fixed": 20 },
"color": {
"field": "soil_moisture_pct",
"fixed": "green"
},
"fillOpacity": 0.8,
"symbol": { "fixed": "circle" }
}
}
]
}
}
Upozornění Grafana for Farm IoT: Kompletní konfigurace
The alerting system is the most critical component for the dashboard's value proposition. An alert 2 hours late due to water stress can be worth thousands of euros in damages plodiny. Grafana Unified Alerting (introduced in Grafana 9, consolidated in v10/v11) provides a complete system with configurable alert rules, contact points and notification policies zcela přes YAML (Infrastructure as Code).
Kontaktní místa: E-mail, Telegram a Slack
# grafana/provisioning/alerting/contact-points.yaml
apiVersion: 1
contactPoints:
# Contact point email per il responsabile tecnico
- orgId: 1
name: email-agronomo
receivers:
- uid: email_agronomo_01
type: email
settings:
addresses: "agronomo@aziendarossi.it;tecnico@aziendarossi.it"
singleEmail: false
message: |
ALERT FARM: {{ .GroupLabels.alertname }}
Appezzamento: {{ .CommonLabels.field_id }}
Valore: {{ .CommonAnnotations.value }}
Ora: {{ .CommonAnnotations.timestamp }}
disableResolveMessage: false
# Telegram Bot per notifiche immediate in campo
- orgId: 1
name: telegram-farm
receivers:
- uid: telegram_farm_01
type: telegram
settings:
bottoken: ${TELEGRAM_BOT_TOKEN}
chatid: ${TELEGRAM_CHAT_ID}
message: |
ALLERTA FARM {{ .CommonLabels.farm_id }}
{{ .CommonAnnotations.description }}
Appezzamento: {{ .CommonLabels.field_id }}
Sensore: {{ .CommonLabels.sensor_id }}
Valore attuale: {{ .CommonAnnotations.current_value }}
Soglia: {{ .CommonAnnotations.threshold }}
# Slack per team operations
- orgId: 1
name: slack-farmops
receivers:
- uid: slack_farmops_01
type: slack
settings:
url: ${SLACK_WEBHOOK_URL}
channel: "#farm-alerts"
title: "FARM ALERT - {{ .GroupLabels.alertname }}"
text: |
*Farm:* {{ .CommonLabels.farm_id }}
*Appezzamento:* {{ .CommonLabels.field_id }}
*Problema:* {{ .CommonAnnotations.description }}
*Valore:* {{ .CommonAnnotations.current_value }}
Výstražná pravidla pro kritické zemědělské podmínky
# grafana/provisioning/alerting/alert-rules.yaml
apiVersion: 1
groups:
- orgId: 1
name: farm-critical-alerts
folder: Farm IoT
interval: 1m
rules:
# Regola 1: Stress idrico - umidita suolo sotto soglia critica
- uid: alert_stress_idrico
title: "Stress Idrico Rilevato"
condition: C
data:
- refId: A
relativeTimeRange:
from: 300 # ultimi 5 minuti
to: 0
datasourceUid: influxdb-farmraw
model:
query: |
from(bucket: "farm_raw")
|> range(start: -5m)
|> filter(fn: (r) => r._measurement == "farm_sensors")
|> filter(fn: (r) => r._field == "soil_moisture_pct")
|> last()
- refId: B
datasourceUid: __expr__
model:
type: reduce
expression: A
reducer: mean
- refId: C
datasourceUid: __expr__
model:
type: threshold
expression: B
conditions:
- evaluator:
type: lt
params: [28.0]
for: 10m # deve persistere 10 minuti prima di scattare
labels:
severity: critical
category: irrigation
farm_alert: "true"
annotations:
summary: "Stress idrico in ${{ $labels.field_id }}"
description: "Umidita suolo sotto soglia critica (28%). Attivare irrigazione."
current_value: "${{ $values.B }}%"
threshold: "28%"
# Regola 2: Rischio gelo - temperatura aria sotto 2 gradi Celsius
- uid: alert_rischio_gelo
title: "Rischio Gelo"
condition: C
data:
- refId: A
relativeTimeRange:
from: 600
to: 0
datasourceUid: influxdb-farmraw
model:
query: |
from(bucket: "farm_raw")
|> range(start: -10m)
|> filter(fn: (r) => r._measurement == "farm_sensors")
|> filter(fn: (r) => r._field == "air_temp_c")
|> last()
- refId: B
datasourceUid: __expr__
model:
type: reduce
expression: A
reducer: last
- refId: C
datasourceUid: __expr__
model:
type: threshold
expression: B
conditions:
- evaluator:
type: lt
params: [2.0]
for: 5m
labels:
severity: critical
category: frost
annotations:
summary: "Rischio gelo in ${{ $labels.field_id }}"
description: "Temperatura aria sotto 2 gradi. Attivare antigelo se disponibile."
# Regola 3: Batteria sensore critica
- uid: alert_batteria_bassa
title: "Batteria Sensore Critica"
condition: C
data:
- refId: A
relativeTimeRange:
from: 300
to: 0
datasourceUid: influxdb-farmraw
model:
query: |
from(bucket: "farm_raw")
|> range(start: -5m)
|> filter(fn: (r) => r._measurement == "farm_sensors")
|> filter(fn: (r) => r._field == "battery_pct")
|> last()
- refId: B
datasourceUid: __expr__
model:
type: reduce
expression: A
reducer: last
- refId: C
datasourceUid: __expr__
model:
type: threshold
expression: B
conditions:
- evaluator:
type: lt
params: [15.0]
for: 0s
labels:
severity: warning
category: maintenance
annotations:
summary: "Batteria bassa sensore ${{ $labels.sensor_id }}"
description: "Batteria sotto 15%. Pianificare sostituzione entro 48 ore."
Zásady oznamování a ztišení
# grafana/provisioning/alerting/notification-policies.yaml
apiVersion: 1
policies:
- orgId: 1
receiver: email-agronomo # receiver di default
group_by: ["farm_id", "field_id", "alertname"]
group_wait: 30s # attendi 30s prima del primo invio
group_interval: 5m # attendi 5m prima di re-inviare per gruppo
repeat_interval: 4h # ripeti notifica ogni 4 ore se alert attivo
routes:
# Priorità alta: gelo e stress idrico critico → Telegram immediato
- receiver: telegram-farm
matchers:
- name: severity
value: critical
isEqual: true
group_wait: 0s # invio immediato
repeat_interval: 1h
# Priorità media: manutenzione batteria → solo email
- receiver: email-agronomo
matchers:
- name: category
value: maintenance
isEqual: true
repeat_interval: 24h
# Team operations: tutti gli alert su Slack
- receiver: slack-farmops
matchers:
- name: farm_alert
value: "true"
isEqual: true
group_interval: 10m
repeat_interval: 12h
Angular Dashboard Custom: Kdy a proč
Grafana pokrývá 90 % případů použití provozního monitorování. Ale jsou scénáře, kde vlastní Angular dashboard a správná volba:
Grafana vs Angular Dashboard: Kdy si vybrat co
| Kritérium | Čistá Grafana | AngularCustom |
|---|---|---|
| Cílový uživatel | Technici, datoví analytici | Agronomové, manažeři, operátoři polí |
| Vlastní UX | Omezeno na typy panelů | Neomezené, mobil-first |
| Obchodní logika | Nevhodné | Vypočítaná upozornění, doporučení AI |
| Stávající integrace aplikací | Tvrdý (iframe) | Nativní Angular |
| Offline první PWA | Není podporováno | Nativní servisní pracovník |
| Obnovovací frekvence | Min 1 s (dotazování) | WebSocket podsekunda |
| Náklady na vývoj | Nízká (konfigurace) | Vysoká (vývoj na zakázku) |
| Údržba | Nízký | Vysoký |
Doporučená strategie pro podniky a IoT farmy hybridní: Grafana pro interní technický monitoring (IT, provoz, pokročilá agronomie) a Angular pro mobilní aplikaci které operátoři v terénu používají. Obě platformy sdílejí stejný zdroj dat (InfluxDB) a Angular mohou také vložit konkrétní panely Grafana prostřednictvím prvků iframe s tokenem pro komplexní analýzy, které se nevyplatí opakovat.
Angular Implementation: IoT Service a Real-Time Dashboard
Šablony TypeScript pro data senzoru
// src/app/models/farm-sensor.model.ts
export interface SensorReading {
sensorId: string;
farmId: string;
fieldId: string;
sensorType: 'soil' | 'weather' | 'ph' | 'par' | 'water';
timestamp: Date;
values: SensorValues;
batteryPct: number;
rssi?: number;
}
export interface SensorValues {
soilTempC?: number;
soilMoisturePct?: number;
soilEcDs?: number;
soilPh?: number;
airTempC?: number;
airHumidityPct?: number;
rainfallMm?: number;
windSpeedMs?: number;
parUmol?: number;
dliMol?: number;
waterLevelCm?: number;
}
export interface AlertEvent {
id: string;
sensorId: string;
fieldId: string;
alertType: 'stress_idrico' | 'gelo' | 'batteria' | 'ph_anomalo' | 'pioggia_intensa';
severity: 'critical' | 'warning' | 'info';
message: string;
value: number;
threshold: number;
timestamp: Date;
acknowledged: boolean;
}
export interface FieldStatus {
fieldId: string;
name: string;
lat: number;
lon: number;
area_ha: number;
crop: string;
sensors: SensorReading[];
overallStatus: 'ok' | 'warning' | 'critical';
activeAlerts: AlertEvent[];
}
Služba IoT s WebSocket a Angular Signals
// src/app/services/farm-iot.service.ts
import { Injectable, signal, computed, effect, DestroyRef, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { Observable, retry, timer, catchError, EMPTY, switchMap } from 'rxjs';
import { SensorReading, AlertEvent, FieldStatus } from '../models/farm-sensor.model';
@Injectable({ providedIn: 'root' })
export class FarmIotService {
private readonly http = inject(HttpClient);
private readonly destroyRef = inject(DestroyRef);
private wsSubject: WebSocketSubject<SensorReading> | null = null;
// === Angular Signals per stato real-time ===
readonly latestReadings = signal<Map<string, SensorReading>>(new Map());
readonly activeAlerts = signal<AlertEvent[]>([]);
readonly fieldStatuses = signal<FieldStatus[]>([]);
readonly isConnected = signal<boolean>(false);
readonly lastUpdateAt = signal<Date | null>(null);
// === Computed signals derivati ===
readonly criticalAlerts = computed(() =>
this.activeAlerts().filter(a => a.severity === 'critical' && !a.acknowledged)
);
readonly sensorsWithLowBattery = computed(() =>
Array.from(this.latestReadings().values())
.filter(r => r.batteryPct < 20)
.sort((a, b) => a.batteryPct - b.batteryPct)
);
readonly avgSoilMoisture = computed(() => {
const readings = Array.from(this.latestReadings().values())
.filter(r => r.sensorType === 'soil' && r.values.soilMoisturePct !== undefined);
if (readings.length === 0) return null;
const sum = readings.reduce((acc, r) => acc + (r.values.soilMoisturePct ?? 0), 0);
return sum / readings.length;
});
constructor() {
// Effect: log quando cambiano gli alert critici
effect(() => {
const criticals = this.criticalAlerts();
if (criticals.length > 0) {
console.warn(`[FarmIoT] ${criticals.length} alert critici attivi`);
}
});
}
// Connessione WebSocket al backend farm
connectWebSocket(farmId: string, wsUrl: string): void {
if (this.wsSubject) {
this.wsSubject.complete();
}
this.wsSubject = webSocket<SensorReading>({
url: `${wsUrl}/farm/${farmId}/stream`,
openObserver: {
next: () => {
this.isConnected.set(true);
console.log('[FarmIoT] WebSocket connesso');
}
},
closeObserver: {
next: () => {
this.isConnected.set(false);
console.log('[FarmIoT] WebSocket disconnesso');
}
}
});
this.wsSubject.pipe(
retry({
count: 5,
delay: (error, retryCount) => timer(Math.min(retryCount * 2000, 30000))
}),
catchError(err => {
console.error('[FarmIoT] WebSocket errore irreversibile:', err);
this.isConnected.set(false);
return EMPTY;
}),
takeUntilDestroyed(this.destroyRef)
).subscribe(reading => {
this.processReading(reading);
});
}
private processReading(reading: SensorReading): void {
// Aggiorna la mappa dei latest readings (immutable update)
this.latestReadings.update(map => {
const newMap = new Map(map);
newMap.set(reading.sensorId, { ...reading, timestamp: new Date(reading.timestamp) });
return newMap;
});
this.lastUpdateAt.set(new Date());
this.checkAlertConditions(reading);
}
private checkAlertConditions(reading: SensorReading): void {
const newAlerts: AlertEvent[] = [];
// Check stress idrico
if (reading.sensorType === 'soil' &&
reading.values.soilMoisturePct !== undefined &&
reading.values.soilMoisturePct < 28) {
newAlerts.push({
id: crypto.randomUUID(),
sensorId: reading.sensorId,
fieldId: reading.fieldId,
alertType: 'stress_idrico',
severity: reading.values.soilMoisturePct < 20 ? 'critical' : 'warning',
message: `Umidita suolo al ${reading.values.soilMoisturePct.toFixed(1)}% (soglia: 28%)`,
value: reading.values.soilMoisturePct,
threshold: 28,
timestamp: new Date(),
acknowledged: false
});
}
// Check rischio gelo
if (reading.sensorType === 'weather' &&
reading.values.airTempC !== undefined &&
reading.values.airTempC < 2) {
newAlerts.push({
id: crypto.randomUUID(),
sensorId: reading.sensorId,
fieldId: reading.fieldId,
alertType: 'gelo',
severity: 'critical',
message: `Temperatura aria a ${reading.values.airTempC.toFixed(1)} gradi (rischio gelo)`,
value: reading.values.airTempC,
threshold: 2,
timestamp: new Date(),
acknowledged: false
});
}
if (newAlerts.length > 0) {
this.activeAlerts.update(alerts => [...alerts, ...newAlerts]);
}
}
acknowledgeAlert(alertId: string): void {
this.activeAlerts.update(alerts =>
alerts.map(a => a.id === alertId ? { ...a, acknowledged: true } : a)
);
}
// Carica storico da API REST InfluxDB
getHistoricalData(
farmId: string,
fieldId: string,
field: string,
from: string = '-1h'
): Observable<Array<{ time: string; value: number }>> {
return this.http.get<Array<{ time: string; value: number }>>(
`/api/farm/${farmId}/history`,
{ params: { fieldId, field, from } }
);
}
}
Hlavní komponenta palubní desky
// src/app/pages/farm-dashboard/farm-dashboard.component.ts
import {
Component, OnInit, inject, signal, computed
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgxChartsModule, Color, ScaleType } from '@swimlane/ngx-charts';
import { FarmIotService } from '../../services/farm-iot.service';
import { AlertPanelComponent } from '../../components/alert-panel/alert-panel.component';
import { SensorGaugeComponent } from '../../components/sensor-gauge/sensor-gauge.component';
interface ChartDataPoint {
name: Date;
value: number;
}
interface ChartSeries {
name: string;
series: ChartDataPoint[];
}
@Component({
selector: 'app-farm-dashboard',
standalone: true,
imports: [CommonModule, NgxChartsModule, AlertPanelComponent, SensorGaugeComponent],
templateUrl: './farm-dashboard.component.html',
styleUrl: './farm-dashboard.component.css'
})
export class FarmDashboardComponent implements OnInit {
readonly farmIot = inject(FarmIotService);
// Config dashboard
readonly farmId = signal('az_rossi_chianti');
readonly selectedField = signal('appezzamento_a1');
// Dati grafici time-series per ngx-charts
readonly temperatureData = signal<ChartSeries[]>([]);
readonly moistureData = signal<ChartSeries[]>([]);
// Computed per statistiche aggregate
readonly fieldSummary = computed(() => {
const readings = Array.from(this.farmIot.latestReadings().values())
.filter(r => r.fieldId === this.selectedField());
return {
totalSensors: readings.length,
avgMoisture: readings
.filter(r => r.values.soilMoisturePct !== undefined)
.reduce((sum, r, _, arr) => sum + (r.values.soilMoisturePct ?? 0) / arr.length, 0),
avgTempSuolo: readings
.filter(r => r.values.soilTempC !== undefined)
.reduce((sum, r, _, arr) => sum + (r.values.soilTempC ?? 0) / arr.length, 0),
criticalCount: readings.filter(r => r.batteryPct < 20).length
};
});
// Colori ngx-charts
readonly colorScheme: Color = {
name: 'farm',
selectable: true,
group: ScaleType.Ordinal,
domain: ['#4CAF50', '#FF9800', '#F44336', '#2196F3', '#9C27B0']
};
ngOnInit(): void {
// Connetti WebSocket
const wsUrl = 'wss://farm-api.aziendarossi.it';
this.farmIot.connectWebSocket(this.farmId(), wsUrl);
// Carica dati storici per i grafici
this.loadHistoricalCharts();
}
private loadHistoricalCharts(): void {
// In un'implementazione reale qui caricheremo da API
// Per ora simuliamo dati di esempio strutturati per ngx-charts
const now = new Date();
const generateSeries = (baseValue: number, variance: number): ChartDataPoint[] =>
Array.from({ length: 60 }, (_, i) => ({
name: new Date(now.getTime() - (59 - i) * 60000),
value: baseValue + (Math.random() - 0.5) * variance
}));
this.temperatureData.set([
{ name: 'Suolo A1', series: generateSeries(22, 4) },
{ name: 'Suolo A2', series: generateSeries(24, 3) },
{ name: 'Aria', series: generateSeries(18, 6) }
]);
this.moistureData.set([
{ name: 'Umidita A1-Ovest', series: generateSeries(42, 8) },
{ name: 'Umidita A1-Est', series: generateSeries(38, 10) }
]);
}
}
Šablona řídicího panelu HTML s grafy ngx
<!-- farm-dashboard.component.html -->
<div class="farm-dashboard">
<!-- Header con stato connessione -->
<header class="dashboard-header">
<div class="header-left">
<h1>Farm Intelligence</h1>
<span class="farm-name">Azienda Rossi - Chianti</span>
</div>
<div class="header-right">
<div class="connection-status" [class.connected]="farmIot.isConnected()">
<span class="status-dot"></span>
{{ farmIot.isConnected() ? 'Live' : 'Offline' }}
</div>
<span class="last-update">
Aggiornato: {{ farmIot.lastUpdateAt() | date:'HH:mm:ss' }}
</span>
</div>
</header>
<!-- Alert Panel: alert critici in evidenza -->
@if (farmIot.criticalAlerts().length > 0) {
<section class="alerts-critical">
<div class="alert-banner">
<span class="alert-icon">ALLERTA</span>
<strong>{{ farmIot.criticalAlerts().length }} alert critici attivi</strong>
</div>
@for (alert of farmIot.criticalAlerts(); track alert.id) {
<div class="alert-card critical">
<div class="alert-info">
<span class="alert-type">{{ alert.alertType }}</span>
<span class="alert-sensor">Sensore: {{ alert.sensorId }}</span>
<p class="alert-msg">{{ alert.message }}</p>
</div>
<button
class="btn-acknowledge"
(click)="farmIot.acknowledgeAlert(alert.id)">
Confermato
</button>
</div>
}
</section>
}
<!-- KPI Summary Cards -->
<section class="kpi-grid">
<div class="kpi-card">
<span class="kpi-label">Sensori Attivi</span>
<span class="kpi-value">{{ fieldSummary().totalSensors }}</span>
</div>
<div class="kpi-card" [class.warning]="fieldSummary().avgMoisture < 30">
<span class="kpi-label">Umidita Media Suolo</span>
<span class="kpi-value">{{ fieldSummary().avgMoisture | number:'1.1-1' }}%</span>
</div>
<div class="kpi-card">
<span class="kpi-label">Temp. Media Suolo</span>
<span class="kpi-value">{{ fieldSummary().avgTempSuolo | number:'1.1-1' }} C</span>
</div>
<div class="kpi-card" [class.critical]="fieldSummary().criticalCount > 0">
<span class="kpi-label">Sensori Batteria Critica</span>
<span class="kpi-value">{{ fieldSummary().criticalCount }}</span>
</div>
</section>
<!-- Grafici Time-Series -->
<section class="charts-grid">
<div class="chart-container">
<h3>Temperatura Suolo e Aria (ultima ora)</h3>
<ngx-charts-line-chart
[results]="temperatureData()"
[scheme]="colorScheme"
[xAxis]="true"
[yAxis]="true"
[legend]="true"
[showXAxisLabel]="true"
xAxisLabel="Ora"
[showYAxisLabel]="true"
yAxisLabel="Temperatura (C)"
[timeline]="false"
[autoScale]="true"
[roundDomains]="true">
</ngx-charts-line-chart>
</div>
<div class="chart-container">
<h3>Umidita Suolo (ultima ora)</h3>
<ngx-charts-area-chart
[results]="moistureData()"
[scheme]="colorScheme"
[xAxis]="true"
[yAxis]="true"
[legend]="true"
[showYAxisLabel]="true"
yAxisLabel="Umidita (%)"
[autoScale]="false"
[yScaleMin]="0"
[yScaleMax]="100">
</ngx-charts-area-chart>
</div>
</section>
<!-- Tabella Sensori con Batteria -->
<section class="sensors-table">
<h3>Stato Sensori - Appezzamento {{ selectedField() }}</h3>
<table>
<thead>
<tr>
<th>Sensore</th>
<th>Tipo</th>
<th>Umidita</th>
<th>Temp. Suolo</th>
<th>Batteria</th>
<th>Ultimo Update</th>
</tr>
</thead>
<tbody>
@for (reading of farmIot.latestReadings() | keyvalue; track reading.key) {
<tr [class.battery-low]="reading.value.batteryPct < 20">
<td>{{ reading.value.sensorId }}</td>
<td>{{ reading.value.sensorType }}</td>
<td>{{ reading.value.values.soilMoisturePct | number:'1.1-1' }}%</td>
<td>{{ reading.value.values.soilTempC | number:'1.1-1' }} C</td>
<td>
<div class="battery-bar">
<div
class="battery-fill"
[style.width.%]="reading.value.batteryPct"
[class.low]="reading.value.batteryPct < 20">
</div>
</div>
{{ reading.value.batteryPct | number:'1.0-0' }}%
</td>
<td>{{ reading.value.timestamp | date:'HH:mm:ss' }}</td>
</tr>
}
</tbody>
</table>
</section>
</div>
Mobile-First Design a PWA pro operátory v terénu
Terénní pracovníci používají chytré telefony, často s nestabilním připojením a podmínkami intenzivního slunečního světla, které ztěžuje čitelnost obrazovek s bílým pozadím. Mobilní design pro farmářské dashboardy vyžaduje specifické úvahy na tradiční podnikové dashboardy.
/* farm-dashboard.component.css - Mobile-First Responsive */
/* Base: Mobile (320px-767px) */
.farm-dashboard {
padding: 8px;
background: #0d1117;
min-height: 100vh;
color: #e6edf3;
}
.dashboard-header {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
padding: 12px;
background: #161b22;
border-radius: 8px;
}
.kpi-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 16px;
}
.kpi-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 12px;
text-align: center;
}
.kpi-card.warning {
border-color: #FF9800;
background: rgba(255, 152, 0, 0.1);
}
.kpi-card.critical {
border-color: #F44336;
background: rgba(244, 67, 54, 0.1);
}
.kpi-value {
display: block;
font-size: 1.8em;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
color: #58a6ff;
}
.charts-grid {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 16px;
}
.chart-container {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 16px;
overflow: hidden;
}
/* Tablet (768px+) */
@media (min-width: 768px) {
.farm-dashboard { padding: 16px; }
.dashboard-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.kpi-grid {
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
}
/* Desktop (1024px+) */
@media (min-width: 1024px) {
.farm-dashboard {
max-width: 1400px;
margin: 0 auto;
padding: 24px;
}
.kpi-value { font-size: 2.2em; }
}
/* Alta luminosita outdoor - contrasto aumentato */
@media (prefers-contrast: high) {
.kpi-card { border-color: #58a6ff; }
.kpi-value { color: #ffffff; }
}
Service Worker PWA pro Offline-First
// ngsw-config.json - Configurazione PWA offline-first
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": ["/favicon.ico", "/index.html", "/*.css", "/*.js"]
}
}
],
"dataGroups": [
{
"name": "farm-api-recent",
"urls": ["/api/farm/*/history*"],
"cacheConfig": {
"strategy": "freshness",
"maxSize": 100,
"maxAge": "15m",
"timeout": "3s"
}
},
{
"name": "farm-api-static",
"urls": ["/api/farm/*/fields", "/api/farm/*/sensors"],
"cacheConfig": {
"strategy": "performance",
"maxSize": 20,
"maxAge": "1h"
}
}
]
}
Docker Compose Full Stack: Připraveno na nasazení
Lze nasadit celý stack (Mosquitto, InfluxDB, Telegraf, Grafana, Angular app)
se singlem docker compose up -d. Toto je výchozí bod pro všechny
nasazení, a to jak on-premise, tak cloudové VPS.
# docker-compose.yml - Farm IoT Stack completo
version: "3.9"
services:
# === MQTT Broker ===
mosquitto:
image: eclipse-mosquitto:2.0.18
container_name: farm-mosquitto
restart: unless-stopped
ports:
- "1883:1883"
- "8883:8883" # MQTT over TLS
- "9001:9001" # WebSocket
volumes:
- ./mosquitto/config:/mosquitto/config:ro
- ./mosquitto/data:/mosquitto/data
- ./mosquitto/log:/mosquitto/log
- ./certs:/mosquitto/certs:ro
networks:
- farm-net
# === InfluxDB Time-Series Database ===
influxdb:
image: influxdb:2.7-alpine
container_name: farm-influxdb
restart: unless-stopped
ports:
- "8086:8086"
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: admin
DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUXDB_ADMIN_PASSWORD}
DOCKER_INFLUXDB_INIT_ORG: azienda_rossi
DOCKER_INFLUXDB_INIT_BUCKET: farm_raw
DOCKER_INFLUXDB_INIT_RETENTION: 90d
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUXDB_ADMIN_TOKEN}
volumes:
- influxdb_data:/var/lib/influxdb2
- ./influxdb/config:/etc/influxdb2:ro
networks:
- farm-net
healthcheck:
test: ["CMD", "influx", "ping"]
interval: 30s
timeout: 10s
retries: 3
# === Telegraf: Bridge MQTT -> InfluxDB ===
telegraf:
image: telegraf:1.30-alpine
container_name: farm-telegraf
restart: unless-stopped
depends_on:
influxdb:
condition: service_healthy
environment:
TELEGRAF_MQTT_PASSWORD: ${TELEGRAF_MQTT_PASSWORD}
INFLUXDB_TOKEN: ${INFLUXDB_TELEGRAF_TOKEN}
volumes:
- ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro
networks:
- farm-net
# === Grafana: Visualizzazione e Alerting ===
grafana:
image: grafana/grafana-oss:11.5.0
container_name: farm-grafana
restart: unless-stopped
ports:
- "3000:3000"
depends_on:
- influxdb
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
GF_SECURITY_SECRET_KEY: ${GRAFANA_SECRET_KEY}
GF_SERVER_ROOT_URL: https://grafana.aziendarossi.it
GF_SMTP_ENABLED: "true"
GF_SMTP_HOST: smtp.gmail.com:587
GF_SMTP_USER: ${SMTP_USER}
GF_SMTP_PASSWORD: ${SMTP_PASSWORD}
INFLUXDB_GRAFANA_TOKEN: ${INFLUXDB_GRAFANA_TOKEN}
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID}
SLACK_WEBHOOK_URL: ${SLACK_WEBHOOK_URL}
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
networks:
- farm-net
# === Angular App: Frontend Mobile-First ===
farm-app:
image: nginx:1.27-alpine
container_name: farm-angular-app
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./dist/farm-app/browser:/usr/share/nginx/html:ro
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- grafana
networks:
- farm-net
# === Backup automatico InfluxDB ===
influxdb-backup:
image: influxdb:2.7-alpine
container_name: farm-backup
restart: "no"
depends_on:
- influxdb
environment:
INFLUXDB_TOKEN: ${INFLUXDB_ADMIN_TOKEN}
volumes:
- ./backups:/backups
- ./scripts/backup.sh:/backup.sh:ro
entrypoint: ["/bin/sh", "/backup.sh"]
networks:
- farm-net
volumes:
influxdb_data:
driver: local
grafana_data:
driver: local
networks:
farm-net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
# .env.example - Variabili d'ambiente (NON committare mai il .env reale!)
INFLUXDB_ADMIN_PASSWORD=ChangeMe_Strong_Password_2025!
INFLUXDB_ADMIN_TOKEN=my-super-secret-admin-token-change-this
INFLUXDB_TELEGRAF_TOKEN=telegraf-write-only-token-change-this
INFLUXDB_GRAFANA_TOKEN=grafana-read-only-token-change-this
GRAFANA_ADMIN_PASSWORD=ChangeMe_Grafana_2025!
GRAFANA_SECRET_KEY=random-32-char-secret-key-here-xx
TELEGRAF_MQTT_PASSWORD=telegraf-mqtt-password
SMTP_USER=notifiche@aziendarossi.it
SMTP_PASSWORD=app-specific-password
TELEGRAM_BOT_TOKEN=123456:ABC-DEF-your-bot-token
TELEGRAM_CHAT_ID=-1001234567890
Výkon a škálovatelnost: Optimalizace dotazů a ukládání do mezipaměti
S 50-200 senzory, které odesílají data každých 30 sekund, množství dat v InfluxDB roste rychle: přibližně 5-20 milionů bodů za den. Bez optimalizačních strategií dotazy na dashboard se zpomalují a uživatelská zkušenost se zhoršuje. Zde jsou techniky zásadní pro udržení doby odezvy pod 500 ms i na skromném hardwaru.
Optimalizační strategie pro IoT Dashboard Farm
| Technika | Implementace | Dopad |
|---|---|---|
| Vícevrstvé převzorkování | Task Flux každou 1h, 1d | 10-50x rychlejší historické dotazy |
| Agregace push-down | agregační okno ve Fluxu, nikoli v paměti | Snížení přenosu dat o 90 %+ |
| Ukládání dotazů řídicího panelu do mezipaměti | Grafana cache vrstva + Redis | -70 % nadbytečných dotazů |
| Zhmotněné pohledy | Segment "farm_kpi" s předagregacemi | Zatížení palubní desky <100 ms |
| WebSocket místo dotazování | Angular WebSocket vs HTTP dotazování | -80% zatížení frontend serveru |
| Progresivní načítání | Nejprve načtěte KPI, později grafy | Vnímaný výkon +60 % |
| Kontrola mohutnosti štítku | Max. 1000 sérií na měření | Zabraňuje degradaci DB |
// Configurazione Grafana per ridurre query ridondanti
// grafana/provisioning/grafana.ini
[caching]
enabled = true
ttl = 60s
max_value_mb = 512
[database]
cache_mode = shared
// Query ottimizzata con aggregazione lato server:
// MALE - trasferisce tutti i punti raw, aggrega in Grafana
from(bucket: "farm_raw")
|> range(start: -24h)
|> filter(fn: (r) => r._field == "soil_moisture_pct")
// BENE - aggrega in InfluxDB, trasferisce solo i punti necessari
from(bucket: "farm_raw")
|> range(start: -24h)
|> filter(fn: (r) => r._field == "soil_moisture_pct")
|> aggregateWindow(every: 5m, fn: mean, createEmpty: false)
|> keep(columns: ["_time", "_value", "sensor_id", "field_id"])
Případová studie: Vinařství Rossi – 50 senzorů, 6 pozemků
Rossi Agricultural Company vyrábí Chianti Classico DOCG na 35 hektarech distribuovaných na 6 pozemky s různými půdními vlastnostmi. Před implementací systému Monitorování internetu věcí bylo založeno na týdenních návštěvách agronoma a ručních měřeních. Byly odhadnuty průměrné ztráty za sezónu ze suboptimálního zavlažování a pozdních mrazů kolem 18 % potenciálního výnosu.
Specifikace nasazení společnosti Rossi
| Komponent | Detail |
|---|---|
| Celkem senzorů | 50 (8-9 na pozemek): půda, počasí, pH, PAR |
| Okrajové brány | 6 Raspberry Pi 4 (jeden na pozemek), připojení 4G |
| Protokol bezdrátového senzoru | LoRaWAN (půda/pH), Zigbee (místní počasí), RS-485 (nádrže) |
| Broker MQTT | Eclipse Mosquitto, VPS OVH Europa, TLS 1.3 |
| Databáze | InfluxDB v2.7, VPS 4 vCPU / 8 GB RAM, 200 GB SSD |
| Panel obnovovací frekvence | 30 sekund pozemní data, 5 sekund kritická upozornění |
| Objem dat | ~3,5 milionu bodů/den, ~1,2 GB/měsíc v surovém stavu |
| Grafana Dashboard | 4 řídicí panely: přehled farmy, podle pozemku, senzory, historie výstrah |
| Mobilní aplikace Angular | PWA, kterou používají operátoři v terénu na Androidu |
| Upozornění | Telegram (okamžitý kritický) + denní přehled e-mailů |
| Celkový hardware | 6 bran + 50 senzorů + cloudové VPS |
| Náklady na infrastrukturu/rok | ~4 200 EUR (VPS + šířka pásma + údržba) |
Výsledky naměřené po 2 sezónách
Metriky úspor a návratnosti investic – Rossi Company (sezóny 2024 a 2025)
| Metrický | Pre-IoT (průměr 2022–2023) | Post-IoT (2024–2025) | Delta |
|---|---|---|---|
| Spotřeba závlahové vody | ~2 800 m3/hektar/sezóna | ~1 750 m3/hektar/sezóna | -37,5 % |
| Ztráty vodního stresu | ~18% potenciální výnos | ~4% potenciální výnos | -78% ztráty |
| Pozdní ztráty mrazem | Variabilní, 0-15 % v kritických letech | 0 % (7 včasných zásahů proti zamrznutí) | Vyřazeno |
| Náklady na hnojivo/ha | Základní 100 % | 73 % výchozí hodnoty | -27 % |
| Sledování hodin/týden | 12-15 hodin agronom | 2-3 hodiny (pouze pod dohledem) | -82 % |
| Průměrný výnos vinic | 7,2 tuny/ha | 8,6 tuny/ha | +19,4 % |
| Extra výrobní hodnota | - | + 42 000 €/sezóna | Návratnost návratnosti investic <18 měsíců |
Nejvýznamnějším údajem není úspora vody (i když významná), aleeliminace kompletní se ztrátami z pozdních mrazů. V sezóně 2024 se ujal systém tři případy poklesu teploty pod 2 stupně mezi březnem a dubnem, automaticky se aktivují Oznámení telegramem v 02:15, 03:40 a 01:20. Ve všech třech případech zaměstnanci kteří žijí na farmě, aktivovali uvnitř protimrazové systémy (rozstřikovače nadbytečných listů). 20 minut. Odhadovaná škoda mrazem bez zásahu by byla přibližně 15 000 EUR pouze na březnovou akci.
Poučení z implementace
- Kalibrace půdního senzoru: Kapacitní senzory vlhkosti jsou citlivé na složení půdy. Kalibrace na místě je vyžadována pro každý typ terénu, nejen pro instalaci typu plug-and-play.
- 4G konektivita jako překážka: V kopcovitých oblastech může být pokrytí 4G přerušované. Lokální vyrovnávací paměť na branách (SQLite, 7 dní) se ukázala jako nepostradatelná pro zamezení ztráty dat.
- Pozor únava: První konfigurace měla příliš mnoho aktivních prahových hodnot. Během dvou týdnů začali operátoři oznámení ignorovat. Je nezbytné začít s několika kritickými výstrahami a postupně přidávat další.
- Školení operátora: Přijetí mobilní aplikace Angular vyžadovalo 3 2hodinové tréninky. Investice do školení jsou stejně důležité jako investice do technologií.
- Údržba senzoru: Půdní senzory ve vinařském prostředí vyžadují čtvrtletní čištění (zbytky po ošetření). Naplánujte si preventivní prohlídky.
Zabezpečení systému IoT Farm: Best Practice
Zemědělský systém IoT připojený k internetu je vektorem útoku, pokud není chráněn přiměřeně. Útočný povrch zahrnuje: odhalený MQTT broker, InfluxDB API, Rozhraní Grafana, aplikace Angular. Následující opatření představují minimum nutné pro produkční nasazení.
Kontrolní seznam zabezpečení farmy IoT
| Plocha | Opatření | Přednost |
|---|---|---|
| Broker MQTT | Certifikáty TLS 1.3 + mTLS pro každé zařízení | Kritika |
| Ověřování MQTT | Jedinečné uživatelské jméno/heslo na zařízení + ACL na téma | Kritika |
| InfluxDB | Samostatné tokeny pouze pro čtení pro aplikace Grafana a Angular | Vysoký |
| InfluxDB | Nevystavujte port 8086 internetu, pouze interní síti | Kritika |
| Grafana | Ověření OAuth2/SAML, zakázat anonymní přihlášení | Vysoký |
| Grafana | Reverzní proxy Nginx s omezením rychlosti | Vysoký |
| Aplikace Angular | HTTPS vždy, hlavičky HSTS, CSP | Vysoký |
| VPS/Server | Firewall potřebuje pouze porty, fail2ban, automatické aktualizace | Kritika |
| Tajemství | Proměnné prostředí, nikdy pevně zakódované, čtvrtletní rotace | Kritika |
| Okrajové brány | Automatické aktualizace firmwaru, VPN do cloudu | Vysoký |
Závěry: Dashboard jako rozhodovací centrum farmy
Dobře navržená farma IoT dashboardů není datový panel: je to digitální mozek moderní zemědělské společnosti. Když Grafana zjistí vlhkost půdy ve vinici A3 klesne pod kritický práh a Angular informuje operátora v poli pomocí senzoru přesné a akční doporučení, smyčka vnímání-analýza-rozhodnutí-akce ano komprimuje od hodin (nebo dnů) po minuty.
Zásobník popsaný v tomto článku, Mosquitto + Telegraf + InfluxDB + Grafana + Angular, a vyspělá, produkčně připravená open-source kombinace. Náklady na infrastrukturu pro středně velká společnost (50-100 senzorů) jsou v řádu 3 000-6 000 EUR za rok oproti agronomickým přínosům, které v případě společnosti Rossi přesáhly 40 000 EUR ročně již od první celé sezóny. ROI nikdy nebyla tak jasná.
Dalším krokem pro společnosti, které zavedly základní monitorování, např aintegrace s prediktivními modely: Sloučit data senzoru s satelitní data (NDVI z Sentinel-2, viz článek 3 této série), Granulované předpovědi počasí a ML modely pro vodní stres a předpověď výnosů. Je to hranice, kde se data IoT stávají prediktivní agronomickou inteligencí.
Technické shrnutí
- InfluxDB: Návrh schématu s oddělením tag/pole, vícevrstvá nádoba, Flux pro automatické převzorkování
- Telegrafovat: Most MQTT-InfluxDB, parsování JSON, extrakce značek ze struktury tématu
- Grafana: Poskytování datových zdrojů YAML, typy panelů pro zemědělské metriky, jednotné upozornění s více kontaktními body
- Úhlové signály: Správa neměnných stavů v reálném čase, počítané deriváty, WebSocket s automatickým opakováním
- ngx-grafy: Spojnicový graf, plošný graf pro časové řady v reálném čase s aktualizací řízenou signálem
- PWA: Service Worker Angular pro offline-first, čerstvost strategie mezipaměti pro nejnovější API
- Docker Compose: Služby plného zásobníku 5, proměnné prostředí, kontrola stavu, izolovaná síť
Další článek v seriálu
Dostali jsme se k předposlednímu článku série FoodTech. V číslo 10, poslední v sérii, budeme řešit Optimalizace potravin v dodavatelském řetězci: jak modely ML optimalizují potravinový dodavatelský řetězec za účelem snížení plýtvání, předvídat poptávku a koordinovat logistiku a distribuci způsobem založeným na datech. Od optimalizace dodacích tras až po předpověď trvanlivosti, článek, který spojuje agronomickou dokonalost s průmyslovou efektivitou.
Prozkoumejte zbytek série FoodTech
- Článek 1: IoT Pipeline pro přesné zemědělství – základy MQTT a Pythonu
- Článek 2: ML Edge pro monitorování plodin – počítačové vidění v polích
- Článek 3: Satelitní API a NDVI s Pythonem a Sentinel-2 – otevřená satelitní data
- Článek 4: Sledovatelnost blockchainu v potravinách – od původu ke spotřebiteli
- Článek 5: Počítačové vidění pro kontrolu kvality v potravinářském průmyslu
- Článek 6: FSMA a Digital Compliance – regulační automatizace FDA/EU
- Článek 7: Vertikální zemědělství – Kontrola životního prostředí s IoT a ML
- Článek 8: Prognóza poptávky pro maloobchod s potravinami s Prophet a LightGBM







