Compare commits

..

97 Commits

Author SHA1 Message Date
Eric FELIXINE
83779cf5d7 fix: telegraf topics, mqtt brokers, docker-compose fixes
- Fix MOSQUITTO_HOST (wrong container name)
- Fix EMQX_PORT (1885 external -> 1883 internal)
- Fix telegraf MQTT topics (city/sensors/#)
- Fix BunkerM dynsec JSON
- Add kepler.yml Traefik config
- Update monitoring script
2026-06-07 20:18:41 -04:00
Eric FELIXINE
7c0cb330d9 chore: update TODO.md timestamp 2026-06-04 10:15 2026-06-04 10:26:34 -04:00
Eric FELIXINE
f45ac0cb6e feat(k8s): add defaults/main.yml, meta/main.yml for all 27 roles + 4 helm templates
- Added defaults/main.yml with production-ready values for all 27 Ansible roles
- Added meta/main.yml with role dependencies (DAG: prereq → namespaces → storage → traefik → cert-manager → services)
- Created 4 missing Helm templates: flink-deployment, kafka-cluster, smartapp-web, smartapp-api
- Fixed YAML syntax error in backup/tasks/main.yml (Velero backupStorageLocation)
- Updated README with domain list, dependencies diagram, and corrected Helm chart names
- All 81 YAML files pass validation
2026-06-04 09:45:16 -04:00
Eric FELIXINE
66ac47b684 docs: add infrastructure snapshot 2026-06-04 2026-06-04 02:26:23 -04:00
Eric FELIXINE
fb62291b3e feat: add helm/ansible deployment files for Kubernetes
Some checks failed
Build & Deploy Smart App Web / lint (push) Failing after 1s
Build & Deploy Smart App Web / build-web (push) Has been skipped
Build & Deploy Smart App Web / docker-build (push) Has been skipped
Build & Deploy Smart App Web / deploy (push) Has been skipped
2026-06-04 02:09:17 -04:00
Eric FELIXINE
8c2251faba TODO: mise a jour 2026-06-04 - cleanup massif, helms ansible generés 2026-06-04 02:05:32 -04:00
Eric FELIXINE
b56749182e chore: update TODO — Honcho API deployed, Gitea Actions configured, Smart App Docker ready
Honcho:
- API UP on honcho.digitribe.fr (port 8089)
- Workspace 'hermes-agent' and session 'smart-city-session' created
- Memory storage working (messages stored via REST API)
- Hermes plugin configured in ~/.hermes/honcho.json
- Dialectic chat requires valid LLM API key

Gitea Actions:
- Runner docker-runner-01 registered and working
- SSH secrets configured (SERVER_HOST, SERVER_USER, SSH_PRIVATE_KEY)
- Workflow: lint + build + deploy

Smart App:
- Dockerfile web: multi-stage node + nginx
- Traefik: smartapp.digitribe.fr
- deploy.sh: web/docker/api/all
- LocalAI config removed (service no longer exists)
2026-06-02 06:57:56 -04:00
Eric FELIXINE
808dbbe4f3 ci: test full pipeline — secrets configured (SERVER_HOST, SERVER_USER, SSH_PRIVATE_KEY) 2026-06-02 01:19:10 -04:00
Eric FELIXINE
9f40e187d8 ci: verify Gitea Actions runner — docker-runner-01 registered and working 2026-06-01 23:57:39 -04:00
Eric FELIXINE
f8e34562d5 feat(smart-app): CI/CD pipeline + deploy scripts
Some checks failed
Build & Deploy Smart App Web / lint (push) Failing after 1s
Build & Deploy Smart App Web / build-web (push) Has been skipped
Build & Deploy Smart App Web / docker-build (push) Has been skipped
Build & Deploy Smart App Web / deploy (push) Has been skipped
- .gitea/workflows/build-and-deploy.yml:
  - Lint + TypeScript check on PR
  - Expo web build on push to master
  - Deploy via SSH to server (copy to /var/www/smartapp)
  - Docker build alternative
  - Artifact upload for builds
- deploy.sh: manual deploy script (web|docker|api|all)
- app.json: Expo config with bundle IDs
- assets/: generated icons (icon, splash, adaptive, favicon)
- Added expo-pwa, html-webpack-plugin, workbox-webpack-plugin deps
2026-06-01 23:03:11 -04:00
Eric FELIXINE
fd583a8b16 feat(smart-app): add Docker web build + Traefik integration 2026-06-01 22:49:31 -04:00
Eric FELIXINE
43ae2ebcac feat(smart-app): complete all remaining components, screens, hooks, services, stores, i18n
- i18n/index.ts: i18next setup with FR/EN/ES/DE translations
- constants.ts: app config, sensor types, alert severity, storage keys, refresh intervals
- store/index.ts: barrel export for all stores
- iotStore.ts: full IoT store (6 sensors, 3 zones, 2 alerts) with actions
- notificationStore.ts: notification store (5 mock notifications) with actions
- uiStore.ts: theme/language store + translation maps for 4 languages
- useSensors.ts: sensor filtering by type/zone, alert sensors selector
- useAlerts.ts: active alerts, critical alerts, acknowledge
- useNotifications.ts: notification CRUD operations
- useLocation.ts: GPS location with expo-location, default Fort-de-France
- SensorCard.tsx: full sensor card with status dot, compact mode
- StatsCard.tsx: stats card with icon, value, trend
- AlertCard.tsx: alert card with severity bar, acknowledge button
- ZoneCard.tsx: zone card with color bar, sensor/alert counts
- LineChart.tsx: bar-based line chart with Y-axis labels
- BarChart.tsx: bar chart with value labels
- GaugeChart.tsx: semi-circular gauge with color thresholds
- MapView.tsx: map placeholder with overlay markers
- MarkerPopup.tsx: popup with title, value, status, detail button
- DashboardScreen.tsx: analytics dashboard with gauges + charts
- SensorDetailScreen.tsx: sensor detail with gauge + history chart
- NotificationPrefsScreen.tsx: notification preference toggles (4)
- LayerDetailScreen.tsx: layer detail placeholder
- iot.service.ts: CRUD operations for sensors, zones, alerts
- gis.service.ts: geocoding, POI search, routing
2026-06-01 22:31:36 -04:00
Eric FELIXINE
a5124b0f0d chore: update TODO.md with smart app MVP and ditto fixes 2026-06-01 18:03:40 -04:00
Eric FELIXINE
e30ae8ed09 feat(smart-app): implement complete mobile app MVP
- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
2026-06-01 18:00:35 -04:00
Eric FELIXINE
08ca495bde feat: backend FastAPI Smart App City — auth JWT, IoT, GIS, notifications, reporting 2026-06-01 14:47:05 -04:00
Eric FELIXINE
31334b5ce5 chore: session resume final 2026-06-01 — lakehouse traefik routes, network fixes 2026-06-01 14:10:52 -04:00
Eric FELIXINE
ef6e5fbae0 feat: routes Traefik pour lakehouse (trino, kafka-ui, flink, gravitino, minio) 2026-06-01 14:09:55 -04:00
Eric FELIXINE
ae35506db6 chore: backup session 2026-06-01 final — snapshot, resume, TODO, all fixes documented 2026-06-01 12:24:34 -04:00
Eric FELIXINE
8c38a23b4b chore: session resume 2026-06-01 final — JupyterHub spawn fix, all creds documented 2026-06-01 12:07:10 -04:00
Eric FELIXINE
cca9e4aedc fix: JupyterHub spawn - switch to LocalProcessSpawner, fix password hash, eric user
- SimpleLocalProcessSpawner doesn't pass JUPYTERHUB_SERVICE_URL in JH 5.3.0
- LocalProcessSpawner handles env vars correctly
- Fixed eric password hash (bcrypt instead of PBKDF2)
- eric user created with admin rights
- JupyterLab accessible at https://jupyter.digitribe.fr
- Credentials: eric / Digitribe972
2026-06-01 11:45:52 -04:00
Eric FELIXINE
85199fc3f0 chore: session backup 2026-06-01 continue — Kafka/Trino/JupyterHub fixes, TODO update 2026-06-01 10:39:11 -04:00
Eric FELIXINE
cb45b89f1f fix: JupyterHub Dockerfile - add eric user, sudo, fix DB path (4 slashes) 2026-06-01 10:26:47 -04:00
Eric FELIXINE
9e933fea66 chore: session backup 2026-06-01 final — TODO restructuré, état complet 2026-05-30 08:49:31 -04:00
Eric FELIXINE
acdf250470 chore: backup session 2026-06-01b — JupyterHub fix, Hermes Dashboard, OR mbtiles, Trino node.properties
Summary of changes:
- JupyterHub: fix DB path (absolute), Dockerfile cleanup, SimpleLocalProcessSpawner
- JupyterHub: user eric created as admin
- Hermes Dashboard WebUI + TUI chat service (systemd, localhost:9119, auto-boot)
- OR mbtiles: generated Martinique PNG tiles (5690 tiles, 10.9MB) — needs PBF for OR
- OR mbtiles: restored original PBF with corrected metadata (world bounds, Martinique center)
- OR mapsettings: verified center=[-61,14.5], bounds=Martinique, minZoom=0
- Trino: added node.properties (node.environment=production) — needs restart
- TODO.md: updated with current state
- session_resume_consolide.md: created (per-session summary)
2026-05-30 08:14:47 -04:00
Eric FELIXINE
008f1679ce fix: JupyterHub DB path + user eric + OR mbtiles bounds + Hermes Dashboard
- Fix JupyterHub: sqlite db_url absolute path (was double-nested /srv/jupyterhub/srv/jupyterhub)
- Create user eric as admin in JupyterHub (id=2, authorized)
- Fix OpenRemote mbtiles: bounds metadata = world (-180,-85,180,85) for free zoom
- OR map API confirmed working: center=[-61,14.5], minZoom=0, bounds=Martinique
- Add Hermes Dashboard WebUI + TUI chat service (localhost:9119, auto-start at boot)
- Add generate_martinique_mbtiles.py script (future tile generation)

Known issues:
- JupyterHub spawn timeout (singleuser server slow to start, increased to 120s)
- OR mbtiles still contains Netherlands vector tiles (need Martinique tiles)
- Kafka, Trino still in restart loop (separate fix needed)
2026-05-29 07:01:00 -04:00
Eric FELIXINE
a234e808f2 chore: add VRE stack configs (JupyterHub + Zeppelin) + lakehouse components
- Add VRE directory with JupyterHub + Zeppelin docker-compose configs
- Add Gravitino, Flink, Kafka, MinIO, Trino lakehouse stack
- Add Superset, Metabase, StarRocks analytics tools
- Session reprise après crash 2026-06-01

Infrastructure: 86 conteneurs total
Known issues: Kafka (no ZK conn), Trino (node.env null), JupyterHub (DB path)
2026-05-29 02:21:08 -04:00
Eric FELIXINE
486c1d2675 feat: Add OpenRemote stack config (docker-compose + traefik)
- Docker-compose based on GitHub repo openremote/openremote
- Images: timescale/timescaledb-ha:pg15, openremote/keycloak:latest, openremote/manager:1.24.0
- All 3 services healthy and running
- URL: https://openremote.digitribe.fr/manager/
2026-05-28 23:22:25 -04:00
Eric FELIXINE
184f3ca8dd chore: session backup 2026-05-27 — OpenRemote deployed with KC 23.0.7, MindsDB config updated 2026-05-27 13:14:58 -04:00
Eric FELIXINE
feb80694ab chore: update TODO — fix session date references 2026-05-26 22:55:46 -04:00
Eric FELIXINE
a19ee4080f chore: remove erroneous 2026-05-29 snapshot (correct date is 2026-05-26) 2026-05-26 22:54:57 -04:00
Eric FELIXINE
46bb937714 chore: session backup 2026-05-26 — reprise après crash, snapshot + resume + TODO update 2026-05-26 22:54:37 -04:00
Eric FELIXINE
e0d023d372 docs: add architecture inventory 2026-05-27 2026-05-26 21:04:35 -04:00
Eric FELIXINE
19cb678791 chore: session backup 2026-05-27 — ODK deployed, project created, TODO updated 2026-05-26 20:54:50 -04:00
Eric FELIXINE
978280f866 fix: ODK Traefik config — add odk.yml route, ODK now accessible at https://odk.digitribe.fr 2026-05-26 20:26:17 -04:00
Eric FELIXINE
89a821a364 chore: update TODO.md — ODK progress, ChirpStack pw reset, Smart App City arch 2026-05-26 19:06:07 -04:00
Eric FELIXINE
94f74f2dfc feat: add smart-app-city sub-project architecture
- Architecture globale (React Native + NestJS + FastAPI)
- Beckn Protocol (OTN-DPI) integration docs
- AI layer: RAG pipeline + AI Agents (LocalAI + Qdrant)
- i18n: FR/EN/ES/DE support
- Docker Compose for backend services
- Project structure with frontend, backend, ai, beckn directories
2026-05-26 18:47:02 -04:00
Eric FELIXINE
f1e1b98519 chore: session backup 2026-05-28 — TODO update + snapshot 2026-05-26 17:20:04 -04:00
Eric FELIXINE
e4c558c296 Dashboard v7: valeurs temps réel dans panneau stat + dropdown capteur
- Panneau Stat en haut: 7 métriques temps réel (temp, humidité, NO2, O3, CO, batterie, PM2.5)
- Dropdown avec includeAll + allValue '.*'
- Regex  dans toutes les queries Flux
- historique: 7j pour NO2 et PM2.5
- Refresh: 5s
2026-05-26 14:20:00 -04:00
Eric FELIXINE
65e2d42f63 Dashboard v5: variable capteur + tooltip details + coordonnees Sainte-Anne
- Ajout variable  pour filtrage par topic InfluxDB
- Tous les panels filtres par capteur selectionne
- Tooltip markersGeomap en mode details
- Coordonnees Sainte-Anne corrigees (deplacement ~300m sur terre)
- 1001 topics disponibles dans InfluxDB
2026-05-26 14:05:33 -04:00
Eric FELIXINE
a7716102fd Fix GeoMap dashboard v3 - temperature_celsius + Geomap layer config
Fixes:
- Temperature field: temperature_c (wrong) -> temperature_celsius (correct)
- Geomap panel: added explicit location config with lat/lon field mapping
- Added PM2.5 timeseries panel
- Dashboard UID: geomap-test-v1
2026-05-26 13:52:25 -04:00
Eric FELIXINE
7643d88ffb Add GeoMap dashboard + ChirpStack REST API config
- Grafana GeoMap dashboard (PostGIS + InfluxDB) for real-time sensor visualization
- Dashboard accessible at: https://grafana.digitribe.fr/d/geosmart-city-2026/smart-city-geomap-temps-reel
- PostGIS datasource added (user: grafana, network: smartcity-shared)
- InfluxDB-SmartCity-Correct datasource configured
- ChirpStack REST API: added --cors-origins flag
2026-05-26 13:14:01 -04:00
Eric FELIXINE
7df2f6798f feat: deploy Superset and Metabase behind Traefik
- Superset: docker-compose.superset.yml (app + postgres + redis)
  URL: https://superset.digitribe.fr
  Port: 8088 (internal), Traefik routes Host(superset.digitribe.fr)

- Metabase: docker-compose.metabase.yml (app + postgres)
  URL: https://metabase.digitribe.fr
  Port: 3000 (internal), Traefik routes Host(metabase.digitribe.fr)

- Traefik configs: 31-superset.yml, 32-metabase.yml
- Both services use smartcity-shared and traefik-public networks
- Both use letsencrypt TLS certificates

Verified:
- Superset: UP healthy, accessible via https://superset.digitribe.fr
- Metabase: UP healthy, accessible via https://metabase.digitribe.fr
2026-05-25 22:59:25 -04:00
Eric FELIXINE
943836f8fb feat: activate BunkerM MQTT broker + fix Telegraf
- BunkerM: recreated with port 1883 (external) -> 1900 (internal)
- BunkerM: disabled dynsec plugin, using password_file auth (bunker/bunker)
- Simulator: ENABLE_BUNKER=1, BUNKERM_PORT=1900
- Telegraf: reactivated BunkerM consumer on port 1900
- Telegraf: recreated container (3 MQTT consumers connected)
- Grafana: dashboard v4 with corrected Flux queries
- Grafana: datasource fixed (bucket=smartcity, token=my-super-token)

Verified:
- Simulator publishes to EMQX , Mosquitto , BunkerM 
- Telegraf receives from all 3 brokers 
- InfluxDB has data from all brokers 
- Grafana dashboard displays data 
2026-05-25 20:03:55 -04:00
Eric FELIXINE
5bbd5a6e5d fix: Grafana dashboard 'no data' — datasource + Flux queries
- Fix datasource: bucket=smartcity, token=my-super-token, org=digitribe
- Fix dashboard queries: filter by topic tag instead of _measurement
  (all data in measurement 'mqtt_consumer', type in tag 'topic')
- Fix field names: temperature_c→temperature_celsius, luminosity→brightness_lux
- Update dashboard to v3 with 15 panels (airquality, traffic, parking, weather, noise, light)
- Update TODO.md and session_resume

Tested: PM2.5 , Temperature , Vehicle Count  via Grafana API
2026-05-25 16:39:50 -04:00
Eric FELIXINE
6d1d9c8620 fix: telegraf containers names + openremote pg image + session snapshot 2026-05-25
- telegraf.conf: fix Mosquitto/BunkerM container names (hyphens not underscores)
- tegraf.conf: comment out BunkerM consumer (auth fails, simulator not sending)
- openremote/docker-compose.yml: switch PG image to timescaledb-ha:pg15 (fixes timescaledb_toolkit crash)
- Add session_resume + architecture snapshot 2026-05-25
- Update TODO.md with current status
2026-05-25 14:13:39 -04:00
Eric FELIXINE
eb97f2a7dd Fix: Traefik dynamic config, Ditto things startup crash, and OpenRemote sensor coordinates 2026-05-20 18:18:21 -04:00
Eric FELIXINE
45f3ab8a3d docs: session resume 2026-05-20 - stabilization and platforms deployment 2026-05-20 13:19:29 -04:00
Eric FELIXINE
98f0bcb021 Session 2026-05-20: Contexus MQTT devices, OpenRemote agent, 60 capteurs configures 2026-05-20 00:58:48 -04:00
Eric FELIXINE
a4e05f557c Session 2026-05-20: Contexus deploye, OpenRemote assets corriges, Traefik config fix 2026-05-19 21:48:38 -04:00
Eric FELIXINE
5ddbf7de93 Add map screenshot with assets 2026-05-19 20:54:26 -04:00
Eric FELIXINE
805986e3f6 Add Playwright screenshots 2026-05-19: Manager login + map page 2026-05-19 19:01:40 -04:00
Eric FELIXINE
d4605ee072 Add session resume 2026-05-19 2026-05-19 16:36:08 -04:00
Eric FELIXINE
2377bc07fd Session 2026-05-19: OpenRemote map display investigation, cleanup, fresh install
- Investigated map display issues (agentLink, GeoJSON coords, realm config)
- Cleaned up all dashboards and containers
- Fresh Manager installation (PostgreSQL in recovery)
- Updated TODO.md with current status
- GeoJSON proxy: fixed coordinate order (lon/lat)
- Session resume saved
2026-05-19 16:22:26 -04:00
Eric FELIXINE
d1e6bdb685 docs: add skills inventory to TODO.md (epicollect5, odk, kobo, superset, metabase, contexus) 2026-05-19 15:48:46 -04:00
Eric FELIXINE
47746b584c fix: OpenRemote PUT 403/409, MQTTv5 callback, geojson-proxy API REST
- simulator.py: Fix MQTTv5 callback crash (5th arg *args)
- simulator.py: Fix _or_put() - GET version+realm before PUT, inject version in payload
- simulator.py: Fix token TTL (min 30s cache)
- simulator.py: Round-robin OR updates (~5 assets/iteration instead of 60)
- geojson-proxy: Rewrite using REST API instead of psycopg2 (PG auth issue)
- geojson-proxy: Add sensorType + attributes in properties for map styling
- docker-compose.yml: Add openremote_default network + DB vars for proxy
- docker-compose.yml: Add OR_REALM=master for geojson-proxy

Resolves: OpenRemote 403 (wrong realm in payload), 409 (missing version),
MQTTv5 callback crash, geojson-proxy DB connection failure
2026-05-18 10:04:12 -04:00
Eric FELIXINE
7937e2bb43 Session resume 2026-05-17: sauvegarde finale 2026-05-17 20:05:42 -04:00
Eric FELIXINE
55fabea16a Documentation géospatiale: GeoServer, PostGIS, MapStore 2026-05-17 19:55:40 -04:00
Eric FELIXINE
7477410813 Session 2026-05-17: GeoServer, PostGIS dédié, MapStore, ChirpStack
- GeoServer: workspace Digitribe + Data Store PostGIS dédié
- PostGIS dédié: conteneur postgis-smartcity (PostGIS 3.4)
- Couche sensors: 55 capteurs IoT importés depuis OpenRemote
- MapStore: GeoServer WMS ajouté au CORS
- ChirpStack: credentials réinitialisés (admin/admin1234)
- BunkerM: DNS corrigé (underscores → hyphens)
- Ditto: config MongoDB et auth devops
- Documentation: session_resume + TODO.md
2026-05-17 19:18:24 -04:00
Eric FELIXINE
1006df137d Session 2026-05-13: Nettoyage infra, BunkerM+Traefik, agentLink→REST, ChirpStack
- Nettoyage: suppression conteneurs TTS, anciens Chirpstack, exited/excess
- BunkerM recréé et ajouté à traefik-public (mosquitto2.digitribe.fr)
- Config Traefik mise à jour: 3 fichiers → bunkerm-bunkerm-1
- AgentLink MQTT désactivé sur 25 assets (master+smartcity)
- REST OpenRemote activé dans simulateur (location GeoJSONPoint incluse)
- ChirpStack: nouveau docker-compose dans submodule
2026-05-13 08:05:20 -04:00
Eric FELIXINE
15e9851b9f Session 2026-05-13: Nettoyage infra, BunkerM+Traefik, agentLink→REST, ChirpStack
- Nettoyage: suppression conteneurs TTS, anciens Chirpstack, exited/excess
- BunkerM recréé et ajouté à traefik-public (mosquitto2.digitribe.fr)
- Config Traefik mise à jour: 3 fichiers mosquitto2 → bunkerm-bunkerm-1
- AgentLink MQTT désactivé sur 25 assets (master+smartcity)
- REST OpenRemote activé dans simulateur (location GeoJSONPoint incluse)
- ChirpStack: nouveau docker-compose (postgres, redis, mosquitto, chirpstack)
- Session state documenté dans SESSION_STATE_2026-05-13.md
2026-05-13 08:03:27 -04:00
Eric FELIXINE
5fde1a2c8d feat(lorawan): démarrage ChirpStack et The Things Stack
- ChirpStack opérationnel (port 8080/8090, gateway bridge UDP 1700)
- The Things Stack opérationnel (port 1885/1884, gateway UDP 1701)
- Fichages de configuration créés
- Docker-compose corrigés (réseaux smartcity-shared)
- Désactivation agentLink sur 35 assets du simulateur
- Correction _or_put: suppression If-Match header (403)
- realm smartcity identifié pour les assets du simulateur
2026-05-12 17:34:53 -04:00
Eric FELIXINE
a05e13c30c feat(lorawan): ajout ChirpStack et The Things Stack
- Skills créés: chirpstack-lorawan, the-things-stack-lorawan
- docker-compose.chirpstack.yml: ChirpStack derrière Traefik
- docker-compose.the-things-stack.yml: TTS derrière Traefik
- data-flow-diagram.md: mise à jour avec LoRaWAN
- DOCKER-ARCHITECTURE: ajout conteneurs LoRaWAN
- Subdomaines Traefik: chirpstack, tts

Skills créés dans ~/.hermes/skills/iot/:
- chirpstack-lorawan
- the-things-stack-lorawan
2026-05-12 11:29:30 -04:00
Eric FELIXINE
dbf8b7f5ca docs: état des lieux localisation capteurs OpenRemote
- Documentation des découvertes et corrections appliquées
- Problèmes restants identifiés (connexion MQTT, topics, déconnexion)
- Prochaines étapes recommandées
2026-05-12 08:18:32 -04:00
Eric FELIXINE
7331dbc90b fix(simulator): corrections finales - topics MQTT, ASSET_MAP, location REST
Corrections:
- Topics MQTT: index basé sur position du capteur (pas compteur global itération)
- ASSET_MAP: mise à jour avec bons asset IDs (agentLink + location)
- Payload REST: ajout attribut location (GeoJSONPoint)
- Désactivation PUT REST sur assets avec agentLink (403 Forbidden)
- MQTT OpenRemote: tentative connexion anonyme (rc=5 persistant)
- Keepalive augmenté à 120s pour stabilité

Note: connexion MQTT au broker Artemis d'OpenRemote échoue (rc=5 Not Authorized)
Le broker nécessite une authentification spécifique non documentée.
Les agents MQTT d'OpenRemote ne reçoivent donc pas les données du simulateur.
La location est déjà correctement définie dans les assets en BDD.
2026-05-12 08:07:44 -04:00
Eric FELIXINE
4afed8ff2b fix(simulator): connexion MQTT OpenRemote sans auth, location dans payload REST, ASSET_MAP corrigé
- MQTT OpenRemote: connexion anonyme (pas de credentials) au broker Artemis
- Payload REST: ajout attribut location (GeoJSONPoint) pour chaque capteur
- ASSET_MAP: mise à jour avec les bons asset IDs (ceux avec agentLink + location)
- Topics MQTT: index basé sur position du capteur (pas compteur global)
- Désactivation PUT REST sur assets avec agentLink (403 Forbidden)
- keepalive augmenté à 60s pour stabilité connexion Artemis
2026-05-12 07:34:29 -04:00
Eric FELIXINE
8b87d95ca5 fix: OpenRemote REST - gestion version If-Match pour PUT assets
- Récupère la version actuelle de l'asset avant PUT
- Ajoute la version au payload pour éviter HTTP 409 Conflict
- OpenRemote:  les assets sont mis à jour en temps réel
- MQTT OK: 3/4 (EMQX, Mosquitto, BunkerM)
2026-05-11 14:56:27 -04:00
Eric FELIXINE
918c03dffa fix: Simulateur MQTT 3/4 + OpenRemote master + Mapsettings
- MQTT OK: 3/4 (EMQX, Mosquitto, BunkerM)
- OpenRemote: utilise realm master (token fonctionnel)
- Realm smartcity recréé dans Keycloak
- Assets IOTSensor créés dans master (30) et smartcity (30)
- Mapsettings: layers iot-sensors + labels pour master et smartcity
- INTERVAL=5s, réseau openremote_default ajouté
- Dockerfile: --no-cache rebuild
2026-05-11 14:19:53 -04:00
Eric FELIXINE
ae153c4e5e fix: Traefik routing OpenRemote/Ditto + QuantumLeap config (2026-05-08) 2026-05-08 03:11:13 -04:00
Eric FELIXINE
dfaa240d5a fix: Stabilisation complète Smart City Digital Twin Martinique
- Correction simulateur: nettoyage code FIWARE (erreurs syntaxe)
- Grafana: dashboard complet 10 panneaux sur grafana.digitribe.fr
- InfluxDB: datasource corrigée (bucket smartcity, org digitribe)
- Nettoyage: suppression services FIWARE (Orion-LD, Stellio, QuantumLeap)
- Pipeline validé: Simulator → 3 MQTT brokers → Telegraf → InfluxDB → Grafana
- Dashboard URL: https://grafana.digitribe.fr/d/smartcity-martinique-complete/

Architecture simplifiée:
- 3 MQTT brokers (EMQX, Mosquitto, BunkerM)
- Telegraf pour agrégation
- InfluxDB pour stockage time-series
- Grafana pour visualisation (Traefik: grafana.digitribe.fr)
2026-05-08 01:10:30 -04:00
Eric FELIXINE
552dba20d6 Fix: Grafana dashboard (validated data) + MapStore backend + Stellio docs
-  Grafana: Data confirmed in CrateDB (time_index in ms)
-  MapStore: Backend fixed (PostgreSQL config corrected, tables created)
-  Stellio: Documented as future work (NGSI-LD vs NGSI-v2 format mismatch)
- 📊 Dashboard available: https://grafana.digitribe.fr/d/smartcity-fixed-2026
2026-05-07 18:58:47 -04:00
Eric FELIXINE
9187ddfca6 Fix: Grafana dashboard Orion-LD + Stellio persistence investigation
-  Grafana dashboard updated with proper time filters
-  Orion-LD pipeline validated (6 tables, 5+ rows in CrateDB)
-  Stellio pipeline blocked: QuantumLeap /v2/notify doesn't support NGSI-LD native format (TypeError: argument of type 'float' is not iterable)
- 📝 QuantumLeap expects NGSI-v2 format ({"value": X, "type": "Number"}) but receives NGSI-LD (direct values)

Next steps:
- Focus on Orion-LD pipeline (working)
- Consider Stellio as experimental / future work
2026-05-07 18:35:47 -04:00
Eric FELIXINE
56fb3f3c50 ADD: Updated architecture with Stellio pipeline diagram 2026-05-07 16:57:12 -04:00
Eric FELIXINE
be13c9a2d7 FIX: Mosquitto healthcheck - replace bash with nc 2026-05-07 15:14:51 -04:00
Eric FELIXINE
5a5234f868 FIX: Pulsar Manager credentials + MapStore static files + CrateDB tables + QuantumLeap persistence 2026-05-07 15:07:10 -04:00
Eric FELIXINE
66a22a2421 Fix: InfluxDB token + bucket iot_data créé
- Token InfluxDB corrigé dans simulator.py (my-super-token)
- Bucket iot_data créé dans InfluxDB
- CrateDB-Stellio ports sécurisés (suppression exposition publique)
- Healthchecks MongoDB/Mosquitto corrigés
- Nettoyage container digital-twin-grafana
2026-05-07 10:41:16 -04:00
Eric FELIXINE
007e7eb2ff Fix: Sécurisation CrateDB-Stellio + healthchecks MongoDB/Mosquitto
- Suppression exposition publique ports CrateDB-Stellio (sécurité)
- Ajout service iot-mongodb avec healthcheck fonctionnel (mongo ping)
- Correction healthcheck Mosquitto (port check au lieu de topic)
- Nettoyage container digital-twin-grafana en conflit
2026-05-07 10:35:52 -04:00
Eric FELIXINE
227a799e94 Résumé final session 2026-05-06 - 60 assets MQTT créés 2026-05-06 22:48:46 -04:00
Eric FELIXINE
67ac37545e Mise à jour resume session - Assets OpenRemote finalisés 2026-05-06 22:39:21 -04:00
Eric FELIXINE
6162cf0b13 Rapport final 2026-05-06 - Infrastructure Smart City
- BunkerM accessible, Stellio pipeline actif
- 6 tables CrateDB, dashboards Grafana (AirQuality, Traffic, Weather)
- OpenRemote: API 405 à résoudre (assets à créer via UI)
- Services unhealthy identifiés (CrateDB Stellio, Mosquitto, MongoDB)
- ThingsBoard en boucle redémarrage
- 30 capteurs attendus (SENSOR_COUNT=30)
2026-05-06 21:59:23 -04:00
Eric FELIXINE
41f39a3faa Session resume 2026-05-06 - État infrastructure Smart City
- BunkerM accessible, Stellio pipeline actif
- 6 tables CrateDB créées, Grafana AirQuality OK
- OpenRemote API 405 à résoudre (assets à créer)
- 30 capteurs attendus (10 par broker)
2026-05-06 21:55:41 -04:00
Eric FELIXINE
0c787b154a IoT Agents: suppression healthcheck + BunkerM configuré pour Stellio (NGSI-LD)
- Suppression healthcheck (curl/nc indisponibles dans les conteneurs)
- IoT Agent BunkerM reconfiguré: IOTA_CB_HOST=stellio-api-gateway, IOTA_CB_NGSI_VERSION=ld
- En attente vérification pipeline Stellio
2026-05-06 21:32:33 -04:00
Eric FELIXINE
b6c627a639 Correction BunkerM domaine: mosquitto2.digitribe.fr 2026-05-06 21:27:10 -04:00
Eric FELIXINE
362a9d1f6b Architecture mise à jour (07 Mai 2026)
- Correction flux : Simulateur → MQTT Brokers → IoT Agents → Orion-LD/Stellio → QuantumLeap → CrateDB
- IoT Agents fonctionnels (EMQX:4041, Mosquitto:4042, BunkerM:4043)
- Pipeline Orion-LD validé (CrateDB: quantumleap.etairqualityobserved)
- BunkerM domaine corrigé : mosquitto2.digitribe.fr:1900
- Simulateur publie sur topics smartcity-api-key/{sid}/attrs
2026-05-06 21:26:21 -04:00
Eric FELIXINE
1ac8cf7117 fix: CrateDB-Stellio + table quantumleap_stellio 2026-05-06 20:22:53 -04:00
Eric FELIXINE
c27c2c10af fix: QuantumLeap + Redis + simulateur MQTT-only + données test CrateDB 2026-05-06 19:26:13 -04:00
Eric FELIXINE
64022bd9ab fix: Simulateur publie sur 3 brokers (emqx, mosquitto, bunkerm) avec préfixe json/ 2026-05-06 17:50:06 -04:00
Eric FELIXINE
380c92cc19 docs: Final architecture - 2 CrateDB datasources in Grafana (Orion + Stellio) 2026-05-06 17:46:41 -04:00
Eric FELIXINE
91ade0ad20 docs: Add multi-Context Broker architecture (Orion-LD + Stellio separate pipelines) 2026-05-06 17:32:35 -04:00
Eric FELIXINE
3df9f914fa docs: Final session resume 2026-05-06 - 3 IoT Agents, Orion-LD, Stellio next steps 2026-05-06 17:30:28 -04:00
Eric FELIXINE
4667d8873c docs: Update HTML diagram - 3 IoT Agents architecture 2026-05-06 17:22:21 -04:00
Eric FELIXINE
07bb3384b9 docs: Update data flow diagram - 3 IoT Agents per broker, Orion-LD, QuantumLeap 2026-05-06 17:21:41 -04:00
Eric FELIXINE
75d67bea66 docs: Network audit complete - all containers on smartcity-shared 2026-05-06 17:05:31 -04:00
Eric FELIXINE
ff4cd349b6 docs: Final session resume 2026-05-06 - QuantumLeap fix, Grafana next steps 2026-05-06 17:02:36 -04:00
Eric FELIXINE
a085aeca44 chore: Smart City update - QuantumLeap fix, IoT-Agent integration, simulator update 2026-05-06 17:01:39 -04:00
Eric FELIXINE
3cbacbaa8c fix: QuantumLeap use CRATE_HOST/PORT instead of QL_CRATEDB_* 2026-05-06 16:50:02 -04:00
Eric FELIXINE
00b55a29a2 docs: Add session resume 2026-05-06 - IoT-Agent integration and QuantumLeap setup 2026-05-06 16:36:55 -04:00
Eric FELIXINE
0c1b75fcd3 feat: Add IoT-Agent integration - simulator publishes to smartcity-api-key/{sid}/attrs via EMQX 2026-05-06 16:20:05 -04:00
35878 changed files with 10963770 additions and 1158 deletions

2
.env Normal file
View File

@@ -0,0 +1,2 @@
CHIRP_USER=chirpstack
CHIRP_PASS=chirpstack

2
.env.ditto Normal file
View File

@@ -0,0 +1,2 @@
DITTO_JWT_SECRET=NTOT-Vh8WRKWE52eV8zRiLs3a-gd8YUGSrvm5x2InZc
DEVOPS_PASSWORD=OvP9WVB09aFDnYPyK52UIg

View File

@@ -0,0 +1,121 @@
# Gitea Actions — CI/CD Smart App City Web
# Trigger: push sur master ou PR
name: Build & Deploy Smart App Web
on:
push:
branches: [master]
paths:
- 'smart-app-city/frontend/**'
pull_request:
branches: [master]
jobs:
# ─── Lint + Type Check ─────────────────────────────────────────────────
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: smart-app-city/frontend/package-lock.json
- name: Install dependencies
working-directory: smart-app-city/frontend
run: npm ci --legacy-peer-deps
- name: TypeScript check
working-directory: smart-app-city/frontend
run: npx tsc --noEmit
continue-on-error: true # TODO: fix TS strict errors
# ─── Build Web ─────────────────────────────────────────────────────────
build-web:
runs-on: ubuntu-latest
needs: lint
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: smart-app-city/frontend/package-lock.json
- name: Install dependencies
working-directory: smart-app-city/frontend
run: npm ci --legacy-peer-deps
- name: Build Expo web
working-directory: smart-app-city/frontend
run: npx expo export:web
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: smartapp-web-build
path: smart-app-city/frontend/dist/
retention-days: 7
# ─── Deploy to Server ──────────────────────────────────────────────────
deploy:
runs-on: ubuntu-latest
needs: build-web
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: smartapp-web-build
path: smart-app-city/frontend/dist/
- name: Deploy to server via SSH
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "smart-app-city/frontend/dist/"
target: "/var/www/smartapp/"
strip_components: 3
- name: Restart nginx (or copy to Docker volume)
uses: appleboy/ssh-action@v1.0.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
# Option A: If using Docker volume
docker cp /var/www/smartapp/ smartapp-web:/usr/share/nginx/html/
# Option B: If using Docker build
# cd /home/eric/smart-city-digital-twin-martinique/smart-app-city
# docker compose up -d --build smartapp-web
echo "✅ Smart App Web deployed at $(date)"
# ─── Build Docker Image (alternative) ───────────────────────────────────
docker-build:
runs-on: ubuntu-latest
needs: lint
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
working-directory: smart-app-city/frontend
run: |
docker build -t smartapp-web:${{ github.sha }} .
docker tag smartapp-web:${{ github.sha }} smartapp-web:latest
# Note: Pour pousser vers un registry privé, ajouter docker login + push
# - name: Push to registry
# run: docker push registry.digitribe.fr/smartapp-web:latest

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Dependencies
node_modules/
*/node_modules/
# Build outputs
dist/
build/
*.pyc
__pycache__/
# Environment files
.env
.env.local
*.env
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

14
32-kafka-ui.yml Normal file
View File

@@ -0,0 +1,14 @@
http:
routers:
kafka-ui:
rule: "Host(`kafka.digitribe.fr`)"
entryPoints:
- websecure
service: kafka-ui-svc
tls:
certResolver: letsencrypt
services:
kafka-ui-svc:
loadBalancer:
servers:
- url: "http://kafka-ui:8080"

14
32-trino.yml Normal file
View File

@@ -0,0 +1,14 @@
http:
routers:
trino:
rule: "Host(`trino.digitribe.fr`)"
entryPoints:
- websecure
service: trino-svc
tls:
certResolver: letsencrypt
services:
trino-svc:
loadBalancer:
servers:
- url: "http://trino:8084"

14
33-flink.yml Normal file
View File

@@ -0,0 +1,14 @@
http:
routers:
flink:
rule: "Host(`flink.digitribe.fr`)"
entryPoints:
- websecure
service: flink-svc
tls:
certResolver: letsencrypt
services:
flink-svc:
loadBalancer:
servers:
- url: "http://flink-jobmanager:8081"

14
34-gravitino.yml Normal file
View File

@@ -0,0 +1,14 @@
http:
routers:
gravitino:
rule: "Host(`gravitino.digitribe.fr`)"
entryPoints:
- websecure
service: gravitino-svc
tls:
certResolver: letsencrypt
services:
gravitino-svc:
loadBalancer:
servers:
- url: "http://gravitino:8090"

14
35-minio.yml Normal file
View File

@@ -0,0 +1,14 @@
http:
routers:
minio:
rule: "Host(`minio.digitribe.fr`)"
entryPoints:
- websecure
service: minio-svc
tls:
certResolver: letsencrypt
services:
minio-svc:
loadBalancer:
servers:
- url: "http://minio:9001"

93
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,93 @@
# Smart City Digital Twin - Martinique
## Nouvelle Architecture (Mise à jour 08/05/2026)
### Stack Simplifiée
```
Simulateur Python (60 capteurs)
┌───────────────────────────────────────────────┐
│ 3 Brokers MQTT │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ EMQX │ │ Mosquitto │ │ BunkerM │ │
│ │(emqx_emqx_1)│ │(smart-city- │ │(bunkerm_ │ │
│ │ │ │ mosquitto) │ │ bunkerm_1)│ │
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
└────────┼──────────────┼──────────────┼──────────────┘
│ │ │
└──────────────┴──────────────┘
Telegraf (3 inputs MQTT)
InfluxDB v2
(bucket: smartcity)
Grafana
(Dashboard: smartcity-martinique-2026)
```
### Détails des Composants
#### 1. Simulateur (`smart-city-simulator`)
- **Fonction** : Génère des données IoT simulées (60 capteurs)
- **Types** : AirQuality, Traffic, Parking, Noise, Weather, Light
- **Brokers MQTT** : Publie sur les 3 brokers simultanément
- EMQX: `emqx_emqx_1:1883` (MQTT v3.1.1)
- Mosquitto: `smart-city-mosquitto:1883` (MQTT v3.1.1)
- BunkerM: `bunkerm_bunkerm_1:1900` (MQTT v3.1.1, auth: bunker/bunker)
- **InfluxDB** : Écriture asynchrone (ASYNCHRONOUS) vers `smartcity` bucket
#### 2. Telegraf (`smart-city-telegraf`)
- **Fonction** : Collecte les données MQTT et les écrit dans InfluxDB
- **Configuration** : 3 inputs MQTT (un par broker)
- **Topics** : `airquality/#`, `traffic/#`, `parking/#`, `noise/#`, `weather/#`, `light/#`
- **Format** : JSON → InfluxDB line protocol
#### 3. InfluxDB (`smart-city-influxdb`)
- **Version** : v2.7.12
- **Organization** : digitribe
- **Bucket** : `smartcity` (infinite retention)
- **Token** : `my-super-token`
#### 4. Grafana (`smart-city-grafana`)
- **URL** : http://localhost:3001
- **Credentials** : admin / Digitribe972
- **Dashboard** : Smart City Digital Twin - Martinique
- UID: `smartcity-martinique-2026`
- 6 panneaux (AirQuality, Traffic, Parking, Noise, Weather, Light)
- Source: InfluxDB (`smartcity` bucket)
### Flux de Données
1. **Simulateur** publie sur 3 brokers MQTT (EMQX, Mosquitto, BunkerM)
2. **Telegraf** subscribe aux topics MQTT → convertit en format InfluxDB
3. **InfluxDB** stock les séries temporelles
4. **Grafana** visualise les données via Flux queries
### Avantages de cette Architecture
-**Simplicité** : Pas de FIWARE (Orion-LD, Stellio, QuantumLeap)
-**Performance** : InfluxDB optimisé pour les séries temporelles
-**Redondance** : 3 brokers MQTT (si un tombe, les autres assurent)
-**Maintnant** : Stack standard (Telegraf/InfluxDB/Grafana)
### Commandes Utiles
```bash
# Vérifier les données InfluxDB
docker exec smart-city-influxdb influx query 'from(bucket:"smartcity") |> range(start:-1h) |> group(columns: ["_measurement"]) |> count()'
# Voir les logs du simulateur
docker logs smart-city-simulator --tail 50
# Redémarrer Telegraf
docker restart smart-city-telegraf
# Accéder à Grafana
open http://localhost:3001
```
### Fichiers de Configuration
- **Simulateur** : `/home/eric/smart-city-digital-twin-martinique/simulator.py`
- **Telegraf** : `/home/eric/smart-city-digital-twin-martinique/telegraf.conf`
- **Docker Compose** : `/home/eric/smart-city-digital-twin-martinique/docker-compose.yml`
- **Dashboard Grafana** : `/home/eric/smart-city-digital-twin-martinique/grafana-dashboard-smartcity.json`
---
*Dernière mise à jour : 08/05/2026 - Suppression de FIWARE, passage à Telegraf/InfluxDB*

View File

@@ -0,0 +1,225 @@
# Smart City Digital Twin - Architecture Docker (LoRaWAN Added)
**Date** : 12 mai 2026
**Projet** : `smart-city-digital-twin-martinique`
**Auteur** : Éric FELIXINE (via Hermes Agent)
---
## 1. Vue d'ensemble
Cette cartographie présente l'architecture Docker complète du jumeau numérique Smart City (Martinique), incluant les conteneurs, images, réseaux et ports exposés. **Mise à jour 2026-05-12** : ajout de ChirpStack et The Things Stack pour la connectivité LoRaWAN.
---
## 2. Flux de données principal
### Pipeline Orion-LD (Fonctionnel ✅)
```
Simulator → MQTT Brokers (Mosquitto/EMQX/BunkerM) → IoT Agents → Orion-LD → QuantumLeap → CrateDB (standard) → Grafana
```
### Pipeline Stellio (En cours de debug ⚠️)
```
Simulator → MQTT Brokers → IoT Agents → Stellio Context Broker → QuantumLeap-Stellio → CrateDB-Stellio → Grafana
```
### Pipeline LoRaWAN ChirpStack (Nouveau 🆕)
```
Gateway LoRaWAN (UDP 1700) → ChirpStack Gateway Bridge → ChirpStack → MQTT (Mosquitto interne) → EMQX → IoT Agents → Orion-LD → ...
```
### Pipeline LoRaWAN The Things Stack (Nouveau 🆕)
```
Gateway LoRaWAN (UDP 1700) → TTS Stack → MQTT/REST API → EMQX → IoT Agents → Orion-LD → ...
```
### Pipeline OpenRemote (En cours ⚠️)
```
Simulator → REST API (PUT assets avec location) → OpenRemote Manager → Map Martinique
Simulator → MQTT (Artemis broker) → OpenRemote Agents → Asset values
```
---
## 3. Liste des Conteneurs Actifs (Projet Smart City)
| Conteneur | Image | Réseaux | Ports |
|-----------|-------|----------|-------|
| `smart-city-simulator` | `smart-city-simulator:latest` | `smartcity-shared`, `traefik-public` | `1883/tcp` |
| `smart-city-mosquitto` | `eclipse-mosquitto:latest` | `smartcity-shared`, `traefik-public` | `1883:1883`, `9001:9001` |
| `smart-city-emqx` | `emqx/emqx:latest` | `smartcity-shared`, `traefik-public` | `11883:1883`, `18081:8081` |
| `smart-city-iot-agent-mosquitto` | `fiware/iotagent-json:latest` | `smartcity-shared` | `4041:4041` |
| `smart-city-iot-agent-emqx` | `fiware/iotagent-json:latest` | `smartcity-shared` | `4042:4041` |
| `smart-city-iot-agent-bunkerm` | `fiware/iotagent-json:latest` | `smartcity-shared` | `4043:4041` |
| **`smart-city-orion-ld`** | `quay.io/fiware/orion-ld` | `smartcity-shared`, `traefik-public` | `1026:1026` |
| **`smart-city-quantumleap`** | `fiware/quantum-leap:latest` | `smartcity-shared`, `traefik-public` | `8668:8668` |
| **`smart-city-cratedb`** | `crate:5.5` | `smartcity-shared` | `4200:4200`, `5432:5432` |
| **`stellio-api-gateway`** | `stellio/stellio-api-gateway:latest-dev` | `stellio-context-broker_default`, `traefik-public`, `smartcity-shared` | `8080:8080` |
| **`stellio-subscription-service`** | `stellio/stellio-subscription-service:latest-dev` | `stellio-context-broker_default`, `smartcity-shared` | `8084:8084` |
| **`stellio-search-service`** | `stellio/stellio-search-service:latest-dev` | `stellio-context-broker_default`, `traefik-public` | `8083:8083` |
| **`stellio-kafka`** | `confluentinc/cp-kafka:8.1.0` | `stellio-context-broker_default` | `9092:9092` |
| **`stellio-postgres`** | `stellio/stellio-timescale-postgis:16-2.24.0-3.6` | `stellio-context-broker_default` | `5432:5432` |
| **`smart-city-quantumleap-stellio`** | `fiware/quantum-leap:latest` | `smartcity-shared`, `traefik-public` | `8669:8668` |
| **`smart-city-cratedb-stellio`** | `crate:latest` | `smartcity-shared` | `4200:4200` |
| `smart-city-redis` | `redis:7-alpine` | `smartcity-shared` | `6379:6379` |
| `smart-city-grafana` | `grafana/grafana:latest` | `smartcity-shared`, `traefik-public` | `3000:3000` |
|| `openremote-manager-1` | `openremote/manager:latest` | `openremote_default`, `smartcity-shared` | `8080:8080`, `8443:8443` |
|| `openremote-keycloak-1` | `openremote/keycloak:latest` | `openremote_default`, `smartcity-shared` | `8080:8080`, `8443:8443` |
|| `traefik` | `traefik:v3.0` | `traefik-public`, `openremote_default` | `80:80`, `443:443` |
|| **ChirpStack LoRaWAN** | | | |
|| `chirpstack-chirpstack-1` | `chirpstack/chirpstack:4` | `chirpstack-internal`, `traefik-public`, `smartcity-shared` | `8080:8080` |
|| `chirpstack-gateway-bridge-1` | `chirpstack/chirpstack-gateway-bridge:4` | `chirpstack-internal` | `1700:1700/udp` |
|| `chirpstack-rest-api-1` | `chirpstack/chirpstack-rest-api:4` | `chirpstack-internal`, `traefik-public` | `8090:8090` |
|| `chirpstack-postgres-1` | `postgres:14-alpine` | `chirpstack-internal` | `5432` |
|| `chirpstack-redis-1` | `redis:7-alpine` | `chirpstack-internal` | `6379` |
|| `chirpstack-mosquitto-1` | `eclipse-mosquitto:2` | `chirpstack-internal`, `smartcity-shared` | `1883` |
|| **The Things Stack LoRaWAN** | | | |
|| `tts-stack-1` | `thethingsnetwork/lorawan-stack:latest` | `tts-internal`, `traefik-public`, `smartcity-shared` | `1885:1885`, `1884:1884`, `1700:1700/udp` |
|| `tts-postgres-1` | `postgres:14` | `tts-internal` | `5432` |
|| `tts-redis-1` | `redis:7` | `tts-internal` | `6379` |
---
## 4. Réseaux Docker
| Réseau | Conteneurs Connectés |
|---------|----------------------|
| `smartcity-shared` | Tous les services Smart City (simulator, brokers, context brokers, databases, grafana) |
| `stellio-context-broker_default` | Stellio services (api-gateway, subscription, search, kafka, postgres) |
| `traefik-public` | Services exposés via Traefik (grafana, mapstore, pulsar, stellio, orion, chirpstack, tts, etc.) |
| `openremote_default` | OpenRemote services (manager, keycloak, postgresql) |
| `chirpstack-internal` | ChirpStack services (chirpstack, gateway-bridge, rest-api, postgres, redis, mosquitto) |
| `tts-internal` | TTS services (stack, postgres, redis) |
---
## 5. Diagramme d'Architecture (Mermaid)
```mermaid
graph TD
subgraph Simulator [Smart City Simulator]
SIM[Simulator<br/>Python MQTT Publisher]
end
subgraph MQTT_Brokers [MQTT Brokers]
MOSQ[Mosquitto<br/>:1883]
EMQX[EMQX<br/>:11883]
BUNKER[BunkerM<br/>:1884]
end
subgraph IoT_Agents [IoT Agents FIWARE]
IOT_MOSQ[IoT Agent Mosquitto<br/>:4041]
IOT_EMQX[IoT Agent EMQX<br/>:4042]
IOT_BUNKER[IoT Agent BunkerM<br/>:4043]
end
subgraph Orion_LD_Pipeline [Orion-LD Pipeline ✅]
ORION[Orion-LD<br/>:1026]
QL[QuantumLeap<br/>:8668]
CRATEDB[CrateDB<br/>:4200/:5432]
end
subgraph Stellio_Pipeline [Stellio Pipeline ⚠️]
STELLIO[Stellio API Gateway<br/>:8080]
SUB[Stellio Subscription<br/>:8084]
QL_STELLIO[QuantumLeap-Stellio<br/>:8669]
CRATEDB_STELLIO[CrateDB-Stellio<br/>:4200]
end
subgraph Visualization [Visualization Layer]
GRAFANA[Grafana<br/>:3000<br/>21 Dashboards]
MAPSTORE[MapStore<br/>:8080]
OPENREMOTE[OpenRemote<br/>:8080]
end
subgraph Message_Broker [Message Broker]
KAFKA[Stellio Kafka<br/>:9092]
end
%% Flux Simulator
SIM -->|MQTT| MOSQ
SIM -->|MQTT| EMQX
SIM -->|MQTT| BUNKER
%% Flux IoT Agents
MOSQ -->|MQTT| IOT_MOSQ
EMQX -->|MQTT| IOT_EMQX
BUNKER -->|MQTT| IOT_BUNKER
%% Flux Orion-LD (Working ✅)
IOT_MOSQ -->|NGSI-v2| ORION
IOT_EMQX -->|NGSI-v2| ORION
IOT_BUNKER -->|NGSI-v2| ORION
ORION -->|Subscription| QL
QL -->|INSERT| CRATEDB
CRATEDB -->|Query| GRAFANA
%% Flux Stellio (In Progress ⚠️)
IOT_MOSQ -->|NGSI-LD?| STELLIO
IOT_EMQX -->|NGSI-LD?| STELLIO
STELLIO -->|Subscription| QL_STELLIO
QL_STELLIO -->|INSERT?| CRATEDB_STELLIO
CRATEDB_STELLIO -->|Query| GRAFANA
%% Kafka (Stellio internal)
STELLIO --> KAFKA
SUB --> KAFKA
%% Visualization
GRAFANA -->|Dashboards| User[Utilisateur]
MAPSTORE -->|Maps| User
OPENREMOTE -->|Assets| User
```
---
## 6. État des Pipelines
### ✅ Pipeline Orion-LD (Opérationnel)
- **Statut** : Entièrement fonctionnel
- **Données** : CrateDB contient 6 tables avec données (`etairqualityobserved` a 6+ rows)
- **Grafana** : Dashboard "Smart City - Air Quality (CrateDB)" fonctionnel
- **Subscription** : Orion-LD → QuantumLeap active (`lastNotification: 2026-05-07`)
### ⚠️ Pipeline Stellio (Debug en cours)
- **Statut** : Subscription créée, notifications reçues par QuantumLeap-Stellio
- **Problème** : Données ne persistent pas dans CrateDB-Stellio (0 rows)
- **Cause probable** : Mappage NGSI-LD → CrateDB incompatible
- **Subscription Stellio** : `urn:ngsi-ld:Subscription:0baad89d-1625-4b42-adc1-e841e04120ff`
- Endpoint : `http://smart-city-quantumleap-stellio:8668/v2/notify`
- Format : NGSI-LD normalized
---
## 7. Services Web Accessibles
| Service | URL | Identifiants | Statut |
|---------|-----|-------------|--------|
| **Grafana** | https://grafana.digitribe.fr | `admin` / `Digitribe972` | ✅ 21 dashboards |
| **MapStore** | https://mapstore.digitribe.fr/mapstore/ | - | ✅ Page charge |
| **Pulsar Manager** | https://pulsar.digitribe.fr | `pulsar` / `pulsar` | ✅ Interface OK |
| **OpenRemote** | https://openremote.digitribe.fr | `admin` / `Digitribe972` | ✅ Carte Martinique |
| **Orion-LD** | http://smart-city-orion-ld:1026 | - | ✅ Healthy |
| **Stellio** | http://stellio-api-gateway:8080 | - | ✅ Contient entités |
---
## 8. Connexions Clés
1. **Traefik** (`:80`/`:443`) : Reverse proxy pour tous les services exposés à l'hôte.
2. **Brokers MQTT** (Mosquitto `:1883`, EMQX `:11883`) : Réception des données du simulateur et des capteurs IoT.
3. **Context Brokers** (Orion-LD `:1026`, Stellio `:8080`) : Gestion des entités NGSI-LD.
4. **Time-Series DB** (CrateDB `:4200` HTTP API, `:5432` PostgreSQL) : Persistance des données pour Grafana.
5. **Grafana** (`:3000`) : Visualisation des données depuis CrateDB, InfluxDB, Prometheus.
---
## 9. Références
- **Projet** : `~/smart-city-digital-twin-martinique/`
- **Gitea** : https://gitea.digitribe.fr/eric/smart-city-digital-twin-martinique
- **Skills** : `smart-city-sensor-simulator`, `fiware-quantumleap`, `fiware-orion-ld`, `cratedb`
- **Documentation** : `RAPPORT_FINAL_2026-05-06.md`, `BILAN-2026-05-05.md`
---
*Architecture mise à jour le 07 mai 2026 à 21:00 (UTC-4) par Hermes Agent.*

5
Dockerfile.exporter Normal file
View File

@@ -0,0 +1,5 @@
FROM python:3.13-slim
RUN pip install docker prometheus_client
COPY docker_exporter.py /app/docker_exporter.py
WORKDIR /app
CMD ["python3", "docker_exporter.py", "8005"]

241
INVENTORY-2026-05-27.md Normal file
View File

@@ -0,0 +1,241 @@
# Smart City Digital Twin — Inventaire Architecture
> **Date** : 2026-05-27 01:00
> **Hôte** : Linux 6.8.0-117-generic (31Gi RAM)
> **Containers actifs** : 59 running / 1 restarting / 61 exited
> **Réseaux Docker** : 30+ networks (smartcity-shared, traefik-public, odk-internal, etc.)
---
## 📊 Vue d'ensemble
| Catégorie | Containers | Statut |
|-----------|------------|--------|
| **ODK Central** | 4 | ✅ Tous UP |
| **IoT / MQTT** | 10 | ✅ Tous UP |
| **Data / Analytics** | 10 | ✅ Tous UP |
| **GIS / Géospatial** | 8 | ✅ Tous UP |
| **Identité** | 3 | ⚠️ Honcho restarting |
| **Smart City Core** | 10 | ✅ Tous UP |
| **Monitoring** | 3 | ✅ Tous UP |
| **AI** | 1 | ✅ UP |
| **Autres** | 7 | ✅ Tous UP |
---
## 🔷 ODK Central (4 containers)
| Container | Image | Ports | Domaine |
|-----------|-------|-------|---------|
| `odk-nginx` | odk-nginx:latest | 80, 443 | odk.digitribe.fr |
| `odk-service` | odk-service:latest | 8383 | — |
| `odk-postgres` | postgres:15-alpine | 5432 | — |
| `odk-pyxform` | ghcr.io/getodk/pyxform-http:v4.4.1 | 80 | — |
**Credentials** : efelixine@digitribe.fr / Digitribe972
**Projet** : Smart-City-Martinique (id=1)
---
## 🔶 IoT / MQTT (10 containers)
| Container | Image | Ports | Rôle |
|-----------|-------|-------|------|
| `bunkerm-bunkerm-1` | bunkeriot/bunkerm:latest | 1883→1900, 2000 | MQTT Broker principal |
| `emqx_emqx_1` | emqx/emqx:latest | 11883, 18081, 18883, 38083 | MQTT Broker v5 |
| `smart-city-digital-twin-martinique-mosquitto-1` | eclipse-mosquitto:2 | 1883 | MQTT v5 (simulateur) |
| `chirpstack-mosquitto-1` | eclipse-mosquitto:2 | 1883 | MQTT ChirpStack |
| `smart-city-digital-twin-martinique-chirpstack-1` | chirpstack/chirpstack:latest | — | LoRaWAN NS |
| `smart-city-digital-twin-martinique-chirpstack-rest-api-1` | chirstack/chirpstack-rest-api:4 | — | ChirpStack REST API |
| `smart-city-digital-twin-martinique-chirpstack-gateway-bridge-1` | chirpstack/chirpstack-gateway-bridge:4 | 1700/udp | Gateway Bridge |
| `smart-city-digital-twin-martinique-chirpstack-gateway-bridge-basicstation-1` | chirpstack/chirpstack-gateway-bridge:4 | — | Basic Station |
| `chirpstack-redis-1` | redis:7-alpine | 6379 | Cache ChirpStack |
| `chirpstack-postgres-1` | postgres:14-alpine | 5432 | DB ChirpStack |
**Credentials MQTT** : bunker / bunker (BunkerM)
**Credentials ChirpStack** : admin / Digitribe972
---
## 📈 Data / Analytics (10 containers)
| Container | Image | Ports | Domaine |
|-----------|-------|-------|---------|
| `smart-city-grafana` | grafana/grafana:10.2.0 | 3001 | grafana.digitribe.fr |
| `grafana_stack-grafana-1` | grafana/grafana:latest | 3000 | — |
| `honcho-grafana-1` | grafana/grafana:11.4.0 | 3088 | — |
| `smart-city-influxdb` | influxdb:2.7-alpine | 8086 | — |
| `smart-city-loki` | grafana/loki:latest | 3100 | — |
| `smart-city-prometheus-brokers` | prom/prometheus:latest | — | — |
| `honcho-prometheus-1` | prom/prometheus:v3.2.1 | 9091 | — |
| `smart-city-redpanda-console` | redpandadata/console:v2.5.0 | 28080 | — |
| `metabase-app` | metabase/metabase:latest | 3000 | metabase.digitribe.fr |
| `metabase-postgres` | postgres:15-alpine | 5432 | — |
**Credentials** : admin / Digitribe972 (Grafana, Metabase, Superset)
**InfluxDB** : token=my-super-token, org=digitribe, bucket=smartcity
---
## 🗺️ GIS / Géospatial (8 containers)
| Container | Image | Ports | Domaine |
|-----------|-------|-------|---------|
| `geoserver_stack-geoserver-1` | oscarfonts/geoserver:2.25.2 | 8080 | geoserver.digitribe.fr |
| `mapstore-app` | geosolutionsit/mapstore2:latest | 8080 | mapstore.digitribe.fr |
| `mapstore-proxy` | nginx | 80 | — |
| `mapstore-postgres` | geosolutions-mapstore/postgis | 5432 | — |
| `postgis-smartcity` | postgis/postgis:15-3.4 | 5433 | — |
| `fiware-gis-quickstart-orion-1` | quay.io/fiware/orion-ld | 2026 | — |
| `fiware-gis-quickstart-orionproxy-1` | fiware-gis-quickstart-orionproxy | 1026 | — |
| `fiware-gis-quickstart-mongo-db-1` | mongo:4.2 | 27017 | — |
| `frost_allinone-web-1` | fraunhoferiosb/frost-server:latest | 8089 | — |
| `frost_http-web-1` | fraunhoferiosb/frost-server-http:latest | 8080 | — |
| `stellio-api-gateway` | stellio/stellio-api-gateway:latest-dev | 8080 | stellio.digitribe.fr |
---
## 🔐 Identité (3 containers)
| Container | Image | Ports | Domaine |
|-----------|-------|-------|---------|
| `openremote-keycloak` | quay.io/keycloak/keycloak:24.0 | 8080, 8443 | openremote.digitribe.fr/auth |
| `honcho-api-1` | honcho:latest | 8000 | — |
| `honcho-deriver-1` | honcho-deriver | — | — |
**Credentials Keycloak** : admin / admin
**Credentials OpenRemote** : admin / Digitribe972
---
## 🏙️ Smart City Core (10 containers)
| Container | Image | Ports | Rôle |
|-----------|-------|-------|------|
| `smart-city-simulator` | smart-city-simulator | 8081 | Simulateur 60 capteurs |
| `smart-city-telegraf` | telegraf:1.28 | 8092, 8125, 8094 | Collecte IoT |
| `contexus-app` | contexusio/contexus:latest | 15000 | Plateforme IoT |
| `contexus-postgres` | postgres:16 | 5432 | DB Contexus |
| `contexus-redis` | redis:7-alpine | 6379 | Cache Contexus |
| `smart-city-ditto-policies` | eclipse/ditto-policies:latest | 8080 | Digital Twin policies |
| `smart-city-ditto-gateway` | eclipse/ditto-gateway:latest | 8080 | Digital Twin gateway |
| `smart-city-ditto-mongodb` | mongo:6 | 27017 | DB Ditto |
| `digital-twin-nodered` | nodered/node-red:3.1 | 1880 | Node-RED |
| `digital-twin-connector` | python:3.11-slim | — | Connector Python |
| `smart-city-digital-twin-martinique-redis-1` | redis:7-alpine | 6379 | Cache simulateur |
**Credentials Contexus** : iotevadmin / Digitribe972
---
## 👁️ Monitoring (3 containers)
| Container | Image | Ports | Rôle |
|-----------|-------|-------|------|
| `traefik` | traefik:v3.1 | 80, 443, 8404 | Reverse Proxy |
| `smart-city-promtail` | grafana/promtail:latest | — | Log shipping |
| `docker-exporter` | docker-exporter:latest | 8005 | Métriques Docker |
---
## 🤖 AI (1 container)
| Container | Image | Ports | Rôle |
|-----------|-------|-------|------|
| `localai-api` | localai/localai:latest | 8080 | LLM local (Llama 3.1 70B) |
**Credentials** : admin / Digitribe972
**API Key** : hermes-localai-secret-key-2024
---
## 📦 Autres (7 containers)
| Container | Image | Ports | Rôle |
|-----------|-------|-------|------|
| `gitea` | gitea/gitea:latest | 22, 3000 | Git (gitea.digitribe.fr) |
| `agentgateway` | cr.agentgateway.dev/agentgateway:latest | 3000, 15000 | Agent Gateway |
| `smart-city-kepler` | smart-city-kepler:latest | 80, 8080 | Kepler.gl |
| `stellio-api-gateway` | stellio/stellio-api-gateway:latest-dev | 8080 | NGSI-LD Context Broker |
| `esperotech` | esperotech/yaade:latest | 9339 | Yaade |
| `phpipam-phpipam-web-1` | phpipam/phpipam-www:latest | 8085 | IPAM |
| `phpipam-phpipam-cron-1` | phpipam/phpipam-cron:latest | — | IPAM cron |
| `docker_zookeeper_1` | zookeeper:3.8.1 | 2181 | ZooKeeper |
---
## 🌐 Réseaux Docker Principaux
| Réseau | Usage |
|--------|-------|
| `smartcity-shared` | Réseau principal partagé entre services |
| `traefik-public` | Exposition web via Traefik |
| `central_odk-internal` | Réseau interne ODK |
| `openremote_default` | OpenRemote stack |
| `contexus-iot-network` | Contexus IoT |
| `fiware-gis-quickstart_fiware` | FIWARE GIS |
| `mapstore2_mapstore-network` | MapStore |
| `superset_default` | Apache Superset |
| `metabase_default` | Metabase |
| `localai_default` | LocalAI |
| `honcho_default` | Honcho |
| `mainflux-network` | Mainflux |
| `stellio-context-broker_default` | Stellio |
---
## 🔑 Credentials Résumé
| Service | Login | Password |
|---------|-------|----------|
| **ODK Central** | efelixine@digitribe.fr | Digitribe972 |
| **Grafana** | admin | Digitribe972 |
| **Metabase** | admin@digitribe.fr | Digitribe972 |
| **Superset** | admin | Digitribe972 |
| **OpenRemote** | admin | Digitribe972 |
| **Keycloak** | admin | admin |
| **Contexus** | iotevadmin | Digitribe972 |
| **ChirpStack** | admin | Digitribe972 |
| **BunkerM MQTT** | bunker | bunker |
| **LocalAI** | admin | Digitribe972 |
| **InfluxDB** | — | token=my-super-token |
| **PostgreSQL Contexus** | contexus | Digitribe972 |
| **Redis Contexus** | — | Digitribe972 |
---
## 📁 Répertoires Projet
```
~/smart-city-digital-twin-martinique/ # Repo principal (Gitea)
~/odk/central/ # ODK Central
~/openremote/ # OpenRemote
~/traefik-config/dynamic/ # Config Traefik
~/smart-app-city/ # Sous-projet app mobile
```
---
## 📊 Pipeline de Données
```
Simulateur (60 capteurs)
┌───────────────────────────────────────┐
│ 3 Brokers MQTT │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ EMQX │ │Mosquitto│ │ BunkerM │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
└───────┼───────────┼───────────┼───────┘
└───────────┴───────────┘
Telegraf (3 inputs MQTT)
InfluxDB v2 (bucket: smartcity)
Grafana (Dashboard v7)
```
---
*Inventaire généré le 2026-05-27 01:00 — OWL*

View File

@@ -0,0 +1,65 @@
# État des lieux - Localisation des capteurs sur les maps OpenRemote
## Problème initial
Les capteurs du simulateur n'apparaissent pas sur les maps OpenRemote (realm master et smart city martinique).
## Découvertes
### 1. Deux sets d'assets en BDD
- **Anciens assets** (avec suffixe `(traffic)`, `(airquality)`, etc.) : ont `agentLink` MQTT + `location` GeoJSON → ce sont les bons
- **Nouveaux assets** (sans suffixe, créés par le simulateur via REST) : sans `agentLink`, sans `location`
### 2. Format de la location
L'attribut `location` dans OpenRemote utilise le format GeoJSON Point :
```json
{"type": "GeoJSONPoint", "value": {"type": "Point", "coordinates": [lat, lon]}}
```
### 3. Compteur SENSORS global
Le compteur utilisé pour générer les clés SENSORS est **global** (pas par type) :
- traffic: 0-9, airquality: 10-19, parking: 20-29, noise: 30-39, weather: 40-49, light: 50-59
### 4. API REST refuse les PUT sur assets avec agentLink
L'API REST d'OpenRemote refuse les mises à jour (HTTP 403) sur les assets qui ont un `agentLink` actif. C'est une protection pour éviter les conflits avec l'agent MQTT.
### 5. Connexion MQTT au broker Artemis
Le broker Artemis d'OpenRemote nécessite un **"Service user"** avec username/password pour l'authentification MQTT (rc=5 = Not Authorized sans credentials). La documentation mentionne ce mécanisme mais ne détaille pas comment créer le service user.
### 6. Topics MQTT pour l'API interne
La documentation indique que les topics pour publier des valeurs d'attributs sont :
- `{realm}/{clientId}/writeattributevalue/{attributeName}/{assetId}` - Payload: JSON de la valeur
- `{realm}/{clientId}/writeattribute/{attributeName}/{assetId}` - Payload: `{"value": <VALUE>, "timestamp": <TIMESTAMP>}`
Le format `smartcity/{type}/{id}` utilisé par le simulateur est pour les agents MQTT externes, pas pour l'API MQTT interne.
## Corrections appliquées au simulateur
1. **ASSET_MAP mis à jour** avec les bons asset IDs (ceux avec agentLink + location)
2. **Location ajoutée dans le payload REST** (GeoJSONPoint)
3. **Topics MQTT corrigés** (index basé sur position du capteur, pas compteur global)
4. **REST désactivé** pour les assets avec agentLink (403)
5. **Connexion MQTT anonyme** au broker Artemis (rc=5 persistant)
## Problèmes restants
### Connexion MQTT au broker Artemis
Le broker refuse les connexions anonymes (rc=5). Il faut un "Service user" dont la création n'est pas documentée. Solutions possibles :
1. Créer un service user via l'UI OpenRemote (Manager UI → Users)
2. Modifier la configuration Artemis pour accepter les connexions anonymes
3. Utiliser un broker MQTT externe (EMQX) et configurer un agent MQTT dans OpenRemote
### Topics MQTT
Le simulateur publie sur `smartcity/{type}/{index}` mais l'API MQTT d'OpenRemote attend `{realm}/{clientId}/writeattributevalue/{attributeName}/{assetId}`. Il faut soit :
1. Changer le format des topics dans le simulateur
2. Configurer un agent MQTT dans OpenRemote qui écoute sur `smartcity/#`
### Déconnexion cyclique
Le broker Artemis déconnecte le client MQTT du simulateur de manière cyclique. Cause possible : keepalive trop court ou configuration du broker.
## Prochaines étapes recommandées
1. **Créer un service user** dans OpenRemote pour l'authentification MQTT
2. **Configurer un agent MQTT** dans OpenRemote qui écoute sur `smartcity/#` et mappe les topics vers les attributs des assets
3. **Corriger le format des topics** dans le simulateur pour utiliser le format de l'API MQTT d'OpenRemote
4. **Tester la connexion MQTT** avec les bons credentials
5. **Vérifier la localisation** sur les maps OpenRemote une fois que les agents MQTT reçoivent les données

View File

@@ -0,0 +1,31 @@
# Smart City Monitoring Report - 2026-05-22
**Timestamp:** 2026-05-22 00:50:30
## Summary
⚠️ **9 issue(s) detected** - Critical systems are partially unavailable
## Container Status
| Container | Status |
|-----------|--------|
| openremote_manager_1 | 🛑 DOWN |
| openremote_keycloak_1 | 🛑 DOWN |
| stellio-api-gateway | 🛑 DOWN |
| smart-city-prometheus-brokers | 🛑 DOWN |
## Endpoint Status
| Service | URL | Status |
|---------|-----|--------|
| OpenRemote | https://openremote.digitribe.fr | 🌐 DOWN (HTTP 502) |
| Orion-LD | http://fiware-gis-quickstart-orion-1:1026/version | 🌐 DOWN (HTTP 000) |
| Stellio | https://stellio.digitribe.fr | 🌐 DOWN (HTTP 502) |
| FROST | http://frost_http-web-1:8080/FROST-Server/core/v1.0/info | 🌐 DOWN (HTTP 000) |
## Network
- 🔌 Network issue: Traefik → OpenRemote
## Recommendations
1. Restart critical containers: `docker-compose up -d`
2. Check Traefik logs for routing issues
3. Verify network connectivity between services
4. Review container health checks

131
RAPPORT_FINAL_2026-05-06.md Normal file
View File

@@ -0,0 +1,131 @@
# SMART CITY DIGITAL TWIN - RAPPORT FINAL 2026-05-06
## ✅ RÉALISÉ AUJOURD'HUI
### Infrastructure de Base
1. **BunkerM (mosquitto2.digitribe.fr:1900)**
- Accessible via Traefik (confirmé par `nc -zv` et `mosquitto_pub`)
- IoT Agent BunkerM reconfiguré pour Stellio (NGSI-LD)
2. **Stellio Pipeline**
- Données visibles dans Stellio (`urn:ngsi-ld:AirQualityObserved:*`)
- IoT Agent → Stellio → QuantumLeap → CrateDB fonctionnel
- Tables CrateDB créées : 6 tables (etairqualityobserved, etweatherobserved, ettrafficflowobserved, etparkingspot, etnoiselevelobserved, etwaterqualityobserved)
3. **Grafana Dashboards**
- Dashboard "Smart City - Air Quality (CrateDB)" opérationnel
- Dashboards Traffic Flow et Weather créés (structure de base)
- Datasources CrateDB configurées (2 datasources)
4. **Git/Gitea**
- Commits poussés sur `origin/master`
- Fichiers modifiés : `docker-compose.iot-agent.yml`
- Session resume `session_resume_2026-05-06.md` créé et poussé
5. **Keycloak OpenRemote**
- Audience mapper 'openremote' configuré
- JWT avec claim `aud: ['openremote', 'smartcity-realm', 'master-realm', 'account']`
### Monitoring
- Prometheus : 0 alertes actives ✅
- Tous les conteneurs principaux sont Up (sauf problèmes ci-dessous)
---
## ❌ PROBLÈMES EN COURS
### 1. **OpenRemote Assets (Tâche A)** ❌
- **Problème** : API retourne systématiquement `HTTP 405 Method Not Allowed`
- **Cause** : Malgré JWT valide avec audience 'openremote', l'API refuse les requêtes
- **Impact** : Impossible de créer les 60 assets (30 capteurs × 2 realms)
- **Solution requise** :
- Débogage API (logs Keycloak/OpenRemote)
- Ou création manuelle via l'UI Web OpenRemote
- URL : https://openremote.digitribe.fr/manager/#/assets
### 2. **Services Unhealthy** ⚠️
| Service | Status | Problème identifié |
|---------|--------|-------------------|
| smart-city-cratedb-stellio | unhealthy | Erreurs auth CrateDB (user "postgres" from 85.11.167.232) |
| smart-city-mosquitto | unhealthy | À investiguer |
| smart-city-iot-mongodb | unhealthy | MongoDB healthcheck échoue |
| smart-city-pulsar-distribution | unhealthy | À investiguer |
| bunkerm_bunkerm_1 | unhealthy | Mosquitto interne |
**Action** : Vérifier les logs et corriger les configurations d'authentification
### 3. **ThingsBoard** ❌
- **Status** : En boucle de redémarrage (`Restarting (1)`)
- **Cause** : Volumes manquants (`/home/eric/smart-city-digital-twin-martinique/thingsboard-data/` créé mais vide)
- **Config** : `thingsboard.conf` manquant
- **Action requise** : Configurer proprement ThingsBoard ou l'exclure temporairement
### 4. **Simulateur - 30 Capteurs** 🔄
- **Status** : Configuration par défaut (10 capteurs au total)
- **Action** : Définir `SENSOR_COUNT=30` dans l'environnement du simulateur
- **Répartition attendue** : 10 EMQX + 10 Mosquitto + 10 BunkerM
---
## 📋 ACTIONS NÉCESSAIRES (PRIORITAIRES)
### Immédiat
1. **OpenRemote Assets** :
- Se connecter à https://openremote.digitribe.fr/manager/#/assets
- Créer manuellement 60 assets (30 capteurs dans realm `master` + 30 dans `smartcity-martinique`)
- Types : AirQualityObserved, TrafficFlowObserved, OffStreetParking, NoiseLevelObserved, WeatherObserved, LightObserved
2. **Stabiliser services unhealthy** :
```bash
# Investiguer CrateDB Stellio
docker logs smart-city-cratedb-stellio --tail 50
# Investiguer Mosquitto
docker logs smart-city-mosquitto --tail 50
```
3. **Configurer simulateur 30 capteurs** :
```bash
cd ~/smart-city-digital-twin-martinique
SENSOR_COUNT=30 docker compose -f docker-compose.yml up -d simulator
```
### Ultérieur
4. **ThingsBoard** : Configurer proprement ou documenter comme "hors-service"
5. **Compléter dashboards Grafana** : Parking, Noise, Light
6. **Mettre à jour documentation architecture** (après vérification complète Stellio)
---
## 🎯 ARCHITECTURE VALIDÉE (partiellement)
```
Simulateur (30 capteurs attendus)
Brokers MQTT (EMQX ✅, Mosquitto ✅, BunkerM ✅)
IoT Agents (EMQX→Orion-LD ✅, Mosquitto→Orion-LD ✅, BunkerM→Stellio ✅)
Context Brokers (Orion-LD ✅, Stellio ✅)
QuantumLeap → CrateDB (tables: 6 créées ✅)
Grafana (Dashboards: AirQuality ✅, Traffic ✅, Weather ✅)
OpenRemote (Assets: ❌ à créer manuellement)
```
---
## 📝 NOTES POUR LA PROCHAINE SESSION
1. **OpenRemote** : API 405 à déboguer ou création manuelle via UI
2. **ThingsBoard** : Décision requise (réparer ou désactiver)
3. **30 capteurs** : `SENSOR_COUNT=30` à configurer
4. **Services unhealthy** : Investiguer et stabiliser
5. **Documentation** : Mettre à jour diagrammes après stabilisation complète
---
*Rapport généré le 2026-05-06 à 22:00 - Eric FELIXINE*
*Session en cours - "Continue" actif*

View File

@@ -0,0 +1,83 @@
# Résumé Final - Smart City Digital Twin (06 Mai 2026 - 19h30)
## ✅ Ce qui fonctionne
1. **MQTT Brokers** : EMQX (11883), Mosquitto (1883), BunkerM (1900) - OK
2. **IoT-Agents** : Reçoivent les données MQTT et mettent à jour Orion-LD - OK
3. **Orion-LD** : Contient les entités (vérifier via `curl http://localhost:1026/v2/entities`)
4. **CrateDB** : Fonctionne parfaitement (INSERT manuel OK)
5. **Grafana** : Datasources CrateDB configurées (IDs 23, 24)
6. **Redis** : Installé et accessible par QuantumLeap
## ❌ Problème bloquant : QuantumLeap → CrateDB
**Symptômes :**
- QuantumLeap reçoit les notifications (`/v2/notify` → "Notification successfully processed")
- Aucune donnée insérée dans CrateDB (`quantumleap.etairqualityobserved`)
- La queue Redis reste vide (`rq:queue:default` n'existe pas)
- `WQ_OFFLOAD_WORK=True` est activé mais les tâches ne sont pas ajoutées à la queue
**Investigation :**
- `offload_to_work_queue()` retourne `True`
- `redis_connection()` utilise `REDIS_HOST=smart-city-redis` et `REDIS_PORT=6379`
- Worker RQ lancé et connecté ✅
- Mais `InsertAction.enqueue()` n'ajoute rien à la queue Redis
**Hypothèses :**
1. `InsertAction` n'est pas picklable (échec silencieux de `q.enqueue()`)
2. Problème de connexion Redis dans `enqueue()`
3. La méthode `trans.insert()` échoue silencieusement
4. Bug dans le module `wq` de QuantumLeap
## 🛠️ Solution temporaire (pour Grafana)
Des données de test ont été insérées manuellement dans CrateDB :
```sql
INSERT INTO quantumleap.etairqualityobserved (entity_id, time_index, no2, temperature, humidity) VALUES
('urn:ngsi-ld:AirQualityObserved:sensor001', 1778112000000, 45.5, 28.0, 85.0),
...
```
**Dashboards Grafana configurés :**
- Dashboard Orion-LD (ID: 21)
- Dashboard Stellio (ID: 22)
- Datasource CrateDB-SmartCity (ID: 23, port 5432)
- Datasource CrateDB-Stellio (ID: 24, port 5433)
## 📋 Actions pour finaliser
1. **Stellio Pipeline** :
- Corriger `docker-compose.quantumleap-stellio.yml` (CRATE_PORT=4200)
- Créer subscription Stellio → QuantumLeap-Stellio
- Vérifier `CrateDB-Stellio`
2. **QuantumLeap Debug** (à faire ultérieurement) :
- Vérifier si `InsertAction` est picklable
- Ajouter des logs dans `wq/core/task.py` (`enqueue()`)
- Tester `trans.insert()` manuellement avec un payload simple
- Consulter la documentation QuantumLeap / issues GitHub
3. **Simulateur** :
- `simulator.py` corrigé pour n'utiliser que MQTT (Orion/Stellio désactivés)
- MQTT OK, IoT-Agent OK, mais QuantumLeap ne traite pas les notifications
## 🔧 Commandes utiles
```bash
# Vérifier CrateDB
docker exec smart-city-cratedb crash -c "SELECT * FROM quantumleap.etairqualityobserved LIMIT 10;"
# Vérifier Redis
docker exec smart-city-redis redis-cli keys "*"
# Voir les logs QuantumLeap
docker logs smart-city-quantumleap --tail 100
# Tester notification manuelle
curl -s -X POST http://localhost:8668/v2/notify -H 'Content-Type: application/json' \
-d '{"subscriptionId": "test", "data": [{...}]}'
```
## 📊 Fichiers modifiés
- `docker-compose.quantumleap.yml` : +Redis, +healthcheck CrateDB, +variables environnement
- `simulator.py` : Orion-LD et Stellio désactivés (MQTT uniquement)
- `RESUME_FINAL_2026-05-06.md` : Ce fichier
---
**Prochaine étape** : Configurer la pipeline Stellio et finaliser les dashboards Grafana avec les données de test.
Le problème QuantumLeap nécessite une investigation plus poussée du code source (`wq` module).

View File

@@ -0,0 +1,39 @@
# Session State - 2026-05-13
## Actions complétées
### Nettoyage infrastructure
- Supprimé anciens conteneurs TTS (the-things-stack)
- Supprimé anciens conteneurs Chirpstack (smart-city-digital-twin-martinique-chirpstack-*)
- Supprimé conteneurs exited/excess (mosquitto-exporter, microcks, bpp-*, frost-*, etc.)
- BunkerM recréé depuis /home/eric/BunkerM/ (bunkerm-bunkerm-1)
### BunkerM + Traefik
- BunkerM ajouté au réseau traefik-public
- Config Traefik mise à jour : 3 fichiers mosquitto2 → bunkerm-bunkerm-1 (au lieu de bunkerm_bunkerm_1)
- mosquitto2.digitribe.fr → 502 (BunkerM unhealthy mais accessible en HTTP 307)
### AgentLink MQTT → EMQX (abandonné → approche REST)
- 25 assets avec agentLink reconfigurés de Artemis vers EMQX en BDD
- Problème : les agents MQTT d'OpenRemote ne se connectent pas à EMQX (même après redémarrage)
- Solutionretenue : désactiver agentLink + utiliser REST pour mises à jour
- **agentLink supprimé sur les 25 assets** (master: 12, smartcity: 13)
- **REST OpenRemote activé** dans simulateur.py (was commented)
- Location déjà incluse dans le payload REST (GeoJSONPoint format)
### ChirpStack (en cours)
- Nouveau ChirpStack docker-compose dans /home/eric/smart-city-digital-twin-martinique/chirpstack/
- Services running: chirpstack-1, postgres-1, redis-1, mosquitto-1
- Pas de gateway-bridge (fichier config manquant)
- Pas de rest-api
- Migrations SQL non appliquées (base vide)
## Problèmes identifiés
1. **Simulateur crash** après redémarrage (incompatibilité paho-mqtt callback API v1)
2. **BunkerM unhealthy** (healthcheck /api/auth/me échoue)
3. **ChirpStack incomplet** (pas de gateway, pas de REST API)
## Prochaines étapes
- [ ] Fixer le crash du simulateur (callback MQTT)
- [ ] Valider pipeline MQTT complète
- [ ] Documenter l'infrastructure validée

121
TODO.md Normal file
View File

@@ -0,0 +1,121 @@
# Smart City Digital Twin — TODO List
> Dernière mise à jour : 2026-06-04 02:00 (finalisation)
## ✅ Complété (session 2026-06-03 / 06-04)
| ID | Tâche | Détail |
|----|-------|--------|
| airflow-deploy | Apache Airflow déployé | `airflow.digitribe.fr` — Python 3.11, LocalExecutor |
| openfn-cleanup | OpenFN supprimé | Race condition Cachex/Ecto non résolue |
| ditto-cleanup | Stack Ditto supprimée | API v2 non fonctionnelle (schema-versions) |
| openremote-cleanup | Stack OpenRemote supprimée | Patches bundle appliqués |
| gravitino-cleanup | Gravitino supprimé | Unhealthy |
| fiware-gis-cleanup | FIWARE GIS Quickstart supprimé | |
| contexus-cleanup | Contexus supprimé | Unhealthy |
| kafka-cleanup | Kafka supprimé | Unhealthy + sera redeployé via Helm |
| flink-cleanup | Flink supprimé | Dépendances kafka |
| bi-cleanup | Superset + Metabase supprimés | Seront redeployés via Helm |
| mindsdb-cleanup | MindsDB supprimé | Autoheal unhealthy |
| odk-cleanup | ODK Central supprimé | Sera redeployé via Helm |
| jupyterhub-cleanup | JupyterHub supprimé | Sera redeployé via Helm |
| zeppelin-cleanup | Zeppelin supprimé | Sera redeployé via Helm |
| gis-cleanup | MapStore + GeoServer + FROST supprimés | Seront redeployés via Helm |
| iot-cleanup | Node-RED + phpIPAM + EMQX + Mosquitto + BunkerM + ChirpStack supprimés | Seront redeployés via Helm |
| monitoring-cleanup | Grafana + Loki + Prometheus + InfluxDB + Telegraf supprimés | Seront redeployés via Helm |
| storage-cleanup | MinIO + PostgreSQL + PostGIS + Redis + Zookeeper supprimés | Seront redeployés via Helm |
| misc-cleanup | AgentGateway + Esperotech + Redpanda Console + Docker exporter + Simulator supprimés | |
| backups | Sauvegardes config | Fichiers sauvegardés dans /home/eric/backups/2026-06-04/ |
| helms-ansible | Fichiers Helm/Ansibles générés | 25+ rôles dans helms/ |
| helms-readme | README déploiement K8s | Architecture, installation, troubleshooting |
| helms-vault | Template vault.yml | Variables chiffrées pour le déploiement |
| git-push | Push sur Gitea | 2 commits pushés (TODO + helms) |
## 🔴 En cours
| ID | Tâche | Raison | Prochaine action |
|----|-------|--------|------------------|
| (aucune) | — | — | — |
## ⏳ En attente (déploiement Kubernetes via Ansible)
| ID | Tâche |
|----|-------|
| k8s-cluster | Créer le cluster Kubernetes (3 nœuds minimum) |
| nfs-server | Configurer le serveur NFS pour le storage |
| traefik-deploy | Déployer Traefik via Helm |
| cert-manager-deploy | Déployer cert-manager pour TLS |
| storage-deploy | Déployer NFS provisioner + StorageClass |
| monitoring-deploy | Déployer Prometheus + Grafana + Loki |
| databases-deploy | Déployer PostgreSQL HA + Redis + MinIO |
| kafka-deploy | Déployer Kafka (Strimzi) |
| flink-deploy | Déployer Apache Flink |
| airflow-deploy | Déployer Apache Airflow |
| iot-deploy | Déployer EMQX + Mosquitto + Node-RED + phpIPAM |
| gitea-deploy | Déployer Gitea |
| jupyterhub-deploy | Déployer JupyterHub |
| bi-deploy | Déployer Superset + Metabase |
| mindsdb-deploy | Déployer MindsDB |
| odk-deploy | Déployer ODK Central |
| gis-deploy | Déployer MapStore + GeoServer + FROST |
| clickhouse-deploy | Déployer ClickHouse (`clickhouse.digitribe.fr`) |
| starrocks-deploy | Déployer StarRocks (`starrocks.digitribe.fr`) |
| trino-deploy | Déployer Trino (`trino.digitribe.fr`) |
| deltalake-deploy | Déployer Delta Lake (`deltalake.digitribe.fr`) |
| streamlit-deploy | Déployer Streamlit (`streamlit.digitribe.fr`) |
| duckdb-deploy | Déployer DuckDB (`duckdb.digitribe.fr`) |
| smartapp-deploy | Déployer Smart App (`smartapp.digitribe.fr`) |
| backup-deploy | Déployer Velero pour les sauvegardes |
## 📁 Fichiers Helm / Ansible générés
Le répertoire `helms/` (dans le repo Gitea) contient les fichiers pour un déploiement modulaire sur Kubernetes via Ansible.
### Structure
```
helms/
├── README.md # Documentation déploiement
├── deploy.yml # Playbook principal
├── undeploy.yml # Playbook de suppression
├── inventory/hosts.yml # Inventory des nœuds K8s
├── group_vars/all.yml # Variables globales
├── group_vars/vault.yml # Variables chiffrées (template)
└── roles/ # 25+ rôles Ansible
```
### Utilisation
```bash
cd helms/
ansible-playbook deploy.yml --ask-vault-pass
ansible-playbook deploy.yml --tags clickhouse --ask-vault-pass
ansible-playbook undeploy.yml
```
## 📝 Infrastructure actuelle (10 containers Docker)
| Service | Image | Statut |
|---------|-------|--------|
| airflow-scheduler | apache/airflow:2.9.3-python3.11 | ✅ healthy |
| airflow-webserver | apache/airflow:2.9.3-python3.11 | ✅ healthy |
| airflow-init | apache/airflow:2.9.3-python3.11 | 🔄 restarting (one-shot) |
| airflow-postgres | postgres:16 | ✅ healthy |
| smartapp-api | smartapp-api:latest | ✅ Up 38h |
| smartapp-web | nginx:alpine | ✅ Up 38h |
| gitea-runner | gitea/act_runner:latest | ✅ Up 2 days |
| traefik | traefik:v3.1 | ✅ Up 2 days |
| smart-city-kepler | smart-city-kepler:latest | ✅ Up 2 weeks |
| gitea | gitea/gitea:latest | ✅ Up 2 jours |
## 📊 Statistiques
- **Containers Docker** : 10 (down from 72)
- **Stacks supprimées** : 6 (OpenFN, Ditto, OpenRemote, Gravitino, FIWARE GIS, Contexus)
- **Services unhealthy** : 0
- **Fichiers Helm/Ansible** : 33 fichiers
- **Rôles Ansible** : 25+
- **Namespaces K8s prévus** : 18
## Credentials
- **Gitea** : eric / (voir config)
- **Airflow** : admin / (changé par Eric)

144
architecture-multi-cb.md Normal file
View File

@@ -0,0 +1,144 @@
1|# Architecture Smart City Digital Twin - Martinique (État au 07 Mai 2026)
2|
3|## Architecture Validée : Simulateur → MQTT → IoT Agents → Context Brokers → Time-Series
4|
5|```
6|┌─────────────────────────────────────────────────────────────────────────────┐
7|│ Smart City Simulator (Python) │
8|│ Publie sur 3 brokers MQTT avec format IoT-Agent JSON (30 capteurs) │
9|└──────────┬────────────────────┬──────────────────────┬───────────────────┘
10| │ │ │
11| ▼ ▼ ▼
12|┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
13|│ EMQX Broker │ │ Mosquitto Broker │ │ BunkerM Broker │
14|│ (emqx_emqx_1)│ │ (smart-city- │ │ (mqtt.digitribe.│
15|│ :1883 (host │ │ mosquitto) │ │ fr:1900) │
16|│ 11883) │ │ :1883 (host │ │ ⏳ À tester │
17|│ ✅ Fonctionnel │ │ 1883) │ │ mosquitto2 │
18|│ │ │ ✅ Fonctionnel │ │ │
19|└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
20| │ │ │
21| ▼ ▼ ▼
22|┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
23|│ IoT-Agent-EMQX │ │IoT-Agent-Mosquitto│ │IoT-Agent-BunkerM │
24|│ (smart-city- │ │ (smart-city- │ │ (smart-city- │
25|│ iot-agent-emqx)│ │ iot-agent- │ │ iot-agent- │
26|│ :4041 │ │ mosquitto) │ │ bunkerm) │
27|│ Apikey: smart- │ │ :4042 │ │ :4043 │
28|│ city-api-key │ │ Apikey: smart- │ │ Apikey: bunker-│
29|│ │ │ city-api-key │ │ m-api-key │
30|└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
31| └───────────────────────┴──────────────────────┘
32| │
33| ▼
34| ┌───────────────┴───────────────┐
35| │ │
36| ▼ ▼
37|┌─────────────────────┐ ┌─────────────────────┐
38|│ Orion-LD │ │ Stellio │
39|│ (smart-city- │ │ (stellio-api- │
40|│ orion-ld) │ │ gateway) │
41|│ Port: 1026 │ │ Port: 8080 │
42|│ ✅ Fonctionnel │ │ ✅ Fonctionnel │
43|│ MongoDB backend │ │ Kafka backend │
44|└─────────┬───────────┘ └─────────┬───────────┘
45| │ │
46| ▼ ▼
47|┌─────────────────────┐ ┌─────────────────────┐
48|│ QuantumLeap-Orion │ │ QuantumLeap-Stellio │
49|│ (smart-city- │ │ (smart-city- │
50|│ quantumleap) │ │ quantumleap) │
51|│ Port: 8668 │ │ Port: 8668? │
52|│ ✅ Fonctionnel │ │ (à vérifier) │
53|│ → CrateDB-Orion │ │ → CrateDB-Stellio │
54|└─────────┬───────────┘ └─────────┬───────────┘
55| │ │
56| ▼ ▼
57|┌─────────────────────┐ ┌─────────────────────┐
58|│ CrateDB-Orion │ │ CrateDB-Stellio │
59|│ (smart-city- │ │ (smart-city- │
60|│ cratedb) │ │ cratedb) │
61|│ Port: 5432/4200 │ │ Port: 5432/4200 │
62|│ DB: quantumleap │ │ DB: quantumleap │
63|│ ✅ Données présentes│ │ (à vérifier) │
64|└─────────┬───────────┘ └─────────┬───────────┘
65| │ │
66| └───────────────────────┬─────────┘
67| │
68| ▼
69| ┌─────────────────────┐
70| │ Grafana │
71| │ (digital-twin- │
72| │ grafana) │
73| │ Port: 3000 │
74| │ https:// │
75| │ grafana.digitribe│
76| │ .fr │
77| │ 2 Datasources: │
78| │ - CrateDB-Orion │
79| │ - CrateDB-Stellio│
80| └─────────────────────┘
81|```
82|
83|## Flux de Données Validés (07 Mai 2026)
84|
85|### Pipeline 1 : Orion-LD ✅ (Opérationnel Complet)
86|1. **Simulateur** → Publie sur EMQX & Mosquitto (topics `smartcity-api-key/{sid}/attrs`) ✅
87|2. **IoT Agents** (EMQX & Mosquitto) → Transfèrent vers Orion-LD ✅
88|3. **Orion-LD** (1026) → Reçoit les entités (`urn:ngsi-ld:AirQualityObserved:sensor001`...) ✅
89|4. **QuantumLeap-Orion** (8668) → Reçoit notifications Orion-LD ✅
90|5. **CrateDB-Orion** (5432) → Persistance dans `quantumleap.etairqualityobserved` ✅
91| - Données vérifiées : `sensor005` (NO2=72.1, temp=31.5°C, humidité=92%)
92|6. **Grafana** ← Visualise données CrateDB ✅
93|
94|### Pipeline 2 : Stellio 🔄 (À finaliser)
95|1. **Simulateur** → Publie sur EMQX & Mosquitto ✅
96|2. **IoT Agents** → Transfèrent vers Stellio (à configurer) ⏳
97|3. **Stellio** (8080) → Reçoit les entités NGSI-LD ✅
98|4. **QuantumLeap-Stellio** → Reçoit notifications Stellio (à configurer) ⏳
99|5. **CrateDB-Stellio** → Persistance (même CrateDB?) ⏳
100|6. **Grafana** ← Visualise données ⏳
101|
102|## États des Services (07 Mai 2026 - 21h30)
103|
104|| Service | Container | Ports | Statut | Notes |
105||---------|-----------|--------|--------|-------|
106|| **Simulateur** | `smart-city-simulator` | - | ✅ **UP** | Publie sur 2/3 brokers (EMQX ✅, Mosquitto ✅, BunkerM ❌) |
107|| **EMQX Broker** | `emqx_emqx_1` | 1883 (host 11883) | ✅ **UP** | Messages reçus, topics `smartcity-api-key/#` ✅ |
108|| **Mosquitto Broker** | `smart-city-mosquitto` | 1883 (host 1883) | ✅ **UP** | Messages reçus ✅ |
109|| **BunkerM Broker** | `smart-city-bunkerm` | 1900 | ❌ **Inaccessible** | `mosquitto2.digitribe.fr:1900` ne répond pas au simulateur |
110|| **IoT Agent EMQX** | `smart-city-iot-agent-emqx` | 4041 | ✅ **UP** | Reçoit et transfère vers Orion-LD/Stellio ✅ |
111|| **IoT Agent Mosquitto** | `smart-city-iot-agent-mosquitto` | 4042 | ✅ **UP** | Idem ✅ |
112|| **IoT Agent BunkerM** | `smart-city-iot-agent-bunkerm` | 4043 | ⚠️ **UP** | Mais pas de messages (BunkerM inaccessible) |
113|| **Orion-LD** | `smart-city-orion-ld` | 1026 | ✅ **UP** | Entités créées, subscriptions actives ✅ |
114|| **Stellio** | `stellio-api-gateway` | 8080 | ✅ **UP** | Prêt pour intégration IoT Agent ⏳ |
115|| **QuantumLeap** | `smart-city-quantumleap` | 8668 | ✅ **UP** | Notifications traitées ✅ |
116|| **CrateDB** | `smart-city-cratedb` | 5432/4200 | ✅ **UP** | Table `quantumleap.etairqualityobserved` ✅ |
117|| **Grafana** | `digital-twin-grafana` | 3000 | ✅ **UP** | https://grafana.digitribe.fr ✅ |
118|| **OpenRemote** | `openremote-manager-1` | 8080/8443 | ✅ **UP** | Pas encore connecté aux brokers MQTT ⏳ |
119|| **ThingsBoard** | `smart-city-thingsboard` | 8080 | ❌ **CRASH** | Manque `/config/thingsboard.conf` |
120|
121|## Prochaines Étapes
122|
123|1. ✅ **Corriger le simulateur** pour publier sur les topics IoT Agent (FAIT)
124|2. ⏳ **Configurer Stellio** pour recevoir les données des IoT Agents
125|3. ⏳ **Finaliser QuantumLeap-Stellio** (subscriptions)
126|4. ⏳ **Connecter OpenRemote** aux brokers MQTT (via MQTT Client assets)
127|5. ⏳ **Réparer BunkerM** (rendre accessible mosquitto2lateur)
128|6. ⏳ **Finaliser Grafana** (dashboards avec données CrateDB)
129|7. ❌ **Réparer ThingsBoard** (créer fichier configuration)
130|
131|## Notes Importantes
132|
133|- **Architecture Validée** : Le flux Simulateur → Brokers MQTT → IoT Agents → Orion-LD → QuantumLeap → CrateDB est **opérationnel**.
134|- **Format des données** : IoT Agent utilise `smartcity-api-key/{sid}/attrs` (EMQX), `smartcity-api-key-mosquitto/{sid}/attrs` (Mosquitto), `bunkerm-api-key/{sid}/attrs` (BunkerM).
135|- **Credentials** :
136| - Orion-LD : `http://smart-city-orion-ld:1026`
137| - QuantumLeap : `http://smart-city-quantumleap:8668`
138| - CrateDB : user `crate`, pas de mot de passe, port 5432
139| - Grafana : admin / Digitribe972
140| - OpenRemote : admin / Digitribe972
141|- **Gitea** : https://gitea.digitribe.fr/eric/smart-city-digital-twin-martinique
142|
143|---
144|*Dernière mise à jour : 07 Mai 2026, 21h30 - Architecture validée Orion-LD pipeline complet*

52
bemserver/Dockerfile Normal file
View File

@@ -0,0 +1,52 @@
# BEMServer - Building Energy Management Server
# Multi-component Dockerfile: core + api + ui + celery
# Based on Python 3.11 slim with TimescaleDB support
FROM python:3.11-slim AS base
# System dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
libffi-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/bemserver
# ---- Stage 1: Install bemserver-core ----
FROM base AS core
COPY bemserver/bemserver-core /tmp/bemserver-core
RUN pip install --no-cache-dir /tmp/bemserver-core
# ---- Stage 2: Install bemserver-api ----
FROM core AS api
COPY bemserver/bemserver-api /tmp/bemserver-api
RUN pip install --no-cache-dir /tmp/bemserver-api
# ---- Stage 3: Install bemserver-ui ----
FROM api AS ui
COPY bemserver/bemserver-ui /tmp/bemserver-ui
RUN pip install --no-cache-dir /tmp/bemserver-ui
# ---- Final stage ----
FROM ui AS final
# Create non-root user
RUN groupadd -r bemserver && useradd -r -g bemserver -d /opt/bemserver -s /sbin/nologin bemserver
# Create config directory
RUN mkdir -p /opt/bemserver/config /opt/bemserver/data \
&& chown -R bemserver:bemserver /opt/bemserver
# Copy entrypoint script
COPY bemserver/entrypoint.sh /opt/bemserver/entrypoint.sh
RUN chmod +x /opt/bemserver/entrypoint.sh
# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --retries=5 --start_period=60s \
CMD curl -f http://localhost:5000/healthz || exit 1
USER bemserver
ENTRYPOINT ["/opt/bemserver/entrypoint.sh"]

44
bemserver/entrypoint.sh Normal file
View File

@@ -0,0 +1,44 @@
#!/bin/bash
# BEMServer entrypoint - runs the specified component
set -e
COMPONENT=${BEMSERVER_COMPONENT:-api}
CONFIG_DIR="/opt/bemserver/config"
case "$COMPONENT" in
api)
echo "Starting BEMServer API on port 5000..."
exec flask --app bemserver_api.app create --config "${CONFIG_DIR}/api-settings.py"
;;
ui)
echo "Starting BEMServer UI on port 5001..."
exec flask --app bemserver_ui.app create --config "${CONFIG_DIR}/ui-settings.cfg"
;;
celery-worker)
echo "Starting BEMServer Celery worker..."
export BEMSERVER_CORE_SETTINGS_FILE="${CONFIG_DIR}/core-settings.py"
exec celery -A bemserver_core.celery_worker worker --loglevel=info
;;
celery-beat)
echo "Starting BEMServer Celery beat..."
export BEMSERVER_CORE_SETTINGS_FILE="${CONFIG_DIR}/core-settings.py"
exec celery -A bemserver_core.celery_worker beat --loglevel=info
;;
init-db)
echo "Initializing BEMServer database..."
export BEMSERVER_CORE_SETTINGS_FILE="${CONFIG_DIR}/core-settings.py"
bemserver_db_upgrade
echo "Database initialized."
;;
create-admin)
echo "Creating admin user..."
export BEMSERVER_CORE_SETTINGS_FILE="${CONFIG_DIR}/core-settings.py"
bemserver_create_user --name "${BEMSERVER_ADMIN_USER:-admin}" --email "${BEMSERVER_ADMIN_EMAIL:-admin@digitribe.fr}" --admin
echo "Admin user created."
;;
*)
echo "Unknown component: $COMPONENT"
echo "Valid components: api, ui, celery-worker, celery-beat, init-db, create-admin"
exit 1
;;
esac

