Angular ve Grafana ile Çiftlik IoT için Gerçek Zamanlı Kontrol Paneli
Bir temmuz gecesi saat sabahın 3.47'si. Chianti bağındaki bir sensör bu sıcaklığı kaydediyor Dördüncü kez toprağın kök seviyesindeki kritik eşiği 28 santigrat dereceyi aştı ardışık saat. Gerçek zamanlı bir izleme sistemi olmadan tarım uzmanı sorunu sabah keşfeder Manuel inceleme sırasında aşağıdakiler. Hasar zaten meydana geldi: akut su stresi, olgunlaşma dengesiz bir şekilde hızlandı; parti veriminde tahmini %12'lik kayıp.
Uyarıya bağlı gerçek zamanlı bir kontrol paneli ile aynı olay, Telgraf 03:48'de. Otomatik sulama sistemi saat 03:49'da devreye girmektedir. 06:00'da, Ziraat uzmanı uyandığında, yapılan müdahaleyi, eğriyi belgeleyen bir rapor bulur. doğru sıcaklık ve mevcut durum: her şey normal. Reaktivite ile arasındaki bu fark proaktiflik ortalama olarak Su stresinden kaynaklanan kayıpların %15-30'u mahsullerde güzel Akdenizli olanlar.
Çiftlik zekası kontrol panelleri bir raporlama aracı değildir; zamanında karar verme sistemleridir gerçek. Birleştirme Grafana operasyonel gösterim için, AkışDB zaman serisi depolaması için, e Açısal özel mobil öncelikli arayüzler için, e Ham sensör verilerini dönüştüren bir çiftlik zekası platformu oluşturmak mümkündür. somut tarımsal eylemler. Bu makale MQTT aracısından panele kadar tüm mimariyi kapsamaktadır. Tarladaki tarım uzmanının telefonundan kontrol.
Bu Makalede Neler Öğreneceksiniz?
- IoT çiftlikleri için eksiksiz bir gösterge panosu sisteminin mimarisi: sensörler, InfluxDB, Grafana, Angular
- Flux ve InfluxQL ile tarımsal veriler için optimize edilmiş InfluxDB şema tasarımı
- Grafana yapılandırması: veri kaynağı, paneller, şablon değişkenleri, YAML sağlama
- Grafana'yı iletişim noktaları (e-posta, Telegram, Slack), uyarı kuralları ve bildirim politikalarıyla uyarma
- Angular özel ve saf Grafana ne zaman kullanılmalı: kullanım örnekleri ve hibrit mimari
- WebSockets, Signals ve ngx grafikleriyle gerçek zamanlı grafikler içeren açısal IoT hizmeti
- Docker Compose tam yığın: Mosquitto + InfluxDB + Grafana + Angular
- Örnek olay: 50 sensör ve 6 parselden oluşan şarap imalathanesi
FoodTech Serisi - Tüm Makaleler
| # | Öğe | Seviye | Durum |
|---|---|---|---|
| 1 | Python ve MQTT ile Hassas Tarım için IoT Pipeline | Gelişmiş | Mevcut |
| 2 | Mahsul İzleme için ML Edge: Tarlalarda Bilgisayarlı Görme | Gelişmiş | Mevcut |
| 3 | Uydu API ve Bitki Örtüsü Endeksleri: Python ve Sentinel-2 ile NDVI | Orta seviye | Mevcut |
| 4 | Gıdada Blockchain izlenebilirliği: Tarladan süpermarkete | Orta seviye | Mevcut |
| 5 | Gıda Endüstrisinde Kalite Kontrol için Bilgisayarlı Görme | Gelişmiş | Mevcut |
| 6 | FSMA ve Dijital Uyumluluk: Düzenleyici Süreçlerin Otomasyonu | Orta seviye | Mevcut |
| 7 | Dikey Tarım: IoT ve ML ile Çevresel Kontrol | Gelişmiş | Mevcut |
| 8 | Prophet ve LightGBM ile Gıda Perakendesinde Talep Tahmini | Orta seviye | Mevcut |
| 9 | Angular ve Grafana ile Çiftlik IoT için Gerçek Zamanlı Kontrol Paneli (şu anda buradasınız) | Gelişmiş | Akım |
| 10 | Tedarik Zinciri Gıda Optimizasyonu: Atıkların Azaltılması için ML | Orta seviye | Mevcut |
Gerçek Zamanlı Kontrol Panelleri Tarımı Neden Değiştiriyor?
Geleneksel tarım uzun gözlem döngüleriyle işler: tarım uzmanı tarlayı ziyaret eder haftada bir veya iki kez görsel parametreleri tespit eder, deneyime dayalı olarak hareket eder. Bu model, çevresel değişkenler yavaş değiştiğinde iyi çalışır. Ama kriz İklim değişkenliği hızlandırdı: sıcak hava dalgaları, ani kuraklıklar, geç donlar ve ani su baskınları, haftalık ziyaretlerin garanti edemeyeceği müdahale süreleri gerektirir.
Ekonomik veriler bu karar gecikmesinin maliyetini ölçer. Bir araştırmaya göre Wageningen Üniversitesi'nin 2024'teki kayıpları, şunlara atfedilebilen kayıplar: sulama yok optimal mahsullerdeki potansiyel verimin %15 ila %30'unu oluştururlar yoğun meyve ve sebzeler. Kaliteli bağcılık için su stresi döneminde Veraison üzümlerde kalıcı hasara yol açarak %20-35 oranında azalmaya neden olabilir. bu parti için ticari değer. Şarap sektörünün önemli olduğu İtalya'da Yılda 15 milyar Euro'nun üzerindepasif izleme arasındaki fark ve reaktif her sezon risk altındaki yüz milyonlarca avroluk değeri temsil ediyor.
Gerçek Zamanlı İzlemenin Ekonomik Etkisi: Karşılaştırma
| Senaryo | Gerçek Zamanlı Olmadan | Gerçek Zamanlı | Delta |
|---|---|---|---|
| Yıllık su tüketimi | Başlangıç %100 | -%25 ila -%40 | Önemli tasarruflar |
| Su stresi kayıpları | %15-30 verim | %3-7 verim | +%8-23 geri kazanılmış verim |
| Gübre maliyetleri | Başlangıç %100 | -%20 ila -%35 | Değişken optimizasyonu |
| Geç don kayıpları | Yüksek, kısmen önlenebilir | Erken uyarıyla azaltıldı | -%60 önlenebilir hasar |
| Çalışma saatlerini izleme | 8-12 saat/hafta | 1-2 saat/hafta | -80% çalışma süresi |
| IoT sistemi yatırım getirisi + kontrol paneli | Yok | 18-36 ay geri ödeme | 2-3 sezon olumlu |
Grafana platformu tarafından kullanılıyor Tarım Teknolojisikonusunda uzmanlaşmış bir şirket 70'in üzerinde ölçüm noktasından gelen verileri yönetmek için endüstriyel kenevir izleme Her 60 saniyede bir yenileme ile çiftlik başına çevresel. Belgelenen sonuç şuydu: Ön izleme temel çizgisine kıyasla üretimde 4 kat artış Girdi maliyetlerinde %28 oranında azalma. Bu gibi vakalar gösteriyor ki teknoloji Gerçek zamanlı gösterge tablolarının kullanımı bir BT maliyeti değildir: ölçülebilir getirisi olan tarımsal bir yatırımdır.
Dashboard Farm IoT Sisteminin Mimarisi
Mimari tasarım ilk kritik adımdır. O bakımdan kötü bir seçim seviye sistem boyunca yayılır ve daha sonra düzeltilmesi pahalı hale gelir. Burada sunduğumuz mimari, gerçek dünyadaki üretim ortamlarında ve ölçekte doğrulanmıştır Mimari değişiklik olmadan 10'dan 10.000'e kadar sensör.
Uçtan Uca Mimari: Kontrol Panelindeki Sensörler
┌──────────────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────────────┘
Bu mimarinin kilit noktası sorumlulukların ayrılması: Grafana, teknik operatörler için operasyonel görselleştirme ve uyarılarla ilgilenir. Angular ise ihtiyaç duyan ziraatçılar ve saha yöneticileri için mobil arayüz sunuyor daha rehberli ve kişiselleştirilmiş bir deneyim. Veriler her zaman InfluxDB'den şu şekilde akar: Tutarsızlıklardan kaçınarak gerçeğin tek kaynağı.
Tarımsal Veriler için InfluxDB: Şema Tasarımı ve Sorgulama
InfluxDB, tarımsal IoT uygulamaları için referans zaman serisi veritabanıdır. yüksek verimli alım, verimli sıkıştırma ve sorguları yönetme yeteneği yerel fırtınalar. InfluxDB v2 (açık kaynak), InfluxDB v3 (OSS yeni mimarisi) arasındaki seçim Apache Arrow/Parquet tabanlı) ve InfluxDB Cloud, dağıtımın boyutuna bağlıdır. 200'e kadar sensöre sahip bir çiftlik için, şirket içinde barındırılan InfluxDB v2 OSS yeterlidir.
Tarımsal Verilere İlişkin Şema Tasarımı
InfluxDB'nin temel kuralı: i Etiketler indekslenir (bunları şunun için kullanın: filtre), ben alan değiller (bunları sayısal değerler için kullanın). Kötü bir adam şema tasarımı, üretimdeki performans düşüşünün bir numaralı nedenidir.
# 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
Uyarı: Yüksek Etiket Kardinalitesi
Etiketlere asla yüksek önem değerleri koymayın: zaman damgaları, rastgele UUID'ler, GPS koordinatları kesin veya sürekli sayısal değerler. Bunlar, bozunan bir "patlayıcı kardinalite" yaratır InfluxDB performansı katlanarak artıyor. Bunun yerine yüksek değerlere yönelik alanları kullanın GPS koordinatları gibi değişkenlikler (bunların yuvarlanması veya etiket olarak kaydedilmesi gerekir) bölgeler için ayrı değerlerle).
MQTT Consumer için Telegraf yapılandırması
Telegraf ve InfluxData veri toplayıcı: MQTT aracısına (parsa i) bağlanır JSON yükünü alır ve bunları doğru ölçüm, etiket ve alan yapılandırmasıyla InfluxDB'ye yazar.
# 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"
Kontrol Panelleri için Akış Sorguları
Flux ve InfluxDB v2/v3 sorgu dili. InfluxQL'den daha etkileyicidir ve destekler ölçüm, gelişmiş istatistiksel işlevler ve bildirimsel alt örnekleme arasında birleşir.
// 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")
Saklama Politikaları ve Alt Örnekleme
# 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: Çiftlik IoT için Kurulum ve Yapılandırma
Grafana, endüstriyel IoT ortamları için en popüler görselleştirme ve uyarı platformudur. Sürüm 11.x (2025), birleştirilmiş uyarıda önemli iyileştirmeler sağladı, coğrafi panellerde (Coğrafi Harita) ve kod olarak sağlamada. IoT çiftliği kurulumu için kendi kendine barındırılan, OSS sürümü yeterlidir: Enterprise sürümü SSO, denetim ekler yalnızca çok çiftlikli kurumsal ortamlarda yararlı olan gelişmiş ve özel eklentiler.
YAML Aracılığıyla Veri Kaynağı Sağlama
# 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
Kontrol Paneli Şablonu Değişkenleri
Grafana'daki şablon değişkenleri, kullanıcının etkileşimli kontrol panelleri oluşturmanıza olanak tanır. sayfanın üst kısmındaki açılır menülerden çiftliği, arsayı ve zaman aralığını seçin.
# 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
}
]
}
}
Tarıma Yönelik Kontrol Panelleri: Tam Kılavuz
Her bir metrik için doğru panel tipinin seçilmesi, metriklerin okunabilirliği açısından çok önemlidir. kontrol paneli. Pil seviyesi ve daha az acil durum için "Zaman Serisi" tipi bir panel bir "Gösterge" veya "İstatistik". Tarımsal ölçümler için en uygun panellerin haritası:
Tarımsal Metriklere Yönelik Panel Çeşitleri
| Metrik | Panel Tipi | Neden | Renk Eşikleri |
|---|---|---|---|
| Toprak/hava sıcaklığı | Zaman Serisi + İstatistikler | Zaman trendi + mevcut değer | Yeşil <25, Turuncu 25-30, Kırmızı >30 |
| Toprak nemi | Gösterge + Zaman Serisi | Gösterge HR ve PWP'ye karşı %'yi gösterir | Kırmızı <25, Turuncu 25-35, Yeşil 35-70 |
| Toprak pH'ı | Ölçer | Dakik değer, yavaşça değişir | Kırmızı <5,5 veya >7,5, Yeşil 6,0-7,0 |
| PAR/DLI | Zaman Serisi | Günlük değişkenlik, güneş düzeni | Eşik yok, bilgilendirici |
| Yağış | Çubuk Grafik | Günlük ayrık veriler | Eşik yok, bilgilendirici |
| Su seviyesi | Gösterge + İstatistik | Tank yüzdesi + mevcut litre | Kırmızı <%20, Turuncu %20-40 |
| Sensör pili | Masa | Tüm sensörleri görüntüleyin, kritik filtreleyin | Kırmızı <%20, Turuncu %20-30 |
| Arsa haritası | Coğrafi harita | Çiftlik durumunun coğrafi gösterimi | Toplam duruma göre renk |
Eşikli Toprak Nemi Paneli Yapılandırması
# 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
}
}
Arsa Görselleştirme için Geomap Paneli
# 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" }
}
}
]
}
}
Çiftlik IoT için Grafana'yı uyarma: Yapılandırmanın Tamamlanması
Uyarı sistemi, gösterge tablosunun değer teklifi için en kritik bileşendir. Su stresi nedeniyle 2 saat geciken alarm binlerce avro değerinde hasara yol açabilir ekinler. Grafana Birleşik Uyarı (Grafana 9'da tanıtıldı, v10/v11'de birleştirildi) şunları sağlar: Yapılandırılabilir uyarı kuralları, iletişim noktaları ve bildirim politikalarıyla eksiksiz bir sistem tamamen YAML (Kod Olarak Altyapı) aracılığıyla.
İletişim Noktaları: E-posta, Telegram ve 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 }}
Kritik Tarım Koşullarına İlişkin Uyarı Kuralları
# 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."
Bildirim Politikaları ve Susturma
# 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 Özel: Ne zaman ve neden
Grafana, operasyonel izleme kullanım durumlarının %90'ını kapsar. Ama öyle senaryolar var ki özel bir Angular kontrol paneli ve doğru seçim:
Grafana vs Angular Dashboard: Ne Zaman Neyi Seçmeli
| Kriter | Saf Grafana | AçısalÖzel |
|---|---|---|
| Kullanıcıyı hedefle | Teknisyenler, veri analistleri | Ziraat uzmanları, yöneticiler, saha operatörleri |
| Özel kullanıcı deneyimi | Panel türleri ile sınırlıdır | Sınırsız, mobil odaklı |
| İş mantığı | Uygun değil | Hesaplanmış uyarılar, yapay zeka önerileri |
| Mevcut uygulama entegrasyonu | Sert (iframe) | Yerel Açısal |
| Çevrimdışı ilk PWA | Desteklenmiyor | Yerel Hizmet Çalışanı |
| Yenileme hızı | Min 1s (oylama) | WebSocket alt saniye |
| Geliştirme maliyeti | Düşük (yapılandırma) | Yüksek (özel geliştirme) |
| Bakım | Düşük | Yüksek |
Kurumsal ve IoT çiftlikleri için önerilen strateji melez: Grafana için mobil uygulama için dahili teknik izleme (BT, operasyonlar, gelişmiş tarım bilimi) ve Angular Sahadaki operatörlerin kullandığı. İki platform aynı veri kaynağını paylaşıyor (InfluxDB) ve Angular ayrıca belirteç imzalı iframe'ler aracılığıyla belirli Grafana panellerini de gömebilir kopyalanmaya değer olmayan karmaşık analizler için.
Açısal Uygulama: IoT Hizmeti ve Gerçek Zamanlı Kontrol Paneli
Sensör Verileri için TypeScript Şablonları
// 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[];
}
WebSocket ve Açısal Sinyallerle IoT Hizmeti
// 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 } }
);
}
}
Ana Kontrol Paneli Bileşeni
// 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) }
]);
}
}
Ngx çizelgeleri içeren HTML Kontrol Paneli Şablonu
<!-- 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>
Saha Operatörleri için Mobil Öncelikli Tasarım ve PWA
Saha çalışanları genellikle dengesiz bağlantılara ve koşullara sahip akıllı telefonlar kullanıyor Beyaz arka plana sahip ekranların okunmasını zorlaştıran yoğun güneş ışığı. Çiftlik kontrol panellerinin mobil öncelikli tasarımı, özel hususlar gerektirir geleneksel kurumsal kontrol panellerine.
/* 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; }
}
Önce Çevrimdışı için Hizmet Çalışanı PWA
// 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: Üretime Hazır Dağıtım
Tüm yığın (Mosquitto, InfluxDB, Telegraf, Grafana, Angular uygulaması) dağıtılabilir
tek bir kişiyle docker compose up -d. Bu herhangi bir başlangıç noktasıdır
hem şirket içi hem de bulut VPS'de dağıtım.
# 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
Performans ve Ölçeklenebilirlik: Sorgu Optimizasyonu ve Önbelleğe Alma
Her 30 saniyede bir veri gönderen 50-200 sensörle InfluxDB'deki veri miktarı artıyor hızlı bir şekilde: günde yaklaşık 5-20 milyon puan. Optimizasyon stratejileri olmadan, kontrol paneli sorguları yavaşlar ve kullanıcı deneyimi kötüleşir. İşte teknikler Mütevazı donanımlarda bile yanıt sürelerini 500 ms'nin altında tutmak için temeldir.
IoT Dashboard Farm için Optimizasyon Stratejileri
| Teknik | Uygulama | Darbe |
|---|---|---|
| Çok katmanlı altörnekleme | Görev Akışı her 1 saatte bir, 1 gün | 10-50 kat daha hızlı geçmiş sorguları |
| Toplama aşağı itme | agregaWindow Flux'ta, bellekte değil | Veri aktarımında azalma %90+ |
| Kontrol paneli sorgusunu önbelleğe alma | Grafana önbellek katmanı + Redis | -%70 gereksiz sorgular |
| Gerçekleştirilmiş görünümler | Ön toplamalara sahip "farm_kpi" paketi | Kontrol paneli yükü <100ms |
| Yoklama yerine WebSocket | Angular WebSocket ve HTTP yoklaması | -%80 ön uç sunucu yükü |
| Aşamalı yükleme | Önce KPI'ları, sonra grafikleri yükleyin | Algılanan performans +%60 |
| Etiket kardinalite kontrolü | Ölçüm başına maksimum 1000 seri | Veritabanı bozulmasını önler |
// 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"])
Örnek Olay: Rossi Şarap İmalathanesi - 50 Sensör, 6 Alan
Rossi Tarım Şirketi, 6 bölgeye dağıtılmış 35 hektarlık alanda Chianti Classico DOCG üretiyor farklı toprak özelliklerine sahip araziler. Sistemi uygulamaya koymadan önce IoT izleme, haftalık ziraat uzmanı ziyaretlerine ve manuel ölçümlere dayanıyordu. Uygun olmayan sulama ve geç dondan kaynaklanan sezon başına ortalama kayıplar tahmin edildi potansiyel verimin yaklaşık %18’i.
Rossi Şirketi Dağıtım Özellikleri
| Bileşen | Detay |
|---|---|
| Toplam sensörler | 50 (parsel başına 8-9): toprak, hava durumu, pH, PAR |
| Kenar ağ geçitleri | 6 Raspberry Pi 4 (parsel başına bir adet), 4G bağlantısı |
| Kablosuz sensör protokolü | LoRaWAN (toprak/pH), Zigbee (yerel hava durumu), RS-485 (tanklar) |
| MQTT Brokerı | Eclipse Sivrisinek, VPS OVH Europa, TLS 1.3 |
| Veritabanları | InfluxDB v2.7, VPS 4 vCPU / 8 GB RAM, 200 GB SSD |
| Yenileme hızı kontrol paneli | 30 saniyelik yer verileri, 5 saniyelik kritik uyarılar |
| Veri hacmi | ~3,5 milyon puan/gün, ~1,2 GB/ay ham |
| Grafana Kontrol Paneli | 4 gösterge paneli: çiftliğe genel bakış, parsel başına, sensörler, uyarı geçmişi |
| Açısal mobil uygulama | Android'de saha operatörleri tarafından kullanılan PWA |
| Uyarı | Telegram (acil kritik) + günlük e-posta özeti |
| Toplam donanım | 6 ağ geçidi + 50 sensör + bulut VPS |
| Altyapı maliyeti/yıl | ~4.200 EUR (VPS + bant genişliği + bakım) |
2 Sezon Sonra Ölçülen Sonuçlar
Tasarruf ve Yatırım Getirisi Ölçümleri - Rossi Company (2024 ve 2025 sezonları)
| Metrik | IoT Öncesi (ortalama 2022-2023) | IoT Sonrası (2024-2025) | Delta |
|---|---|---|---|
| Sulama suyu tüketimi | ~2.800 m3/hektar/sezon | ~1.750 m3/hektar/sezon | -%37,5 |
| Su stresi kayıpları | ~%18 potansiyel verim | ~%4 potansiyel verim | -78% kayıp |
| Geç don kayıpları | Değişken, kritik yıllarda %0-15 | %0 (7 zamanında antifriz müdahalesi) | elendi |
| Gübre maliyetleri/ha | Başlangıç %100 | Taban çizgisinin %73'ü | -27% |
| İzleme saatleri/hafta | 12-15 saat tarım uzmanı | 2-3 saat (yalnızca denetim) | -82% |
| Ortalama bağ verimi | 7,2 ton/ha | 8,6 ton/ha | +%19,4 |
| Ekstra üretim değeri | - | +42.000€/sezon | ROI geri ödemesi <18 ay |
En önemli veri su tasarrufu değil (önemli olmasına rağmen), ancakeliminasyon geç dondan kaynaklanan kayıplarla tamamlandı. 2024 sezonunda sistem devreye girdi Mart ve Nisan ayları arasında sıcaklığın 2 derecenin altına düştüğü üç olay otomatik olarak etkinleşiyor Telgraf bildirimleri 02:15, 03:40 ve 01:20'de. Her üç durumda da çalışanlar Çiftlikte yaşayanlar, buzlanma önleme sistemlerini (yaprak üstü yağmurlama sistemleri) devreye aldı. 20 dakika. Müdahale olmasaydı tahmini donma hasarı yaklaşık 15.000 Euro olacaktı sadece Mart etkinliği için.
Uygulamadan Öğrenilen Dersler
- Toprak sensörü kalibrasyonu: Kapasitif nem sensörleri toprak bileşimine duyarlıdır. Yalnızca tak ve çalıştır kurulum için değil, her türlü arazi için yerinde kalibrasyon gereklidir.
- Bir darboğaz olarak 4G bağlantısı: Dağlık bölgelerde 4G kapsama alanı kesintili olabilir. Ağ geçitlerindeki yerel arabelleğin (SQLite, 7 gün) veri kaybını önlemek için vazgeçilmez olduğu kanıtlandı.
- Uyarı yorgunluğu: İlk konfigürasyonda çok fazla aktif eşik vardı. İki hafta içinde operatörler bildirimleri görmezden gelmeye başladı. Birkaç kritik uyarıyla başlamak ve yavaş yavaş daha fazlasını eklemek önemlidir.
- Operatör eğitimi: Angular mobil uygulamasını benimsemek için 2 saatlik 3 eğitim seansı gerekiyordu. Teknolojiye yatırım kadar eğitime yatırım da önemlidir.
- Sensör bakımı: Bağcılık ortamındaki toprak sensörleri üç ayda bir temizlenmeyi gerektirir (tedavi kalıntıları). Önleyici bakım turlarını planlayın.
IoT Çiftlik Sisteminin Güvenliği: En İyi Uygulama
İnternete bağlı bir tarımsal IoT sistemi, korunmadığı takdirde bir saldırı vektörüdür yeterince. Saldırı yüzeyi şunları içerir: açığa çıkan MQTT komisyoncusu, InfluxDB API, Grafana arayüzü, Angular uygulaması. Aşağıdaki önlemler gerekli minimum önlemlerdir bir üretim dağıtımı için.
IoT Çiftlik Güvenliği Kontrol Listesi
| Alan | Ölçüm | Öncelik |
|---|---|---|
| MQTT Brokerı | Her cihaz için TLS 1.3 + mTLS sertifikaları | Eleştiri |
| MQTT Kimlik Doğrulaması | Cihaz başına benzersiz kullanıcı adı/şifre + konu başına ACL | Eleştiri |
| AkışDB | Grafana ve Angular uygulamaları için ayrı salt okunur belirteçler | Yüksek |
| AkışDB | 8086 numaralı bağlantı noktasını internete, yalnızca dahili ağa maruz bırakmayın | Eleştiri |
| Grafana | OAuth2/SAML kimlik doğrulaması, anonim oturum açmayı devre dışı bırakın | Yüksek |
| Grafana | Hız sınırlamalı Nginx ters proxy | Yüksek |
| Açısal Uygulama | HTTPS her zaman, HSTS başlıkları, CSP | Yüksek |
| VPS/Sunucu | Yalnızca güvenlik duvarı bağlantı noktaları gerekli, fail2ban, otomatik güncellemeler | Eleştiri |
| Sırlar | Ortam değişkenleri, asla sabit kodlanmaz, üç aylık rotasyon | Eleştiri |
| Kenar ağ geçitleri | Otomatik ürün yazılımı güncellemeleri, VPN'den buluta | Yüksek |
Sonuçlar: Çiftliğin Karar Merkezi Olarak Kontrol Paneli
İyi tasarlanmış bir IoT kontrol paneli grubu, bir veri kontrol paneli değildir: dijital beyindir modern tarım şirketinin. Grafana bağdaki toprak nemini tespit ettiğinde A3 kritik eşiğin altına düşer ve Angular, saha operatörünü sensörle bilgilendirir kesin ve eylem önerisi, algı-analiz-karar-eylem döngüsü evet saatlerden (veya günlerden) dakikalara sıkıştırılır.
Bu makalede açıklanan yığın, Mosquitto + Telegraf + InfluxDB + Grafana + Angular, ve olgun, üretime hazır bir açık kaynak kombinasyonu. Altyapı maliyetleri orta ölçekli bir şirket (50-100 sensör) 3.000-6.000 Euro civarındadır Rossi şirketinin durumunda aşan tarımsal faydalarla karşılaştırıldığında yıllık İlk sezondan itibaren yıllık 40.000 Euro. ROI hiç bu kadar net olmamıştı.
Temel izleme e-postasını uygulayan şirketler için bir sonraki adım thetahmine dayalı modellerle entegrasyon: Sensör verilerini şununla birleştir: uydu verileri (Sentinel-2'den NDVI, bu serinin 3. makalesinde görülmektedir), Su stresi ve verim tahmini için ayrıntılı hava durumu tahminleri ve ML modelleri. Bu, IoT verilerinin öngörücü tarımsal zekaya dönüştüğü sınırdır.
Teknik Özet
- Akış veritabanı: Etiket/alan ayırmalı şema tasarımı, çok katmanlı kova, otomatik altörnekleme için Flux
- Telgraf: MQTT-InfluxDB köprüsü, JSON ayrıştırma, konu yapısından etiket çıkarma
- Grafana: Veri kaynağı sağlama YAML, tarımsal ölçümler için panel türleri, birden fazla iletişim noktasıyla birleşik uyarı
- Açısal Sinyaller: Gerçek zamanlı değişmez durum yönetimi, bilgisayarlı türevler, otomatik yeniden denemeli WebSocket
- ngx çizelgeleri: Çizgi grafiği, Sinyal odaklı güncelleme ile gerçek zamanlı zaman serisi için alan grafiği
- PWA: En son API'ler için önce çevrimdışı, önbellek stratejisinin güncelliği için Service Worker Angular
- Docker Oluşturma: Tam yığın 5 hizmet, ortam değişkenleri, sağlık kontrolü, yalıtılmış ağ
Serideki Sonraki Makale
FoodTech serisinin sondan bir önceki makalesine ulaştık. içinde 10 numara, serinin sonuncusunda, ele alacağız Tedarik Zinciri Gıda Optimizasyonu: ML modellerinin israfı azaltmak için gıda tedarik zincirini nasıl optimize ettiğini, talebi tahmin edin ve lojistik ile dağıtımı veriye dayalı bir şekilde koordine edin. Teslimat rotalarının optimizasyonundan raf ömrü tahminine kadar, tarımsal mükemmelliği endüstriyel verimlilikle birleştiren bir makale.
FoodTech Serisinin Geri Kalanını Keşfedin
- Madde 1: Hassas Tarım için IoT Boru Hattı - MQTT ve Python temelleri
- Madde 2: Mahsul İzleme için ML Edge - Tarlalarda Bilgisayarlı Görme
- Madde 3: Python ve Sentinel-2 ile Uydu API'si ve NDVI - açık uydu verileri
- Madde 4: Gıdada Blockchain izlenebilirliği - menşeden tüketiciye
- Madde 5: Gıda Endüstrisinde Kalite Kontrolüne Yönelik Bilgisayarlı Görme
- Madde 6: FSMA ve Dijital Uyumluluk - FDA/AB düzenleme otomasyonu
- Madde 7: Dikey Tarım - IoT ve ML ile Çevresel Kontrol
- Madde 8: Prophet ve LightGBM ile Gıda Perakendesinde Talep Tahmini







