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
This commit is contained in:
35
TODO.md
Normal file
35
TODO.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Smart City Digital Twin — TODO List
|
||||
|
||||
> Dernière mise à jour : 2026-05-17 11:15
|
||||
|
||||
## ✅ Complété
|
||||
| ID | Tâche |
|
||||
|----|-------|
|
||||
| p1-bunkerm | BunkerM: DNS corrigé (underscores → hyphens) |
|
||||
| p2-geoserver | GeoServer: workspace Digitribe + Data Store PostGIS dédié |
|
||||
| p2-postgis | PostGIS dédié: conteneur postgis-smartcity UP (PostGIS 3.4) |
|
||||
|
||||
## 🔴 Bloqué
|
||||
| ID | Tâche | Raison |
|
||||
|----|-------|--------|
|
||||
| p1-or | OpenRemote agents MQTT | API 403, UI headless ne rend pas |
|
||||
| p4-ditto | Ditto.digitribe.fr | MongoDB localhost hardcodé |
|
||||
| p1-prometheus | Prometheus + Grafana | Réseau interne inaccessible |
|
||||
|
||||
## ⏳ En attente
|
||||
| ID | Tâche |
|
||||
|----|-------|
|
||||
| p2-mapstore | MapStore: interconnecter avec GeoServer via PostGIS |
|
||||
| p3-analyse | Analyse: GeoMesa + KeplerGL |
|
||||
| p1-ngsi | NGSI-LD: validation pipeline (basse priorité) |
|
||||
| p5-docs | Documentation + commits Gitea |
|
||||
|
||||
## 🔄 En cours
|
||||
| ID | Tâche |
|
||||
|----|-------|
|
||||
| p0-chirpstack | ChirpStack: login API gRPC-REST |
|
||||
|
||||
## Credentials
|
||||
- **GeoServer**: admin / Digitribe972
|
||||
- **PostGIS dédié**: smartcity / SmartCity972 (port 5433)
|
||||
- **ChirpStack**: admin / admin1234
|
||||
@@ -1,12 +1,5 @@
|
||||
version: "3.8"
|
||||
|
||||
# =============================================================================
|
||||
# ChirpStack LoRaWAN Network Server — Smart City Digital Twin
|
||||
# =============================================================================
|
||||
# Image officielle chirpstack/chirpstack:latest
|
||||
# Credentials par défaut: admin/admin
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
chirpstack:
|
||||
image: chirpstack/chirpstack:latest
|
||||
@@ -14,14 +7,11 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./configuration/chirpstack:/etc/chirpstack:ro
|
||||
depends_on:
|
||||
- postgres
|
||||
- mosquitto
|
||||
- redis
|
||||
environment:
|
||||
- MQTT_BROKER_HOST=mosquitto
|
||||
- REDIS_HOST=redis
|
||||
- POSTGRESQL_HOST=postgres
|
||||
- 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`)"
|
||||
@@ -29,41 +19,6 @@ services:
|
||||
- "traefik.http.routers.chirpstack.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.chirpstack.loadbalancer.server.port=8080"
|
||||
networks:
|
||||
- traefik-public
|
||||
- smartcity-shared
|
||||
|
||||
chirpstack-gateway-bridge:
|
||||
image: chirpstack/chirpstack-gateway-bridge:4
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "1700:1700/udp"
|
||||
volumes:
|
||||
- ./configuration/chirpstack-gateway-bridge:/etc/chirpstack-gateway-bridge:ro
|
||||
environment:
|
||||
- INTEGRATION__MQTT__EVENT_TOPIC_TEMPLATE=eu868/gateway/{{ .GatewayID }}/event/{{ .EventType }}
|
||||
- INTEGRATION__MQTT__STATE_TOPIC_TEMPLATE=eu868/gateway/{{ .GatewayID }}/state/{{ .StateType }}
|
||||
- INTEGRATION__MQTT__COMMAND_TOPIC_TEMPLATE=eu868/gateway/{{ .GatewayID }}/command/#
|
||||
depends_on:
|
||||
- mosquitto
|
||||
networks:
|
||||
- smartcity-shared
|
||||
|
||||
chirpstack-gateway-bridge-basicstation:
|
||||
image: chirpstack/chirpstack-gateway-bridge:4
|
||||
restart: unless-stopped
|
||||
command: -c /etc/chirpstack-gateway-bridge/chirpstack-gateway-bridge-basicstation-eu868.toml
|
||||
volumes:
|
||||
- ./configuration/chirpstack-gateway-bridge:/etc/chirpstack-gateway-bridge:ro
|
||||
depends_on:
|
||||
- mosquitto
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.chirpstack-ws.rule=Host(`chirpstack-ws.digitribe.fr`)"
|
||||
- "traefik.http.routers.chirpstack-ws.entrypoints=websecure"
|
||||
- "traefik.http.routers.chirpstack-ws.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.chirpstack-ws.loadbalancer.server.port=3001"
|
||||
networks:
|
||||
- traefik-public
|
||||
- smartcity-shared
|
||||
|
||||
chirpstack-rest-api:
|
||||
@@ -79,44 +34,8 @@ services:
|
||||
- "traefik.http.routers.chirpstack-api.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.chirpstack-api.loadbalancer.server.port=8090"
|
||||
networks:
|
||||
- traefik-public
|
||||
- smartcity-shared
|
||||
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- chirpstack-postgresqldata:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER=chirpstack
|
||||
- POSTGRES_PASSWORD=chirpstack
|
||||
- POSTGRES_DB=chirpstack
|
||||
networks:
|
||||
- smartcity-shared
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: redis-server --save 300 1 --save 60 100 --appendonly no
|
||||
volumes:
|
||||
- chirpstack-redisdata:/data
|
||||
networks:
|
||||
- smartcity-shared
|
||||
|
||||
mosquitto:
|
||||
image: eclipse-mosquitto:2
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./configuration/mosquitto/config/:/mosquitto/config/:ro
|
||||
networks:
|
||||
- smartcity-shared
|
||||
|
||||
volumes:
|
||||
chirpstack-postgresqldata:
|
||||
chirpstack-redisdata:
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
smartcity-shared:
|
||||
external: true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Eclipse Ditto - Smart City Digital Twin (MongoDB fix)
|
||||
# Eclipse Ditto - Smart City Digital Twin
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
@@ -21,10 +21,13 @@ services:
|
||||
depends_on:
|
||||
- ditto-mongodb
|
||||
environment:
|
||||
- DITTO_JWT_SECRET=my-ditto-secret-12345
|
||||
- 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=Policies
|
||||
- MONGODB_URI=mongodb://smart-city-ditto-mongodb:27017/Policies
|
||||
- AKKA_REMOTE_ENABLED=false
|
||||
networks:
|
||||
traefik-public:
|
||||
@@ -46,10 +49,13 @@ services:
|
||||
- ditto-mongodb
|
||||
- ditto-policies
|
||||
environment:
|
||||
- DITTO_JWT_SECRET=my-ditto-secret-12345
|
||||
- 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
|
||||
networks:
|
||||
traefik-public:
|
||||
@@ -71,12 +77,17 @@ services:
|
||||
- ditto-things
|
||||
- ditto-policies
|
||||
environment:
|
||||
- DITTO_JWT_SECRET=my-ditto-secret-12345
|
||||
- TZ=Europe/Berlin
|
||||
- BIND_HOSTNAME=0.0.0.0
|
||||
- DITTO_JWT_SECRET=my-ditto-jwt-secret-key-12345
|
||||
- DITTO_GATEWAY_PROXY_ENABLED=true
|
||||
- AKKA_REMOTE_ENABLED=false
|
||||
- DITTO_GW_STREAMING_ENABLED=true
|
||||
- DITTO_GW_MQTT_BROKER=smart-city-mosquitto:1883
|
||||
- DITTO_GW_MQTT_TOPIC_FILTER=smartcity/#
|
||||
- DEVOPS_PASSWORD=ditto-devops-secret
|
||||
- ENABLE_PRE_AUTHENTICATION=true
|
||||
- JAVA_TOOL_OPTIONS=-Dditto.gateway.authentication.devops.password=ditto-devops-secret -Dditto.gateway.authentication.devops.secured=true -Dditto.gateway.authentication.devops.devops-authentication-method=basic
|
||||
networks:
|
||||
traefik-public:
|
||||
aliases:
|
||||
@@ -85,7 +96,8 @@ services:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.ditto-gateway.rule=Host(`ditto.digitribe.fr`)"
|
||||
- "traefik.http.routers.ditto-gateway.entrypoints=web"
|
||||
- "traefik.http.routers.ditto-gateway.entrypoints=websecure"
|
||||
- "traefik.http.routers.ditto-gateway.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.ditto-gateway.loadbalancer.server.port=8080"
|
||||
|
||||
networks:
|
||||
|
||||
@@ -28,7 +28,7 @@ services:
|
||||
- ENABLE_EMQX=1
|
||||
- ENABLE_MOSQUITTO=1
|
||||
- ENABLE_BUNKER=1
|
||||
- BUNKERM_HOST=bunkerm_bunkerm_1
|
||||
- BUNKERM_HOST=bunkerm-bunkerm-1
|
||||
- BUNKERM_PORT=1900
|
||||
# Context Brokers (DESACTIVE - tout passe par les IoT Agents via MQTT)
|
||||
- ENABLE_ORION=false
|
||||
@@ -57,6 +57,25 @@ services:
|
||||
labels:
|
||||
- "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
|
||||
environment:
|
||||
- OR_URL=http://openremote_manager_1:8080
|
||||
- OR_ADMIN_USER=admin
|
||||
- OR_ADMIN_PASS=Digitribe972
|
||||
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
|
||||
|
||||
5
geojson-proxy/Dockerfile
Normal file
5
geojson-proxy/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
COPY geojson_proxy.py .
|
||||
EXPOSE 8080
|
||||
CMD ["python", "geojson_proxy.py"]
|
||||
137
geojson-proxy/geojson_proxy.py
Normal file
137
geojson-proxy/geojson_proxy.py
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""GeoJSON proxy service for OpenRemote assets map display."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
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", "")
|
||||
OR_TOKEN = os.environ.get("OR_TOKEN", "")
|
||||
|
||||
|
||||
def get_token():
|
||||
"""Fetch an OpenRemote access token using admin credentials."""
|
||||
if OR_TOKEN:
|
||||
return OR_TOKEN
|
||||
data = json.dumps({
|
||||
"username": OR_ADMIN_USER,
|
||||
"password": OR_ADMIN_PASS,
|
||||
"grant_type": "password",
|
||||
"client_id": "openremote"
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{OR_URL}/auth/realms/master/protocol/openid-connect/token",
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
body = json.loads(resp.read())
|
||||
return body["access_token"]
|
||||
|
||||
|
||||
def fetch_assets(token):
|
||||
"""Fetch IoT sensor assets from OpenRemote."""
|
||||
url = f"{OR_URL}/api/master/assets?type=IOTSensor"
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/json"
|
||||
},
|
||||
method="GET"
|
||||
)
|
||||
resp = urllib.request.urlopen(req, timeout=15)
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def to_geojson(assets):
|
||||
"""Convert OpenRemote assets to a GeoJSON FeatureCollection."""
|
||||
features = []
|
||||
for asset in assets:
|
||||
attrs = asset.get("attributes", {})
|
||||
location = attrs.get("location", {})
|
||||
if not location:
|
||||
continue
|
||||
value = location.get("value")
|
||||
if not value:
|
||||
continue
|
||||
coords = value.get("coordinates")
|
||||
if not coords or len(coords) < 2:
|
||||
continue
|
||||
|
||||
# Build properties from asset attributes
|
||||
properties = {}
|
||||
for attr_name, attr_val in attrs.items():
|
||||
v = attr_val.get("value")
|
||||
if v is not None:
|
||||
properties[attr_name] = v
|
||||
# Ensure key fields are at top level for Mapbox filters
|
||||
properties.setdefault("id", asset.get("id"))
|
||||
properties.setdefault("name", asset.get("name", ""))
|
||||
properties.setdefault("type", asset.get("type", ""))
|
||||
properties.setdefault("sensorType", attrs.get("sensorType", {}).get("value", ""))
|
||||
|
||||
features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [coords[0], coords[1]]
|
||||
},
|
||||
"properties": properties
|
||||
})
|
||||
|
||||
return {
|
||||
"type": "FeatureCollection",
|
||||
"features": features
|
||||
}
|
||||
|
||||
|
||||
class GeoJSONHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
path = self.path.split("?")[0]
|
||||
if path == "/geojson":
|
||||
try:
|
||||
token = get_token()
|
||||
assets = fetch_assets(token)
|
||||
geojson = to_geojson(assets)
|
||||
body = json.dumps(geojson).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "*")
|
||||
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()
|
||||
260
scripts/create_or_agents.py
Normal file
260
scripts/create_or_agents.py
Normal file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Crée les 60 agents MQTT OpenRemote pour lier les topics du simulateur
|
||||
aux assets IOTSensor existants (realm smartcity).
|
||||
|
||||
Chaque agent MQTT:
|
||||
- type: urn:openremote:agent:mqtt
|
||||
- agentLink -> asset IOTSensor (avec location)
|
||||
- topic: smartcity/{type}/{index} (1-indexé)
|
||||
|
||||
Utilise l'API REST OpenRemote avec auth admin.
|
||||
"""
|
||||
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import uuid
|
||||
import sys
|
||||
import time
|
||||
|
||||
OR_BASE = "http://localhost:8080"
|
||||
OR_REALM = "smartcity"
|
||||
OR_USER = "admin"
|
||||
OR_PASS = "Digitribe972"
|
||||
|
||||
# Mapping des 60 capteurs simulateur -> noms d'assets OpenRemote
|
||||
# Format: (type, index_1based) -> nom_partiel_asset
|
||||
# Les assets ont des noms comme "Fort-de-France Centre (traffic)", "Fort-de-France Lamartine (airquality)", etc.
|
||||
SENSOR_TOPIC_TO_ASSET = {
|
||||
# traffic (index 1-10)
|
||||
("traffic", 1): "Fort-de-France Centre (traffic)",
|
||||
("traffic", 2): "Le Lamentin Aéroport (traffic)",
|
||||
("traffic", 3): "Le Robert D110 (traffic)",
|
||||
("traffic", 4): "Sainte-Anne Plage (traffic)",
|
||||
("traffic", 5): "Saint-Joseph D1 (traffic)",
|
||||
("traffic", 6): "Trinité Centre",
|
||||
("traffic", 7): "Le François D2",
|
||||
("traffic", 8): "Ducos Penitencier",
|
||||
("traffic", 9): "Schœlcher Morne",
|
||||
("traffic", 10): "Case-Pilote Bourg",
|
||||
# airquality (index 1-10)
|
||||
("airquality", 1): "Fort-de-France Lamartine (airquality)",
|
||||
("airquality", 2): "Le Lamentin Zac (airquality)",
|
||||
("airquality", 3): "Le Robert Bourg (airquality)",
|
||||
("airquality", 4): "Sainte-Anne Village (airquality)",
|
||||
("airquality", 5): "Saint-Joseph Morne (airquality)",
|
||||
("airquality", 6): "Trinité Eglise",
|
||||
("airquality", 7): "Le François Bourg",
|
||||
("airquality", 8): "Ducos Centre",
|
||||
("airquality", 9): "Schœlcher Plage",
|
||||
("airquality", 10): "Case-Pilote D1",
|
||||
# parking (index 1-10)
|
||||
("parking", 1): "Fort-de-France Place Clémenceau (parking)",
|
||||
("parking", 2): "Le Lamentin Centre Commercial (parking)",
|
||||
("parking", 3): "Le Robert Stade (parking)",
|
||||
("parking", 4): "Sainte-Anne Mairie (parking)",
|
||||
("parking", 5): "Saint-Joseph Ecole (parking)",
|
||||
("parking", 6): "Trinité Port",
|
||||
("parking", 7): "Le François Mairie",
|
||||
("parking", 8): "Ducos ZI",
|
||||
("parking", 9): "Schœlcher Bourg",
|
||||
("parking", 10): "Case-Pilote Stade",
|
||||
# noise (index 1-10)
|
||||
("noise", 1): "Fort-de-France Théâtre (noise)",
|
||||
("noise", 2): "Le Lamentin Zone Industrielle (noise)",
|
||||
("noise", 3): "Le Robert Bourg (noise)",
|
||||
("noise", 4): "Sainte-Anne Plage (noise)",
|
||||
("noise", 5): "Saint-Joseph Morne (noise)",
|
||||
("noise", 6): "Trinité Centre",
|
||||
("noise", 7): "Le François Bourg",
|
||||
("noise", 8): "Ducos Penitencier",
|
||||
("noise", 9): "Schœlcher Morne",
|
||||
("noise", 10): "Case-Pilote Village",
|
||||
# weather (index 1-10)
|
||||
("weather", 1): "Fort-de-France Meteo (weather)",
|
||||
("weather", 2): "Le Lamentin Aéroport (weather)",
|
||||
("weather", 3): "Le Robert Bourg (weather)",
|
||||
("weather", 4): "Sainte-Anne Village (weather)",
|
||||
("weather", 5): "Saint-Joseph Morne (weather)",
|
||||
("weather", 6): "Trinité Eglise",
|
||||
("weather", 7): "Le François Bourg",
|
||||
("weather", 8): "Ducos Centre",
|
||||
("weather", 9): "Schœlcher Plage",
|
||||
("weather", 10): "Case-Pilote D1",
|
||||
# light (index 1-10)
|
||||
("light", 1): "Fort-de-France Place (light)",
|
||||
("light", 2): "Le Lamentin Rond-point (light)",
|
||||
("light", 3): "Le Robert D110 (light)",
|
||||
("light", 4): "Sainte-Anne Plage (light)",
|
||||
("light", 5): "Saint-Joseph D1 (light)",
|
||||
("light", 6): "Trinité Centre",
|
||||
("light", 7): "Le François D2",
|
||||
("light", 8): "Ducos Penitencier",
|
||||
("light", 9): "Schœlcher Morne",
|
||||
("light", 10): "Case-Pilote Bourg",
|
||||
}
|
||||
|
||||
|
||||
def http_request(url, method="GET", data=None, headers=None, auth=None):
|
||||
"""Effectue une requête HTTP et retourne (status_code, body)"""
|
||||
req = urllib.request.Request(url, method=method)
|
||||
if headers:
|
||||
for k, v in headers.items():
|
||||
req.add_header(k, v)
|
||||
if auth:
|
||||
import base64
|
||||
creds = base64.b64encode(f"{auth[0]}:{auth[1]}".encode()).decode()
|
||||
req.add_header("Authorization", f"Basic {creds}")
|
||||
if data:
|
||||
req.data = json.dumps(data).encode("utf-8")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
return resp.status, resp.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode("utf-8")
|
||||
except Exception as e:
|
||||
return 0, str(e)
|
||||
|
||||
|
||||
def get_admin_token():
|
||||
"""Obtient un token admin via l'API Keycloak"""
|
||||
url = f"{OR_BASE}/auth/realms/{OR_REALM}/protocol/openid-connect/token"
|
||||
data = urllib.parse.urlencode({
|
||||
"grant_type": "password",
|
||||
"client_id": "openremote",
|
||||
"username": OR_USER,
|
||||
"password": OR_PASS,
|
||||
}).encode()
|
||||
req = urllib.request.Request(url, data=data, method="POST")
|
||||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
body = json.loads(resp.read().decode())
|
||||
return body.get("access_token")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur auth: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def find_asset_by_name(token, name):
|
||||
"""Recherche un asset par nom exact"""
|
||||
url = f"{OR_BASE}/api/{OR_REALM}/asset?name={urllib.parse.quote(name)}&limit=1"
|
||||
status, body = http_request(url, headers={"Authorization": f"Bearer {token}"})
|
||||
if status == 200:
|
||||
assets = json.loads(body)
|
||||
if assets:
|
||||
return assets[0]
|
||||
return None
|
||||
|
||||
|
||||
def find_assets_by_name_prefix(token, prefix):
|
||||
"""Recherche des assets par préfixe de nom"""
|
||||
url = f"{OR_BASE}/api/{OR_REALM}/asset?name={urllib.parse.quote(prefix)}&limit=20"
|
||||
status, body = http_request(url, headers={"Authorization": f"Bearer {token}"})
|
||||
if status == 200:
|
||||
return json.loads(body)
|
||||
return []
|
||||
|
||||
|
||||
def create_mqtt_agent(token, agent_name, asset_id, topic):
|
||||
"""Crée un agent MQTT et le lie à un asset via agentLink"""
|
||||
agent_id = str(uuid.uuid4())[:22] # OpenRemote utilise des IDs tronqués
|
||||
agent = {
|
||||
"name": agent_name,
|
||||
"type": "urn:openremote:agent:mqtt",
|
||||
"realm": OR_REALM,
|
||||
"attributes": {
|
||||
"agentLink": {
|
||||
"name": "agentLink",
|
||||
"type": "JSON",
|
||||
"value": {"type": "AgentLink", "id": asset_id},
|
||||
},
|
||||
"topic": {
|
||||
"name": "topic",
|
||||
"type": "Text",
|
||||
"value": topic,
|
||||
},
|
||||
},
|
||||
}
|
||||
url = f"{OR_BASE}/api/{OR_REALM}/agent"
|
||||
status, body = http_request(
|
||||
url,
|
||||
method="POST",
|
||||
data=agent,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
return status, body
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("OpenRemote — Création des 60 agents MQTT")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. Authentification
|
||||
print("\n🔑 Authentification...")
|
||||
token = get_admin_token()
|
||||
if not token:
|
||||
print("❌ Impossible de s'authentifier. Vérifiez les credentials.")
|
||||
sys.exit(1)
|
||||
print("✅ Token obtenu")
|
||||
|
||||
# 2. Créer les agents
|
||||
created = 0
|
||||
failed = 0
|
||||
skipped = 0
|
||||
|
||||
for (sensor_type, index), asset_name in SENSOR_TOPIC_TO_ASSET.items():
|
||||
topic = f"smartcity/{sensor_type}/{index}"
|
||||
agent_name = f"MQTT Agent {asset_name}"
|
||||
|
||||
print(f"\n[{created + failed + skipped + 1}/60] {sensor_type}/{index} → {asset_name}")
|
||||
|
||||
# Chercher l'asset
|
||||
asset = find_asset_by_name(token, asset_name)
|
||||
if not asset:
|
||||
# Essayer sans le suffixe (type)
|
||||
short_name = asset_name.split(" (")[0]
|
||||
assets = find_assets_by_name_prefix(token, short_name)
|
||||
if assets:
|
||||
# Prendre celui avec location
|
||||
for a in assets:
|
||||
attrs = a.get("attributes", {})
|
||||
if attrs.get("location", {}).get("value", {}).get("coordinates"):
|
||||
asset = a
|
||||
break
|
||||
if not asset and assets:
|
||||
asset = assets[0]
|
||||
|
||||
if not asset:
|
||||
print(f" ⚠️ Asset non trouvé: '{asset_name}', skipping")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
asset_id = asset["id"]
|
||||
print(f" 📍 Asset trouvé: {asset.get('name', '?')} ({asset_id})")
|
||||
|
||||
# Créer l'agent MQTT
|
||||
status, body = create_mqtt_agent(token, agent_name, asset_id, topic)
|
||||
if status == 200:
|
||||
print(f" ✅ Agent créé → topic: {topic}")
|
||||
created += 1
|
||||
elif status == 409:
|
||||
print(f" ⏭️ Agent déjà existant, skipping")
|
||||
skipped += 1
|
||||
else:
|
||||
print(f" ❌ Erreur {status}: {body[:200]}")
|
||||
failed += 1
|
||||
|
||||
time.sleep(0.1) # Pas surcharger l'API
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"📊 Résultat: {created} créés, {failed} erreurs, {skipped} skipped")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
53
session_resume_2026-05-13.md
Normal file
53
session_resume_2026-05-13.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Session Resume — 2026-05-13 (final)
|
||||
|
||||
## État de l'infrastructure
|
||||
- 92+ conteneurs UP
|
||||
- Services OK: OpenRemote, Grafana, InfluxDB, Orion-LD, Traefik, Simulator, BunkerM
|
||||
- Services KO: Stellio (404), FROST (404), GeoServer (404), Ditto (404)
|
||||
|
||||
## Tâches complétées
|
||||
|
||||
### ✅ BunkerM — FIXÉ
|
||||
- Cause: DNS `bunkerm_bunkerm_1` (underscores) non résolvable → `bunkerm-bunkerm-1`
|
||||
- Fix: docker-compose.yml + simulator.py modifiés, image reconstruite
|
||||
- Résultat: `→ emqx,bunkerm` dans les logs du simulateur
|
||||
|
||||
### ✅ ChirpStack — Partiellement résolu
|
||||
- Cause: base PostgreSQL jamais initialisée (0 tables), mot de passe `***` corrompait le DSN
|
||||
- Fix: mot de passe → `chirpstack`, schéma SQL initialisé depuis migrations Diesel
|
||||
- 15 tables créées, admin user créé (admin/admin1234)
|
||||
- Reste: login API à investiguer (gRPC-REST gateway)
|
||||
|
||||
## Tâches en cours / bloquées
|
||||
|
||||
### 🔴 OpenRemote Agents MQTT — BLOQUÉ
|
||||
- **Problème**: API REST `/api/smartcity/*` retourne 403 "role not allowed"
|
||||
- **Cause**: OpenRemote a son propre système d'authorization (pas Keycloak). Les rôles Keycloak ne sont pas mappés aux permissions OpenRemote.
|
||||
- **Approches tentées** (toutes échouées):
|
||||
- Token client_credentials → 403
|
||||
- Token password grant → 403
|
||||
- Cookie de session Keycloak → 403
|
||||
- Désactivation UMA → 403
|
||||
- Mapper de rôles Keycloak → pas d'effet
|
||||
- Basic auth → 403
|
||||
- Headers X-Requested-With, Accept → 403
|
||||
- **Script prêt**: `scripts/create_or_agents.py` (60 agents MQTT)
|
||||
- **Pistes restantes**:
|
||||
1. Trouver comment OpenRemote mappe les rôles Keycloak → permissions internes
|
||||
2. Utiliser l'UI Manager via browser headless
|
||||
3. Modifier la config OpenRemote pour désactiver l'auth sur l'API
|
||||
4. Créer les agents via une autre méthode (JMX, config file, etc.)
|
||||
|
||||
## Fichiers créés/modifiés
|
||||
- `scripts/create_or_agents.py` — script création 60 agents MQTT
|
||||
- `TODO.md` — todo list
|
||||
- `simulator.py` — BUNKERM_HOST corrigé
|
||||
- `docker-compose.yml` — BUNKERM_HOST corrigé
|
||||
- `configuration/chirpstack/chirpstack.toml` — DSN corrigé
|
||||
- `docker-compose.chirpstack.yml` — mot de passe corrigé
|
||||
|
||||
## Leçons apprises
|
||||
- ChirpStack 4.x n'initialise pas automatiquement le schéma PostgreSQL au premier démarrage
|
||||
- Docker Compose masque les mots de passe contenant `@` par `***`
|
||||
- Les noms de conteneurs Docker Compose utilisent des underscores, mais la DNS resolution utilise des hyphens
|
||||
- OpenRemote a son propre système d'authorization indépendant de Keycloak
|
||||
64
session_resume_2026-05-17.md
Normal file
64
session_resume_2026-05-17.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Session Resume — 2026-05-17
|
||||
|
||||
## Infrastructure Smart City Digital Twin — Martinique
|
||||
|
||||
### Complété cette session
|
||||
|
||||
#### 1. GeoServer configuré ✅
|
||||
- **URL** : `https://geoserver.digitribe.fr`
|
||||
- **Credentials** : admin / Digitribe972
|
||||
- **Workspace** : Digitribe
|
||||
- **Data Store** : postgis-smartcity (PostGIS dédié)
|
||||
- **Couche** : sensors (55 capteurs IoT importés depuis OpenRemote)
|
||||
|
||||
#### 2. PostGIS dédié créé ✅
|
||||
- **Conteneur** : postgis-smartcity
|
||||
- **Image** : postgis/postgis:15-3.4
|
||||
- **Port** : 5433
|
||||
- **Base** : smartcity / smartcity / SmartCity972
|
||||
- **Table** : sensors (id, name, type, location, attributes)
|
||||
- **Données** : 55 capteurs importés depuis OpenRemote
|
||||
|
||||
#### 3. MapStore configuré ✅
|
||||
- **URL** : `https://mapstore.digitribe.fr`
|
||||
- **CORS** : GeoServer ajouté
|
||||
- **Couche GeoServer** : sensors accessible via WMS
|
||||
|
||||
#### 4. ChirpStack credentials réinitialisés ✅
|
||||
- **URL** : `https://chirpstack.digitribe.fr`
|
||||
- **Credentials** : admin / admin1234
|
||||
|
||||
### Bloqués
|
||||
|
||||
#### OpenRemote Agents MQTT (403)
|
||||
- L'API REST retourne 403 malgré tous les tokens Keycloak
|
||||
- L'UI Manager ne rend pas dans les navigateurs headless (Web Components)
|
||||
- **Solution recommandée** : se connecter manuellement via un navigateur réel
|
||||
|
||||
#### Ditto (MongoDB localhost)
|
||||
- Les images Docker de Ditto 3.8.12 hardcodent `localhost:27017` pour MongoDB
|
||||
- Les variables d'environnement `MONGO_HOST`, `MONGODB_URI` ne sont pas reconnues
|
||||
- **Solution** : modifier le JAR ou utiliser un hostname `localhost` qui pointe vers MongoDB
|
||||
|
||||
#### Prometheus (réseau interne)
|
||||
- Le conteneur Prometheus ne peut pas accéder aux services internes
|
||||
- **Solution** : reconfigurer le réseau ou utiliser les endpoints exposés
|
||||
|
||||
### Fichiers modifiés
|
||||
- `docker-compose.yml` — BUNKERM_HOST corrigé
|
||||
- `simulator.py` — BUNKERM_HOST corrigé
|
||||
- `docker-compose.chirpstack.yml` — mot de passe corrigé
|
||||
- `configuration/chirpstack/chirpstore.toml` — DSN corrigé
|
||||
- `docker-compose.ditto.yml` — recréé avec config MongoDB et auth devops
|
||||
- `docker-compose.postgis.yml` — nouveau PostGIS dédié
|
||||
- `traefik-config/dynamic/routes.yml` — GeoServer ajouté au CORS MapStore
|
||||
- `traefik-config/dynamic/10-lorawan.yml` — ChirpStack corrigé
|
||||
|
||||
### TODO list
|
||||
| Tâche | Statut |
|
||||
|-------|--------|
|
||||
| OpenRemote agents MQTT | 🔴 Bloqué |
|
||||
| ChirpStack login API | 🔄 En cours |
|
||||
| NGSI-LD pipeline | ⏳ En attente |
|
||||
| GeoMesa + KeplerGL | ⏳ En attente |
|
||||
| Documentation + Gitea | 🔄 En cours |
|
||||
@@ -55,7 +55,7 @@ EMQX_HOST = os.environ.get("EMQX_HOST", "emqx_emqx_1")
|
||||
EMQX_PORT = int(os.environ.get("EMQX_PORT", "1883"))
|
||||
MOSQUITTO_HOST = os.environ.get("MOSQUITTO_HOST", "smart-city-mosquitto")
|
||||
MOSQUITTO_PORT = int(os.environ.get("MOSQUITTO_PORT", "1883"))
|
||||
BUNKERM_HOST = os.environ.get("BUNKERM_HOST", "bunkerm_bunkerm_1")
|
||||
BUNKERM_HOST = os.environ.get("BUNKERM_HOST", "bunkerm-bunkerm-1")
|
||||
BUNKERM_PORT = int(os.environ.get("BUNKERM_PORT", "1900"))
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user