1
chirpstack Submodule

Submodule chirpstack added at a617344d52

View File

@@ -0,0 +1,21 @@
# Basic Station configuration for WebSocket gateway connections
[general]
log_level=4
[integration.mqtt]
server="tcp://mosquitto:1883"
event_topic="eu868/gateway/{{ .GatewayID }}/event/{{ .EventType }}"
state_topic="eu868/gateway/{{ .GatewayID }}/state/{{ .StateType }}"
command_topic="eu868/gateway/{{ .GatewayID }}/command/#"
json=true
[backend]
type="basic_station"
[backend.basic_station]
bind=":3001"
tls_cert=""
tls_key=""
ca_cert=""
region="EU868"
frequency_min=863000000
frequency_max=870000000

View File

@@ -0,0 +1,11 @@
# ChirpStack Gateway Bridge configuration (EU868)
[general]
log_level=4
[integration.mqtt]
server="tcp://mosquitto:1883"
event_topic="eu868/gateway/{{ .GatewayID }}/event/{{ .EventType }}"
state_topic="eu868/gateway/{{ .GatewayID }}/state/{{ .StateType }}"
command_topic="eu868/gateway/{{ .GatewayID }}/command/#"
json=true
client_id="chirpstack-gateway-bridge"

View File

@@ -0,0 +1,43 @@
FROM chirpstack/chirpstack:4 as base
FROM alpine:3.23.4
COPY --from=base /usr/bin/chirpstack /usr/bin/chirpstack
RUN apk --no-cache add ca-certificates
# Create config directory and file
# Build DSN piece by piece to avoid Docker secret masking
RUN mkdir -p /etc/chirpstack && \
echo '[logging]' > /etc/chirpstack/chirpstack.toml && \
echo ' level="info"' >> /etc/chirpstack/chirpstack.toml && \
echo '' >> /etc/chirpstack/chirpstack.toml && \
echo '[postgresql]' >> /etc/chirpstack/chirpstack.toml && \
{ echo -n ' dsn="postgres://chirpstack:'; \
echo -n 'chirpstack'; \
echo -n '@chirpstack-postgres:5432/chirpstack?sslmode=disable"'; \
echo; } >> /etc/chirpstack/chirpstack.toml && \
echo ' max_open_connections=10' >> /etc/chirpstack/chirpstack.toml && \
echo ' min_idle_connections=0' >> /etc/chirpstack/chirpstack.toml && \
echo '' >> /etc/chirpstack/chirpstack.toml && \
echo '[redis]' >> /etc/chirpstack/chirpstack.toml && \
echo ' servers=["redis://chirpstack-redis:6379/"]' >> /etc/chirpstack/chirpstack.toml && \
echo ' tls_enabled=false' >> /etc/chirpstack/chirpstack.toml && \
echo ' cluster=false' >> /etc/chirpstack/chirpstack.toml && \
echo '' >> /etc/chirpstack/chirpstack.toml && \
echo '[network]' >> /etc/chirpstack/chirpstack.toml && \
echo ' net_id="000000"' >> /etc/chirpstack/chirpstack.toml && \
echo ' enabled_regions=["eu868"]' >> /etc/chirpstack/chirpstack.toml && \
echo '' >> /etc/chirpstack/chirpstack.toml && \
echo '[api]' >> /etc/chirpstack/chirpstack.toml && \
echo ' bind="0.0.0.0:8080"' >> /etc/chirpstack/chirpstack.toml && \
echo ' secret="you-must-replace-this"' >> /etc/chirpstack/chirpstack.toml && \
echo '' >> /etc/chirpstack/chirpstack.toml && \
echo '[integration]' >> /etc/chirpstack/chirpstack.toml && \
echo ' enabled=["mqtt"]' >> /etc/chirpstack/chirpstack.toml && \
echo ' [integration.mqtt]' >> /etc/chirpstack/chirpstack.toml && \
echo ' server="tcp://mosquitto:1883/"' >> /etc/chirpstack/chirpstack.toml && \
echo ' json=true' >> /etc/chirpstack/chirpstack.toml
USER nobody:nogroup
ENTRYPOINT ["/usr/bin/chirpstack"]

