diff --git a/TODO.md b/TODO.md index b7d61662..511e7913 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,6 @@ # Smart City Digital Twin — TODO List -> Dernière mise à jour : 2026-05-25 18:50 +> Dernière mise à jour : 2026-05-26 ## ✅ Complété | ID | Tâche | @@ -17,13 +17,12 @@ | contexus-devices | 7 devices créés dans Contexus | | telegraf-fix | Noms containers corrigés + BunkerM désactivé | | or-pg-fix | Image PostgreSQL changée → timescaledb-ha:pg15 | +| grafana-fix | Dashboard "no data" corrigé — datasource + requêtes Flux | ## 🔴 En cours | ID | Tâche | Notes | |----|-------|-------| -| p1-or-restart | Redémarrer OpenRemote | PG recréé, à relancer | -| p1-telegraf | Vérifier données InfluxDB | Telegraf fixé, pipeline à valider | -| p1-grafana | Vérifier dashboard Smart City | Dépend de InfluxDB | +| p1-or-restart | Redémarrer OpenRemote | PG recréé, à relancer après reclonage répertoire | ## 🔴 Bloqué | ID | Tâche | Raison | @@ -42,13 +41,12 @@ | p0-chirpstack | ChirpStack: login API gRPC-REST | | p1-thingsboard | Relayer ThingsBoard (si CPU dispo) | -## 📝 Notes 2026-05-25 -- **OpenRemote** : Image PG changée (timescaledb-ha:pg15), container recréé -- **Telegraf** : Fixé — noms containers corrigés + BunkerM commenté -- **Simulator** : 60 capteurs, MQTT OK 1/2 (EMQX OK, Mosquitto ?) -- **Pipeline** : Simulateur → EMQX/Mosquitto → Telegraf → InfluxDB → Grafana -- **Contexus** : https://contexus.digitribe.fr — login: iotevadmin / Digitribe972 -- **OpenRemote** : https://openremote.digitribe.fr/manager/ +## 📝 Notes 2026-05-26 +- **Grafana** : Dashboard smartcity-martinique-complete v3 — données confirmées (PM2.5, temp, traffic, etc.) +- **Pipeline** : Simulateur → EMQX/Mosquitto → Telegraf → InfluxDB → Grafana ✅ +- **InfluxDB** : bucket `smartcity`, measurement `mqtt_consumer`, tag `topic` pour le type +- **OpenRemote** : Abandon pour l'instant, l'utilisateur va recloner le répertoire +- **Keycloak** : Recréé manuellement (ancien supprimé par docker-compose up échoué) ## Credentials - **Contexus**: iotevadmin / Digitribe972 @@ -56,3 +54,4 @@ - **PostgreSQL Contexus**: contexus / Digitribe972 - **Redis Contexus**: Digitribe972 - **Telegraf InfluxDB**: token=my-super-token, org=digitribe, bucket=smartcity +- **Grafana**: admin / Digitribe972 diff --git a/grafana-dashboard-smartcity.json b/grafana-dashboard-smartcity.json index f8c8d7c9..c95ae2b0 100644 --- a/grafana-dashboard-smartcity.json +++ b/grafana-dashboard-smartcity.json @@ -1,7 +1,5 @@ { - "annotations": { - "list": [] - }, + "annotations": {"list": []}, "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, @@ -9,135 +7,166 @@ "links": [], "panels": [ { - "title": "Air Quality (PM2.5)", + "title": "Air Quality — PM2.5 (µg/m³)", "type": "timeseries", - "datasource": { - "type": "influxdb", - "uid": "influxdb-smartcity" - }, + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, "targets": [ { - "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"airquality\") |> filter(fn: (r) => r[\"_field\"] == \"pm25_ugm3\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")" + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/airquality/)\n |> filter(fn: (r) => r[\"_field\"] == \"pm25_ugm3\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" } ], - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 - } + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0} }, { - "title": "Traffic Flow (Vehicles)", + "title": "Air Quality — NO2 (µg/m³)", "type": "timeseries", - "datasource": { - "type": "influxdb", - "uid": "influxdb-smartcity" - }, + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, "targets": [ { - "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"traffic\") |> filter(fn: (r) => r[\"_field\"] == \"vehicle_count\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")" + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/airquality/)\n |> filter(fn: (r) => r[\"_field\"] == \"no2_ugm3\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" } ], - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 0 - } + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0} }, { - "title": "Parking Occupancy (%)", + "title": "Temperature (°C)", "type": "timeseries", - "datasource": { - "type": "influxdb", - "uid": "influxdb-smartcity" - }, + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, "targets": [ { - "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"parking\") |> filter(fn: (r) => r[\"_field\"] == \"occupancy_percent\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")" + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/weather/)\n |> filter(fn: (r) => r[\"_field\"] == \"temperature_celsius\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" } ], - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 8 - } + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8} }, { - "title": "Noise Levels (dB)", + "title": "Humidity (%)", "type": "timeseries", - "datasource": { - "type": "influxdb", - "uid": "influxdb-smartcity" - }, + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, "targets": [ { - "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"noise\") |> filter(fn: (r) => r[\"_field\"] == \"noise_level_db\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")" + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/weather/)\n |> filter(fn: (r) => r[\"_field\"] == \"humidity_percent\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" } ], - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 8 - } + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8} }, { - "title": "Weather (Temperature \u00b0C)", + "title": "Wind Speed (km/h)", "type": "timeseries", - "datasource": { - "type": "influxdb", - "uid": "influxdb-smartcity" - }, + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, "targets": [ { - "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"weather\") |> filter(fn: (r) => r[\"_field\"] == \"temperature_c\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")" + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/weather/)\n |> filter(fn: (r) => r[\"_field\"] == \"wind_speed_kmh\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" } ], - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 16 - } + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 16} }, { - "title": "Light Levels", + "title": "Rain (mm)", "type": "timeseries", - "datasource": { - "type": "influxdb", - "uid": "influxdb-smartcity" - }, + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, "targets": [ { - "query": "from(bucket:\"smartcity\") |> range(start: v.timeRangeStart, stop:v.timeRangeStop) |> filter(fn: (r) => r[\"_measurement\"] == \"light\") |> filter(fn: (r) => r[\"_field\"] == \"luminosity\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")" + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/weather/)\n |> filter(fn: (r) => r[\"_field\"] == \"rain_mm\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" } ], - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 16 - } + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 16} + }, + { + "title": "Traffic — Vehicle Count", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/traffic/)\n |> filter(fn: (r) => r[\"_field\"] == \"vehicle_count\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 24} + }, + { + "title": "Traffic — Avg Speed (km/h)", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/traffic/)\n |> filter(fn: (r) => r[\"_field\"] == \"average_speed_kmh\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 24} + }, + { + "title": "Parking — Available Spots", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/parking/)\n |> filter(fn: (r) => r[\"_field\"] == \"available_spots\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 32} + }, + { + "title": "Parking — Occupancy (%)", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/parking/)\n |> filter(fn: (r) => r[\"_field\"] == \"occupancy_percent\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 32} + }, + { + "title": "Noise Level (dB)", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/noise/)\n |> filter(fn: (r) => r[\"_field\"] == \"noise_level_db\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 40} + }, + { + "title": "Light — Brightness (lux)", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/light/)\n |> filter(fn: (r) => r[\"_field\"] == \"brightness_lux\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 40} + }, + { + "title": "UV Index", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/weather/)\n |> filter(fn: (r) => r[\"_field\"] == \"uv_index\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 48} + }, + { + "title": "Pressure (hPa)", + "type": "timeseries", + "datasource": {"type": "influxdb", "uid": "influxdb-smartcity"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/weather/)\n |> filter(fn: (r) => r[\"_field\"] == \"pressure_hpa\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 48} } ], "schemaVersion": 36, "style": "dark", - "tags": [ - "smartcity", - "martinique", - "iot" - ], - "templating": { - "list": [] - }, - "time": { - "from": "now-1h", - "to": "now" - }, - "title": "Smart City Digital Twin - Martinique", + "tags": ["smartcity", "martinique", "iot"], + "templating": {"list": []}, + "time": {"from": "now-1h", "to": "now"}, + "title": "Smart City Digital Twin — Martinique", "uid": "smartcity-martinique-v2", - "version": 1 -} \ No newline at end of file + "version": 2 +} diff --git a/grafana-datasources.yml b/grafana-datasources.yml index e49655b1..a2f8ddf1 100644 --- a/grafana-datasources.yml +++ b/grafana-datasources.yml @@ -2,13 +2,18 @@ apiVersion: 1 datasources: - - name: InfluxDB + - name: influxdb-smartcity type: influxdb access: proxy - url: http://docker-influxdb-1:8086 - database: iot_data - user: admin - password: digitribe972 + url: http://smart-city-influxdb:8086 + database: smartcity + jsonData: + version: Flux + organization: digitribe + defaultBucket: smartcity + tlsSkipVerify: true + secureJsonData: + token: my-super-token isDefault: true readOnly: false diff --git a/grafana/provisioning/dashboards/smart-city-dashboards.json b/grafana/provisioning/dashboards/smart-city-dashboards.json index c186a49b..5c9dce61 100644 --- a/grafana/provisioning/dashboards/smart-city-dashboards.json +++ b/grafana/provisioning/dashboards/smart-city-dashboards.json @@ -3,14 +3,170 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, + "id": null, "links": [], - "panels": [], + "panels": [ + { + "title": "Air Quality — PM2.5 (µg/m³)", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/airquality/)\n |> filter(fn: (r) => r[\"_field\"] == \"pm25_ugm3\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0} + }, + { + "title": "Air Quality — NO2 (µg/m³)", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/airquality/)\n |> filter(fn: (r) => r[\"_field\"] == \"no2_ugm3\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0} + }, + { + "title": "Temperature (°C)", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/weather/)\n |> filter(fn: (r) => r[\"_field\"] == \"temperature_celsius\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8} + }, + { + "title": "Humidity (%)", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/weather/)\n |> filter(fn: (r) => r[\"_field\"] == \"humidity_percent\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 8} + }, + { + "title": "Wind Speed (km/h)", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/weather/)\n |> filter(fn: (r) => r[\"_field\"] == \"wind_speed_kmh\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 16} + }, + { + "title": "Rain (mm)", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/weather/)\n |> filter(fn: (r) => r[\"_field\"] == \"rain_mm\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 16} + }, + { + "title": "Traffic — Vehicle Count", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/traffic/)\n |> filter(fn: (r) => r[\"_field\"] == \"vehicle_count\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 24} + }, + { + "title": "Traffic — Avg Speed (km/h)", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/traffic/)\n |> filter(fn: (r) => r[\"_field\"] == \"average_speed_kmh\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 24} + }, + { + "title": "Parking — Available Spots", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/parking/)\n |> filter(fn: (r) => r[\"_field\"] == \"available_spots\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 32} + }, + { + "title": "Parking — Occupancy (%)", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/parking/)\n |> filter(fn: (r) => r[\"_field\"] == \"occupancy_percent\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 32} + }, + { + "title": "Noise Level (dB)", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/noise/)\n |> filter(fn: (r) => r[\"_field\"] == \"noise_level_db\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 40} + }, + { + "title": "Light — Brightness (lux)", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/light/)\n |> filter(fn: (r) => r[\"_field\"] == \"brightness_lux\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 40} + }, + { + "title": "UV Index", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/weather/)\n |> filter(fn: (r) => r[\"_field\"] == \"uv_index\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 48} + }, + { + "title": "Pressure (hPa)", + "type": "timeseries", + "datasource": {"type": "influxdb", "name": "Influxdb-v2"}, + "targets": [ + { + "query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"topic\"] =~ /smartcity\\/weather/)\n |> filter(fn: (r) => r[\"_field\"] == \"pressure_hpa\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")" + } + ], + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 48} + } + ], "schemaVersion": 36, "style": "dark", - "tags": ["smart-city"], + "tags": ["smartcity", "martinique", "iot"], "templating": {"list": []}, - "time": {"from": "now-24h", "to": "now"}, - "title": "Smart City Dashboards", - "timezone": "Americas/Martinique", - "uid": "smart-city-dashboards" + "time": {"from": "now-1h", "to": "now"}, + "title": "Smart City Digital Twin — Martinique", + "uid": "smartcity-martinique-v2", + "version": 2 } diff --git a/grafana/provisioning/datasources/datasources.yml b/grafana/provisioning/datasources/datasources.yml index ec71f1a2..420f9f72 100644 --- a/grafana/provisioning/datasources/datasources.yml +++ b/grafana/provisioning/datasources/datasources.yml @@ -1,9 +1,8 @@ # Grafana datasources - Smart City Digital Twin Martinique -# Each datasource is editable and uses the container DNS name in smartcity-shared network apiVersion: 1 datasources: - # ── InfluxDB v2 (time-series IoT data) ────────────────────────────────────── + # InfluxDB v2 (time-series IoT data) - name: InfluxDB-v2 type: influxdb access: proxy @@ -13,12 +12,12 @@ datasources: jsonData: version: Flux organization: digitribe - defaultBucket: iot_data + defaultBucket: smartcity + tlsSkipVerify: true secureJsonData: - token: my-super-secret-admin-token + token: my-super-token - # ── FIWARE Orion-LD (NGSI-LD context broker) ──────────────────────────────── - # Requires grafana-simple-json-datasource plugin + # FIWARE Orion-LD - name: FIWARE Orion type: grafana-simple-json-datasource access: proxy @@ -28,8 +27,7 @@ datasources: queryURLTemplate: "/ngsi-ld/v1/entities?type={{type}}" method: GET - # ── GeoServer WMS (spatial data) ──────────────────────────────────────────── - # GeoServer is an external service reachable via its container name + # GeoServer WMS - name: GeoServer WMS type: grafana-simple-json-datasource access: proxy @@ -39,8 +37,7 @@ datasources: queryURLTemplate: "/geoserver/wfs?service=WFS&version=2.0&request=GetFeature&typeName={{type}}" method: GET - # ── FROST-Server (SensorThings API) ────────────────────────────────────────── - # Requires grafana-simple-json-datasource plugin + # FROST-Server - name: FROST-Server type: grafana-simple-json-datasource access: proxy diff --git a/session_resume_2026-05-26.md b/session_resume_2026-05-26.md new file mode 100644 index 00000000..9a7b4802 --- /dev/null +++ b/session_resume_2026-05-26.md @@ -0,0 +1,70 @@ +# Session Resume - 2026-05-26 + +## Objectifs +Reprendre la session Smart City Digital Twin : fix OpenRemote + Grafana "no data" + +## Actions effectuées + +### 1. OpenRemote — Tentative de fix (abandonnée par l'utilisateur) +- **Problème identifié**: Flyway migration V20191202_01__Schema.sql échoue car les tables JPA + (REALM, USER_ENTITY) ne sont pas encore créées quand Flyway s'exécute +- **Solution partielle**: Créé les tables manuellement dans `public` + marqué les 19 migrations + Flyway comme appliquées dans `flyway_schema_history` +- **Résultat**: Flyway passé, Manager connecté à Keycloak, mais PG a été recréé +- **Décision utilisateur**: Abandonner OpenRemote pour l'instant, recloner le répertoire + +### 2. OpenRemote Keycloak — Recréé manuellement +- L'ancien container Keycloak avait été supprimé par un `docker-compose up` échoué +- Recréé avec `docker run` sur le réseau `smartcity-shared` +- **⚠️ Note**: Les credentials dans le docker-compose ont été masqués par Docker Compose + (user:password → ***). Il faut utiliser les credentials réels pour les backups. + +### 3. Grafana "no data" — CORRIGÉ ✅ + +#### Causes identifiées: +1. **Datasource wrong bucket**: `defaultBucket: iot_data` → corrigé en `smartcity` +2. **Datasource wrong token**: `my-super-secret-admin-token` → corrigé en `my-super-token` +3. **Dashboard requêtes wrong**: Les panels utilisaient `r["_measurement"] == "airquality"` etc. + mais Telegraf écrit tout dans la measurement `mqtt_consumer` avec un tag `topic` qui + contient le type de capteur (ex: `smartcity/airquality/1`) + +#### Corrections appliquées: +- **Datasource** (`/etc/grafana/provisioning/datasources/datasources.yml` dans le container): + - `defaultBucket: smartcity` + - `token: my-super-token` + - `organization: digitribe` + - `version: Flux` +- **Dashboard** `smartcity-martinique-complete` (version 3): + - Remplacé `_measurement == "xxx"` par `topic =~ /smartcity\/xxx/` dans les 10 panels + - Remplacé `temperature_c` par `temperature_celsius` + - Remplacé `luminosity` par `brightness_lux` + - Supprimé les UID datasource (utilise la datasource par défaut) + +#### Validation: +- Test requêtes via Grafana API: PM2.5 ✅, Temperature ✅, Vehicle Count ✅ + +### 4. Types de capteurs et fields confirmés dans InfluxDB: +- **airquality**: battery_level, co_mgm3, humidity_percent, no2_ugm3, o3_ugm3, pm10_ugm3, pm25_ugm3, temperature_celsius +- **parking**: available_spots, occupancy_percent, total_spots, turnover_per_hour +- **traffic**: average_speed_kmh, congestion_level, vehicle_count +- **weather**: battery_level, humidity_percent, pressure_hpa, rain_mm, temperature_celsius, uv_index, wind_speed_kmh +- **light**: battery_level, brightness_lux, power_consumption_w +- **noise**: battery_level, noise_level_db, peak_db + +## Containers critiques +- `smart-city-grafana`: ✅ Up, datasource corrigé, dashboard v3 +- `smart-city-influxdb`: ✅ Up healthy, bucket `smartcity` avec données +- `smart-city-telegraf`: ✅ Up, connecté à EMQX + Mosquitto +- `smart-city-simulator`: ✅ Up, 60 capteurs +- `openremote-postgresql`: Up healthy (timescaledb-ha:pg15) +- `openremote-keycloak`: Recréé manuellement +- `openremote-manager`: En crash loop (problème Flyway résolu mais PG recéré) +- `grafana_stack-grafana-1`: Autre instance Grafana (5 jours) +- `honcho-grafana-1`: Grafana Honcho (5 jours, healthy) + +## Fichiers modifiés +- `grafana-datasources.yml` — datasource corrigé (référence, pas utilisé directement) +- `grafana-dashboard-smartcity.json` — dashboard dans le projet mis à jour +- `/etc/grafana/provisioning/datasources/datasources.yml` (dans container) +- `/etc/grafana/provisioning/dashboards/smart-city-dashboards.json` (dans container) +- `docker-compose.yml` (OpenRemote) — OR_SETUP_TYPE: "default" ajouté