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:
Eric FELIXINE
2026-05-17 19:18:24 -04:00
parent 1006df137d
commit 7477410813
11 changed files with 598 additions and 92 deletions

2
.env Normal file
View File

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

35
TODO.md Normal file
View 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

View File

@@ -1,12 +1,5 @@
version: "3.8" version: "3.8"
# =============================================================================
# ChirpStack LoRaWAN Network Server — Smart City Digital Twin
# =============================================================================
# Image officielle chirpstack/chirpstack:latest
# Credentials par défaut: admin/admin
# =============================================================================
services: services:
chirpstack: chirpstack:
image: chirpstack/chirpstack:latest image: chirpstack/chirpstack:latest
@@ -14,14 +7,11 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./configuration/chirpstack:/etc/chirpstack:ro - ./configuration/chirpstack:/etc/chirpstack:ro
depends_on:
- postgres
- mosquitto
- redis
environment: environment:
- MQTT_BROKER_HOST=mosquitto - MQTT_BROKER_HOST=chirpstack-mosquitto-1
- REDIS_HOST=redis - REDIS_HOST=chirpstack-redis-1
- POSTGRESQL_HOST=postgres - POSTGRESQL_HOST=chirpstack-postgres-1
- DATABASE_URL=postgres://chirpstack:chirpstack@chirpstack-postgres-1/chirpstack?sslmode=disable
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.chirpstack.rule=Host(`chirpstack.digitribe.fr`)" - "traefik.http.routers.chirpstack.rule=Host(`chirpstack.digitribe.fr`)"
@@ -29,41 +19,6 @@ services:
- "traefik.http.routers.chirpstack.tls.certresolver=letsencrypt" - "traefik.http.routers.chirpstack.tls.certresolver=letsencrypt"
- "traefik.http.services.chirpstack.loadbalancer.server.port=8080" - "traefik.http.services.chirpstack.loadbalancer.server.port=8080"
networks: 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 - smartcity-shared
chirpstack-rest-api: chirpstack-rest-api:
@@ -79,44 +34,8 @@ services:
- "traefik.http.routers.chirpstack-api.tls.certresolver=letsencrypt" - "traefik.http.routers.chirpstack-api.tls.certresolver=letsencrypt"
- "traefik.http.services.chirpstack-api.loadbalancer.server.port=8090" - "traefik.http.services.chirpstack-api.loadbalancer.server.port=8090"
networks: networks:
- traefik-public
- smartcity-shared - 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: networks:
traefik-public:
external: true
smartcity-shared: smartcity-shared:
external: true external: true

View File

@@ -1,4 +1,4 @@
# Eclipse Ditto - Smart City Digital Twin (MongoDB fix) # Eclipse Ditto - Smart City Digital Twin
version: '3.8' version: '3.8'
services: services:
@@ -21,10 +21,13 @@ services:
depends_on: depends_on:
- ditto-mongodb - ditto-mongodb
environment: 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_HOST=smart-city-ditto-mongodb
- MONGO_PORT=27017 - MONGO_PORT=27017
- MONGO_DB=Policies - MONGO_DB=Policies
- MONGODB_URI=mongodb://smart-city-ditto-mongodb:27017/Policies
- AKKA_REMOTE_ENABLED=false - AKKA_REMOTE_ENABLED=false
networks: networks:
traefik-public: traefik-public:
@@ -46,10 +49,13 @@ services:
- ditto-mongodb - ditto-mongodb
- ditto-policies - ditto-policies
environment: 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_HOST=smart-city-ditto-mongodb
- MONGO_PORT=27017 - MONGO_PORT=27017
- MONGO_DB=Things - MONGO_DB=Things
- MONGODB_URI=mongodb://smart-city-ditto-mongodb:27017/Things
- AKKA_REMOTE_ENABLED=false - AKKA_REMOTE_ENABLED=false
networks: networks:
traefik-public: traefik-public:
@@ -71,12 +77,17 @@ services:
- ditto-things - ditto-things
- ditto-policies - ditto-policies
environment: 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 - DITTO_GATEWAY_PROXY_ENABLED=true
- AKKA_REMOTE_ENABLED=false - AKKA_REMOTE_ENABLED=false
- DITTO_GW_STREAMING_ENABLED=true - DITTO_GW_STREAMING_ENABLED=true
- DITTO_GW_MQTT_BROKER=smart-city-mosquitto:1883 - DITTO_GW_MQTT_BROKER=smart-city-mosquitto:1883
- DITTO_GW_MQTT_TOPIC_FILTER=smartcity/# - 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: networks:
traefik-public: traefik-public:
aliases: aliases:
@@ -85,7 +96,8 @@ services:
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.ditto-gateway.rule=Host(`ditto.digitribe.fr`)" - "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" - "traefik.http.services.ditto-gateway.loadbalancer.server.port=8080"
networks: networks:

View File

@@ -28,7 +28,7 @@ services:
- ENABLE_EMQX=1 - ENABLE_EMQX=1
- ENABLE_MOSQUITTO=1 - ENABLE_MOSQUITTO=1
- ENABLE_BUNKER=1 - ENABLE_BUNKER=1
- BUNKERM_HOST=bunkerm_bunkerm_1 - BUNKERM_HOST=bunkerm-bunkerm-1
- BUNKERM_PORT=1900 - BUNKERM_PORT=1900
# Context Brokers (DESACTIVE - tout passe par les IoT Agents via MQTT) # Context Brokers (DESACTIVE - tout passe par les IoT Agents via MQTT)
- ENABLE_ORION=false - ENABLE_ORION=false
@@ -57,6 +57,25 @@ services:
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
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 - traduce les msgs MQTT bunker/bunker vers Orion-LD
iot-agent-bunkerm: iot-agent-bunkerm:
image: fiware/iotagent-json:latest image: fiware/iotagent-json:latest

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,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
View 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()

View 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

View 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 |

View File

@@ -55,7 +55,7 @@ EMQX_HOST = os.environ.get("EMQX_HOST", "emqx_emqx_1")
EMQX_PORT = int(os.environ.get("EMQX_PORT", "1883")) EMQX_PORT = int(os.environ.get("EMQX_PORT", "1883"))
MOSQUITTO_HOST = os.environ.get("MOSQUITTO_HOST", "smart-city-mosquitto") MOSQUITTO_HOST = os.environ.get("MOSQUITTO_HOST", "smart-city-mosquitto")
MOSQUITTO_PORT = int(os.environ.get("MOSQUITTO_PORT", "1883")) 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")) BUNKERM_PORT = int(os.environ.get("BUNKERM_PORT", "1900"))
# ============================================================================= # =============================================================================