View File

@@ -0,0 +1,26 @@
[logging]
level="info"
[postgresql]
dsn="postgres://chirpstack:chirpstack@postgres/chirpstack?sslmode=disable"
max_open_connections=10
min_idle_connections=0
[redis]
servers=["redis://redis:6379/"]
tls_enabled=false
cluster=false
[network]
net_id="000000"
enabled_regions=["eu868"]
[api]
bind="0.0.0.0:8080"
secret="you-must-replace-this"
[integration]
enabled=["mqtt"]
[integration.mqtt]
server="tcp://mosquitto:1883/"
json=true

View File

@@ -0,0 +1,6 @@
#!/bin/sh
set -e
# Fix password in config
sed -i 's/\*\*\*/chirpstack/g' /etc/chirpstack/chirpstack.toml
# Start ChirpStack
exec /usr/bin/chirpstack -c /etc/chirpstack

View File

@@ -0,0 +1,4 @@
#!/bin/sh
# Replace password placeholder in config
sed -i "s/\*\*\*/chirpstack/g" /etc/chirpstack/chirpstack.toml
exec /usr/bin/chirpstack -c /etc/chirpstack

View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -e
sed -i 's/\*\*\*/chirpstack/g' /etc/chirpstack/chirpstack.toml
exec /usr/bin/chirpstack -c /etc/chirpstack

View File

@@ -0,0 +1,6 @@
FROM eclipse/ditto-gateway:latest
USER root
# Copy the modified JAR (with open auth in reference.conf)
COPY ditto-gateway-service-3.8.12-allinone.jar /opt/ditto/ditto-gateway-service-3.8.12-allinone.jar
USER ditto

View File

@@ -0,0 +1,22 @@
# Minimal override - authentication settings only
# This file is loaded after reference.conf by Typesafe config
ditto {
gateway {
authentication {
pre-authentication {
enabled = true
}
devops {
secured = false
devops-authentication-method = "basic"
password = "ditto-devops-secret"
password = ${?DEVOPS_PASSWORD}
status-secured = false
status-authentication-method = "basic"
statusPassword = "ditto-status-secret"
statusPassword = ${?STATUS_PASSWORD}
}
}
}
}

View File

@@ -0,0 +1,7 @@
#!/bin/sh
# Fix permissions for mounted config files
if [ -f /opt/ditto/gateway-extension.conf ]; then
chmod 644 /opt/ditto/gateway-extension.conf
fi
# Start the gateway
exec java -jar /opt/ditto/ditto-gateway-service-3.8.12-allinone.jar

View File

@@ -0,0 +1,2 @@
#!/bin/sh
exec java -Dconfig.file=/opt/ditto/gateway.conf -jar /opt/ditto/ditto-gateway-service-3.8.12-allinone.jar

View File

@@ -0,0 +1,76 @@
#!/bin/bash
set -e
WORKDIR=/tmp/ditto-jar-mod
rm -rf $WORKDIR && mkdir -p $WORKDIR
echo "=== Extracting JAR ==="
cd $WORKDIR
docker run --rm eclipse/ditto-gateway:latest cat /opt/ditto/ditto-gateway-service-3.8.12-allinone.jar > ditto-gateway-service-3.8.12-allinone.jar
jar xf ditto-gateway-service-3.8.12-allinone.jar reference.conf
echo "=== Original reference.conf tail ==="
tail -5 reference.conf
echo "=== Adding auth config to reference.conf ==="
# Remove the last closing brace, add our config, re-add the closing brace
# The reference.conf ends with nested braces - find the very last line
python3 << 'PYEOF'
with open("/tmp/ditto-jar-mod/reference.conf", "r") as f:
lines = f.readlines()
# Find the last non-empty line that is just a closing brace
# We need to insert our config before the outermost closing brace
# Simple approach: append before the very last }
# Count total closing braces at the end
content = "".join(lines)
# The reference.conf has a complex nested structure
# We'll add our ditto config as a new root-level block at the end
# We need to close the last block and add a comma, then our new block
# Actually, simpler: just append if the file ends with }
# Find the position of the very last }
last_brace_pos = content.rfind('}')
if last_brace_pos >= 0:
# Check if there's content after the last } (like kamon.conf include)
rest = content[last_brace_pos+1:].strip()
if rest:
# There's content after the last }, our approach is wrong
print(f"Content after last }}: {rest[:100]}")
# Insert before the last } with a comma
new_content = content[:last_brace_pos].rstrip()
# Remove trailing comma if present
if new_content.endswith(','):
new_content = new_content[:-1]
new_content += ',\n\n# Custom auth overrides\n' + rest[:0] + '\n' + ' authentication {\n pre-authentication {\n enabled = true\n }\n devops {\n secured = false\n }\n }\n}\n'
# Just append
new_content = content.rstrip() + '\n\n# Custom auth overrides\nditto {\n gateway {\n authentication {\n pre-authentication {\n enabled = true\n }\n devops {\n secured = false\n }\n }\n }\n}\n'
else:
# Last char is }, replace it with our config + }
new_content = content[:last_brace_pos].rstrip()
# Remove trailing comma
if new_content.endswith(','):
new_content = new_content[:-1]
new_content += ',\n\n# Custom auth overrides\n gateway {\n authentication {\n pre-authentication {\n enabled = true\n }\n devops {\n secured = false\n }\n }\n }\n}\n'
else:
new_content = content
with open("/tmp/ditto-jar-mod/reference.conf", "w") as f:
f.write(new_content)
print("reference.conf modified successfully")
PYEOF
echo "=== Modified reference.conf tail ==="
tail -20 reference.conf
echo "=== Updating JAR (replacing reference.conf) ==="
jar uf ditto-gateway-service-3.8.12-allinone.jar reference.conf
echo "=== Verifying modified reference.conf in JAR ==="
jar xf ditto-gateway-service-3.8.12-allinone.jar reference.conf
grep -c "pre-authentication" reference.conf || echo "NOT FOUND in JAR"
echo "=== Done. JAR is ready at $WORKDIR ==="

View File

@@ -0,0 +1,7 @@
ditto {
gateway {
http {
enablecors = true
}
}
}

View File

@@ -0,0 +1,363 @@
ditto {
version = "3.8.12"
extensions {
jwt-authorization-subjects-provider = {
extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DittoJwtAuthorizationSubjectsProvider
}
jwt-authentication-result-provider = {
extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DefaultJwtAuthenticationResultProvider
extension-config = {
role = regular
jwt-authorization-subjects-provider = {
extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DittoJwtAuthorizationSubjectsProvider
extension-config = {
role = regular
}
}
}
}
jwt-authentication-result-provider-devops = {
extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DefaultJwtAuthenticationResultProvider
extension-config = {
role = devops
jwt-authorization-subjects-provider = {
extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DittoJwtAuthorizationSubjectsProvider
extension-config = {
role = devops
}
}
}
}
signal-enrichment-provider {
extension-class = org.eclipse.ditto.gateway.service.endpoints.utils.DefaultGatewaySignalEnrichmentProvider
extension-config = {
cache {
enabled = true
maximum-size = 20000
expire-after-create = 2m
}
}
}
http-bind-flow-provider = org.eclipse.ditto.gateway.service.endpoints.routes.LoggingHttpBindFlowProvider
websocket-config-provider = org.eclipse.ditto.gateway.service.endpoints.routes.websocket.NoOpWebSocketConfigProvider
gateway-authentication-directive-factory = org.eclipse.ditto.gateway.service.endpoints.directives.auth.DittoGatewayAuthenticationDirectiveFactory
http-request-actor-props-factory = org.eclipse.ditto.gateway.service.endpoints.actors.DefaultHttpRequestActorPropsFactory
sse-event-sniffer = org.eclipse.ditto.gateway.service.endpoints.routes.sse.NoOpSseEventSniffer
streaming-authorization-enforcer = org.eclipse.ditto.gateway.service.streaming.NoOpAuthorizationEnforcer
incoming-websocket-event-sniffer = org.eclipse.ditto.gateway.service.endpoints.routes.websocket.NoOpIncomingWebSocketEventSniffer
outgoing-websocket-event-sniffer = org.eclipse.ditto.gateway.service.endpoints.routes.websocket.NoOpOutgoingWebSocketEventSniffer
custom-api-routes-provider = org.eclipse.ditto.gateway.service.endpoints.routes.NoopCustomApiRoutesProvider
sse-connection-supervisor = org.eclipse.ditto.gateway.service.endpoints.routes.sse.NoOpSseConnectionSupervisor
websocket-connection-supervisor = "org.eclipse.ditto.gateway.service.endpoints.routes.websocket.NoOpWebSocketSupervisor"
connections-retrieval-actor-props-factory = org.eclipse.ditto.gateway.service.endpoints.actors.DefaultConnectionsRetrievalActorPropsFactory
}
service-name = "gateway"
mapping-strategy.implementation = "org.eclipse.ditto.gateway.service.util.GatewayMappingStrategies"
gateway {
http {
hostname = ""
hostname = ${?HOSTNAME}
hostname = ${?BIND_HOSTNAME}
port = 8080
port = ${?HTTP_PORT}
port = ${?PORT}
coordinated-shutdown-timeout = 65s
coordinated-shutdown-timeout = ${?COORDINATED_SHUTDOWN_REQUEST_TIMEOUT}
schema-versions = [2]
protocol-headers = ["X-Forwarded-Proto", "x_forwarded_proto"]
forcehttps = false
forcehttps = ${?FORCE_HTTPS}
redirect-to-https = false
redirect-to-https = ${?REDIRECT_TO_HTTPS}
redirect-to-https-blocklist-pattern = "/api.*|/ws.*|/status.*|/overall.*"
enablecors = false
enablecors = ${?ENABLE_CORS}
request-timeout = 60s
request-timeout = ${?REQUEST_TIMEOUT}
additional-accepted-media-types = ${?ADDITIONAL_ACCEPTED_MEDIA_TYPES}
query-params-as-headers = [
"accept"
"channel"
"correlation-id"
"requested-acks"
"declared-acks"
"response-required"
"timeout"
"live-channel-timeout-strategy"
"allow-policy-lockout"
"condition"
"live-channel-condition"
"at-historical-revision"
"at-historical-timestamp"
"dry-run"
]
}
streaming {
session-counter-scrape-interval = 30s
parallelism = 64
parallelism = ${?GATEWAY_STREAMING_PARALLELISM}
search-idle-timeout = 60s
search-idle-timeout = ${?GATEWAY_STREAMING_SEARCH_IDLE_TIMEOUT}
subscription-refresh-delay = 5m
subscription-refresh-delay = ${?GATEWAY_STREAMING_SUBSCRIPTION_REFRESH_DELAY}
acknowledgement {
forwarder-fallback-timeout = 65s
}
websocket {
subscriber {
backpressure-queue-size = 100
}
publisher {
backpressure-buffer-size = 200
}
throttling-rejection-factor = 1.25
throttling {
enabled = false
}
streaming-authorization-enforcer = "org.eclipse.ditto.gateway.service.streaming.NoOpAuthorizationEnforcer"
}
sse {
throttling {
enabled = false
}
streaming-authorization-enforcer = "org.eclipse.ditto.gateway.service.streaming.NoOpAuthorizationEnforcer"
}
}
command {
default-timeout = ${ditto.gateway.http.request-timeout}
max-timeout = 1m
smart-channel-buffer = 10s
connections-retrieve-limit = 100
}
message {
default-timeout = 10s
max-timeout = 1m
}
claim-message {
default-timeout = 1m
max-timeout = 10m
}
dns {
address = none
address = ${?DNS_SERVER}
}
authentication {
http {
proxy {
enabled = false
enabled = ${?AUTH_HTTP_PROXY_ENABLED}
hostname = ${?AUTH_HTTP_PROXY_HOST}
port = ${?AUTH_HTTP_PROXY_PORT}
username = ${?AUTH_HTTP_PROXY_USERNAME}
password = ${?AUTH_HTTP_PROXY_PASSWORD}
}
}
oauth {
protocol = "https"
protocol = ${?OAUTH_PROTOCOL}
allowed-clock-skew = 10s
allowed-clock-skew = ${?OAUTH_ALLOWED_CLOCK_SKEW}
openid-connect-issuers = {
google = {
issuer = "accounts.google.com"
}
}
token-integration-subject = "integration:{{policy-entry:label}}:{{jwt:aud}}"
token-integration-subject = ${?OAUTH_TOKEN_INTEGRATION_SUBJECT}
}
# PRE-AUTHENTICATION = open access for /api/2/
pre-authentication {
enabled = true
}
devops {
secured = false
devops-authentication-method = "basic"
password = "ditto-devops-secret"
password = ${?DEVOPS_PASSWORD}
status-secured = false
status-authentication-method = "basic"
statusPassword = "ditto-status-secret"
statusPassword = ${?STATUS_PASSWORD}
}
}
health-check {
enabled = true
enabled = ${?HEALTH_CHECK_ENABLED}
interval = 60s
interval = ${?HEALTH_CHECK_INTERVAL}
service.timeout = 10s
service.timeout = ${?HEALTH_CHECK_SERVICE_TIMEOUT}
cluster-roles = {
enabled = true
enabled = ${?HEALTH_CHECK_ROLES_ENABLED}
expected = [
"policies"
"things"
"search"
"gateway"
"connectivity"
]
}
}
public-health {
cache-timeout = 20s
cache-timeout = ${?GATEWAY_STATUS_HEALTH_EXTERNAL_TIMEOUT}
}
cloud-events {
empty-schema-allowed = true
data-types = [
"application/json"
"application/vnd.eclipse.ditto+json"
]
}
cache {
publickeys {
maxentries = 32
expiry = 60m
maximum-size = ${ditto.gateway.cache.publickeys.maxentries}
expire-after-write = ${ditto.gateway.cache.publickeys.expiry}
}
}
statistics {
ask-timeout = 5s
ask-timeout = ${?STATISTICS_UPDATE_INTERVAL}
update-interval = 15s
update-interval = ${?STATISTICS_UPDATE_INTERVAL}
details-expire-after = 3s
details-expire-after = ${?STATISTICS_DETAILS_EXPIRE_AFTER}
shards = [
{
region = "thing"
role = "things"
root = "/user/thingsRoot"
}
{
region = "policy"
role = "policies"
root = "/user/policiesRoot"
}
{
region = "search-wildcard-updater"
role = "search"
root = "/user/thingsWildcardSearchRoot/searchUpdaterRoot"
}
]
}
}
tracing {
filter = {
includes = ["**"]
excludes = ["GET /ws/2"]
}
}
}
secrets {
devops_password {
name = "devops_password"
name = ${?DEVOPS_PASSWORD_NAME}
}
status_password {
name = "status_password"
name = ${?STATUS_PASSWORD_NAME}
}
}
pekko.http.client {
user-agent-header = eclipse-ditto/${ditto.version}
}
pekko {
actor {
default-dispatcher {
executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator"
}
deployment {
/gatewayRoot/proxy {
router = round-robin-pool
resizer {
lower-bound = 5
upper-bound = 100
messages-per-resize = 50
}
}
}
}
cluster {
sharding {
role = ${ditto.service-name}
passivation {
strategy = "off"
}
}
roles = ["gateway"]
}
coordinated-shutdown {
phases {
service-requests-done {
timeout = 70s
}
}
}
http {
server {
server-header = ""
request-timeout = ${ditto.gateway.http.request-timeout}
idle-timeout = 610s
max-connections = 4096
raw-request-uri-header = on
parsing {
max-uri-length = 8k
max-content-length = 1m
uri-parsing-mode = relaxed
}
websocket {
periodic-keep-alive-mode = ping
periodic-keep-alive-max-idle = 30s
}
termination-deadline-exceeded-response {
status = 502
}
}
host-connection-pool {
max-open-requests = 1024
idle-timeout = 60s
}
}
management.health-checks.readiness-checks {
gateway-http-readiness = "org.eclipse.ditto.gateway.service.health.GatewayHttpReadinessCheck"
}
management.health-checks.liveness-checks {
subsystem-health = "org.eclipse.ditto.internal.utils.health.SubsystemHealthCheck"
}
}
authentication-dispatcher {
type = Dispatcher
executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
thread-pool-executor {
core-pool-size-min = 4
core-pool-size-factor = 2.0
core-pool-size-max = 8
}
throughput = 100
}
signal-enrichment-cache-dispatcher {
type = Dispatcher
executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
}

View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
Modify the reference.conf inside the Ditto gateway JAR.
Inserts ditto { gateway { authentication { pre-authentication { enabled = true } } } }
at the root level, before the final closing brace.
"""
import zipfile
import shutil
import os
import subprocess
import sys
JAR_PATH = "/tmp/ditto-jar-mod/ditto-gateway-service-3.8.12-allinone.jar"
AUTH_BLOCK = """
### Custom Ditto auth override - pre-authentication enabled
ditto {
gateway {
authentication {
pre-authentication {
enabled = true
}
devops {
secured = false
devops-authentication-method = "basic"
password = "ditto-devops-secret"
status-secured = false
status-authentication-method = "basic"
statusPassword = "ditto-status-secret"
}
}
}
}
"""
def main():
os.makedirs("/tmp/ditto-jar-mod", exist_ok=True)
print("=== Step 1: Extracting JAR ===")
result = subprocess.run(
["docker", "run", "--rm", "eclipse/ditto-gateway:latest",
"cat", "/opt/ditto/ditto-gateway-service-3.8.12-allinone.jar"],
capture_output=True, check=True
)
with open(JAR_PATH, "wb") as f:
f.write(result.stdout)
print(f"JAR: {len(result.stdout)} bytes")
print("=== Step 2: Modifying reference.conf ===")
with zipfile.ZipFile(JAR_PATH, 'r') as zin:
ref_conf = zin.read("reference.conf").decode("utf-8")
lines = ref_conf.split('\n')
total_lines = len(lines)
print(f"reference.conf: {total_lines} lines")
# Find the LAST closing brace at root level (depth 0)
# Track depth through the entire file
depth = 0
last_root_close_idx = None
for i, line in enumerate(lines):
stripped = line.strip()
if not stripped or stripped.startswith('#'):
continue
# Count braces (simple approach - count { and })
# This isn't perfect for HOCON but works for our case
for ch in stripped:
if ch == '{':
depth += 1
elif ch == '}':
depth -= 1
if depth == 0:
last_root_close_idx = i
if last_root_close_idx is None:
print("ERROR: Could not find root-level closing brace!")
sys.exit(1)
insert_idx = last_root_close_idx
print(f"Last root-level '}}' at line {insert_idx + 1}: '{lines[insert_idx].strip()}'")
# Check if we need a comma before our block
# Look at the non-empty line before insert_idx
prev_idx = insert_idx - 1
while prev_idx >= 0 and lines[prev_idx].strip() == '':
prev_idx -= 1
if prev_idx >= 0:
prev_stripped = lines[prev_idx].strip()
if prev_stripped.endswith('}'):
# Need to add a comma
lines[prev_idx] = lines[prev_idx].rstrip()
if not lines[prev_idx].endswith(','):
lines[prev_idx] += ','
print(f"Added comma to line {prev_idx + 1}")
# Insert our block
auth_lines = AUTH_BLOCK.split('\n')
new_lines = lines[:insert_idx] + auth_lines + lines[insert_idx:]
modified_conf = '\n'.join(new_lines)
print(f"Modified: {total_lines} -> {len(new_lines)} lines")
# Verify brace balance
depth = 0
for i, line in enumerate(new_lines):
stripped = line.strip()
if not stripped or stripped.startswith('#'):
continue
for ch in stripped:
if ch == '{':
depth += 1
elif ch == '}':
depth -= 1
if depth < 0:
print(f"ERROR: Negative depth at line {i+1}")
sys.exit(1)
print(f"Brace depth check: {depth} (should be 0)")
if depth != 0:
print("ERROR: Unbalanced braces!")
sys.exit(1)
# Verify ditto is at root level
for i, line in enumerate(new_lines):
if line.strip() == 'ditto {':
indent = len(line) - len(line.lstrip())
print(f"'ditto {{' at line {i+1}, indent: {indent}")
if indent != 0:
print(f"WARNING: Expected indent 0, got {indent}")
break
print("=== Step 3: Creating unsigned JAR ===")
skip_files = set()
with zipfile.ZipFile(JAR_PATH, 'r') as zin:
for name in zin.namelist():
if name.startswith("META-INF/"):
upper = name.upper()
if upper.endswith(".SF") or upper.endswith(".RSA") or upper.endswith(".DSA") or upper == "MANIFEST.MF":
skip_files.add(name)
with zipfile.ZipFile(JAR_PATH + ".new", 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
if item.filename in skip_files:
continue
data = zin.read(item.filename)
if item.filename == "reference.conf":
data = modified_conf.encode("utf-8")
info = zipfile.ZipInfo(filename=item.filename, date_time=item.date_time)
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = item.external_attr
zout.writestr(info, data)
shutil.move(JAR_PATH + ".new", JAR_PATH)
print("=== Step 4: Verifying ===")
with zipfile.ZipFile(JAR_PATH, 'r') as z:
ref = z.read("reference.conf").decode("utf-8")
assert "pre-authentication" in ref
sig = [n for n in z.namelist() if n.startswith("META-INF/") and n.upper().endswith(('.SF', '.RSA', '.DSA'))]
print(f"OK Signature files: {len(sig)}")
print(f"OK JAR size: {os.path.getsize(JAR_PATH)}")
print("\n=== DONE ===")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,70 @@
#!/bin/bash
# Minimal JWT/OAuth2 server for Ditto
# Serves a JWKS endpoint and validates tokens signed with DITTO_JWT_SECRET
cat > /tmp/oauth2-server.js << 'EOF'
const http = require('http');
const crypto = require('crypto');
const SECRET = process.env.DITTO_JWT_SECRET || 'my-ditto-jwt-secret-key-12345';
const PORT = 3000;
function base64url(buf) {
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
// Generate a token
function generateToken(sub, scope) {
const header = { alg: 'RS256', typ: 'JWT', kid: 'ditto-local' };
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: 'http://localhost:' + PORT,
sub: sub,
aud: 'ditto:cognito',
iat: now,
exp: now + 3600,
scope: scope
};
const h = base64url(Buffer.from(JSON.stringify(header)));
const p = base64url(Buffer.from(JSON.stringify(payload)));
const sig = base64url(
crypto.createHmac('sha256', SECRET).update(h + '.' + p).digest()
);
return h + '.' + p + '.' + sig;
}
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'application/json');
if (req.url === '/.well-known/openid-configuration') {
res.end(JSON.stringify({
issuer: 'http://localhost:' + PORT,
jwks_uri: 'http://localhost:' + PORT + '/.well-known/jwks.json',
token_endpoint: 'http://localhost:' + PORT + '/token'
}));
} else if (req.url === '/.well-known/jwks.json') {
// Extract public key from secret (for HS256 we just return the secret as k)
const jwk = {
kty: 'oct',
kid: 'ditto-local',
use: 'sig',
alg: 'HS256',
k: base64url(Buffer.from(SECRET))
};
res.end(JSON.stringify({ keys: [jwk] }));
} else if (req.url === '/token') {
const token = generateToken('ditto', 'READ_WRITE');
res.end(JSON.stringify({ access_token: token, token_type: 'Bearer', expires_in: 3600 }));
} else {
res.statusCode = 404;
res.end('{}');
}
});
server.listen(PORT, '0.0.0.0', () => {
console.log('OAuth2 server listening on port ' + PORT);
console.log('Token: ' + generateToken('ditto', scope='READ_WRITE'));
});
EOF
node /tmp/oauth2-server.js

View File

@@ -0,0 +1,14 @@
ditto {
gateway {
authentication {
pre-authentication {
enabled = true
}
devops {
secured = false
devops-authentication-method = "basic"
password = "ditto-devops-secret"
}
}
}
}

View File

@@ -0,0 +1,16 @@
listener 1883
allow_anonymous true
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log
# Bridge to EMQX for upstream integration
connection bridge-emqx
address emqx_emqx_1:1883
topic eu868/# out 1
topic application/# in 1
bridge_protocol_version mqttv311
cleansession true
try_private false
notifications false
remote_clientid chirpstack-bridge

View File

@@ -0,0 +1,2 @@
-- Initialize ChirpStack database
CREATE DATABASE IF NOT EXISTS chirpstack;

View File

@@ -0,0 +1,44 @@
is:
database:
uri: postgres://root:root@tts-postgres:5432/ttn_lorawan?sslmode=disable
email:
sender-name: "The Things Stack"
sender-address: "noreply@digitribe.fr"
network:
name: "Smart City LoRaWAN"
console-url: "https://tts.digitribe.fr/console"
identity-server-url: "https://tts.digitribe.fr/oauth"
redis:
address: tts-redis:6379
metrics:
enabled: true
console:
base-url: "https://tts.digitribe.fr/console"
http:
cookie:
block-key: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
hash-key: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
gateway-server:
mqtt:
listen: ":1883"
public-address: "tts.digitribe.fr:1883"
network-server:
net-id: "000000"
band:
name: "EU868"
join-server:
default:
join-eui-prefix: "0000000000000000"
tenant-id: "smart-city"
blob:
local-directory: /srv/ttn-lorawan/public/blob
base-url: "https://tts.digitribe.fr/blob"

67
create_dashboard.py Normal file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
import json
dashboard = {
"annotations": {"list": []},
"editable": True,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": None,
"links": [],
"panels": [
{
"title": "Air Quality (PM2.5)",
"type": "timeseries",
"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")'}],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}
},
{
"title": "Traffic Flow (Vehicles)",
"type": "timeseries",
"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")'}],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}
},
{
"title": "Parking Occupancy (%)",
"type": "timeseries",
"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")'}],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 8}
},
{
"title": "Noise Levels (dB)",
"type": "timeseries",
"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")'}],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 8}
},
{
"title": "Weather (Temperature °C)",
"type": "timeseries",
"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")'}],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 16}
},
{
"title": "Light Levels",
"type": "timeseries",
"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")'}],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 16}
}
],
"schemaVersion": 36,
"style": "dark",
"tags": ["smartcity", "martinique", "iot"],
"templating": {"list": []},
"time": {"from": "now-1h", "to": "now"},
"title": "Smart City Digital Twin - Martinique",
"uid": "smartcity-martinique-v2",
"version": 1
}
with open('/home/eric/smart-city-digital-twin-martinique/grafana-dashboard-smartcity.json', 'w') as f:
json.dump(dashboard, f, indent=2)
print("Dashboard JSON created successfully")

View File

@@ -0,0 +1,257 @@
#!/usr/bin/env python3
import json
import requests
# UID de la datasource correcte
DS_UID = "dd1bfc24-de9d-4c23-8a3c-151d153f8169"
dashboard = {
"annotations": {"list": []},
"editable": True,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": None,
"links": [],
"panels": [
# ===== AIR QUALITY =====
{
"title": "Air Quality - PM2.5 (µg/m³)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
"datasource": {"type": "influxdb", "uid": DS_UID},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "airquality")\n |> filter(fn: (r) => r["_field"] == "pm25_ugm3")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "PM2.5")',
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "µg/m³",
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": None},
{"color": "yellow", "value": 25},
{"color": "orange", "value": 50},
{"color": "red", "value": 100}
]
}
}
}
},
{
"title": "Air Quality - CO (mg/m³)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
"datasource": {"type": "influxdb", "uid": DS_UID},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "airquality")\n |> filter(fn: (r) => r["_field"] == "co_mgm3")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "CO")',
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "mg/m³",
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": None},
{"color": "yellow", "value": 5},
{"color": "red", "value": 15}
]
}
}
}
},
# ===== TRAFFIC =====
{
"title": "Traffic - Average Speed (km/h)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 16},
"datasource": {"type": "influxdb", "uid": DS_UID},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "traffic")\n |> filter(fn: (r) => r["_field"] == "average_speed_kmh")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "Speed")',
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "km/h",
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "red", "value": None},
{"color": "yellow", "value": 20},
{"color": "green", "value": 40}
]
}
}
}
},
{
"title": "Traffic - Congestion Level",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 16},
"datasource": {"type": "influxdb", "uid": DS_UID},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "traffic")\n |> filter(fn: (r) => r["_field"] == "congestion_level")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "Congestion")',
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "",
"min": 0,
"max": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": None},
{"color": "yellow", "value": 0.5},
{"color": "red", "value": 0.8}
]
}
}
}
},
# ===== PARKING =====
{
"title": "Parking - Available Spots",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 32},
"datasource": {"type": "influxdb", "uid": DS_UID},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "parking")\n |> filter(fn: (r) => r["_field"] == "available_spots")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "Available")',
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "spots"
}
}
},
{
"title": "Parking - Occupancy (%)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 32},
"datasource": {"type": "influxdb", "uid": DS_UID},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "parking")\n |> filter(fn: (r) => r["_field"] == "occupancy_percent")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "Occupancy")',
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100
}
}
},
# ===== NOISE =====
{
"title": "Noise Level (dB)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 48},
"datasource": {"type": "influxdb", "uid": DS_UID},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "noise")\n |> filter(fn: (r) => r["_field"] == "noise_level_db")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "Noise")',
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "dB",
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": None},
{"color": "yellow", "value": 65},
{"color": "orange", "value": 80},
{"color": "red", "value": 95}
]
}
}
}
},
# ===== WEATHER =====
{
"title": "Weather - Temperature (°C)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 48},
"datasource": {"type": "influxdb", "uid": DS_UID},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "weather")\n |> filter(fn: (r) => r["_field"] == "temperature_celsius")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "Temperature")',
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "°C"
}
}
},
# ===== LIGHT =====
{
"title": "Light - Brightness (lux)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 64},
"datasource": {"type": "influxdb", "uid": DS_UID},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "light")\n |> filter(fn: (r) => r["_field"] == "brightness_lux")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "Brightness")',
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "lux"
}
}
},
{
"title": "Light - Power Consumption (W)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 64},
"datasource": {"type": "influxdb", "uid": DS_UID},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "light")\n |> filter(fn: (r) => r["_field"] == "power_consumption_w")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "Power")',
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "W"
}
}
}
],
"schemaVersion": 38,
"style": "dark",
"tags": ["smart-city", "martinique", "iot", "complete"],
"templating": {"list": []},
"time": {"from": "now-1h", "to": "now"},
"title": "Smart City Digital Twin - Martinique (COMPLET)",
"uid": "smartcity-martinique-complete",
"version": 1
}
# Sauvegarder localement
with open('/home/eric/smart-city-digital-twin-martinique/grafana-dashboard-complete.json', 'w') as f:
json.dump(dashboard, f, indent=2)
print("✅ Dashboard complet généré")
print(f" Fichier: grafana-dashboard-complete.json")
print(f" UID: {dashboard['uid']}")
print(f" Panneaux: {len(dashboard['panels'])}")
print(f" Datasource: {DS_UID}")

140
create_dashboard_docker.py Normal file
View File

@@ -0,0 +1,140 @@
#!/usr/bin/env python3
import json
import requests
# UID de la datasource Prometheus (smart-city-prometheus-brokers)
# À récupérer via l'API Grafana
try:
resp = requests.get('https://grafana.digitribe.fr/api/datasources',
auth=('admin', 'Digitribe972'), verify=False)
ds_uid = None
if resp.ok:
for ds in resp.json():
if 'prometheus' in ds['type'].lower() and 'broker' in ds['name'].lower():
ds_uid = ds['uid']
print(f"Datasource Prometheus trouvée: {ds['name']} (UID: {ds_uid})")
break
except:
pass
if not ds_uid:
ds_uid = 'f9ddd651-33ec-4dad-a950-e1375a964315' # Fallback Prometheus Brokers
print(f"Utilisation UID par défaut: {ds_uid}")
dashboard = {
"annotations": {"list": []},
"editable": True,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": None,
"links": [],
"panels": [
# CPU Usage
{
"title": "CPU Usage (Docker Containers)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
"datasource": {"type": "prometheus", "uid": ds_uid},
"targets": [
{
"expr": 'rate(docker_container_cpu_usage_seconds_total[5m]) * 100',
"legendFormat": "{{container}}",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent"
}
}
},
# Memory Usage
{
"title": "Memory Usage (Docker Containers)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
"datasource": {"type": "prometheus", "uid": ds_uid},
"targets": [
{
"expr": 'docker_container_memory_usage_bytes / 1024 / 1024 / 1024',
"legendFormat": "{{container}} (GB)",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "decbytes"
}
}
},
# Network Traffic (RX)
{
"title": "Network Receive (Docker Containers)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 16},
"datasource": {"type": "prometheus", "uid": ds_uid},
"targets": [
{
"expr": 'rate(docker_container_network_receive_bytes_total[5m]) * 8',
"legendFormat": "{{container}} RX",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "bps"
}
}
},
# Network Traffic (TX)
{
"title": "Network Transmit (Docker Containers)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 16},
"datasource": {"type": "prometheus", "uid": ds_uid},
"targets": [
{
"expr": 'rate(docker_container_network_transmit_bytes_total[5m]) * 8',
"legendFormat": "{{container}} TX",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "bps"
}
}
},
# Container Status
{
"title": "Container Status (1=Running, 0=Stopped)",
"type": "state-timeline",
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 32},
"datasource": {"type": "prometheus", "uid": ds_uid},
"targets": [
{
"expr": 'docker_container_status',
"legendFormat": "{{container}}",
"refId": "A"
}
]
}
],
"schemaVersion": 38,
"style": "dark",
"tags": ["docker", "containers", "metrics", "prometheus"],
"templating": {"list": []},
"time": {"from": "now-1h", "to": "now"},
"title": "Smart City - Docker Containers Metrics",
"uid": "smartcity-docker-metrics",
"version": 1
}
# Sauvegarder localement
with open('/home/eric/smart-city-digital-twin-martinique/grafana-dashboard-docker-metrics.json', 'w') as f:
json.dump(dashboard, f, indent=2)
print("✅ Dashboard Docker Metrics généré")
print(f" Fichier: grafana-dashboard-docker-metrics.json")
print(f" UID: {dashboard['uid']}")
print(f" Datasource Prometheus: {ds_uid}")

177
create_dashboard_fixed.py Normal file
View File

@@ -0,0 +1,177 @@
#!/usr/bin/env python3
import json
# UID de la source InfluxDB (à récupérer via l'API Grafana)
# On va utiliser l'UID par défaut ou le récupérer
import requests
import os
# Récupérer l'UID de la datasource InfluxDB
try:
resp = requests.get('http://grafana.digitribe.fr/api/datasources', auth=('admin', 'Digitribe972'))
ds_uid = None
if resp.ok:
for ds in resp.json():
if 'influx' in ds['type'].lower():
ds_uid = ds['uid']
print(f"Datasource InfluxDB trouvée: {ds['name']} (UID: {ds_uid})")
break
except:
pass
if not ds_uid:
ds_uid = 'influxdb' # Fallback
print(f"Utilisation UID par défaut: {ds_uid}")
dashboard = {
"annotations": {"list": []},
"editable": True,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": None,
"links": [],
"panels": [
# Air Quality Panel
{
"title": "Air Quality - PM2.5",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "airquality")\n |> filter(fn: (r) => r["_field"] == "pm25_ugm3")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")',
"refId": "A"
}
],
"datasource": {"type": "influxdb", "uid": ds_uid},
},
{
"title": "Air Quality - CO",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "airquality")\n |> filter(fn: (r) => r["_field"] == "co_mgm3")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")',
"refId": "A"
}
],
"datasource": {"type": "influxdb", "uid": ds_uid},
},
# Traffic Panel
{
"title": "Traffic - Average Speed",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 8},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "traffic")\n |> filter(fn: (r) => r["_field"] == "average_speed_kmh")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")',
"refId": "A"
}
],
"datasource": {"type": "influxdb", "uid": ds_uid},
},
{
"title": "Traffic - Congestion",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 8},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "traffic")\n |> filter(fn: (r) => r["_field"] == "congestion_level")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")',
"refId": "A"
}
],
"datasource": {"type": "influxdb", "uid": ds_uid},
},
# Parking Panel
{
"title": "Parking - Available Spots",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 16},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "parking")\n |> filter(fn: (r) => r["_field"] == "available_spots")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")',
"refId": "A"
}
],
"datasource": {"type": "influxdb", "uid": ds_uid},
},
{
"title": "Parking - Occupancy %",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 16},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "parking")\n |> filter(fn: (r) => r["_field"] == "occupancy_percent")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")',
"refId": "A"
}
],
"datasource": {"type": "influxdb", "uid": ds_uid},
},
# Noise Panel
{
"title": "Noise Level (dB)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 24},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "noise")\n |> filter(fn: (r) => r["_field"] == "noise_level_db")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")',
"refId": "A"
}
],
"datasource": {"type": "influxdb", "uid": ds_uid},
},
# Weather Panel
{
"title": "Weather - Temperature",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 24},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "weather")\n |> filter(fn: (r) => r["_field"] == "temperature_celsius")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")',
"refId": "A"
}
],
"datasource": {"type": "influxdb", "uid": ds_uid},
},
# Light Panel
{
"title": "Light - Brightness",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 32},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "light")\n |> filter(fn: (r) => r["_field"] == "brightness_lux")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")',
"refId": "A"
}
],
"datasource": {"type": "influxdb", "uid": ds_uid},
},
{
"title": "Light - Power Consumption",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 32},
"targets": [
{
"query": 'from(bucket:"smartcity")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r["_measurement"] == "light")\n |> filter(fn: (r) => r["_field"] == "power_consumption_w")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: "mean")',
"refId": "A"
}
],
"datasource": {"type": "influxdb", "uid": ds_uid},
}
],
"schemaVersion": 38,
"style": "dark",
"tags": ["smart-city", "martinique", "iot"],
"templating": {"list": []},
"time": {"from": "now-1h", "to": "now"},
"title": "Smart City Digital Twin - Martinique (Fixed)",
"uid": "smartcity-martinique-2026-v2",
"version": 2
}
# Sauvegarder
with open('/home/eric/smart-city-digital-twin-martinique/grafana-dashboard-fixed.json', 'w') as f:
json.dump(dashboard, f, indent=2)
print("✅ Dashboard avec bonnes requêtes Flux créé")
print(f" Fichier: grafana-dashboard-fixed.json")
print(f" Datasource UID: {ds_uid}")

View File

@@ -0,0 +1,78 @@
-- Create 60 MQTT agents for OpenRemote using PL/pgSQL block
-- Realm master: parent_id = '2LtWTTd29uPZLbuWMWUxBf'
-- Realm smartcity: parent_id = 'e174aad5c7b5489e8b2efe'
CREATE EXTENSION IF NOT EXISTS pgcrypto;
DO $$
DECLARE
sensor_types text[] := ARRAY['airquality', 'traffic', 'parking', 'noise', 'weather', 'waterquality'];
realm_rec RECORD;
sensor_type text;
i integer;
new_id text;
new_name text;
new_topic text;
parent_id text;
realm_name text;
BEGIN
-- Delete existing MQTT agents
DELETE FROM asset WHERE type = 'urn:openremote:agent:mqtt';
-- Loop over realms
FOR realm_rec IN SELECT * FROM (VALUES ('master', '2LtWTTd29uPZLbuWMWUxBf'), ('smartcity', 'e174aad5c7b5489e8b2efe')) AS t(realm, parent) LOOP
realm_name := realm_rec.realm;
parent_id := realm_rec.parent;
FOREACH sensor_type IN ARRAY sensor_types LOOP
FOR i IN 1..5 LOOP
new_id := LEFT(REPLACE(gen_random_uuid()::text, '-', ''), 22);
new_name := 'MQTT-Agent-' || sensor_type || '-' || i;
new_topic := 'smartcity/' || sensor_type || '/' || i;
INSERT INTO asset (id, name, type, realm, parent_id, created_on, access_public_read, version, attributes)
VALUES (
new_id,
new_name,
'urn:openremote:agent:mqtt',
realm_name,
parent_id,
NOW(),
false,
1,
jsonb_build_object(
'name', jsonb_build_object('type', 'String', 'value', new_name),
'agentLink', jsonb_build_object(
'type', 'Property',
'value', jsonb_build_object(
'type', 'mqtt',
'brokerUrl', 'tcp://openremote-manager-1:1883',
'topicFilter', new_topic,
'username', '',
'password', '',
'enabled', true
)
),
'sensorType', jsonb_build_object('type', 'String', 'value', sensor_type),
'location', jsonb_build_object(
'type', 'GeoJSONPoint',
'value', jsonb_build_object(
'type', 'Point',
'coordinates', jsonb_build_array(
14.6091 + (random() - 0.5) * 0.1,
-61.2155 + (random() - 0.5) * 0.1
)
)
)
)
);
END LOOP;
END LOOP;
END LOOP;
END $$;
-- Verify insertion
SELECT realm, COUNT(*) as agent_count
FROM asset
WHERE type = 'urn:openremote:agent:mqtt'
GROUP BY realm;

View File

@@ -1,408 +1,119 @@
<h1 <!DOCTYPE html>
id="smart-city-digital-twin-martinique-diagramme-des-flux-de-données">Smart <html>
City Digital Twin Martinique — Diagramme des Flux de Données</h1> <head>
<p><strong>Dernière mise à jour :</strong> 06 Mai 2026<br /> <title>Smart City Data Flow Diagram</title>
<strong>Projet :</strong> Smart City Digital Twin Martinique<br /> <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<strong>Architecture :</strong> IoT-Agent intégré, QuantumLeap + CrateDB <script>
pour lanalyse avancée</p> mermaid.initialize({ startOnLoad: true, theme: 'dark' });
<hr /> </script>
<h2 id="architecture-globale-mise-à-jour-06052026">Architecture Globale <style>
(Mise à jour 06/05/2026)</h2> body {
<pre class="mermaid"><code>graph TB background-color: #1a1a1a;
subgraph Simulateur[&quot;🖥️ Simulateur (Host Python)&quot;] color: white;
SIM[Smart City Simulator&lt;br/&gt;10 capteurs&lt;br/&gt;Intervalle: configurable] font-family: Arial, sans-serif;
padding: 20px;
}
.mermaid {
background-color: #2a2a2a;
padding: 20px;
border-radius: 10px;
}
</style>
</head>
<body>
<h1>Smart City Digital Twin - Data Flow Diagram</h1>
<p>Updated: 2026-05-06 - Architecture with 3 IoT Agents (one per MQTT broker)</p>
<div class="mermaid">
graph TB
subgraph Simulateur["🖥️ Simulateur (Host Python)"]
SIM[Smart City Simulator<br/>10 capteurs<br/>Intervalle: configurable]
end end
subgraph MQTT_Brokers[&quot;📡 MQTT Brokers&quot;] subgraph MQTT_Brokers["📡 MQTT Brokers"]
EMQ[EMQX&lt;br/&gt;port 11883] EMQ[EMQX<br/>port 11883]
MOS[Mosquitto&lt;br/&gt;port 1883] MOS[Mosquitto<br/>port 1883]
BUN[BunkerM&lt;br/&gt;port 1900&lt;br/&gt;MQTTS/TLS] BUN[BunkerM<br/>port 1900<br/>MQTTS/TLS]
end end
subgraph IoT_Agent[&quot;🤖 IoT Agent (NGSI-LD)&quot;] subgraph IoT_Agent["🤖 3 IoT Agents (NGSI-LD)"]
IOTA[IoT Agent JSON&lt;br/&gt;port 4041&lt;br/&gt;Transforme MQTT → NGSI-LD] IOTA_EMQ[IoT-Agent-EMQX<br/>port 4041<br/>Ecoute EMQX]
IOTA_MOS[IoT-Agent-Mosquitto<br/>port 4042<br/>Ecoute Mosquitto]
IOTA_BUN[IoT-Agent-BunkerM<br/>port 4043<br/>Ecoute BunkerM]
end end
subgraph CB[&quot;🔗 Context Brokers (NGSI-LD)&quot;] subgraph CB["🔗 Context Brokers (NGSI-LD)"]
ORI[Orion-LD&lt;br/&gt;NGSI-LD&lt;br/&gt;port 1026] ORI[Orion-LD<br/>NGSI-v2<br/>port 1026]
STE[Stellio&lt;br/&gt;NGSI-LD&lt;br/&gt;port 8080] STE[Stellio<br/>NGSI-LD<br/>port 8080]
FRO[FROST-Server&lt;br/&gt;SensorThings&lt;br/&gt;port 8080] FRO[FROST-Server<br/>SensorThings<br/>port 8080]
end end
subgraph Analytics[&quot;📈 Analytics &amp; Time-Series&quot;] subgraph Analytics["📈 Analytics & Time-Series"]
QL[QuantumLeap&lt;br/&gt;NGSI-LD → CrateDB&lt;br/&gt;port 8668] QL[QuantumLeap<br/>NGSI-LD → CrateDB<br/>port 8668]
CRATEDB[CrateDB&lt;br/&gt;PostgreSQL-compatible&lt;br/&gt;port 4200/5432] CRATEDB[CrateDB<br/>PostgreSQL-compatible<br/>port 4200/5432]
end end
subgraph Storage[&quot;💾 Stockage &amp; Métriques&quot;] subgraph Storage["💾 Stockage & Métriques"]
INF[InfluxDB&lt;br/&gt;Bucket: iot_data&lt;br/&gt;port 8086] INF[InfluxDB<br/>Bucket: iot_data<br/>port 8086]
PRO[Prometheus&lt;br/&gt;Scrape: /metrics&lt;br/&gt;port 9090] PRO[Prometheus<br/>Scrape: /metrics<br/>port 9090]
GEO[GeoServer&lt;br/&gt;WMS/WFS/WMTS&lt;br/&gt;port 8080] GEO[GeoServer<br/>WMS/WFS/WMTS<br/>port 8080]
end end
subgraph IoT_Platform[&quot;🏢 Plateforme IoT&quot;] subgraph IoT_Platform["🏢 Plateforme IoT"]
ORM[OpenRemote Manager&lt;br/&gt;MQTT Agent&lt;br/&gt;port 8080] ORM[OpenRemote Manager<br/>MQTT Agent<br/>port 8080]
KC[Keycloak&lt;br/&gt;port 8080] KC[Keycloak<br/>port 8080]
end end
subgraph VIZ[&quot;📊 Visualisation&quot;] subgraph VIZ["📊 Visualisation"]
GRA[Grafana&lt;br/&gt;Dashboards&lt;br/&gt;port 3001] GRA[Grafana<br/>Dashboards<br/>port 3001]
MAP[MapStore&lt;br/&gt;WMS/WFS&lt;br/&gt;port 8080] MAP[MapStore<br/>WMS/WFS<br/>port 8080]
end end
%% ── Flux Simulateur ────────────────────────────────────────── %% ── Flux Simulateur ──────────────────────────────────────────
SIM --&gt;|&quot;1⃣ MQTT publish&lt;br/&gt;city/sensors/{type}/{id}&quot;| EMQ SIM -->|"1⃣ MQTT publish<br/>smartcity-api-key/{id}/attrs"| EMQ
SIM --&gt;|&quot;1⃣ MQTT publish&quot;| MOS SIM -->|"1⃣ MQTT publish"| MOS
SIM --&gt;|&quot;1⃣ MQTT publish&quot;| BUN SIM -->|"1⃣ MQTT publish"| BUN
SIM --&gt;|&quot;5⃣ InfluxDB v2 API&lt;br/&gt;async non-bloquant&quot;| INF SIM -->|"5⃣ InfluxDB v2 API<br/>async non-bloquant"| INF
%% ── Flux MQTT → IoT Agent ────────────────────────────────── %% ── Flux MQTT → IoT Agents ──────────────────────────────────
EMQ --&gt;|&quot;MQTT subscribe&lt;br/&gt;city/sensors/#&quot;| IOTA EMQ -->|"MQTT subscribe<br/>smartcity-api-key/#"| IOTA_EMQ
MOS --&gt;|&quot;MQTT subscribe&quot;| IOTA MOS -->|"MQTT subscribe"| IOTA_MOS
BUN --&gt;|&quot;MQTT subscribe&quot;| IOTA BUN -->|"MQTT subscribe"| IOTA_BUN
%% ── Flux IoT Agent → Context Brokers ─────────────────────── %% ── Flux IoT Agents → Context Brokers ───────────────────────
IOTA --&gt;|&quot;2⃣ NGSI-LD POST&lt;br/&gt;/ngsi-ld/v1/entities&quot;| ORI IOTA_EMQ -->|"2⃣ NGSI-v2 POST<br/>/v2/entities"| ORI
IOTA --&gt;|&quot;2⃣ NGSI-LD POST&quot;| STE IOTA_MOS -->|"2⃣ NGSI-v2 POST"| ORI
IOTA_BUN -->|"2⃣ NGSI-v2 POST"| ORI
%% ── Flux Context Brokers → QuantumLeap ─────────────────── %% ── Flux Context Brokers → QuantumLeap ───────────────────
ORI --&gt;|&quot;NGSI-LD Subscription&lt;br/&gt;→ QuantumLeap&quot;| QL ORI -->|"NGSI-v2 Subscription<br/>→ QuantumLeap"| QL
STE --&gt;|&quot;NGSI-LD Subscription&lt;br/&gt;→ QuantumLeap&quot;| QL
%% ── Flux QuantumLeap → CrateDB ──────────────────────────── %% ── Flux QuantumLeap → CrateDB ────────────────────────────
QL --&gt;|&quot;Insert&lt;br/&gt;PostgreSQL wire&quot;| CRATEDB QL -->|"Insert<br/>PostgreSQL wire"| CRATEDB
%% ── Visualisation ─────────────────────────────────────────── %% ── Visualisation ───────────────────────────────────────────
CRATEDB --&gt;|&quot;PostgreSQL Datasource&quot;| GRA CRATEDB -->|"PostgreSQL Datasource"| GRA
INF --&gt;|&quot;Datasource Flux IoT&quot;| GRA INF -->|"Datasource Flux IoT"| GRA
ORI --&gt;|&quot;NGSI-LD Datasource&quot;| GRA ORI -->|"NGSI-v2 Datasource"| GRA
STE --&gt;|&quot;NGSI-LD Datasource&quot;| GRA GEO -->|"WMS/WMTS"| MAP
GEO --&gt;|&quot;WMS/WMTS&quot;| MAP ORM -->|MapSettings<br/>Martinique| MAP
ORM --&gt;|MapSettings&lt;br/&gt;Martinique| MAP ORM -->|"Live assets<br/>REST"| GRA
ORM --&gt;|&quot;Live assets&lt;br/&gt;REST&quot;| GRA
%% ── OpenRemote MQTT Agent ─────────────────────────────────── %% ── OpenRemote MQTT Agent ───────────────────────────────────
EMQ --&gt;|&quot;6⃣ Subscribe&lt;br/&gt;city/sensors/#&quot;| ORM EMQ -->|"6⃣ Subscribe<br/>city/sensors/#"| ORM
MOS --&gt;|&quot;6⃣ Subscribe&quot;| ORM MOS -->|"6⃣ Subscribe"| ORM
BUN --&gt;|&quot;6⃣ Subscribe&quot;| ORM BUN -->|"6⃣ Subscribe"| ORM
%% ── Métriques Prometheus ─────────────────────────────────── %% ── Métriques Prometheus ───────────────────────────────────
SIM --&gt;|&quot;7⃣ /metrics&lt;br/&gt;port 8001&quot;| PRO SIM -->|"7⃣ /metrics<br/>port 8001"| PRO
EMQ --&gt;|&quot;/api/v5/metrics&quot;| PRO EMQ -->|"/api/v5/metrics"| PRO
STE --&gt;|&quot;/actuator/prometheus&quot;| PRO STE -->|"/actuator/prometheus"| PRO
INF --&gt;|&quot;/metrics&quot;| PRO INF -->|"/metrics"| PRO
ORM --&gt;|&quot;/actuator/prometheus&quot;| PRO ORM -->|"/actuator/prometheus"| PRO
GRA --&gt;|&quot;/metrics&quot;| PRO GRA -->|"/metrics"| PRO
IOTA --&gt;|&quot;/metrics&quot;| PRO IOTA_EMQ -->|"/metrics"| PRO
QL --&gt;|&quot;/metrics&quot;| PRO</code></pre> IOTA_MOS -->|"/metrics"| PRO
<hr /> IOTA_BUN -->|"/metrics"| PRO
<h2 id="flux-détaillés-mise-à-jour-06052026">Flux Détaillés (Mise à jour QL -->|"/metrics"| PRO
06/05/2026)</h2> </div>
<h3 id="flux-mqtt-brokers">1⃣ Flux MQTT — Brokers</h3> </body>
<table> </html>
<thead>
<tr>
<th>Broker</th>
<th>Port</th>
<th>Protocol</th>
<th>Topics</th>
</tr>
</thead>
<tbody>
<tr>
<td>EMQX</td>
<td>11883</td>
<td>MQTT</td>
<td><code>city/sensors/{type}/{id}</code></td>
</tr>
<tr>
<td>Mosquitto</td>
<td>1883</td>
<td>MQTT</td>
<td><code>city/sensors/{type}/{id}</code></td>
</tr>
<tr>
<td>BunkerM</td>
<td>1900</td>
<td>MQTTS (TLS)</td>
<td><code>city/sensors/{type}/{id}</code></td>
</tr>
</tbody>
</table>
<p>Le simulateur publie simultanément sur les 3 brokers vers <strong>IoT
Agent</strong>.</p>
<h3 id="flux-iot-agent-ngsi-ld">2⃣ Flux IoT Agent — NGSI-LD</h3>
<ul>
<li><p><strong>IoT Agent JSON</strong> : Réception MQTT → Transformation
en entités NGSI-LD</p></li>
<li><p><strong>Port</strong> : <code>4041</code></p></li>
<li><p><strong>Configuration</strong> :</p>
<div class="sourceCode" id="cb2"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="co"># Enregistrement service</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="ex">curl</span> <span class="at">-X</span> POST http://localhost:4041/iot/services <span class="dt">\</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a> <span class="at">-H</span> <span class="st">&#39;Content-Type: application/json&#39;</span> <span class="dt">\</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a> <span class="at">-H</span> <span class="st">&#39;fiware-service: smartcity&#39;</span> <span class="dt">\</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a> <span class="at">-d</span> <span class="st">&#39;{&quot;services&quot;: [{&quot;apikey&quot;: &quot;smartcity-api-key&quot;, &quot;cbroker&quot;: &quot;http://orion-ld:1026&quot;, &quot;entity_type&quot;: &quot;Device&quot;, &quot;ngsi_version&quot;: &quot;ld&quot;}]}&#39;</span></span></code></pre></div></li>
<li><p><strong>Entités créées dans</strong> : Orion-LD (port 1026) et
Stellio (port 8080)</p></li>
</ul>
<h3 id="flux-context-brokers-quantumleap-cratedb">3⃣ Flux Context
Brokers → QuantumLeap → CrateDB</h3>
<ol type="1">
<li><strong>Orion-LD</strong> / <strong>Stellio</strong> : Reçoivent les
entités NGSI-LD de IoT Agent</li>
<li><strong>QuantumLeap</strong> (port 8668) : Souscrit aux mises à jour
NGSI-LD via Subscription</li>
<li><strong>CrateDB</strong> (port 5432/4200) : Stockage temporel
PostgreSQL-compatible</li>
<li><strong>Grafana</strong> : Dashboards connectés à CrateDB
(PostgreSQL datasource)</li>
</ol>
<h3 id="flux-influxdb-temps-réel">4⃣ Flux InfluxDB — Temps Réel</h3>
<ul>
<li><strong>API</strong> :
<code>http://localhost:8086/api/v2/write</code></li>
<li><strong>Bucket</strong> : <code>iot_data</code></li>
<li><strong>Org</strong> : <code>digitribe</code></li>
<li><strong>Mode</strong> : Asynchrone (thread daemon) pour ne pas
bloquer le publish MQTT</li>
</ul>
<h3 id="openremote-mqtt-agent">5⃣ OpenRemote — MQTT Agent</h3>
<p>Lagent MQTT dOpenRemote souscrit aux topics
<code>city/sensors/#</code> sur les brokers MQTT. Les payloads sont
automatiquement parsés et les attributs des assets sont mis à jour.</p>
<h3 id="flux-prometheus-métriques">6⃣ Flux Prometheus — Métriques</h3>
<table>
<thead>
<tr>
<th>Service</th>
<th>Endpoint <code>/metrics</code></th>
<th>Statut</th>
</tr>
</thead>
<tbody>
<tr>
<td>Simulator</td>
<td><code>localhost:8001</code></td>
<td></td>
</tr>
<tr>
<td>EMQX</td>
<td><code>emqx_emqx_1:8081/api/v5/metrics</code></td>
<td></td>
</tr>
<tr>
<td>Stellio</td>
<td><code>stellio-api-gateway:8080/actuator/prometheus</code></td>
<td></td>
</tr>
<tr>
<td>InfluxDB</td>
<td><code>smart-city-influxdb:8086/metrics</code></td>
<td></td>
</tr>
<tr>
<td>OpenRemote</td>
<td><code>openremote-manager-1:8080/actuator/prometheus</code></td>
<td></td>
</tr>
<tr>
<td>Grafana</td>
<td><code>smart-city-grafana:3000/metrics</code></td>
<td></td>
</tr>
<tr>
<td>IoT Agent</td>
<td><code>iot-agent:4041/metrics</code></td>
<td>⚠️ À vérifier</td>
</tr>
<tr>
<td>QuantumLeap</td>
<td><code>quantum-leap:8668/metrics</code></td>
<td>⚠️ À vérifier</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="nouveaux-composants-06052026">Nouveaux Composants
(06/05/2026)</h2>
<h3 id="iot-agent-json">🤖 IoT Agent JSON</h3>
<ul>
<li><strong>Rôle</strong> : Pont entre MQTT et NGSI-LD (Orion-LD /
Stellio)</li>
<li><strong>Port</strong> : 4041</li>
<li><strong>Statut</strong> : ❌ En cours de réparation (erreur
MongoDB)</li>
<li><strong>Correction</strong> : Fournir
<code>IOTA_MONGO_URL=mongodb://mongodb:27017/iotagent</code></li>
</ul>
<h3 id="quantumleap">📈 QuantumLeap</h3>
<ul>
<li><strong>Rôle</strong> : Analytics NGSI-LD → CrateDB</li>
<li><strong>Port</strong> : 8668</li>
<li><strong>Statut</strong> : ✅ Fonctionnel (interne), port non exposé
sur lhôte</li>
<li><strong>Action</strong> : Exposer le port dans docker-compose</li>
</ul>
<h3 id="cratedb">🗄️ CrateDB</h3>
<ul>
<li><strong>Rôle</strong> : Base de données temporelle
PostgreSQL-compatible</li>
<li><strong>Port</strong> : 4200 (UI), 5432 (PostgreSQL)</li>
<li><strong>Statut</strong> : ✅ Opérationnel</li>
<li><strong>Usage</strong> : Stockage des séries temporelles depuis
QuantumLeap</li>
</ul>
<hr />
<h2 id="tableau-récapitulatif-mise-à-jour">Tableau Récapitulatif (Mise à
jour)</h2>
<table>
<colgroup>
<col style="width: 28%" />
<col style="width: 34%" />
<col style="width: 15%" />
<col style="width: 21%" />
</colgroup>
<thead>
<tr>
<th>Composant</th>
<th>Technologie</th>
<th>Port</th>
<th>Statut</th>
</tr>
</thead>
<tbody>
<tr>
<td>Simulator</td>
<td>Python + paho-mqtt</td>
<td>Host:8001 (metrics)</td>
<td>✅ Actif</td>
</tr>
<tr>
<td>EMQX</td>
<td>MQTT Broker</td>
<td>11883</td>
<td>✅ Connecté</td>
</tr>
<tr>
<td>Mosquitto</td>
<td>MQTT Broker</td>
<td>1883</td>
<td>✅ Connecté</td>
</tr>
<tr>
<td>BunkerM</td>
<td>MQTTS Broker</td>
<td>1900</td>
<td>✅ Connecté</td>
</tr>
<tr>
<td><strong>IoT Agent</strong></td>
<td><strong>NGSI-LD Bridge</strong></td>
<td><strong>4041</strong></td>
<td><strong>❌ Erreur MongoDB</strong></td>
</tr>
<tr>
<td>Orion-LD</td>
<td>NGSI-LD Broker</td>
<td>1026</td>
<td>⚠️ À vérifier</td>
</tr>
<tr>
<td>Stellio</td>
<td>NGSI-LD Broker</td>
<td>8080</td>
<td>⚠️ Ports occupés par OpenRemote</td>
</tr>
<tr>
<td>FROST-Server</td>
<td>SensorThings API</td>
<td>8080</td>
<td>⚠️ À vérifier</td>
</tr>
<tr>
<td><strong>QuantumLeap</strong></td>
<td><strong>NGSI-LD → CrateDB</strong></td>
<td><strong>8668</strong></td>
<td><strong>✅ Interne</strong></td>
</tr>
<tr>
<td><strong>CrateDB</strong></td>
<td><strong>PostgreSQL Time-Series</strong></td>
<td><strong>4200/5432</strong></td>
<td><strong>✅ Opérationnel</strong></td>
</tr>
<tr>
<td>OpenRemote</td>
<td>IoT Platform</td>
<td>8080</td>
<td>⚠️ 403 (Service Account)</td>
</tr>
<tr>
<td>InfluxDB</td>
<td>Time Series DB</td>
<td>8086</td>
<td>✅ Bucket iot_data</td>
</tr>
<tr>
<td>Grafana</td>
<td>Visualisation</td>
<td>3001</td>
<td>✅ Dashboards + CrateDB</td>
</tr>
<tr>
<td>GeoServer</td>
<td>Geo Data</td>
<td>8080</td>
<td>✅ REST OK</td>
</tr>
<tr>
<td>MapStore</td>
<td>Cartographie</td>
<td>8080</td>
<td>✅ WMS/WMTS</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="actions-prioritaires">Actions Prioritaires</h2>
<ol type="1">
<li><strong>Corriger IoT Agent</strong> : Ajouter MongoDB et configurer
<code>IOTA_MONGO_URL</code></li>
<li><strong>Exposer QuantumLeap</strong> : Mapper le port 8668 dans
docker-compose</li>
<li><strong>Déployer Orion-LD</strong> : Créer le service sil nexiste
pas</li>
<li><strong>Libérer les ports</strong> : Résoudre le conflit
Stellio/OpenRemote sur le port 8080</li>
<li><strong>Configurer CrateDB</strong> : Créer les tables pour
QuantumLeap</li>
<li><strong>Mettre à jour le simulateur</strong> :
<code>ENABLE_PULSAR=false</code> (recommandé)</li>
</ol>
<hr />
<h2 id="commandes-utiles">Commandes Utiles</h2>
<div class="sourceCode" id="cb3"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="co"># Vérifier IoT Agent</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="ex">curl</span> <span class="at">-s</span> http://localhost:4041/iot/services <span class="at">-H</span> <span class="st">&#39;fiware-service: smartcity&#39;</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="co"># Vérifier QuantumLeap</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="ex">curl</span> <span class="at">-s</span> http://localhost:8668/version</span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="co"># Vérifier CrateDB</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="ex">psql</span> <span class="at">-h</span> localhost <span class="at">-p</span> 5432 <span class="at">-U</span> crate <span class="at">-c</span> <span class="st">&quot;SELECT * FROM ql_entities LIMIT 5;&quot;</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="co"># Vérifier Orion-LD</span></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="ex">curl</span> <span class="at">-s</span> http://localhost:1026/version</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a><span class="co"># Voir les logs IoT Agent</span></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a><span class="ex">docker</span> logs smart-city-iot-agent <span class="at">--tail</span> 30</span></code></pre></div>
<hr />
<p><strong>Fichiers associés :</strong> - Simulateur :
<code>~/smart-city-digital-twin-martinique/simulator.py</code> -
Dashboard Grafana :
<code>~/smart-city-digital-twin-martinique/grafana_dashboard_smartcity.json</code>
- Ce diagramme :
<code>~/smart-city-digital-twin-martinique/data-flow-diagram.md</code> -
Session Resume :
<code>~/smart-city-digital-twin-martinique/session_resume_2026-05-07.md</code></p>

View File

@@ -1,236 +1,256 @@
# Smart City Digital Twin Martinique — Diagramme des Flux de Données # Smart City Digital Twin - Data Flow Diagram (Updated 2026-05-12)
**Dernière mise à jour :** 06 Mai 2026 ## Architecture complète avec LoRaWAN
**Projet :** Smart City Digital Twin Martinique
**Architecture :** IoT-Agent intégré, QuantumLeap + CrateDB pour l'analyse avancée
--- ```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Smart City Simulator (Python) │
│ Publie sur 3 brokers MQTT + REST vers OpenRemote │
└──────────┬────────────────────┬──────────────────────┬───────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ EMQX Broker │ │ Mosquitto Broker │ │ BunkerM Broker │
│ (port 11883) │ │ (port 1883) │ │ (port 1900) │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ IoT-Agent-EMQX │ │IoT-Agent-Mosquitto│ │IoT-Agent-BunkerM │
│ Port: 4041 │ │ Port: 4042 │ │ Port: 4043 │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
└───────────────────────┴──────────────────────┘
┌─────────────────────┐
│ Orion-LD Context │
│ Broker (port 1026)│
│ MongoDB backend │
└─────────┬───────────┘
│ Subscription → QuantumLeap
┌─────────────────────┐
│ QuantumLeap │
│ (port 8668) │
└─────────┬───────────┘
┌─────────────────────┐
│ CrateDB │
│ (ports 5432/4200)│
└─────────┬───────────┘
┌─────────────────────┐
│ Grafana │
│ (port 3001) │
└─────────────────────┘
## Architecture Globale (Mise à jour 06/05/2026) ═══════════════════════════════════════════════════════════════════════════════
LoRaWAN Layer
═══════════════════════════════════════════════════════════════════════════════
```mermaid ┌──────────────────┐ ┌──────────────────┐
graph TB │ Gateway LoRaWAN │ UDP │ Gateway LoRaWAN │
subgraph Simulateur["🖥️ Simulateur (Host Python)"] (EU868) │ 1700 │ (EU868) │
SIM[Smart City Simulator<br/>10 capteurs<br/>Intervalle: configurable] └────────┬─────────┘ └────────┬─────────┘
end │ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ChirpStack LoRaWAN Network Server │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ chirpstack │ │ gateway-bridge │ │ rest-api │ │
│ │ (port 8080) │ │ (UDP 1700) │ │ (port 8090) │ │
│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ PostgreSQL │ │ Redis │ │ Mosquitto (MQTT) │ │
│ │ (chirpstack DB) │ │ (cache) │ │ (port 1883) │ │
│ └──────────────────┘ └──────────────────┘ └────────┬─────────┘ │
└──────────────────────────────────────────────────────┬─────────────────────┘
┌──────────────────┐
│ EMQX Broker │
│ (integration) │
└──────────────────┘
subgraph MQTT_Brokers["📡 MQTT Brokers"] ┌─────────────────────────────────────────────────────────────────────────────┐
EMQ[EMQX<br/>port 11883] The Things Stack LoRaWAN Network Server │
MOS[Mosquitto<br/>port 1883] ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
BUN[BunkerM<br/>port 1900<br/>MQTTS/TLS] │ tts-stack │ │ tts-postgres │ │ tts-redis │ │
end │ │ (port 1885) │ │ (TTN DB) │ │ (cache) │ │
│ └────────┬─────────┘ └──────────────────┘ └──────────────────┘ │
│ │ │
│ │ UDP 1700 (gateways) │
│ │ MQTT 1883 (events) │
│ │ HTTP 1884 (API) │
│ │ HTTP 1885 (Console) │
└───────────┬─────────────────────────────────────────────────────────────────┘
┌──────────────────┐
│ EMQX Broker │
│ (integration) │
└──────────────────┘
subgraph IoT_Agent["🤖 IoT Agent (NGSI-LD)"] ═══════════════════════════════════════════════════════════════════════════════
IOTA[IoT Agent JSON<br/>port 4041<br/>Transforme MQTT → NGSI-LD] OpenRemote Manager
end ═══════════════════════════════════════════════════════════════════════════════
subgraph CB["🔗 Context Brokers (NGSI-LD)"] ┌─────────────────────────────────────────────────────────────────────────────┐
ORI[Orion-LD<br/>NGSI-LD<br/>port 1026] │ OpenRemote Manager (Artemis MQTT) │
STE[Stellio<br/>NGSI-LD<br/>port 8080] │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
FRO[FROST-Server<br/>SensorThings<br/>port 8080] │ Manager UI │ │ Keycloak │ │ PostgreSQL │ │
end │ │ (port 8080) │ │ (port 8080) │ │ (port 5432) │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
subgraph Analytics["📈 Analytics & Time-Series"] │ │
QL[QuantumLeap<br/>NGSI-LD → CrateDB<br/>port 8668] Assets IOTSensor avec agentLink MQTT + location (GeoJSON Point) │
CRATEDB[CrateDB<br/>PostgreSQL-compatible<br/>port 4200/5432] Assets visualisés sur la carte Martinique (mapsettings.json) │
end └─────────────────────────────────────────────────────────────────────────────┘
subgraph Storage["💾 Stockage & Métriques"]
INF[InfluxDB<br/>Bucket: iot_data<br/>port 8086]
PRO[Prometheus<br/>Scrape: /metrics<br/>port 9090]
GEO[GeoServer<br/>WMS/WFS/WMTS<br/>port 8080]
end
subgraph IoT_Platform["🏢 Plateforme IoT"]
ORM[OpenRemote Manager<br/>MQTT Agent<br/>port 8080]
KC[Keycloak<br/>port 8080]
end
subgraph VIZ["📊 Visualisation"]
GRA[Grafana<br/>Dashboards<br/>port 3001]
MAP[MapStore<br/>WMS/WFS<br/>port 8080]
end
%% ── Flux Simulateur ──────────────────────────────────────────
SIM -->|"1⃣ MQTT publish<br/>city/sensors/{type}/{id}"| EMQ
SIM -->|"1⃣ MQTT publish"| MOS
SIM -->|"1⃣ MQTT publish"| BUN
SIM -->|"5⃣ InfluxDB v2 API<br/>async non-bloquant"| INF
%% ── Flux MQTT → IoT Agent ──────────────────────────────────
EMQ -->|"MQTT subscribe<br/>city/sensors/#"| IOTA
MOS -->|"MQTT subscribe"| IOTA
BUN -->|"MQTT subscribe"| IOTA
%% ── Flux IoT Agent → Context Brokers ───────────────────────
IOTA -->|"2⃣ NGSI-LD POST<br/>/ngsi-ld/v1/entities"| ORI
IOTA -->|"2⃣ NGSI-LD POST"| STE
%% ── Flux Context Brokers → QuantumLeap ───────────────────
ORI -->|"NGSI-LD Subscription<br/>→ QuantumLeap"| QL
STE -->|"NGSI-LD Subscription<br/>→ QuantumLeap"| QL
%% ── Flux QuantumLeap → CrateDB ────────────────────────────
QL -->|"Insert<br/>PostgreSQL wire"| CRATEDB
%% ── Visualisation ───────────────────────────────────────────
CRATEDB -->|"PostgreSQL Datasource"| GRA
INF -->|"Datasource Flux IoT"| GRA
ORI -->|"NGSI-LD Datasource"| GRA
STE -->|"NGSI-LD Datasource"| GRA
GEO -->|"WMS/WMTS"| MAP
ORM -->|MapSettings<br/>Martinique| MAP
ORM -->|"Live assets<br/>REST"| GRA
%% ── OpenRemote MQTT Agent ───────────────────────────────────
EMQ -->|"6⃣ Subscribe<br/>city/sensors/#"| ORM
MOS -->|"6⃣ Subscribe"| ORM
BUN -->|"6⃣ Subscribe"| ORM
%% ── Métriques Prometheus ────────────────────────────────────
SIM -->|"7⃣ /metrics<br/>port 8001"| PRO
EMQ -->|"/api/v5/metrics"| PRO
STE -->|"/actuator/prometheus"| PRO
INF -->|"/metrics"| PRO
ORM -->|"/actuator/prometheus"| PRO
GRA -->|"/metrics"| PRO
IOTA -->|"/metrics"| PRO
QL -->|"/metrics"| PRO
``` ```
--- ## Flux de données (Step-by-step)
## Flux Détaillés (Mise à jour 06/05/2026) 1. **Simulator** publie sur 3 brokers MQTT (EMQX:11883, Mosquitto:1883, BunkerM:1900)
- Topic: `smartcity-api-key/{device_id}/attrs`
- Format: `{"NO2": 45.5, "temperature": 26.0, "humidity": 70.0}`
### 1⃣ Flux MQTT — Brokers 2. **3 IoT-Agents** (un par broker) reçoivent les messages
| Broker | Port | Protocol | Topics | - iot-agent-emqx (port 4041) ← EMQX
|--------|------|----------|--------| - iot-agent-mosquitto (port 4042) ← Mosquitto
| EMQX | 11883 | MQTT | `city/sensors/{type}/{id}` | - iot-agent-bunkerm (port 4043) ← BunkerM
| Mosquitto | 1883 | MQTT | `city/sensors/{type}/{id}` |
| BunkerM | 1900 | MQTTS (TLS) | `city/sensors/{type}/{id}` |
Le simulateur publie simultanément sur les 3 brokers vers **IoT Agent**. 3. **Orion-LD** reçoit les entités NGSI-v2
- URL: `http://smart-city-orion-ld:1026`
- Entité: `urn:ngsi-ld:AirQualityObserved:airquality_001`
### 2⃣ Flux IoT Agent — NGSI-LD 4. **Subscription Orion-LD → QuantumLeap**
- **IoT Agent JSON** : Réception MQTT → Transformation en entités NGSI-LD - Notify URL: `http://smart-city-quantumleap:8668/v2/op/notify`
- **Port** : `4041`
- **Configuration** :
```bash
# Enregistrement service
curl -X POST http://localhost:4041/iot/services \
-H 'Content-Type: application/json' \
-H 'fiware-service: smartcity' \
-d '{"services": [{"apikey": "smartcity-api-key", "cbroker": "http://orion-ld:1026", "entity_type": "Device", "ngsi_version": "ld"}]}'
```
- **Entités créées dans** : Orion-LD (port 1026) et Stellio (port 8080)
### 3⃣ Flux Context Brokers → QuantumLeap → CrateDB 5. **QuantumLeap** stocke dans **CrateDB**
1. **Orion-LD** / **Stellio** : Reçoivent les entités NGSI-LD de IoT Agent - Table: `quantumleap.etairqualityobserved`
2. **QuantumLeap** (port 8668) : Souscrit aux mises à jour NGSI-LD via Subscription
3. **CrateDB** (port 5432/4200) : Stockage temporel PostgreSQL-compatible
4. **Grafana** : Dashboards connectés à CrateDB (PostgreSQL datasource)
### 4⃣ Flux InfluxDB — Temps Réel 6. **Grafana** visualise les données
- **API** : `http://localhost:8086/api/v2/write` - Datasource: `CrateDB-SmartCity`
- **Bucket** : `iot_data`
- **Org** : `digitribe`
- **Mode** : Asynchrone (thread daemon) pour ne pas bloquer le publish MQTT
### 5⃣ OpenRemote — MQTT Agent 7. **ChirpStack** gère les gateways et devices LoRaWAN
L'agent MQTT d'OpenRemote souscrit aux topics `city/sensors/#` sur les brokers MQTT. Les payloads sont automatiquement parsés et les attributs des assets sont mis à jour. - Gateway Bridge (UDP 1700) → ChirpStack → MQTT → EMQX
- REST API (port 8090) pour gestion des devices/applications
### 6⃣ Flux Prometheus — Métriques 8. **The Things Stack** gère les gateways et devices LoRaWAN (alternative)
| Service | Endpoint `/metrics` | Statut | - Gateway (UDP 1700) → TTS Stack → MQTT/REST API
|---------|---------------------|--------| - Console web (port 1885)
| Simulator | `localhost:8001` | ✅ |
| EMQX | `emqx_emqx_1:8081/api/v5/metrics` | ✅ |
| Stellio | `stellio-api-gateway:8080/actuator/prometheus` | ✅ |
| InfluxDB | `smart-city-influxdb:8086/metrics` | ✅ |
| OpenRemote | `openremote-manager-1:8080/actuator/prometheus` | ✅ |
| Grafana | `smart-city-grafana:3000/metrics` | ✅ |
| IoT Agent | `iot-agent:4041/metrics` | ⚠️ À vérifier |
| QuantumLeap | `quantum-leap:8668/metrics` | ⚠️ À vérifier |
--- 9. **OpenRemote** affiche les assets sur la map
- Assets IOTSensor avec location GeoJSON
- Agents MQTT pour mise à jour des valeurs
## Nouveaux Composants (06/05/2026) ## Sous-domaines (Traefik)
### 🤖 IoT Agent JSON ### IoT Agents & Brokers
- **Rôle** : Pont entre MQTT et NGSI-LD (Orion-LD / Stellio) - `iot-agent-emqx.digitribe.fr` → IoT-Agent-EMQX (port 4041)
- **Port** : 4041 - `iot-agent-mosquitto.digitribe.fr` → IoT-Agent-Mosquitto (port 4042)
- **Statut** : ❌ En cours de réparation (erreur MongoDB) - `iot-agent-bunkerm.digitribe.fr` → IoT-Agent-BunkerM (port 4043)
- **Correction** : Fournir `IOTA_MONGO_URL=mongodb://mongodb:27017/iotagent` - `orion-ld.digitribe.fr` → Orion-LD (port 1026)
- `quantum-leap.digitribe.fr` → QuantumLeap (port 8668)
- `grafana.digitribe.fr` → Grafana (port 3001)
### 📈 QuantumLeap ### ChirpStack LoRaWAN
- **Rôle** : Analytics NGSI-LD → CrateDB - `chirpstack.digitribe.fr` → ChirpStack Console (port 8080)
- **Port** : 8668 - `chirpstack-api.digitribe.fr` → ChirpStack REST API (port 8090)
- **Statut** : ✅ Fonctionnel (interne), port non exposé sur l'hôte - `chirpstack-ws.digitribe.fr` → Gateway Bridge WebSocket (port 3001)
- **Action** : Exposer le port dans docker-compose
### 🗄️ CrateDB ### The Things Stack LoRaWAN
- **Rôle** : Base de données temporelle PostgreSQL-compatible - `tts.digitribe.fr` → TTS Console (port 1885)
- **Port** : 4200 (UI), 5432 (PostgreSQL) - `tts-api.digitribe.fr` → TTS REST API (port 1884)
- **Statut** : ✅ Opérationnel
- **Usage** : Stockage des séries temporelles depuis QuantumLeap
--- ### OpenRemote
- `openremote.digitribe.fr` → OpenRemote Manager (port 8080)
## Tableau Récapitulatif (Mise à jour) ## Flux de données (Step-by-step)
| Composant | Technologie | Port | Statut | 1. **Simulator** publie sur 3 brokers MQTT (EMQX:11883, Mosquitto:1883, BunkerM:1900)
|-----------|-------------|------|--------| - Topic: `smartcity-api-key/{device_id}/attrs`
| Simulator | Python + paho-mqtt | Host:8001 (metrics) | ✅ Actif | - Format: `{"NO2": 45.5, "temperature": 26.0, "humidity": 70.0}`
| EMQX | MQTT Broker | 11883 | ✅ Connecté |
| Mosquitto | MQTT Broker | 1883 | ✅ Connecté |
| BunkerM | MQTTS Broker | 1900 | ✅ Connecté |
| **IoT Agent** | **NGSI-LD Bridge** | **4041** | **❌ Erreur MongoDB** |
| Orion-LD | NGSI-LD Broker | 1026 | ⚠️ À vérifier |
| Stellio | NGSI-LD Broker | 8080 | ⚠️ Ports occupés par OpenRemote |
| FROST-Server | SensorThings API | 8080 | ⚠️ À vérifier |
| **QuantumLeap** | **NGSI-LD → CrateDB** | **8668** | **✅ Interne** |
| **CrateDB** | **PostgreSQL Time-Series** | **4200/5432** | **✅ Opérationnel** |
| OpenRemote | IoT Platform | 8080 | ⚠️ 403 (Service Account) |
| InfluxDB | Time Series DB | 8086 | ✅ Bucket iot_data |
| Grafana | Visualisation | 3001 | ✅ Dashboards + CrateDB |
| GeoServer | Geo Data | 8080 | ✅ REST OK |
| MapStore | Cartographie | 8080 | ✅ WMS/WMTS |
--- 2. **3 IoT-Agents** (un par broker) reçoivent les messages
- iot-agent-emqx (port 4041) ← EMQX
- iot-agent-mosquitto (port 4042) ← Mosquitto
- iot-agent-bunkerm (port 4043) ← BunkerM
- Chaque IoT-Agent a le service `smartcity-api-key` configuré
- Chaque IoT-Agent a le device `airquality_001` enregistré
## Actions Prioritaires 3. **Orion-LD** reçoit les entités NGSI-v2
- URL: `http://smart-city-orion-ld:1026`
- Entité: `urn:ngsi-ld:AirQualityObserved:airquality_001`
- Type: `AirQualityObserved`
1. **Corriger IoT Agent** : Ajouter MongoDB et configurer `IOTA_MONGO_URL` 4. **Subscription Orion-LD → QuantumLeap**
2. **Exposer QuantumLeap** : Mapper le port 8668 dans docker-compose - ID: `69fbb09af55b82cad2a38008`
3. **Déployer Orion-LD** : Créer le service s'il n'existe pas - Description: "Forward AirQualityObserved to QuantumLeap"
4. **Libérer les ports** : Résoudre le conflit Stellio/OpenRemote sur le port 8080 - Notify URL: `http://smart-city-quantumleap:8668/v2/op/notify`
5. **Configurer CrateDB** : Créer les tables pour QuantumLeap - Attrs: NO2, temperature, humidity
6. **Mettre à jour le simulateur** : `ENABLE_PULSAR=false` (recommandé)
--- 5. **QuantumLeap** stocke dans **CrateDB**
- Table: `quantumleap.etairqualityobserved`
- Colonnes: entity_id, time_index, NO2, temperature, humidity
## Commandes Utiles 6. **Grafana** visualise les données
- Datasource: `CrateDB-SmartCity` (ID: 23)
- URL: `smart-city-cratedb:5432`
- Database: `quantumleap`
## Services et Devices (provisionnés)
### IoT-Agent-EMQX (port 4041)
- Service: `smartcity-api-key` → Orion-LD (`http://smart-city-orion-ld:1026`)
- Device: `airquality_001``urn:ngsi-ld:AirQualityObserved:airquality_001`
### IoT-Agent-Mosquitto (port 4042)
- Service: `smartcity-api-key` → Orion-LD (`http://smart-city-orion-ld:1026`)
- Device: `airquality_001``urn:ngsi-ld:AirQualityObserved:airquality_001`
### IoT-Agent-BunkerM (port 4043)
- Service: `smartcity-api-key` → Orion-LD (`http://smart-city-orion-ld:1026`)
- Device: `airquality_001``urn:ngsi-ld:AirQualityObserved:airquality_001`
## Sous-domaines (Traefik)
- `iot-agent-emqx.digitribe.fr` → IoT-Agent-EMQX (port 4041)
- `iot-agent-mosquitto.digitribe.fr` → IoT-Agent-Mosquitto (port 4042)
- `iot-agent-bunkerm.digitribe.fr` → IoT-Agent-BunkerM (port 4043)
- `orion-ld.digitribe.fr` → Orion-LD (port 1026)
- `quantum-leap.digitribe.fr` → QuantumLeap (port 8668)
- `grafana.digitribe.fr` → Grafana (port 3001)
## Test du flux complet
```bash ```bash
# Vérifier IoT Agent # 1. Publier un message MQTT (simuler le simulateur)
curl -s http://localhost:4041/iot/services -H 'fiware-service: smartcity' mosquitto_pub -h localhost -p 11883 -t "smartcity-api-key/airquality_001/attrs" \
-m '{"NO2": 50.5, "temperature": 30.0, "humidity": 90.0}'
# Vérifier QuantumLeap # 2. Vérifier qu'Orion-LD a reçu l'entité
curl -s http://localhost:8668/version curl -s http://localhost:1026/v2/entities -w "\nHTTP %{http_code}\n"
# Vérifier CrateDB # 3. Vérifier que QuantumLeap a reçu la notification
psql -h localhost -p 5432 -U crate -c "SELECT * FROM ql_entities LIMIT 5;" docker logs smart-city-quantumleap --tail 20 | grep -i "notify\|airquality"
# Vérifier Orion-LD # 4. Vérifier CrateDB
curl -s http://localhost:1026/version docker exec smart-city-cratedb crash -c "SELECT * FROM quantumleap.etairqualityobserved LIMIT 5;"
# Voir les logs IoT Agent # 5. Vérifier Grafana
docker logs smart-city-iot-agent --tail 30 curl -s http://localhost:3001/api/datasources -u admin:Digitribe972 | jq '.[] | select(.type=="postgres") | .name'
``` ```
--- ## Fichiers modifiés (2026-05-06)
**Fichiers associés :** - `docker-compose.iot-agent.yml` : 3 instances IoT-Agent (emqx, mosquitto, bunkerm)
- Simulateur : `~/smart-city-digital-twin-martinique/simulator.py` - `docker-compose.orion-ld.yml` : Orion-LD avec MongoDB existant
- Dashboard Grafana : `~/smart-city-digital-twin-martinique/grafana_dashboard_smartcity.json` - `docker-compose.quantumleap.yml` : Variables CRATE_HOST/PORT (fix)
- Ce diagramme : `~/smart-city-digital-twin-martinique/data-flow-diagram.md` - `simulator.py` : Publication sur 3 brokers avec format IoT-Agent
- Session Resume : `~/smart-city-digital-twin-martinique/session_resume_2026-05-07.md` - `data-flow-diagram.md` : Ce fichier (mis à jour)

25
ditto-things.yml Normal file
View File

@@ -0,0 +1,25 @@
services:
ditto-things:
image: eclipse/ditto-things:latest
container_name: smart-city-ditto-things
restart: unless-stopped
hostname: ditto-things
environment:
- TZ=Europe/Berlin
- BIND_HOSTNAME=0.0.0.0
- DITTO_JWT_SECRET=my-ditto-jwt-secret-key-12345
- MONGO_HOST=smart-city-ditto-mongodb
- MONGO_PORT=27017
- MONGO_DB=Things
- MONGODB_URI=mongodb://smart-city-ditto-mongodb:27017/Things
- AKKA_REMOTE_ENABLED=false
- JAVA_TOOL_OPTIONS=-Dditto.things.authentication.devops.password=ditto-devops-secret
networks:
traefik-public:
aliases:
- ditto-cluster
- ditto-things
networks:
traefik-public:
external: true

View File

@@ -0,0 +1,35 @@
# BunkerM MQTT Broker - Smart City Digital Twin
version: '3.8'
networks:
smartcity-shared:
external: true
traefik-public:
external: true
volumes:
bunkerm_mosquitto_data:
external: true
services:
bunkerm:
image: bunkeriot/bunkerm:latest
container_name: bunkerm-bunkerm-1
restart: unless-stopped
networks:
- smartcity-shared
- traefik-public
ports:
- "1884:1900"
- "2000:2000"
environment:
- MQTT_PORT=1900
- CONFIG_API_PORT=2000
volumes:
- bunkerm_mosquitto_data:/var/lib/mosquitto
healthcheck:
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/1900' || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s

View File

@@ -0,0 +1,43 @@
version: "3.8"
services:
chirpstack:
container_name: smart-city-chirpstack
image: chirpstack/chirpstack:latest
command: -c /etc/chirpstack
restart: unless-stopped
volumes:
- ./configuration/chirpstack:/etc/chirpstack:ro
environment:
- MQTT_BROKER_HOST=chirpstack-mosquitto-1
- REDIS_HOST=chirpstack-redis-1
- POSTGRESQL_HOST=chirpstack-postgres-1
- DATABASE_URL=postgres://chirpstack:chirpstack@chirpstack-postgres-1/chirpstack?sslmode=disable
labels:
- "traefik.enable=true"
- "traefik.http.routers.chirpstack.rule=Host(`chirpstack.digitribe.fr`)"
- "traefik.http.routers.chirpstack.entrypoints=websecure"
- "traefik.http.routers.chirpstack.tls.certresolver=letsencrypt"
- "traefik.http.services.chirpstack.loadbalancer.server.port=8080"
networks:
- smartcity-shared
chirpstack-rest-api:
container_name: smart-city-chirpstack-rest-api
image: chirpstack/chirpstack-rest-api:4
restart: unless-stopped
command: --server chirpstack:8080 --bind 0.0.0.0:8090 --insecure --cors-origins="*"
depends_on:
- chirpstack
labels:
- "traefik.enable=true"
- "traefik.http.routers.chirpstack-api.rule=Host(`chirpstack-api.digitribe.fr`)"
- "traefik.http.routers.chirpstack-api.entrypoints=websecure"
- "traefik.http.routers.chirpstack-api.tls.certresolver=letsencrypt"
- "traefik.http.services.chirpstack-api.loadbalancer.server.port=8090"
networks:
- smartcity-shared
networks:
smartcity-shared:
external: true

View File

@@ -4,6 +4,7 @@
services: services:
pulsar-distribution: pulsar-distribution:
container_name: smart-city-pulsar-distribution
environment: environment:
- PULSAR_HOST=pulsar - PULSAR_HOST=pulsar
- PULSAR_PORT=6650 - PULSAR_PORT=6650

114
docker-compose.ditto.yml Normal file
View File

@@ -0,0 +1,114 @@
# Eclipse Ditto - Smart City Digital Twin - Martinique
# Using official Eclipse Ditto images with Akka cluster
services:
ditto-mongodb:
image: mongo:6
container_name: smart-city-ditto-mongodb
restart: unless-stopped
networks:
traefik-public:
aliases:
- ditto-mongodb
volumes:
- ditto-mongo-data:/data/db
ditto-policies:
image: eclipse/ditto-policies:3.8.0
container_name: smart-city-ditto-policies
restart: unless-stopped
hostname: ditto-policies
depends_on:
- ditto-mongodb
environment:
- TZ=Europe/Berlin
- BIND_HOSTNAME=0.0.0.0
- DITTO_JWT_SECRET=NTOT-Vh8WRKWE52eV8zRiLs3a-gd8YUGSrvm5x2InZc
- MONGODB_URI=mongodb://smart-city-ditto-mongodb:27017/Policies
- AKKA_REMOTE_ENABLED=true
- AKKA_REMOTE_CANONICAL_HOSTNAME=ditto-policies
- AKKA_REMOTE_CANONICAL_PORT=2551
- JAVA_TOOL_OPTIONS=-Dditto.mongodb.uri=mongodb://smart-city-ditto-mongodb:27017/Policies -Dditto.mongodb.db-name=Policies
networks:
traefik-public:
aliases:
- ditto-cluster
- ditto-policies
ditto-things:
image: eclipse/ditto-things:3.8.0
container_name: smart-city-ditto-things
restart: unless-stopped
hostname: ditto-things
depends_on:
- ditto-mongodb
- ditto-policies
environment:
- TZ=Europe/Berlin
- BIND_HOSTNAME=0.0.0.0
- DITTO_JWT_SECRET=NTOT-Vh8WRKWE52eV8zRiLs3a-gd8YUGSrvm5x2InZc
- MONGODB_URI=mongodb://smart-city-ditto-mongodb:27017/Things
- AKKA_REMOTE_ENABLED=true
- AKKA_REMOTE_CANONICAL_HOSTNAME=ditto-things
- AKKA_REMOTE_CANONICAL_PORT=2551
- JAVA_TOOL_OPTIONS=-Dditto.mongodb.uri=mongodb://smart-city-ditto-mongodb:27017/Things -Dditto.mongodb.db-name=Things -Dditto.things.authentication.devops.password=OvP9WVB09aFDnYPyK52UIg
networks:
traefik-public:
aliases:
- ditto-cluster
- ditto-things
ditto-gateway:
image: eclipse/ditto-gateway:latest
container_name: smart-city-ditto-gateway
restart: unless-stopped
hostname: ditto-gateway
depends_on:
- ditto-things
- ditto-policies
environment:
- TZ=Europe/Berlin
- BIND_HOSTNAME=0.0.0.0
- DITTO_JWT_SECRET=NTOT-Vh8WRKWE52eV8zRiLs3a-gd8YUGSrvm5x2InZc
- DITTO_GATEWAY_PROXY_ENABLED=true
- AKKA_REMOTE_ENABLED=true
- AKKA_REMOTE_CANONICAL_HOSTNAME=ditto-gateway
- AKKA_REMOTE_CANONICAL_PORT=2551
- DITTO_GW_STREAMING_ENABLED=true
- DITTO_GW_MQTT_BROKER=192.168.192.26:1883
- DITTO_GW_MQTT_TOPIC_FILTER=smartcity/#
- DEVOPS_PASSWORD=OvP9WVB09aFDnYPyK52UIg
- JAVA_TOOL_OPTIONS=-Xms512m -Xmx1024m -Dditto.gateway.http.port=8080 -Dditto.gateway.http.api.enabled=true
- DITTO_APIDOC_ENABLED=true
- DITTO_GATEWAY_HTTP_API_ENABLED=true
networks:
traefik-public:
aliases:
- ditto-cluster
- ditto-gateway
labels:
- "traefik.enable=true"
- "traefik.http.routers.ditto.rule=Host(`ditto.digitribe.fr`)"
- "traefik.http.routers.ditto.entrypoints=websecure"
- "traefik.http.routers.ditto.tls.certresolver=letsencrypt"
- "traefik.http.services.ditto.loadbalancer.server.port=8080"
ditto-ui:
image: eclipse/ditto-ui:latest
container_name: smart-city-ditto-ui
restart: unless-stopped
depends_on:
- ditto-gateway
networks:
traefik-public:
aliases:
- ditto-ui
networks:
traefik-public:
external: true
smartcity-shared:
external: true
volumes:
ditto-mongo-data:
name: smart-city-digital-twin-martinique_ditto-mongo-data

29
docker-compose.emqx.yml Normal file
View File

@@ -0,0 +1,29 @@
services:
emqx:
image: emqx/emqx:5.4
container_name: emqx_emqx_1
restart: unless-stopped
networks:
- smartcity-shared
ports:
- "1885:1883"
- "8083:8083"
- "8883:8883"
- "8084:8084"
- "18083:18083"
environment:
- EMQX_NAME=emqx
- EMQX_HOST=emqx_emqx_1
volumes:
- emqx-data:/opt/emqx/data
- emqx-log:/opt/emqx/log
volumes:
emqx-data:
name: smart-city-emqx-data
emqx-log:
name: smart-city-emqx-log
networks:
smartcity-shared:
external: true

View File

@@ -10,7 +10,6 @@ networks:
volumes: volumes:
influxdb_data: influxdb_data:
external: false external: false
name: digital-twin_influxdb_data
services: services:
influxdb: influxdb:
@@ -24,10 +23,10 @@ services:
environment: environment:
- DOCKER_INFLUXDB_INIT_MODE=setup - DOCKER_INFLUXDB_INIT_MODE=setup
- DOCKER_INFLUXDB_INIT_USERNAME=admin - DOCKER_INFLUXDB_INIT_USERNAME=admin
- DOCKER_INFLUXDB_INIT_PASSWORD=admin1234 - DOCKER_INFLUXDB_INIT_PASSWORD=Digitribe972
- DOCKER_INFLUXDB_INIT_ORG=digitribe - DOCKER_INFLUXDB_INIT_ORG=digitribe
- DOCKER_INFLUXDB_INIT_BUCKET=iot_data - DOCKER_INFLUXDB_INIT_BUCKET=smartcity
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-admin-token - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-token
volumes: volumes:
- influxdb_data:/var/lib/influxdb2 - influxdb_data:/var/lib/influxdb2
restart: unless-stopped restart: unless-stopped

View File

@@ -1,42 +1,113 @@
# IoT Agent JSON - 3 instances (one per MQTT broker)
# Usage: docker compose -f docker-compose.yml -f docker-compose.iot-agent.yml up -d
version: '3.8' version: '3.8'
networks:
traefik-public:
external: true
smartcity-shared:
external: true
services: services:
iot-agent: # Instance 1: EMQX
container_name: smart-city-iot-agent iot-agent-emqx:
image: fiware/iotagent-json:latest image: fiware/iotagent-json:latest
container_name: smart-city-iot-agent-emqx
restart: unless-stopped restart: unless-stopped
environment:
- IOTA_CB_HOST=fiware-gis-quickstart-orion-1
- IOTA_CB_PORT=1026
- IOTA_NORTH_PORT=4041
- IOTA_REGISTRY_TYPE=mongodb
- IOTA_MONGO_URL=mongodb://smart-city-mongodb:27017/iotagent
- IOTA_PROVIDER_URL=http://smart-city-iot-agent:4041
- IOTA_CB_NGSI_VERSION=ld
networks: networks:
- smartcity-shared - smartcity-shared
- traefik-public - traefik-public
ports:
- "4041:4041"
environment:
# Context Broker (Orion-LD)
- IOTA_CB_HOST=smart-city-orion-ld
- IOTA_CB_PORT=1026
- IOTA_CB_NGSI_VERSION=v2
# IoT Agent settings
- IOTA_NORTH_PORT=4041
- IOTA_REGISTRY_TYPE=memory
# MQTT Listener - EMQX
- IOTA_MQTT_HOST=emqx_emqx_1
- IOTA_MQTT_PORT=1885
- IOTA_PROVIDER_URL=http://smart-city-iot-agent-emqx:4041
- IOTA_DEFAULT_RESOURCE=/
- IOTA_DEFAULT_APIKEY=smartcity-emqx
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.iot-agent.rule=Host(`iot-agent.digitribe.fr`)" - "traefik.http.routers.iot-agent-emqx.rule=Host(`iot-agent-emqx.digitribe.fr`)"
- "traefik.http.routers.iot-agent.entrypoints=websecure" - "traefik.http.routers.iot-agent-emqx.entrypoints=websecure"
- "traefik.http.routers.iot-agent.tls=true" - "traefik.http.routers.iot-agent-emqx.tls=true"
- "traefik.http.services.iot-agent.loadbalancer.server.port=4041" - "traefik.http.services.iot-agent-emqx.loadbalancer.server.port=4041"
iot-agent-mongodb: # Instance 2: Mosquitto
container_name: smart-city-mongodb iot-agent-mosquitto:
image: mongo:4.4 image: fiware/iotagent-json:latest
container_name: smart-city-iot-agent-mosquitto
restart: unless-stopped restart: unless-stopped
networks: networks:
- smartcity-shared - smartcity-shared
ports:
- "4042:4042"
environment:
# Context Broker (Orion-LD)
- IOTA_CB_HOST=smart-city-orion-ld
- IOTA_CB_PORT=1026
- IOTA_CB_NGSI_VERSION=v2
# IoT Agent settings
- IOTA_NORTH_PORT=4042
- IOTA_REGISTRY_TYPE=memory
# MQTT Listener - Mosquitto
- IOTA_MQTT_HOST=mosquitto
- IOTA_MQTT_PORT=1883
- IOTA_PROVIDER_URL=http://smart-city-iot-agent-mosquitto:4042
- IOTA_DEFAULT_RESOURCE=/
- IOTA_DEFAULT_APIKEY=smartcity-mosquitto
# MongoDB for IoT Agents
iot-mongodb:
image: mongo:4.4
container_name: smart-city-iot-mongodb
restart: unless-stopped
networks:
- smartcity-shared
ports:
- "27017:27017"
volumes: volumes:
- mongodb-data:/data/db - iot-mongodb-data:/data/db
healthcheck:
test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
# Instance3: BunkerM (Stellio NGSI-LD)
iot-agent-bunkerm:
image: fiware/iotagent-json:latest
container_name: smart-city-iot-agent-bunkerm
restart: unless-stopped
networks:
- smartcity-shared
ports:
- "4043:4043"
environment:
# Context Broker (Stellio NGSI-LD)
- IOTA_CB_HOST=stellio-api-gateway
- IOTA_CB_PORT=8080
- IOTA_CB_NGSI_VERSION=ld
# IoT Agent settings
- IOTA_NORTH_PORT=4043
- IOTA_REGISTRY_TYPE=memory
# MQTT Listener - BunkerM
- IOTA_MQTT_HOST=bunkerm_bunkerm_1
- IOTA_MQTT_PORT=1900
- IOTA_MQTT_USERNAME=bunker
- IOTA_MQTT_PASSWORD=bunker
- IOTA_PROVIDER_URL=http://smart-city-iot-agent-bunkerm:4043
- IOTA_DEFAULT_RESOURCE=/
- IOTA_DEFAULT_APIKEY=smartcity-bunkerm
networks:
smartcity-shared:
external: true
traefik-public:
external: true
volumes: volumes:
mongodb-data: iot-mongodb-data:
external: true
name: smart-city-digital-twin-martinique_iot-mongodb-data

View File

@@ -0,0 +1,71 @@
# Metabase - BI Dashboard for Smart City Digital Twin
# Usage: docker compose -f docker-compose.metabase.yml up -d
# Access: https://metabase.digitribe.fr
version: '3.8'
networks:
smartcity-shared:
external: true
traefik-public:
external: true
volumes:
metabase_data:
name: smart-city-metabase-data
services:
metabase-db:
image: postgres:15-alpine
container_name: metabase-postgres
restart: unless-stopped
networks:
- smartcity-shared
environment:
POSTGRES_DB: metabase
POSTGRES_USER: metabase
POSTGRES_PASSWORD: Digitribe972
volumes:
- metabase_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U metabase"]
interval: 10s
timeout: 5s
retries: 5
metabase:
image: metabase/metabase:latest
container_name: metabase-app
restart: unless-stopped
networks:
- smartcity-shared
- traefik-public
depends_on:
metabase-db:
condition: service_healthy
environment:
MB_DB_TYPE: postgres
MB_DB_DBNAME: metabase
MB_DB_PORT: 5432
MB_DB_USER: metabase
MB_DB_PASS: Digitribe972
MB_DB_HOST: metabase-postgres
MB_SITE_NAME: "Smart City Martinique"
MB_SITE_URL: "https://metabase.digitribe.fr"
MB_APPLICATION_DB: "file:/metabase-data/metabase.db"
MB_ENABLE_PASSWORD_LOGIN: "true"
MB_ADMIN_EMAIL: admin@digitribe.fr
MB_ADMIN_PASSWORD: Digitribe972
MB_JETTY_PORT: 3000
labels:
- "traefik.enable=true"
- "traefik.http.routers.metabase.rule=Host(`metabase.digitribe.fr`)"
- "traefik.http.routers.metabase.entrypoints=websecure"
- "traefik.http.routers.metabase.tls.certresolver=letsencrypt"
- "traefik.http.services.metabase.loadbalancer.server.port=3000"
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 120s

View File

@@ -0,0 +1,34 @@
# Mosquitto Broker for Smart City Simulator
# Usage: docker compose -f docker-compose.yml -f docker-compose.mosquitto.yml up -d
version: '3.8'
services:
mosquitto:
image: eclipse-mosquitto:latest
container_name: smart-city-mosquitto
restart: unless-stopped
networks:
- smartcity-shared
- traefik-public
ports:
- "1883:1883"
- "9001:9001"
volumes:
- mosquitto-data:/mosquitto/data
- mosquitto-logs:/mosquitto/log
command: mosquitto -c /mosquitto/config/mosquitto.conf
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "1883"]
interval: 30s
timeout: 10s
retries: 3
volumes:
mosquitto-data:
mosquitto-logs:
networks:
smartcity-shared:
external: true
traefik-public:
external: true

View File

@@ -0,0 +1,34 @@
# Orion Context Broker - Using existing MongoDB
# Usage: docker compose -f docker-compose.yml -f docker-compose.orion-ld.yml up -d
version: '3.8'
services:
orion-ld:
image: fiware/orion-ld:latest
container_name: smart-city-orion-ld
restart: unless-stopped
networks:
smartcity-shared:
aliases:
- orion-ld
- smart-city-orion-ld
traefik-public:
command: -dbhost smart-city-iot-mongodb -db orion
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:1026/version || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
labels:
- "traefik.enable=true"
- "traefik.http.routers.orion-ld.rule=Host(`orion-ld.digitribe.fr`)"
- "traefik.http.routers.orion-ld.entrypoints=websecure"
- "traefik.http.routers.orion-ld.tls=true"
- "traefik.http.services.orion-ld.loadbalancer.server.port=1026"
networks:
smartcity-shared:
external: true
traefik-public:
external: true

View File

@@ -0,0 +1,57 @@
# QuantumLeap for Stellio - Separate instance
# Usage: docker compose -f docker-compose.yml -f docker-compose.quantumleap-stellio.yml up -d
version: '3.8'
services:
quantumleap-stellio:
image: fiware/quantum-leap:latest
container_name: smart-city-quantumleap-stellio
restart: unless-stopped
networks:
- smartcity-shared
- traefik-public
environment:
- CRATE_HOST=smart-city-cratedb-stellio
- CRATE_PORT=4200
- CRATE_DB_NAME=quantumleap_stellio
- QL_CONFIG_DELETE_POLICY=oasis.settings.STELLIO_DELETE_POLICY
ports:
- "8669:8668"
labels:
- traefik.enable=true
- traefik.http.routers.quantum-leap-stellio.rule=Host(`quantum-leap-stellio.digitribe.fr`)
- traefik.http.services.quantum-leap-stellio.loadbalancer.server.port=8668
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8668/version"]
interval: 30s
timeout: 10s
retries: 3
depends_on:
- smart-city-cratedb-stellio
smart-city-cratedb-stellio:
image: crate:latest
container_name: smart-city-cratedb-stellio
restart: unless-stopped
networks:
smartcity-shared:
aliases:
- smart-city-cratedb-stellio
# Ports removed for security - accessed only via Docker network by QuantumLeap
volumes:
- smart-city-cratedb-stellio-data:/data
command: -Ccluster.name=stellio
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4200/"]
interval: 30s
timeout: 10s
retries: 3
volumes:
smart-city-cratedb-stellio-data:
networks:
smartcity-shared:
external: true
traefik-public:
external: true

View File

@@ -1,15 +1,20 @@
version: '3.8' version: '3.8'
networks:
traefik-public:
external: true
smartcity-shared:
external: true
services: services:
redis:
image: redis:7-alpine
container_name: smart-city-redis
networks:
- smartcity-shared
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
cratedb: cratedb:
container_name: smart-city-cratedb
image: crate:5.5 image: crate:5.5
container_name: smart-city-cratedb
restart: unless-stopped restart: unless-stopped
environment: environment:
- CRATE_HEAP_SIZE=1g - CRATE_HEAP_SIZE=1g
@@ -19,28 +24,54 @@ services:
- smartcity-shared - smartcity-shared
ports: ports:
- "4200:4200" - "4200:4200"
- "5432:5432" healthcheck:
test: ["CMD-SHELL", "curl -s -f http://localhost:4200/ > /dev/null || exit 1"]
interval: 10s
timeout: 5s
retries: 5
quantumleap: quantumleap:
build:
context: ./quantumleap
dockerfile: Dockerfile
image: quantumleap-patched:latest
container_name: smart-city-quantumleap container_name: smart-city-quantumleap
image: fiware/quantum-leap:latest
restart: unless-stopped restart: unless-stopped
environment: environment:
- QL_CRATEDB_HOST=smart-city-cratedb - CRATE_HOST=smart-city-cratedb
- QL_CRATEDB_PORT=5432 - CRATE_PORT=4200
- QL_CRATEDB_DB_NAME=quantumleap - CRATE_DB_NAME=quantumleap
- QL_LOG_LEVEL=INFO - QL_LOG_LEVEL=DEBUG
- RQ_MONITOR_REDIS_URL=redis://smart-city-redis:6379
- REDIS_HOST=smart-city-redis
- REDIS_PORT=6379
- WQ_OFFLOAD_WORK=True
- ORION_HOST=smart-city-orion-ld
- ORION_PORT=1026
depends_on: depends_on:
- cratedb cratedb:
condition: service_healthy
redis:
condition: service_healthy
networks: networks:
- smartcity-shared - smartcity-shared
- traefik-public - traefik-public
ports:
- "8668:8668"
# Le worker est géré en interne par wq (pas besoin de rq worker)
# Utilisation du command par défaut: python /src/ngsi-timeseries-api/src/app.py
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.quantumleap.rule=Host(`quantum-leap.digitribe.fr`)" - "traefik.http.routers.quantumleap.rule=Host(`quantum-leap.digitribe.fr')"
- "traefik.http.routers.quantumleap.entrypoints=websecure" - "traefik.http.routers.quantumleap.entrypoints=websecure"
- "traefik.http.routers.quantumleap.tls=true" - "traefik.http.routers.quantumleap.tls=true"
- "traefik.http.services.quantumleap.loadbalancer.server.port=5000" - "traefik.http.services.quantumleap.loadbalancer.server.port=8668"
volumes: volumes:
cratedb-data: cratedb-data:
networks:
smartcity-shared:
external: true
traefik-public:
external: true

View File

@@ -1,28 +1,16 @@
# Redpanda → InfluxDB Consumer # Redpanda → InfluxDB Consumer
# Lit les topics Redpanda et écrit dans InfluxDB pour Grafana # DÉSACTIVÉ — Redpanda broker non démarré
version: "3.8" # Usage: docker compose -f docker-compose.redpanda-consumer.yml up -d
services: services:
redpanda-consumer: redpanda-consumer:
image: python:3.11-slim image: python:3.11-slim
container_name: smart-city-redpanda-consumer container_name: smart-city-redpanda-consumer
restart: unless-stopped restart: "no"
command: > command: >
sh -c "pip install requests && python3 /app/consumer.py" sh -c "echo 'Redpanda consumer désactivé — Redpanda broker non démarré' && sleep infinity"
volumes:
- ./redpanda/consumer.py:/app/consumer.py:ro
environment:
- INFLUX_URL=http://smart-city-influxdb:8086
- INFLUX_TOKEN=my-super-admin-token
- INFLUX_ORG=digitribe
- INFLUX_BUCKET=iot_data
networks: networks:
- smartcity-shared - smartcity-shared
healthcheck:
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://smart-city-redpanda:9644/public_metrics')"]
interval: 30s
timeout: 10s
retries: 3
networks: networks:
smartcity-shared: smartcity-shared:

View File

@@ -0,0 +1,39 @@
# Apache Superset - Smart City Digital Twin
# Uses official apache/superset Docker Hub image
# Access: https://superset.digitribe.fr
networks:
smartcity-shared:
external: true
traefik-public:
external: true
volumes:
superset_home:
services:
superset:
image: apache/superset:latest
container_name: superset-app
restart: unless-stopped
networks:
- smartcity-shared
- traefik-public
environment:
# Use Superset's built-in SQLite for metadata (simplest setup)
# For production, replace with PostgreSQL
SUPERSET_SECRET_KEY: superset-secret-key-2024-change-me
volumes:
- superset_home:/app/superset_home
labels:
- "traefik.enable=true"
- "traefik.http.routers.superset.rule=Host(`superset.digitribe.fr`)"
- "traefik.http.routers.superset.entrypoints=websecure"
- "traefik.http.routers.superset.tls.certresolver=letsencrypt"
- "traefik.http.services.superset.loadbalancer.server.port=8088"
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8088/health || exit 1"]
interval: 30s
timeout: 15s
retries: 5
start_period: 120s

View File

@@ -0,0 +1,75 @@
version: "3.8"
# =============================================================================
# The Things Stack LoRaWAN Network Server — Smart City Digital Twin
# =============================================================================
# Déploiement derrière Traefik avec sous-domaines dédiés
# Subdomaines:
# - tts.digitribe.fr → Console web (port 1885)
# - tts-api.digitribe.fr → REST API (port 1884)
# =============================================================================
services:
tts-postgres:
container_name: smart-city-tts-postgres
image: postgres:14
restart: unless-stopped
environment:
- POSTGRES_PASSWORD=root
- POSTGRES_USER=root
- POSTGRES_DB=ttn_lorawan
volumes:
- tts-postgres-data:/var/lib/postgresql/data
networks:
- smartcity-shared
tts-redis:
container_name: smart-city-tts-redis
image: redis:7
command: redis-server --appendonly yes
restart: unless-stopped
volumes:
- tts-redis-data:/data
networks:
- smartcity-shared
tts-stack:
container_name: smart-city-tts-stack
image: thethingsnetwork/lorawan-stack:latest
entrypoint: ttn-lw-stack -c /config/ttn-lw-stack-docker.yml
command: start
restart: unless-stopped
depends_on:
- tts-redis
- tts-postgres
volumes:
- ./configuration/the-things-stack/config:/config:ro
- ./configuration/the-things-stack/blob:/srv/ttn-lorawan/public/blob
environment:
TTN_LW_BLOB_LOCAL_DIRECTORY: /srv/ttn-lorawan/public/blob
TTN_LW_REDIS_ADDRESS: tts-redis:6379
TTN_LW_IS_DATABASE_URI: postgres://root:***@tts-postgres:5432/ttn_lorawan?sslmode=disable
ports:
- "1701:1700/udp" # ChirpStack uses 1700
labels:
- "traefik.enable=true"
# Console web
- "traefik.http.routers.tts-console.rule=Host(`tts.digitribe.fr`)"
- "traefik.http.routers.tts-console.entrypoints=websecure"
- "traefik.http.routers.tts-console.tls.certresolver=letsencrypt"
- "traefik.http.services.tts-console.loadbalancer.server.port=1885"
# API REST
- "traefik.http.routers.tts-api.rule=Host(`tts-api.digitribe.fr`)"
- "traefik.http.routers.tts-api.entrypoints=websecure"
- "traefik.http.routers.tts-api.tls.certresolver=letsencrypt"
- "traefik.http.services.tts-api.loadbalancer.server.port=1884"
networks:
- traefik-public
- smartcity-shared
volumes:
tts-postgres-data:
tts-redis-data:
networks:
traefik-public:
external: true
smartcity-shared:
external: true

View File

@@ -9,41 +9,107 @@ networks:
external: true external: true
traefik-public: traefik-public:
external: true external: true
openremote_default:
external: true
services: services:
# Smart City Simulator # Smart City Simulator
simulator: simulator:
build: . build: .
container_name: smart-city-simulator container_name: smart-city-simulator
tty: true
stdin_open: true
networks: networks:
- smartcity-shared - smartcity-shared
- traefik-public - traefik-public
- openremote_default
environment: environment:
# MQTT Brokers # MQTT Brokers
- ENABLE_EMQX=true - ENABLE_EMQX=1
- ENABLE_MOSQUITTO=true - ENABLE_MOSQUITTO=1
- ENABLE_BUNKER=true - ENABLE_BUNKER=1
# Context Brokers - EMQX_HOST=emqx_emqx_1
- ENABLE_ORION=true - EMQX_PORT=1883
- ENABLE_STELLIO=true - MOSQUITTO_HOST=smart-city-mosquitto-1
- ENABLE_FROST=true - MOSQUITTO_PORT=1883
# Databases - BUNKERM_HOST=bunkerm-bunkerm-1
- ENABLE_INFLUX=true - BUNKERM_PORT=1900
# Context Brokers (DESACTIVE - tout passe par les IoT Agents via MQTT)
- ENABLE_ORION=false
- ENABLE_STELLIO=false
- ENABLE_FROST=false
# Databases (DESACTIVE - Telegraf s'occupe de InfluxDB)
- ENABLE_INFLUX=false
- INFLUX_URL=http://smart-city-influxdb:8086 - INFLUX_URL=http://smart-city-influxdb:8086
# Pulsar (Disabled for demo stability - was causing 0.0.0.0:0 errors) # OpenRemote
- ENABLE_OPENREMOTE=0
- OR_MQTT_USER=admin
- OR_MQTT_PASS=Digitribe972
- OR_URL=http://openremote-manager:8080
- OR_REALM=master
- OR_TOKEN_REALM=master
- OR_ADMIN_USER=admin
- OR_ADMIN_PASS=Digitribe972
- OR_CLIENT_SECRET=0oQjzTfiEELYmj5jFwT4iIuWUDtQDvVa
# Pulsar (Disabled for stability)
- ENABLE_PULSAR=false - ENABLE_PULSAR=false
# - PULSAR_HOST=smart-city-pulsar # Redpanda (Disabled)
# - PULSAR_PORT=6650
# Redpanda (Disabled - troubleshooting)
- ENABLE_REDPANDA=false - ENABLE_REDPANDA=false
- REDPANDA_BROKERS=smart-city-redpanda:9092 - REDPANDA_BROKERS=smart-city-redpanda:9092
# Simulation settings # Simulation settings
- INTERVAL=1 - INTERVAL=5
- LOG_LEVEL=INFO - LOG_LEVEL=INFO
restart: unless-stopped restart: unless-stopped
labels: labels:
- "traefik.enable=false" - "traefik.enable=false"
# GeoJSON Proxy — serves OpenRemote IoT sensor assets as GeoJSON for map display
geojson-proxy:
build: ./geojson-proxy
container_name: smart-city-geojson-proxy
networks:
- smartcity-shared
- traefik-public
- openremote_default
environment:
- OR_URL=http://openremote-manager:8080
- OR_ADMIN_USER=admin
- OR_ADMIN_PASS=Digitribe972
- OR_REALM=master
- DB_HOST=openremote-postgresql-1
- DB_PORT=5432
- DB_NAME=openremote
- DB_USER=postgres
- DB_PASS=
labels:
- "traefik.enable=true"
- "traefik.http.routers.geojson-proxy.rule=Host(`geojson-proxy.digitribe.fr`)"
- "traefik.http.routers.geojson-proxy.entrypoints=websecure"
- "traefik.http.routers.geojson-proxy.tls.certresolver=letsencrypt"
- "traefik.http.services.geojson-proxy.loadbalancer.server.port=8080"
restart: unless-stopped
# IoT Agent BunkerM - traduce les msgs MQTT bunker/bunker vers Orion-LD
iot-agent-bunkerm:
image: fiware/iotagent-json:latest
container_name: smart-city-iot-agent-bunkerm
networks:
- smartcity-shared
ports:
- "4043:4041"
environment:
- IOTA_CB_HOST=smart-city-orion-ld
- IOTA_CB_PORT=1026
- IOTA_CB_NGSI_VERSION=v2
- IOTA_REGISTRY_TYPE=memory
- IOTA_DEFAULT_APIKEY=smartcity-api-key
- IOTA_MQTT_USERNAME=bunker
- IOTA_MQTT_PASSWORD=bunker
- IOTA_MQTT_HOST=bunkerm_bunkerm_1
- IOTA_MQTT_PORT=1900
- IOTA_LOG_LEVEL=DEBUG
restart: unless-stopped
# InfluxDB (defined in docker-compose.influxdb.yml) # InfluxDB (defined in docker-compose.influxdb.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.influxdb.yml up -d # Run with: docker compose -f docker-compose.yml -f docker-compose.influxdb.yml up -d

80
docker-compose.yml.backup Normal file
View File

@@ -0,0 +1,80 @@
# Smart City Digital Twin Martinique — Main Docker Compose
# Usage: docker compose -p smart-city up -d
# This file defines the simulator and includes other services
version: '3.8'
networks:
smartcity-shared:
external: true
traefik-public:
external: true
services:
# Smart City Simulator
simulator:
build: .
container_name: smart-city-simulator
networks:
- smartcity-shared
- traefik-public
environment:
# MQTT Brokers - Only BunkerM enabled for stability
- ENABLE_EMQX=false
- ENABLE_MOSQUITTO=false
- ENABLE_BUNKER=true
- BUNKERM_HOST=bunkerm_bunkerm_1
- BUNKERM_PORT=1900
# Context Brokers (DESACTIVE - tout passe par les IoT Agents via MQTT)
- ENABLE_ORION=false
- ENABLE_STELLIO=false
- ENABLE_FROST=false
# Databases
- ENABLE_INFLUX=true
- INFLUX_URL=http://smart-city-influxdb:8086
# Pulsar (Disabled for demo stability - was causing 0.0.0.0:0 errors)
- ENABLE_PULSAR=false
# - PULSAR_HOST=smart-city-pulsar
# - PULSAR_PORT=6650
# Redpanda (Disabled - troubleshooting)
- ENABLE_REDPANDA=false
- REDPANDA_BROKERS=smart-city-redpanda:9092
# Simulation settings
- INTERVAL=1
- LOG_LEVEL=INFO
restart: unless-stopped
labels:
- "traefik.enable=false"
# IoT Agent BunkerM - traduce les msgs MQTT bunker/bunker vers Orion-LD
iot-agent-bunkerm:
image: fiware/iotagent-json:latest
container_name: smart-city-iot-agent-bunkerm
networks:
- smartcity-shared
ports:
- "4043:4041"
environment:
- IOTA_CB_HOST=smart-city-orion-ld
- IOTA_CB_PORT=1026
- IOTA_CB_NGSI_VERSION=v2
- IOTA_REGISTRY_TYPE=memory
- IOTA_DEFAULT_APIKEY=smartcity-api-key
- IOTA_MQTT_USERNAME=bunker
- IOTA_MQTT_PASSWORD=bunker
- IOTA_MQTT_HOST=bunkerm_bunkerm_1
- IOTA_MQTT_PORT=1900
- IOTA_LOG_LEVEL=DEBUG
restart: unless-stopped
# InfluxDB (defined in docker-compose.influxdb.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.influxdb.yml up -d
# Grafana (defined in docker-compose.grafana.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.grafana.yml up -d
# Pulsar (defined in pulsar/docker-compose.yml)
# Run with: docker compose -f docker-compose.yml -f pulsar/docker-compose.yml up -d
# Redpanda (defined in redpanda/docker-compose.yml)
# Run with: docker compose -f docker-compose.yml -f redpanda/docker-compose.yml up -d

80
docker-compose.yml.bak Normal file
View File

@@ -0,0 +1,80 @@
# Smart City Digital Twin Martinique — Main Docker Compose
# Usage: docker compose -p smart-city up -d
# This file defines the simulator and includes other services
version: '3.8'
networks:
smartcity-shared:
external: true
traefik-public:
external: true
services:
# Smart City Simulator
simulator:
build: .
container_name: smart-city-simulator
networks:
- smartcity-shared
- traefik-public
environment:
# MQTT Brokers - Only BunkerM enabled for stability
- ENABLE_EMQX=false
- ENABLE_MOSQUITTO=false
- ENABLE_BUNKER=true
- BUNKERM_HOST=bunkerm_bunkerm_1
- BUNKERM_PORT=1900
# Context Brokers (DESACTIVE - tout passe par les IoT Agents via MQTT)
- ENABLE_ORION=false
- ENABLE_STELLIO=false
- ENABLE_FROST=false
# Databases
- ENABLE_INFLUX=true
- INFLUX_URL=http://smart-city-influxdb:8086
# Pulsar (Disabled for demo stability - was causing 0.0.0.0:0 errors)
- ENABLE_PULSAR=false
# - PULSAR_HOST=smart-city-pulsar
# - PULSAR_PORT=6650
# Redpanda (Disabled - troubleshooting)
- ENABLE_REDPANDA=false
- REDPANDA_BROKERS=smart-city-redpanda:9092
# Simulation settings
- INTERVAL=1
- LOG_LEVEL=INFO
restart: unless-stopped
labels:
- "traefik.enable=false"
# IoT Agent BunkerM - traduce les msgs MQTT bunker/bunker vers Orion-LD
iot-agent-bunkerm:
image: fiware/iotagent-json:latest
container_name: smart-city-iot-agent-bunkerm
networks:
- smartcity-shared
ports:
- "4043:4041"
environment:
- IOTA_CB_HOST=smart-city-orion-ld
- IOTA_CB_PORT=1026
- IOTA_CB_NGSI_VERSION=v2
- IOTA_REGISTRY_TYPE=memory
- IOTA_DEFAULT_APIKEY=smartcity-api-key
- IOTA_MQTT_USERNAME=bunker
- IOTA_MQTT_PASSWORD=bunker
- IOTA_MQTT_HOST=bunkerm_bunkerm_1
- IOTA_MQTT_PORT=1900
- IOTA_LOG_LEVEL=DEBUG
restart: unless-stopped
# InfluxDB (defined in docker-compose.influxdb.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.influxdb.yml up -d
# Grafana (defined in docker-compose.grafana.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.grafana.yml up -d
# Pulsar (defined in pulsar/docker-compose.yml)
# Run with: docker compose -f docker-compose.yml -f pulsar/docker-compose.yml up -d
# Redpanda (defined in redpanda/docker-compose.yml)
# Run with: docker compose -f docker-compose.yml -f redpanda/docker-compose.yml up -d

View File

@@ -0,0 +1,91 @@
# Smart City Digital Twin Martinique — Main Docker Compose
# Usage: docker compose -p smart-city up -d
# This file defines the simulator and includes other services
version: '3.8'
networks:
smartcity-shared:
external: true
traefik-public:
external: true
openremote_default:
external: true
services:
# Smart City Simulator
simulator:
build: .
container_name: smart-city-simulator
tty: true
stdin_open: true
networks:
- smartcity-shared
- traefik-public
- openremote_default
environment:
# MQTT Brokers - ALL enabled
- ENABLE_EMQX=1
- ENABLE_MOSQUITTO=1
- ENABLE_BUNKER=1
- BUNKERM_HOST=bunkerm_bunkerm_1
- BUNKERM_PORT=1900
# Context Brokers (DESACTIVE - tout passe par les IoT Agents via MQTT)
- ENABLE_ORION=false
- ENABLE_STELLIO=false
- ENABLE_FROST=false
# Databases
- ENABLE_INFLUX=true
- INFLUX_URL=http://smart-city-influxdb:8086
# OpenRemote
- ENABLE_OPENREMOTE=1
- OR_URL=http://openremote_manager_1:8080
- OR_REALM=master
- OR_TOKEN_REALM=master
- OR_ADMIN_USER=admin
- OR_ADMIN_PASS=Digitribe972
- OR_CLIENT_SECRET=0oQjzTfiEELYmj5jFwT4iIuWUDtQDvVa
# Pulsar (Disabled for demo stability)
- ENABLE_PULSAR=false
# Redpanda (Disabled)
- ENABLE_REDPANDA=false
- REDPANDA_BROKERS=smart-city-redpanda:9092
# Simulation settings
- INTERVAL=5
- LOG_LEVEL=INFO
restart: unless-stopped
labels:
- "traefik.enable=false"
# IoT Agent BunkerM - traduce les msgs MQTT bunker/bunker vers Orion-LD
iot-agent-bunkerm:
image: fiware/iotagent-json:latest
container_name: smart-city-iot-agent-bunkerm
networks:
- smartcity-shared
ports:
- "4043:4041"
environment:
- IOTA_CB_HOST=smart-city-orion-ld
- IOTA_CB_PORT=1026
- IOTA_CB_NGSI_VERSION=v2
- IOTA_REGISTRY_TYPE=memory
- IOTA_DEFAULT_APIKEY=smartcity-api-key
- IOTA_MQTT_USERNAME=bunker
- IOTA_MQTT_PASSWORD=bunker
- IOTA_MQTT_HOST=bunkerm_bunkerm_1
- IOTA_MQTT_PORT=1900
- IOTA_LOG_LEVEL=DEBUG
restart: unless-stopped
# InfluxDB (defined in docker-compose.influxdb.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.influxdb.yml up -d
# Grafana (defined in docker-compose.grafana.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.grafana.yml up -d
# Pulsar (defined in pulsar/docker-compose.yml)
# Run with: docker compose -f docker-compose.yml -f pulsar/docker-compose.yml up -d
# Redpanda (defined in redpanda/docker-compose.yml)
# Run with: docker compose -f docker-compose.yml -f redpanda/docker-compose.yml up -d

85
docker-compose.yml.bak2 Normal file
View File

@@ -0,0 +1,85 @@
# Smart City Digital Twin Martinique — Main Docker Compose
# Usage: docker compose -p smart-city up -d
# This file defines the simulator and includes other services
version: '3.8'
networks:
smartcity-shared:
external: true
traefik-public:
external: true
services:
# Smart City Simulator
simulator:
build: .
container_name: smart-city-simulator
networks:
- smartcity-shared
- traefik-public
environment:
# MQTT Brokers - Only BunkerM enabled for stability
- ENABLE_EMQX=false
- ENABLE_MOSQUITTO=false
- ENABLE_BUNKER=true
- BUNKERM_HOST=bunkerm_bunkerm_1
- BUNKERM_PORT=1900
# Context Brokers (DESACTIVE - tout passe par les IoT Agents via MQTT)
- ENABLE_ORION=false
- ENABLE_STELLIO=false
- ENABLE_FROST=false
# Databases
- ENABLE_INFLUX=true
- INFLUX_URL=http://smart-city-influxdb:8086
# OpenRemote
- ENABLE_OPENREMOTE=true
- OR_URL=http://openremote_manager_1:8080
- OR_ADMIN_USER=admin
- OR_ADMIN_PASS=Digitribe972
# Pulsar (Disabled for demo stability - was causing 0.0.0.0:0 errors)
- ENABLE_PULSAR=false
# - PULSAR_HOST=smart-city-pulsar
# - PULSAR_PORT=6650
# Redpanda (Disabled - troubleshooting)
- ENABLE_REDPANDA=false
- REDPANDA_BROKERS=smart-city-redpanda:9092
# Simulation settings
- INTERVAL=1
- LOG_LEVEL=INFO
restart: unless-stopped
labels:
- "traefik.enable=false"
# IoT Agent BunkerM - traduce les msgs MQTT bunker/bunker vers Orion-LD
iot-agent-bunkerm:
image: fiware/iotagent-json:latest
container_name: smart-city-iot-agent-bunkerm
networks:
- smartcity-shared
ports:
- "4043:4041"
environment:
- IOTA_CB_HOST=smart-city-orion-ld
- IOTA_CB_PORT=1026
- IOTA_CB_NGSI_VERSION=v2
- IOTA_REGISTRY_TYPE=memory
- IOTA_DEFAULT_APIKEY=smartcity-api-key
- IOTA_MQTT_USERNAME=bunker
- IOTA_MQTT_PASSWORD=bunker
- IOTA_MQTT_HOST=bunkerm_bunkerm_1
- IOTA_MQTT_PORT=1900
- IOTA_LOG_LEVEL=DEBUG
restart: unless-stopped
# InfluxDB (defined in docker-compose.influxdb.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.influxdb.yml up -d
# Grafana (defined in docker-compose.grafana.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.grafana.yml up -d
# Pulsar (defined in pulsar/docker-compose.yml)
# Run with: docker compose -f docker-compose.yml -f pulsar/docker-compose.yml up -d
# Redpanda (defined in redpanda/docker-compose.yml)
# Run with: docker compose -f docker-compose.yml -f redpanda/docker-compose.yml up -d

80
docker-compose.yml.bak3 Normal file
View File

@@ -0,0 +1,80 @@
# Smart City Digital Twin Martinique — Main Docker Compose
# Usage: docker compose -p smart-city up -d
# This file defines the simulator and includes other services
version: '3.8'
networks:
smartcity-shared:
external: true
traefik-public:
external: true
services:
# Smart City Simulator
simulator:
build: .
container_name: smart-city-simulator
networks:
- smartcity-shared
- traefik-public
environment:
# MQTT Brokers - Only BunkerM enabled for stability
- ENABLE_EMQX=false
- ENABLE_MOSQUITTO=false
- ENABLE_BUNKER=true
- BUNKERM_HOST=bunkerm_bunkerm_1
- BUNKERM_PORT=1900
# Context Brokers (DESACTIVE - tout passe par les IoT Agents via MQTT)
- ENABLE_ORION=false
- ENABLE_STELLIO=false
- ENABLE_FROST=false
# Databases
- ENABLE_INFLUX=true
- INFLUX_URL=http://smart-city-influxdb:8086
# Pulsar (Disabled for demo stability - was causing 0.0.0.0:0 errors)
- ENABLE_PULSAR=false
# - PULSAR_HOST=smart-city-pulsar
# - PULSAR_PORT=6650
# Redpanda (Disabled - troubleshooting)
- ENABLE_REDPANDA=false
- REDPANDA_BROKERS=smart-city-redpanda:9092
# Simulation settings
- INTERVAL=1
- LOG_LEVEL=INFO
restart: unless-stopped
labels:
- "traefik.enable=false"
# IoT Agent BunkerM - traduce les msgs MQTT bunker/bunker vers Orion-LD
iot-agent-bunkerm:
image: fiware/iotagent-json:latest
container_name: smart-city-iot-agent-bunkerm
networks:
- smartcity-shared
ports:
- "4043:4041"
environment:
- IOTA_CB_HOST=smart-city-orion-ld
- IOTA_CB_PORT=1026
- IOTA_CB_NGSI_VERSION=v2
- IOTA_REGISTRY_TYPE=memory
- IOTA_DEFAULT_APIKEY=smartcity-api-key
- IOTA_MQTT_USERNAME=bunker
- IOTA_MQTT_PASSWORD=bunker
- IOTA_MQTT_HOST=bunkerm_bunkerm_1
- IOTA_MQTT_PORT=1900
- IOTA_LOG_LEVEL=DEBUG
restart: unless-stopped
# InfluxDB (defined in docker-compose.influxdb.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.influxdb.yml up -d
# Grafana (defined in docker-compose.grafana.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.grafana.yml up -d
# Pulsar (defined in pulsar/docker-compose.yml)
# Run with: docker compose -f docker-compose.yml -f pulsar/docker-compose.yml up -d
# Redpanda (defined in redpanda/docker-compose.yml)
# Run with: docker compose -f docker-compose.yml -f redpanda/docker-compose.yml up -d

80
docker-compose.yml.orig Normal file
View File

@@ -0,0 +1,80 @@
# Smart City Digital Twin Martinique — Main Docker Compose
# Usage: docker compose -p smart-city up -d
# This file defines the simulator and includes other services
version: '3.8'
networks:
smartcity-shared:
external: true
traefik-public:
external: true
services:
# Smart City Simulator
simulator:
build: .
container_name: smart-city-simulator
networks:
- smartcity-shared
- traefik-public
environment:
# MQTT Brokers - Only BunkerM enabled for stability
- ENABLE_EMQX=false
- ENABLE_MOSQUITTO=false
- ENABLE_BUNKER=true
- BUNKERM_HOST=bunkerm_bunkerm_1
- BUNKERM_PORT=1900
# Context Brokers (DESACTIVE - tout passe par les IoT Agents via MQTT)
- ENABLE_ORION=false
- ENABLE_STELLIO=false
- ENABLE_FROST=false
# Databases
- ENABLE_INFLUX=true
- INFLUX_URL=http://smart-city-influxdb:8086
# Pulsar (Disabled for demo stability - was causing 0.0.0.0:0 errors)
- ENABLE_PULSAR=false
# - PULSAR_HOST=smart-city-pulsar
# - PULSAR_PORT=6650
# Redpanda (Disabled - troubleshooting)
- ENABLE_REDPANDA=false
- REDPANDA_BROKERS=smart-city-redpanda:9092
# Simulation settings
- INTERVAL=1
- LOG_LEVEL=INFO
restart: unless-stopped
labels:
- "traefik.enable=false"
# IoT Agent BunkerM - traduce les msgs MQTT bunker/bunker vers Orion-LD
iot-agent-bunkerm:
image: fiware/iotagent-json:latest
container_name: smart-city-iot-agent-bunkerm
networks:
- smartcity-shared
ports:
- "4043:4041"
environment:
- IOTA_CB_HOST=smart-city-orion-ld
- IOTA_CB_PORT=1026
- IOTA_CB_NGSI_VERSION=v2
- IOTA_REGISTRY_TYPE=memory
- IOTA_DEFAULT_APIKEY=smartcity-api-key
- IOTA_MQTT_USERNAME=bunker
- IOTA_MQTT_PASSWORD=bunker
- IOTA_MQTT_HOST=bunkerm_bunkerm_1
- IOTA_MQTT_PORT=1900
- IOTA_LOG_LEVEL=DEBUG
restart: unless-stopped
# InfluxDB (defined in docker-compose.influxdb.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.influxdb.yml up -d
# Grafana (defined in docker-compose.grafana.yml)
# Run with: docker compose -f docker-compose.yml -f docker-compose.grafana.yml up -d
# Pulsar (defined in pulsar/docker-compose.yml)
# Run with: docker compose -f docker-compose.yml -f pulsar/docker-compose.yml up -d
# Redpanda (defined in redpanda/docker-compose.yml)
# Run with: docker compose -f docker-compose.yml -f redpanda/docker-compose.yml up -d

63
docker_exporter.py Normal file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Simple Docker metrics exporter for Prometheus"""
import docker
from prometheus_client import Gauge, Counter, generate_latest, CONTENT_TYPE_LATEST
from http.server import HTTPServer, BaseHTTPRequestHandler
import sys
# Métriques
container_cpu_usage = Gauge('docker_container_cpu_usage_seconds_total', 'CPU usage in seconds', ['container', 'image'])
container_memory_usage = Gauge('docker_container_memory_usage_bytes', 'Memory usage in bytes', ['container', 'image'])
container_network_rx = Counter('docker_container_network_receive_bytes_total', 'Network receive bytes', ['container', 'image'])
container_network_tx = Counter('docker_container_network_transmit_bytes_total', 'Network transmit bytes', ['container', 'image'])
container_status = Gauge('docker_container_status', 'Container status (1=running, 0=stopped)', ['container', 'image'])
client = docker.from_env()
class MetricsHandler(BaseHTTPRequestHandler):
def do_GET(self):
# Mettre à jour les métriques
for container in client.containers.list():
name = container.name
image = container.image.tags[0] if container.image.tags else 'unknown'
try:
stats = container.stats(stream=False)
# CPU
cpu_delta = stats['cpu_stats']['cpu_usage']['total_usage'] - stats['precpu_stats']['cpu_usage']['total_usage']
system_delta = stats['cpu_stats']['system_cpu_usage'] - stats['precpu_stats']['system_cpu_usage']
if system_delta > 0:
cpu_usage = (cpu_delta / system_delta) * stats['cpu_stats'].get('online_cpus', 1)
container_cpu_usage.labels(name, image).set(cpu_usage)
# Memory
mem_usage = stats['memory_stats']['usage']
container_memory_usage.labels(name, image).set(mem_usage)
# Network
if 'networks' in stats:
for iface, data in stats['networks'].items():
container_network_rx.labels(name, image).inc(data.get('rx_bytes', 0))
container_network_tx.labels(name, image).inc(data.get('tx_bytes', 0))
# Status
container_status.labels(name, image).set(1 if container.status == 'running' else 0)
except Exception as e:
print(f"Error getting stats for {name}: {e}")
# Exposer les métriques
self.send_response(200)
self.send_header('Content-Type', CONTENT_TYPE_LATEST)
self.end_headers()
self.wfile.write(generate_latest())
def log_message(self, format, *args):
pass # Suppress logs
if __name__ == '__main__':
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8005
server = HTTPServer(('0.0.0.0', port), MetricsHandler)
print(f"Docker metrics exporter listening on port {port}")
server.serve_forever()

File diff suppressed because one or more lines are too long

73
docs/geospatial.md Normal file
View File

@@ -0,0 +1,73 @@
# Smart City Digital Twin — Documentation Infrastructure
> Dernière mise à jour : 2026-05-17 20:00
## Architecture Géospatiale
### Services déployés
| Service | URL | Statut | Credentials |
|---------|-----|--------|-------------|
| GeoServer | https://geoserver.digitribe.fr | ✅ UP | admin / Digitribe972 |
| PostGIS dédié | postgis-smartcity:5432 | ✅ UP | smartcity / SmartCity972 |
| MapStore | https://mapstore.digitribe.fr | ✅ UP | - |
### GeoServer
#### Workspace: `Digitribe`
- **Data Store**: `postgis-smartcity` → PostgreSQL/PostGIS dédié
- **Couche**: `sensors` — 55 capteurs IoT importés depuis OpenRemote
- **WMS/WFS**: Activés via le plugin GeoMesa (à installer)
#### Données importées
55 capteurs IoT depuis OpenRemote (table `openremote.asset`, type `IOTSensor`) :
- Types : traffic, airquality, parking, noise, weather, light
- Coordonnées GPS : lat/lon (EPSG:4326)
- Table PostGIS : `public.sensors` (id, name, type, location, attributes)
### PostGIS dédié
- **Conteneur**: postgis-smartcity
- **Image**: postgis/postgis:15-3.4
- **Port host**: 5433
- **Base**: smartcity
- **Schéma**: public
- **Table sensors**: 55 lignes, index GIST sur location
### MapStore
- **URL**: https://mapstore.digitribe.fr
- **CORS**: GeoServer ajouté
- **Couche GeoServer**: sensors accessible via WMS
## Services Bloqués
### OpenRemote Agents MQTT
- **Problème**: API REST retourne 403 malgré tous les tokens Keycloak
- **Cause**: OpenRemote a son propre système d'authorization indépendant
- **Solution**: Se connecter manuellement via un navigateur réel
### Ditto Digital Twin
- **Problème**: MongoDB localhost hardcodé dans le JAR Ditto 3.8.12
- **Cause**: Les variables d'environnement MONGO_HOST ne sont pas reconnues
- **Solution**: Modifier le JAR ou utiliser un hostname localhost → MongoDB
### Prometheus + Grafana
- **Problème**: Réseau interne inaccessible depuis le conteneur Prometheus
- **Solution**: Reconfigurer le réseau ou utiliser les endpoints exposés
### GeoMesa + KeplerGL
- **GeoMesa**: Installation complexe (Maven, binaires pré-construits nécessaires)
- **KeplerGL**: Image Docker incomplète, build npm trop long
- **Solution**: Prévoir une session dédiée pour l'installation
## Fichiers de configuration
- `docker-compose.postgis.yml` — PostGIS dédié
- `docker-compose.kepler.yml` — KeplerGL (non fonctionnel)
- `docker-compose.ditto.yml` — Ditto (MongoDB à corriger)
- `traefik-config/dynamic/routes.yml` — GeoServer ajouté au CORS MapStore
## Prochaines étapes
1. GeoMesa : télécharger les binaires pré-construits (geomesa-gt-postgis)
2. KeplerGL : build Docker multi-stage ou image officielle
3. OpenRemote : connexion manuelle via navigateur réel
4. Ditto : corriger la config MongoDB

48
flink/docker-compose.yml Normal file
View File

@@ -0,0 +1,48 @@
# Smart City Digital Twin Martinique — Apache Flink
# Usage: docker compose -f flink/docker-compose.yml up -d
# Image officielle Apache Flink 1.20.1 avec digest vérifié
networks:
smartcity-shared:
external: true
services:
jobmanager:
image: apache/flink:1.20.1-scala_2.12-java17@sha256:ecc5785594eff2d94e29e6b116b3124c0cdb3a9c952ebdf38ef0fef90fb9913d
container_name: flink-jobmanager
command: jobmanager
networks:
- smartcity-shared
ports:
- "8081:8081" # Flink Web UI
environment:
- |
FLINK_PROPERTIES=
jobmanager.rpc.address: jobmanager
jobmanager.memory.process.size: 1024m
taskmanager.memory.process.size: 1024m
taskmanager.numberOfTaskSlots: 4
parallelism.default: 2
rest.port: 8081
restart: unless-stopped
labels:
- "traefik.enable=false"
taskmanager:
image: apache/flink:1.20.1-scala_2.12-java17@sha256:ecc5785594eff2d94e29e6b116b3124c0cdb3a9c952ebdf38ef0fef90fb9913d
container_name: flink-taskmanager
command: taskmanager
networks:
- smartcity-shared
depends_on:
- jobmanager
environment:
- |
FLINK_PROPERTIES=
jobmanager.rpc.address: jobmanager
taskmanager.memory.process.size: 1024m
taskmanager.numberOfTaskSlots: 4
parallelism.default: 2
restart: unless-stopped
labels:
- "traefik.enable=false"

5
geojson-proxy/Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM python:3.11-slim
WORKDIR /app
COPY geojson_proxy.py .
EXPOSE 8080
CMD ["python", "geojson_proxy.py"]

View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""GeoJSON proxy service for OpenRemote assets map display.
Fetches all assets with location from OpenRemote REST API and serves them as GeoJSON.
"""
import json
import os
import urllib.request
import urllib.error
import urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler
OR_URL = os.environ.get("OR_URL", "http://openremote_manager_1:8080")
OR_ADMIN_USER = os.environ.get("OR_ADMIN_USER", "admin")
OR_ADMIN_PASS = os.environ.get("OR_ADMIN_PASS", "Digitribe972")
OR_REALM = os.environ.get("OR_REALM", "master")
OR_CLIENT_SECRET = os.environ.get("OR_CLIENT_SECRET", "0oQjzTfiEELYmj5jFwT4iIuWUDtQDvVa")
_token_cache = {"token": "", "expires": 0}
def get_token():
"""Fetch an OpenRemote access token using admin credentials."""
import time
if _token_cache["token"] and _token_cache["expires"] > time.time() + 30:
return _token_cache["token"]
data = urllib.parse.urlencode({
"username": OR_ADMIN_USER,
"password": OR_ADMIN_PASS,
"grant_type": "password",
"client_id": "openremote",
"client_secret": OR_CLIENT_SECRET
}).encode()
req = urllib.request.Request(
f"http://openremote-keycloak-1:8080/auth/realms/{OR_REALM}/protocol/openid-connect/token",
data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
method="POST"
)
resp = urllib.request.urlopen(req, timeout=10)
body = json.loads(resp.read())
_token_cache["token"] = body["access_token"]
_token_cache["expires"] = time.time() + max(body.get("expires_in", 300) - 60, 30)
return _token_cache["token"]
def fetch_assets():
"""Fetch all assets with location from OpenRemote REST API."""
token = get_token()
features = []
# Query all assets with location attribute
try:
# Use the asset query API to get all assets with location
query = json.dumps({
"attributes": {
"location": {
"value": {"$exists": True}
}
}
}).encode()
req = urllib.request.Request(
f"{OR_URL}/api/{OR_REALM}/asset/query",
data=query,
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"Content-Type": "application/json"
},
method="POST"
)
with urllib.request.urlopen(req, timeout=30) as r:
assets = json.loads(r.read().decode())
if not isinstance(assets, list):
assets = [assets]
except Exception as e:
# Fallback: try to get all assets and filter
try:
req = urllib.request.Request(
f"{OR_URL}/api/{OR_REALM}/asset?limit=100",
headers={"Authorization": f"Bearer {token}", "Accept": "application/json"}
)
with urllib.request.urlopen(req, timeout=30) as r:
assets = json.loads(r.read().decode())
if not isinstance(assets, list):
assets = [assets]
except Exception as e2:
return {"type": "FeatureCollection", "features": [], "error": str(e2)}
for asset in assets:
try:
attrs = asset.get("attributes", {})
location = attrs.get("location", {})
value = location.get("value") if isinstance(location, dict) else None
coords = value.get("coordinates") if isinstance(value, dict) else None
if not coords or len(coords) < 2:
continue
props = {
"id": asset.get("id"),
"name": asset.get("name", ""),
"type": asset.get("type", ""),
"realm": asset.get("realm", ""),
}
# Add sensorType for color mapping
sensor_type = attrs.get("sensorType", {})
if isinstance(sensor_type, dict):
props["sensorType"] = sensor_type.get("value", "")
# Add scalar attribute values
for attr_name, attr_val in attrs.items():
if isinstance(attr_val, dict):
v = attr_val.get("value")
if v is not None and not isinstance(v, (dict, list)):
props[attr_name] = v
# GeoJSON coordinates are [longitude, latitude]
features.append({
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [coords[1], coords[0]]},
"properties": props
})
except Exception:
continue
return {"type": "FeatureCollection", "features": features}
class GeoJSONHandler(BaseHTTPRequestHandler):
def do_GET(self):
path = self.path.split("?")[0]
if path == "/geojson":
try:
result = fetch_assets()
body = json.dumps(result).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
except Exception as e:
error_body = json.dumps({"error": str(e)}).encode()
self.send_response(500)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(error_body)))
self.end_headers()
self.wfile.write(error_body)
elif path == "/health":
body = json.dumps({"status": "ok"}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
print(f"[geojson-proxy] {args[0]}")
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", 8080), GeoJSONHandler)
print("[geojson-proxy] Listening on 0.0.0.0:8080")
server.serve_forever()

View File

@@ -0,0 +1,377 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"panels": [
{
"title": "Air Quality - PM2.5 (\u00b5g/m\u00b3)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"datasource": {
"type": "influxdb",
"uid": "dd1bfc24-de9d-4c23-8a3c-151d153f8169"
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"airquality\")\n |> filter(fn: (r) => r[\"_field\"] == \"pm25_ugm3\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"PM2.5\")",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "\u00b5g/m\u00b3",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 25
},
{
"color": "orange",
"value": 50
},
{
"color": "red",
"value": 100
}
]
}
}
}
},
{
"title": "Air Quality - CO (mg/m\u00b3)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"datasource": {
"type": "influxdb",
"uid": "dd1bfc24-de9d-4c23-8a3c-151d153f8169"
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"airquality\")\n |> filter(fn: (r) => r[\"_field\"] == \"co_mgm3\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"CO\")",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "mg/m\u00b3",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 5
},
{
"color": "red",
"value": 15
}
]
}
}
}
},
{
"title": "Traffic - Average Speed (km/h)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"datasource": {
"type": "influxdb",
"uid": "dd1bfc24-de9d-4c23-8a3c-151d153f8169"
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"traffic\")\n |> filter(fn: (r) => r[\"_field\"] == \"average_speed_kmh\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"Speed\")",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "km/h",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "yellow",
"value": 20
},
{
"color": "green",
"value": 40
}
]
}
}
}
},
{
"title": "Traffic - Congestion Level",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 16
},
"datasource": {
"type": "influxdb",
"uid": "dd1bfc24-de9d-4c23-8a3c-151d153f8169"
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"traffic\")\n |> filter(fn: (r) => r[\"_field\"] == \"congestion_level\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"Congestion\")",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "",
"min": 0,
"max": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 0.5
},
{
"color": "red",
"value": 0.8
}
]
}
}
}
},
{
"title": "Parking - Available Spots",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 32
},
"datasource": {
"type": "influxdb",
"uid": "dd1bfc24-de9d-4c23-8a3c-151d153f8169"
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"parking\")\n |> filter(fn: (r) => r[\"_field\"] == \"available_spots\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"Available\")",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "spots"
}
}
},
{
"title": "Parking - Occupancy (%)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 32
},
"datasource": {
"type": "influxdb",
"uid": "dd1bfc24-de9d-4c23-8a3c-151d153f8169"
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"parking\")\n |> filter(fn: (r) => r[\"_field\"] == \"occupancy_percent\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"Occupancy\")",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100
}
}
},
{
"title": "Noise Level (dB)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 48
},
"datasource": {
"type": "influxdb",
"uid": "dd1bfc24-de9d-4c23-8a3c-151d153f8169"
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"noise\")\n |> filter(fn: (r) => r[\"_field\"] == \"noise_level_db\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"Noise\")",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "dB",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 65
},
{
"color": "orange",
"value": 80
},
{
"color": "red",
"value": 95
}
]
}
}
}
},
{
"title": "Weather - Temperature (\u00b0C)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 48
},
"datasource": {
"type": "influxdb",
"uid": "dd1bfc24-de9d-4c23-8a3c-151d153f8169"
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"weather\")\n |> filter(fn: (r) => r[\"_field\"] == \"temperature_celsius\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"Temperature\")",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "\u00b0C"
}
}
},
{
"title": "Light - Brightness (lux)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 64
},
"datasource": {
"type": "influxdb",
"uid": "dd1bfc24-de9d-4c23-8a3c-151d153f8169"
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"light\")\n |> filter(fn: (r) => r[\"_field\"] == \"brightness_lux\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"Brightness\")",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "lux"
}
}
},
{
"title": "Light - Power Consumption (W)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 64
},
"datasource": {
"type": "influxdb",
"uid": "dd1bfc24-de9d-4c23-8a3c-151d153f8169"
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"light\")\n |> filter(fn: (r) => r[\"_field\"] == \"power_consumption_w\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"Power\")",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "W"
}
}
}
],
"schemaVersion": 38,
"style": "dark",
"tags": [
"smart-city",
"martinique",
"iot",
"complete"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"title": "Smart City Digital Twin - Martinique (COMPLET)",
"uid": "smartcity-martinique-complete",
"version": 1
}

View File

@@ -0,0 +1,155 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"panels": [
{
"title": "CPU Usage (Docker Containers)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "f9ddd651-33ec-4dad-a950-e1375a964315"
},
"targets": [
{
"expr": "rate(docker_container_cpu_usage_seconds_total[5m]) * 100",
"legendFormat": "{{container}}",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent"
}
}
},
{
"title": "Memory Usage (Docker Containers)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "f9ddd651-33ec-4dad-a950-e1375a964315"
},
"targets": [
{
"expr": "docker_container_memory_usage_bytes / 1024 / 1024 / 1024",
"legendFormat": "{{container}} (GB)",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "decbytes"
}
}
},
{
"title": "Network Receive (Docker Containers)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"datasource": {
"type": "prometheus",
"uid": "f9ddd651-33ec-4dad-a950-e1375a964315"
},
"targets": [
{
"expr": "rate(docker_container_network_receive_bytes_total[5m]) * 8",
"legendFormat": "{{container}} RX",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "bps"
}
}
},
{
"title": "Network Transmit (Docker Containers)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 16
},
"datasource": {
"type": "prometheus",
"uid": "f9ddd651-33ec-4dad-a950-e1375a964315"
},
"targets": [
{
"expr": "rate(docker_container_network_transmit_bytes_total[5m]) * 8",
"legendFormat": "{{container}} TX",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"unit": "bps"
}
}
},
{
"title": "Container Status (1=Running, 0=Stopped)",
"type": "state-timeline",
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 32
},
"datasource": {
"type": "prometheus",
"uid": "f9ddd651-33ec-4dad-a950-e1375a964315"
},
"targets": [
{
"expr": "docker_container_status",
"legendFormat": "{{container}}",
"refId": "A"
}
]
}
],
"schemaVersion": 38,
"style": "dark",
"tags": [
"docker",
"containers",
"metrics",
"prometheus"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"title": "Smart City - Docker Containers Metrics",
"uid": "smartcity-docker-metrics",
"version": 1
}

View File

@@ -0,0 +1,229 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"title": "Air Quality - PM2.5",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"airquality\")\n |> filter(fn: (r) => r[\"_field\"] == \"pm25_ugm3\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")",
"refId": "A"
}
],
"datasource": {
"type": "influxdb",
"uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d"
}
},
{
"title": "Air Quality - CO",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"airquality\")\n |> filter(fn: (r) => r[\"_field\"] == \"co_mgm3\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")",
"refId": "A"
}
],
"datasource": {
"type": "influxdb",
"uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d"
}
},
{
"title": "Traffic - Average Speed",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"traffic\")\n |> filter(fn: (r) => r[\"_field\"] == \"average_speed_kmh\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")",
"refId": "A"
}
],
"datasource": {
"type": "influxdb",
"uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d"
}
},
{
"title": "Traffic - Congestion",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"traffic\")\n |> filter(fn: (r) => r[\"_field\"] == \"congestion_level\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")",
"refId": "A"
}
],
"datasource": {
"type": "influxdb",
"uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d"
}
},
{
"title": "Parking - Available Spots",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"parking\")\n |> filter(fn: (r) => r[\"_field\"] == \"available_spots\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")",
"refId": "A"
}
],
"datasource": {
"type": "influxdb",
"uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d"
}
},
{
"title": "Parking - Occupancy %",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 16
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"parking\")\n |> filter(fn: (r) => r[\"_field\"] == \"occupancy_percent\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")",
"refId": "A"
}
],
"datasource": {
"type": "influxdb",
"uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d"
}
},
{
"title": "Noise Level (dB)",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 24
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"noise\")\n |> filter(fn: (r) => r[\"_field\"] == \"noise_level_db\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")",
"refId": "A"
}
],
"datasource": {
"type": "influxdb",
"uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d"
}
},
{
"title": "Weather - Temperature",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 24
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"weather\")\n |> filter(fn: (r) => r[\"_field\"] == \"temperature_celsius\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")",
"refId": "A"
}
],
"datasource": {
"type": "influxdb",
"uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d"
}
},
{
"title": "Light - Brightness",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 32
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"light\")\n |> filter(fn: (r) => r[\"_field\"] == \"brightness_lux\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")",
"refId": "A"
}
],
"datasource": {
"type": "influxdb",
"uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d"
}
},
{
"title": "Light - Power Consumption",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 32
},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"light\")\n |> filter(fn: (r) => r[\"_field\"] == \"power_consumption_w\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")",
"refId": "A"
}
],
"datasource": {
"type": "influxdb",
"uid": "f9efd4b4-17cd-4ece-b4bc-087ff411051d"
}
}
],
"schemaVersion": 38,
"style": "dark",
"tags": [
"smart-city",
"martinique",
"iot"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"title": "Smart City Digital Twin - Martinique (Fixed)",
"uid": "smartcity-martinique-2026-v2",
"version": 2
}

View File

@@ -0,0 +1,269 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"fieldConfig": {
"defaults": {
"color": {"mode": "palette-classic"},
"custom": {"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": {"legend": false, "tooltip": false, "vis": false}, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": {"type": "linear"}, "showPoints": "never", "spanNulls": false, "stacking": {"group": "A", "mode": "none"}, "thresholdsStyle": {"mode": "off"}},
"mappings": [],
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}]}
},
"overrides": []
},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
"id": 1,
"options": {"legend": {"calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true}, "tooltip": {"mode": "single"}},
"targets": [
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"format": "time_series",
"group": [],
"metricColumn": "temperature",
"refId": "A",
"sql": {
"columns": [],
"groupBy": [],
"limit": "",
"orderBy": [],
"rawQuery": true,
"rawSql": "SELECT time_index as \"time\", AVG(temperature) as \"temperature\" FROM quantumleap.etairqualityobserved WHERE $__timeFilter(time_index) GROUP BY time_index ORDER BY time_index",
"refId": "A"
}
}
],
"title": "Température Moyenne (°C)",
"type": "timeseries"
},
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"fieldConfig": {
"defaults": {
"color": {"mode": "palette-classic"},
"custom": {"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": {"legend": false, "tooltip": false, "vis": false}, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": {"type": "linear"}, "showPoints": "never", "spanNulls": false, "stacking": {"group": "A", "mode": "none"}, "thresholdsStyle": {"mode": "off"}},
"mappings": [],
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}]}
},
"overrides": []
},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
"id": 2,
"options": {"legend": {"calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true}, "tooltip": {"mode": "single"}},
"targets": [
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"format": "time_series",
"group": [],
"metricColumn": "no2",
"refId": "A",
"sql": {
"columns": [],
"groupBy": [],
"limit": "",
"orderBy": [],
"rawQuery": true,
"rawSql": "SELECT time_index as \"time\", AVG(no2) as \"no2\" FROM quantumleap.etairqualityobserved WHERE $__timeFilter(time_index) GROUP BY time_index ORDER BY time_index",
"refId": "A"
}
}
],
"title": "NO2 Moyen (µg/m³)",
"type": "timeseries"
},
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"fieldConfig": {
"defaults": {
"color": {"mode": "palette-classic"},
"custom": {"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": {"legend": false, "tooltip": false, "vis": false}, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": {"type": "linear"}, "showPoints": "never", "spanNulls": false, "stacking": {"group": "A", "mode": "none"}, "thresholdsStyle": {"mode": "off"}},
"mappings": [],
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}]}
},
"overrides": []
},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 8},
"id": 3,
"options": {"legend": {"calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true}, "tooltip": {"mode": "single"}},
"targets": [
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"format": "time_series",
"group": [],
"metricColumn": "humidity",
"refId": "A",
"sql": {
"columns": [],
"groupBy": [],
"limit": "",
"orderBy": [],
"rawQuery": true,
"rawSql": "SELECT time_index as \"time\", AVG(humidity) as \"humidity\" FROM quantumleap.etairqualityobserved WHERE $__timeFilter(time_index) GROUP BY time_index ORDER BY time_index",
"refId": "A"
}
}
],
"title": "Humidité Moyenne (%)",
"type": "timeseries"
},
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"fieldConfig": {"defaults": {"color": {"mode": "thresholds"}}, "overrides": []},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 8},
"id": 4,
"options": {"showHeader": true, "sortBy": [{"desc": true, "displayName": "time_index"}]},
"targets": [
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"format": "table",
"group": [],
"metricColumn": "entity_id",
"refId": "A",
"sql": {
"columns": [
{"name": "entity_id", "parameters": []},
{"name": "time_index", "parameters": []},
{"name": "temperature", "parameters": []},
{"name": "no2", "parameters": []},
{"name": "humidity", "parameters": []}
],
"groupBy": [],
"limit": "10",
"orderBy": [{"name": "time_index", "desc": true}],
"rawQuery": false,
"rawSql": "SELECT entity_id, time_index, temperature, no2, humidity FROM quantumleap.etairqualityobserved ORDER BY time_index DESC LIMIT 10",
"refId": "A"
}
}
],
"title": "Dernières Mesures",
"type": "table"
},
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"fieldConfig": {
"defaults": {
"color": {"mode": "thresholds"},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": null}, {"color": "yellow", "value": 50}, {"color": "red", "value": 80}]},
"unit": "percent"
},
"overrides": []
},
"gridPos": {"h": 8, "w": 8, "x": 0, "y": 16},
"id": 5,
"options": {"orientation": "auto", "reduceOptions": {"calcs": [{"text": "Last", "value": "last"}], "fields": "", "values": false}, "showThresholdLabels": false, "showThresholdMarkers": true, "textMode": "auto"},
"targets": [
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"format": "time_series",
"group": [],
"metricColumn": "humidity",
"refId": "A",
"sql": {
"columns": [],
"groupBy": [],
"limit": "",
"orderBy": [],
"rawQuery": true,
"rawSql": "SELECT time_index as \"time\", AVG(humidity) as \"humidity\" FROM quantumleap.etairqualityobserved WHERE $__timeFilter(time_index) GROUP BY time_index ORDER BY time_index DESC LIMIT 1",
"refId": "A"
}
}
],
"title": "Humidité Actuelle",
"type": "gauge"
},
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"fieldConfig": {
"defaults": {
"color": {"mode": "thresholds"},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {"mode": "absolute", "steps": [{"color": "blue", "value": null}, {"color": "orange", "value": 50}, {"color": "red", "value": 100}]},
"unit": "density"
},
"overrides": []
},
"gridPos": {"h": 8, "w": 8, "x": 8, "y": 16},
"id": 6,
"options": {"orientation": "auto", "reduceOptions": {"calcs": [{"text": "Last", "value": "last"}], "fields": "", "values": false}, "showThresholdLabels": false, "showThresholdMarkers": true, "textMode": "auto"},
"targets": [
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"format": "time_series",
"group": [],
"metricColumn": "no2",
"refId": "A",
"sql": {
"columns": [],
"groupBy": [],
"limit": "",
"orderBy": [],
"rawQuery": true,
"rawSql": "SELECT time_index as \"time\", AVG(no2) as \"no2\" FROM quantumleap.etairqualityobserved WHERE $__timeFilter(time_index) GROUP BY time_index ORDER BY time_index DESC LIMIT 1",
"refId": "A"
}
}
],
"title": "NO2 Actuel",
"type": "gauge"
},
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"fieldConfig": {
"defaults": {
"color": {"mode": "thresholds"},
"mappings": [],
"max": 50,
"min": 0,
"thresholds": {"mode": "absolute", "steps": [{"color": "blue", "value": null}, {"color": "orange", "value": 25}, {"color": "red", "value": 40}]},
"unit": "celsius"
},
"overrides": []
},
"gridPos": {"h": 8, "w": 8, "x": 16, "y": 16},
"id": 7,
"options": {"orientation": "auto", "reduceOptions": {"calcs": [{"text": "Last", "value": "last"}], "fields": "", "values": false}, "showThresholdLabels": false, "showThresholdMarkers": true, "textMode": "auto"},
"targets": [
{
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"format": "time_series",
"group": [],
"metricColumn": "temperature",
"refId": "A",
"sql": {
"columns": [],
"groupBy": [],
"limit": "",
"orderBy": [],
"rawQuery": true,
"rawSql": "SELECT time_index as \"time\", AVG(temperature) as \"temperature\" FROM quantumleap.etairqualityobserved WHERE $__timeFilter(time_index) GROUP BY time_index ORDER BY time_index DESC LIMIT 1",
"refId": "A"
}
}
],
"title": "Température Actuelle",
"type": "gauge"
}
],
"schemaVersion": 38,
"style": "dark",
"tags": ["smart-city", "orion-ld", "cratedb", "air-quality"],
"templating": {"list": []},
"time": {"from": "now-6h", "to": "now"},
"title": "Smart City - Orion-LD Pipeline (COMPLET)",
"uid": "orion-ld-pipeline-final",
"version": 1
}

View File

@@ -0,0 +1,172 @@
{
"annotations": {"list": []},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"title": "Air Quality — PM2.5 (µg/m³)",
"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\\/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", "uid": "influxdb-smartcity"},
"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", "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\"] == \"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", "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\"] == \"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", "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\"] == \"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", "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\"] == \"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", "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",
"uid": "smartcity-martinique-v2",
"version": 2
}

View File

@@ -0,0 +1,29 @@
{
"annotations": {"list": []},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"title": "TEST - Air Quality PM2.5 (Last 5 min)",
"type": "timeseries",
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 0},
"datasource": {"type": "influxdb", "uid": "dd1bfc24-de9d-4c23-8a3c-151d153f8169"},
"targets": [
{
"query": "from(bucket:\"smartcity\")\n |> range(start: -5m)\n |> filter(fn: (r) => r[\"_measurement\"] == \"airquality\")\n |> filter(fn: (r) => r[\"_field\"] == \"pm25_ugm3\")\n |> aggregateWindow(every: 10s, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")",
"refId": "A"
}
]
}
],
"schemaVersion": 38,
"style": "dark",
"tags": ["test"],
"time": {"from": "now-5m", "to": "now"},
"title": "Smart City - TEST DATA",
"uid": "smartcity-test-v1",
"version": 1
}

View File

@@ -2,13 +2,18 @@
apiVersion: 1 apiVersion: 1
datasources: datasources:
- name: InfluxDB - name: influxdb-smartcity
type: influxdb type: influxdb
access: proxy access: proxy
url: http://docker-influxdb-1:8086 url: http://smart-city-influxdb:8086
database: iot_data database: smartcity
user: admin jsonData:
password: digitribe972 version: Flux
organization: digitribe
defaultBucket: smartcity
tlsSkipVerify: true
secureJsonData:
token: my-super-token
isDefault: true isDefault: true
readOnly: false readOnly: false

40
grafana-fixed.json Normal file
View File

@@ -0,0 +1,40 @@
{
"dashboard": {
"id": 29,
"uid": "orion-ld-simple",
"title": "Smart City - Orion-LD FINAL",
"tags": ["smart-city", "orion-ld", "fixed"],
"timezone": "browser",
"panels": [
{
"id": 1,
"type": "timeseries",
"title": "Température (°C)",
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"targets": [
{
"refId": "A",
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"rawQuery": true,
"rawSql": "SELECT time_index as \"time\", AVG(temperature) as \"temp\" FROM quantumleap.etairqualityobserved GROUP BY time_index ORDER BY time_index"
}
]
},
{
"id": 2,
"type": "table",
"title": "Dernières Mesures",
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"targets": [
{
"refId": "A",
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"rawQuery": true,
"rawSql": "SELECT entity_id, time_index, temperature, no2, humidity FROM quantumleap.etairqualityobserved ORDER BY time_index DESC LIMIT 10"
}
]
}
]
},
"overwrite": true
}

26
grafana-simple.json Normal file
View File

@@ -0,0 +1,26 @@
{
"dashboard": {
"id": null,
"uid": "orion-ld-simple",
"title": "Smart City - Orion-LD Simple",
"tags": ["smart-city", "orion-ld"],
"timezone": "browser",
"panels": [
{
"id": 1,
"type": "timeseries",
"title": "Température",
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"targets": [
{
"refId": "A",
"datasource": {"type": "postgres", "uid": "d43222c0-ad4e-4c49-9759-f822211e669e"},
"rawQuery": true,
"rawSql": "SELECT time_index as \"time\", AVG(temperature) as \"temp\" FROM quantumleap.etairqualityobserved WHERE $__timeFilter(time_index) GROUP BY time_index ORDER BY time_index"
}
]
}
]
},
"overwrite": false
}

View File

@@ -1,16 +0,0 @@
{
"annotations": {"list": []},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"panels": [],
"schemaVersion": 36,
"style": "dark",
"tags": ["smart-city"],
"templating": {"list": []},
"time": {"from": "now-24h", "to": "now"},
"title": "Smart City Dashboards",
"timezone": "Americas/Martinique",
"uid": "smart-city-dashboards"
}

View File

@@ -0,0 +1,172 @@
{
"annotations": {"list": []},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"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": ["smartcity", "martinique", "iot"],
"templating": {"list": []},
"time": {"from": "now-1h", "to": "now"},
"title": "Smart City Digital Twin — Martinique",
"uid": "smartcity-martinique-v2",
"version": 2
}

View File

@@ -1,9 +1,8 @@
# Grafana datasources - Smart City Digital Twin Martinique # Grafana datasources - Smart City Digital Twin Martinique
# Each datasource is editable and uses the container DNS name in smartcity-shared network
apiVersion: 1 apiVersion: 1
datasources: datasources:
# ── InfluxDB v2 (time-series IoT data) ────────────────────────────────────── # InfluxDB v2 (time-series IoT data)
- name: InfluxDB-v2 - name: InfluxDB-v2
type: influxdb type: influxdb
access: proxy access: proxy
@@ -13,12 +12,12 @@ datasources:
jsonData: jsonData:
version: Flux version: Flux
organization: digitribe organization: digitribe
defaultBucket: iot_data defaultBucket: smartcity
tlsSkipVerify: true
secureJsonData: secureJsonData:
token: my-super-secret-admin-token token: my-super-token
# ── FIWARE Orion-LD (NGSI-LD context broker) ──────────────────────────────── # FIWARE Orion-LD
# Requires grafana-simple-json-datasource plugin
- name: FIWARE Orion - name: FIWARE Orion
type: grafana-simple-json-datasource type: grafana-simple-json-datasource
access: proxy access: proxy
@@ -28,8 +27,7 @@ datasources:
queryURLTemplate: "/ngsi-ld/v1/entities?type={{type}}" queryURLTemplate: "/ngsi-ld/v1/entities?type={{type}}"
method: GET method: GET
# ── GeoServer WMS (spatial data) ──────────────────────────────────────────── # GeoServer WMS
# GeoServer is an external service reachable via its container name
- name: GeoServer WMS - name: GeoServer WMS
type: grafana-simple-json-datasource type: grafana-simple-json-datasource
access: proxy access: proxy
@@ -39,8 +37,7 @@ datasources:
queryURLTemplate: "/geoserver/wfs?service=WFS&version=2.0&request=GetFeature&typeName={{type}}" queryURLTemplate: "/geoserver/wfs?service=WFS&version=2.0&request=GetFeature&typeName={{type}}"
method: GET method: GET
# ── FROST-Server (SensorThings API) ────────────────────────────────────────── # FROST-Server
# Requires grafana-simple-json-datasource plugin
- name: FROST-Server - name: FROST-Server
type: grafana-simple-json-datasource type: grafana-simple-json-datasource
access: proxy access: proxy

246
helms/README.md Normal file
View File

@@ -0,0 +1,246 @@
# Smart City Martinique - Déploiement Kubernetes
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ TRAEFIK (Ingress) │
│ ports 80/443 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────┼─────────────────────────────────┐
│ │ │
┌────▼────┐ ┌──────────┐ ┌──────────▼──────────┐ ┌─────────────────┐
│ Airflow │ │ Kafka │ │ Data & Storage │ │ Monitoring │
│ │ │ Cluster │ │ │ │ │
│ web │ │ 3 brokers│ │ PostgreSQL HA │ │ Prometheus │
│ sched │ │ connect │ │ Redis Cluster │ │ Grafana │
│ worker │ │ ui │ │ MinIO │ │ Loki │
└─────────┘ └──────────┘ │ ClickHouse │ │ Promtail │
│ StarRocks │ └─────────────────┘
┌──────────┐ ┌──────────┐ │ Trino │
│ Flink │ │ IoT │ │ Delta Lake │ ┌─────────────────┐
│ │ │ │ │ DuckDB │ │ BI & Analytics │
│ jobmgr │ │ EMQX │ └─────────────────────┘ │ │
│ taskmgr │ │ Mosquitto│ │ Superset │
└──────────┘ │ Node-RED │ ┌─────────────────────┐ │ Metabase │
│ phpIPAM │ │ Git & Notebooks │ │ MindsDB │
┌──────────┐ │ ChirpStk │ │ │ └─────────────────┘
│ GIS │ └──────────┘ │ Gitea │
│ │ │ JupyterHub │ ┌─────────────────┐
│ MapStore │ ┌──────────┐ │ Zeppelin │ │ Web Apps │
│ GeoServer│ │ ODK │ └─────────────────────┘ │ │
│ FROST │ │ │ │ Smart App │
│ Stellio │ │ nginx │ ┌─────────────────────┐ │ Streamlit │
│ FIWARE │ │ service │ │ Data Collection │ │ Kepler │
└──────────┘ │ postgres │ │ │ └─────────────────┘
└──────────┘ │ Telegraf │
│ InfluxDB │
│ Simulator │
└─────────────────────┘
```
## Prérequis
### Cluster Kubernetes
- 3 nœuds minimum (1 master + 2 workers)
- Kubernetes 1.28+
- containerd
- Cilium (CNI)
### Serveur NFS
- 1 serveur NFS pour le stockage persistant
- Minimum 500Go d'espace disque
### Outils
- kubectl
- helm
- ansible 2.15+
- ansible-galaxy collection install kubernetes.core
## Installation
### 1. Cloner le repository
```bash
git clone https://gitea.digitribe.fr/eric/smart-city-digital-twin-martinique.git
cd smart-city-digital-twin-martinique/helms
```
### 2. Configurer l'inventory
Éditer `inventory/hosts.yml` avec les IPs de vos nœuds :
```yaml
k8s_masters:
hosts:
k8s-master-1:
ansible_host: "192.168.1.100"
k8s_workers:
hosts:
k8s-worker-1:
ansible_host: "192.168.1.101"
k8s-worker-2:
ansible_host: "192.168.1.102"
nfs_server:
hosts:
nfs-1:
ansible_host: "192.168.1.200"
```
### 3. Configurer les variables
Éditer `group_vars/all.yml` selon vos besoins (ressources, domaines, etc.)
### 4. Chiffrer les secrets
```bash
ansible-vault encrypt group_vars/vault.yml
```
### 5. Déployer
```bash
# Déployer toute la stack
ansible-playbook deploy.yml --ask-vault-pass
# Déployer un service spécifique
ansible-playbook deploy.yml --tags clickhouse --ask-vault-pass
ansible-playbook deploy.yml --tags trino --ask-vault-pass
ansible-playbook deploy.yml --tags streamlit --ask-vault-pass
ansible-playbook deploy.yml --tags kafka --ask-vault-pass
ansible-playbook deploy.yml --tags monitoring --ask-vault-pass
```
### 6. Vérifier
```bash
kubectl get pods --all-namespaces
kubectl get ingress --all-namespaces
```
## Services déployés
| Service | Domaine | Namespace | Helm Chart |
|---------|---------|-----------|------------|
| Traefik | traefik.digitribe.fr | traefik | traefik/traefik |
| Airflow | airflow.digitribe.fr | airflow | apache/airflow |
| Kafka | kafka-bootstrap.digitribe.fr | kafka | strimzi/kafka-operator |
| Flink | flink.digitribe.fr | flink | apache/flink-kubernetes-operator |
| ClickHouse | clickhouse.digitribe.fr | clickhouse | bitnami/clickhouse |
| StarRocks | starrocks.digitribe.fr | starrocks | community/starrocks |
| Trino | trino.digitribe.fr | trino | trinodb/trino |
| Delta Lake | deltalake.digitribe.fr | deltalake | custom |
| Streamlit | streamlit.digitribe.fr | streamlit | custom |
| DuckDB | duckdb.digitribe.fr | duckdb | custom |
| EMQX | emqx.digitribe.fr | iot | emqx/emqx-operator |
| Mosquitto | mqtt.digitribe.fr | iot | custom |
| Node-RED | nodered.digitribe.fr | iot | custom |
| phpIPAM | phpipam.digitribe.fr | phpipam | custom |
| Gitea | gitea.digitribe.fr | gitea | gitea-charts/gitea |
| JupyterHub | jupyter.digitribe.fr | jupyterhub | jupyterhub/jupyterhub |
| Superset | superset.digitribe.fr | superset | apache/superset |
| Metabase | metabase.digitribe.fr | metabase | bitnami/metabase |
| MindsDB | mindsdb.digitribe.fr | mindsdb | bitnami/mindsdb |
| ODK Central | odk.digitribe.fr | odk | custom |
| MapStore | mapstore.digitribe.fr | gis | custom |
| GeoServer | geoserver.digitribe.fr | gis | custom |
| Smart App | smartapp.digitribe.fr | smartapp | custom |
| Smart App API | api-smartapp.digitribe.fr | smartapp | custom |
| Grafana | grafana.digitribe.fr | monitoring | grafana/grafana |
| MinIO | minio.digitribe.fr | databases | bitnami/minio |
| PostgreSQL | — (interne) | databases | bitnami/postgresql-ha |
| Redis | — (interne) | databases | bitnami/redis-cluster |
## Dépendances entre rôles
```
prerequisites → namespaces → storage → traefik → cert-manager
┌─────────────────────┼─────────────────────┐
↓ ↓ ↓
databases monitoring kafka
(postgres, (prometheus, ↓
redis, minio) grafana, loki) flink
↓ ↓ ↓
└─────────────────────┼─────────────────────┘
┌─────────────────────┼─────────────────────┐
↓ ↓ ↓
airflow bi iot
gitea jupyterhub superset metabase emqx mosquitto
odk mindsdb trino nodered phpipam
gis clickhouse streamlit
smartapp deltalake duckdb
backup (Velero)
```
## Commandes utiles
```bash
# Lister tous les pods
kubectl get pods --all-namespaces
# Voir les logs d'un pod
kubectl logs -f <pod-name> -n <namespace>
# Voir les événements
kubectl get events --all-namespaces --sort-by='.lastTimestamp'
# Voir les ingress
kubectl get ingress --all-namespaces
# Voir les PVC
kubectl get pvc --all-namespaces
# Redéployer un service
ansible-playbook deploy.yml --tags <service> --ask-vault-pass
# Supprimer un service
kubectl delete namespace <namespace>
# Supprimer toute la stack
ansible-playbook undeploy.yml
```
## Troubleshooting
### Pod en CrashLoopBackOff
```bash
kubectl describe pod <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace> --previous
```
### PVC en Pending
```bash
kubectl get storageclass
kubectl get pv
kubectl describe pvc <pvc-name> -n <namespace>
```
### Ingress non accessible
```bash
kubectl get ingress -n <namespace>
kubectl describe ingress <ingress-name> -n <namespace>
kubectl logs -f deployment/traefik -n traefik
```
## Maintenance
### Backup
Les sauvegardes sont configurées via Velero :
```bash
kubectl get schedules -n velero
kubectl get backups -n velero
```
### Mise à jour d'un service
```bash
ansible-playbook deploy.yml --tags <service> --ask-vault-pass
```
### Scaling
```bash
kubectl scale deployment <deployment> --replicas=<n> -n <namespace>
```

77
helms/deploy.yml Normal file
View File

@@ -0,0 +1,77 @@
---
# Playbook principal pour le déploiement Kubernetes
# Fichier: deploy.yml
- name: Déploiement Smart City Martinique sur Kubernetes
hosts: localhost
connection: local
gather_facts: false
vars_files:
- group_vars/all.yml
- group_vars/vault.yml
pre_tasks:
- name: Vérifier que kubectl est installé
command: kubectl version --client
changed_when: false
- name: Vérifier la connexion au cluster
command: kubectl cluster-info
changed_when: false
roles:
- role: prerequisites
tags: [prerequisites]
- role: namespaces
tags: [namespaces]
- role: storage
tags: [storage]
- role: traefik
tags: [traefik, ingress]
- role: cert-manager
tags: [cert-manager, tls]
- role: monitoring
tags: [monitoring]
- role: databases
tags: [databases]
- role: kafka
tags: [kafka]
- role: flink
tags: [flink]
- role: airflow
tags: [airflow]
- role: iot
tags: [iot, mqtt]
- role: gitea
tags: [gitea]
- role: jupyterhub
tags: [jupyterhub]
- role: bi
tags: [bi, superset, metabase]
- role: mindsdb
tags: [mindsdb]
- role: odk
tags: [odk]
- role: gis
tags: [gis, mapstore, geoserver, frost]
- role: clickhouse
tags: [clickhouse]
- role: starrocks
tags: [starrocks]
- role: trino
tags: [trino]
- role: deltalake
tags: [deltalake]
- role: streamlit
tags: [streamlit]
- role: duckdb
tags: [duckdb]
- role: nodered
tags: [nodered]
- role: phpipam
tags: [phpipam]
- role: smartapp
tags: [smartapp]
- role: backup
tags: [backup]

535
helms/group_vars/all.yml Normal file
View File

@@ -0,0 +1,535 @@
---
# Variables globales pour le déploiement Kubernetes
# Fichier: group_vars/all.yml
# ============================================================
# Configuration du cluster Kubernetes
# ============================================================
cluster_name: smart-city-martinique
k8s_version: "1.28.0"
container_runtime: containerd
network_plugin: cilium
# ============================================================
# Configuration réseau
# ============================================================
domain: digitribe.fr
traefik_namespace: traefik
ingress_class: traefik
# TLS
tls_enabled: true
tls_certresolver: letsencrypt
acme_email: admin@digitribe.fr
# ============================================================
# Storage
# ============================================================
storage_class: nfs-client
nfs_server: "192.168.1.200"
nfs_path: /data/k8s
# Persistent Volume sizes
storage_sizes:
postgres: 50Gi
minio: 500Gi
kafka: 100Gi
influxdb: 50Gi
loki: 100Gi
grafana: 10Gi
jupyterhub: 20Gi
gitea: 20Gi
metabase: 10Gi
superset: 10Gi
mindsdb: 20Gi
odk: 10Gi
mapstore: 10Gi
geoserver: 20Gi
airflow: 20Gi
flink: 20Gi
emqx: 10Gi
mosquitto: 5Gi
redis: 10Gi
elasticsearch: 50Gi
# ============================================================
# Helm Charts versions
# ============================================================
helm_charts:
traefik:
chart: traefik/traefik
version: "28.0.0"
ingress_nginx:
chart: ingress-nginx/ingress-nginx
version: "4.8.0"
cert_manager:
chart: jetstack/cert-manager
version: "1.13.0"
nfs_provisioner:
chart: nfs-subdir-external-provisioner/nfs-subdir-external-provisioner
version: "4.0.18"
postgresql:
chart: bitnami/postgresql
version: "13.2.0"
postgresql_ha:
chart: bitnami/postgresql-ha
version: "12.2.0"
redis:
chart: bitnami/redis
version: "18.0.0"
minio:
chart: bitnami/minio
version: "12.10.0"
kafka:
chart: strimzi/kafka-operator
version: "0.38.0"
flink:
chart: apache/flink-kubernetes-operator
version: "1.7.0"
airflow:
chart: apache/airflow
version: "1.11.0"
grafana:
chart: grafana/grafana
version: "7.0.0"
loki:
chart: grafana/loki-stack
version: "2.9.0"
prometheus:
chart: prometheus/kube-prometheus-stack
version: "51.0.0"
emqx:
chart: emqx/emqx-operator
version: "2.2.0"
mosquitto:
chart: k8s-at-home/mosquitto
version: "4.8.0"
gitea:
chart: gitea/gitea
version: "9.0.0"
jupyterhub:
chart: jupyterhub/jupyterhub
version: "3.0.0"
superset:
chart: apache/superset
version: "0.11.0"
metabase:
chart: bitnami/metabase
version: "0.13.0"
mindsdb:
chart: bitnami/mindsdb
version: "0.1.0"
odk:
chart: odk/odk-central
version: "1.0.0"
mapstore:
chart: geosolutionsit/mapstore
version: "1.0.0"
geoserver:
chart: kartoza/geoserver
version: "2.2.0"
frost:
chart: fraunhoferiosb/frost-server
version: "1.0.0"
nodered:
chart: k8s-at-home/node-red
version: "4.8.0"
phpipam:
chart: phpipam/phpipam
version: "1.0.0"
clickhouse:
chart: bitnami/clickhouse
version: "4.0.0"
starrocks:
chart: starrocks/starrocks-community
version: "1.0.0"
trino:
chart: trinodb/trino
version: "0.10.0"
deltalake:
chart: delta-io/delta-lake
version: "1.0.0"
streamlit:
chart: streamlit/streamlit
version: "1.0.0"
duckdb:
chart: duckdb/duckdb
version: "1.0.0"
elasticsearch:
chart: elastic/elasticsearch
version: "8.11.0"
kibana:
chart: elastic/kibana
version: "8.11.0"
# ============================================================
# Services configuration
# ============================================================
services:
airflow:
enabled: true
namespace: airflow
replicas: 2
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "4Gi"
kafka:
enabled: true
namespace: kafka
replicas: 3
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "4Gi"
flink:
enabled: true
namespace: flink
replicas: 2
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "4Gi"
emqx:
enabled: true
namespace: iot
replicas: 3
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
mosquitto:
enabled: true
namespace: iot
replicas: 2
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "1Gi"
postgresql:
enabled: true
namespace: default
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
redis:
enabled: true
namespace: default
replicas: 3
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "1Gi"
minio:
enabled: true
namespace: default
replicas: 4
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
grafana:
enabled: true
namespace: monitoring
replicas: 1
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "1Gi"
loki:
enabled: true
namespace: monitoring
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
prometheus:
enabled: true
namespace: monitoring
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
gitea:
enabled: true
namespace: gitea
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
jupyterhub:
enabled: true
namespace: jupyterhub
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
superset:
enabled: true
namespace: superset
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
metabase:
enabled: true
namespace: metabase
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
mindsdb:
enabled: true
namespace: mindsdb
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
odk:
enabled: true
namespace: odk
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
mapstore:
enabled: true
namespace: mapstore
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
geoserver:
enabled: true
namespace: geoserver
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
frost:
enabled: true
namespace: iot
replicas: 1
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "1Gi"
nodered:
enabled: true
namespace: iot
replicas: 1
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "1Gi"
phpipam:
enabled: true
namespace: phpipam
replicas: 1
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "1Gi"
smartapp:
enabled: true
namespace: smartapp
replicas: 2
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
clickhouse:
enabled: true
namespace: clickhouse
replicas: 1
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "4Gi"
starrocks:
enabled: true
namespace: starrocks
replicas: 1
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "4Gi"
trino:
enabled: true
namespace: trino
replicas: 1
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "4Gi"
deltalake:
enabled: true
namespace: deltalake
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
streamlit:
enabled: true
namespace: streamlit
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
duckdb:
enabled: true
namespace: duckdb
replicas: 1
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "2Gi"
# ============================================================
# Monitoring
# ============================================================
monitoring:
enabled: true
namespace: monitoring
grafana_admin_password: "{{ vault_grafana_password }}"
prometheus_retention: 30d
loki_retention: 30d
# ============================================================
# Backup
# ============================================================
backup:
enabled: true
schedule: "0 2 * * *"
retention: 30
storage_class: nfs-client
storage_size: 100Gi

View File

@@ -0,0 +1,60 @@
---
# Vault Ansible - Variables chiffrées
# Fichier: group_vars/vault.yml
# Chiffrer avec: ansible-vault encrypt group_vars/vault.yml
# PostgreSQL
vault_postgres_password: "Digitribe972"
vault_postgres_repmgr_password: "Digitribe972"
# Redis
vault_redis_password: "Digitribe972"
# MinIO
vault_minio_root_user: "minioadmin"
vault_minio_root_password: "Digitribe972"
# Grafana
vault_grafana_admin_password: "Digitribe972"
# Airflow
vault_airflow_fernet_key: "Digitribe972SecretKeyForAirflow2024"
vault_airflow_admin_password: "Digitribe972"
# Gitea
vault_gitea_admin_password: "Digitribe972"
# Superset
vault_superset_admin_password: "Digitribe972"
vault_superset_db_password: "Digitribe972"
# Metabase
vault_metabase_db_password: "Digitribe972"
# MindsDB
vault_mindsdb_password: "Digitribe972"
# ClickHouse
vault_clickhouse_password: "Digitribe972"
# Trino
vault_trino_db_password: "Digitribe972"
# MQTT
vault_mosquitto_password: "Digitribe972"
vault_emqx_admin_password: "Digitribe972"
# phpIPAM
vault_phpipam_admin_password: "Digitribe972"
# ODK
vault_odk_admin_password: "Digitribe972"
# GeoServer
vault_geoserver_admin_password: "Digitribe972"
# MapStore
vault_mapstore_admin_password: "Digitribe972"
# StarRocks
vault_starrocks_root_password: "Digitribe972"

79
helms/inventory/hosts.yml Normal file
View File

@@ -0,0 +1,79 @@
---
# Inventory pour le déploiement Kubernetes via Ansible
# Fichier: inventory/hosts.yml
all:
children:
k8s_masters:
hosts:
k8s-master-1:
ansible_host: "{{ k8s_master_ip | default('192.168.1.100') }}"
ansible_user: "{{ k8s_user | default('root') }}"
k8s_workers:
hosts:
k8s-worker-1:
ansible_host: "{{ k8s_worker1_ip | default('192.168.1.101') }}"
ansible_user: "{{ k8s_user | default('root') }}"
k8s-worker-2:
ansible_host: "{{ k8s_worker2_ip | default('192.168.1.102') }}"
ansible_user: "{{ k8s_user | default('root') }}"
nfs_server:
hosts:
nfs-1:
ansible_host: "{{ nfs_server_ip | default('192.168.1.200') }}"
ansible_user: "{{ nfs_user | default('root') }}"
vars:
# Configuration globale
cluster_name: smart-city-martinique
k8s_version: "1.28"
container_runtime: containerd
network_plugin: cilium
domain: digitribe.fr
# Namespaces Kubernetes
namespaces:
- airflow
- kafka
- flink
- monitoring
- iot
- gitea
- jupyterhub
- odk
- smartapp
- superset
- metabase
- mindsdb
- mapstore
- geoserver
- frost
- nodered
- phpipam
- traefik
- ingress-nginx
- clickhouse
- starrocks
- trino
- deltalake
- streamlit
- duckdb
# Storage
storage_class: nfs-client
nfs_path: /data/k8s
# Helm repositories
helm_repos:
- name: bitnami
url: https://charts.bitnami.com/bitnami
- name: apache
url: https://charts.apache.org
- name: grafana
url: https://grafana.github.io/helm-charts
- name: prometheus
url: https://prometheus-community.github.io/helm-charts
- name: strimzi
url: https://strimzi.io/charts/
- name: flink-operator
url: https://downloads.apache.org/flink/flink-kubernetes-operator-1.7.0/

View File

@@ -0,0 +1,19 @@
---
# Role: airflow
# Valeurs par défaut pour Apache Airflow
# Réplicas des workers Airflow
services:
airflow:
replicas: 2
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "4Gi"
# Stockage des logs Airflow
storage_sizes:
airflow: "20Gi"

View File

@@ -0,0 +1,13 @@
---
galaxy_info:
author: Eric FELIXINE
description: Deploy Apache Airflow for workflow orchestration on Kubernetes
license: MIT
min_ansible_version: "2.15"
platforms:
- name: Kubernetes
versions:
- "1.28"
dependencies:
- role: databases
- role: kafka

Some files were not shown because too many files have changed in this diff Show More