diff --git a/.env b/.env new file mode 100644 index 00000000..9a248206 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +CHIRP_USER=chirpstack +CHIRP_PASS=chirpstack diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..b9dbaed7 --- /dev/null +++ b/TODO.md @@ -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 diff --git a/docker-compose.chirpstack.yml b/docker-compose.chirpstack.yml index 87e1415c..a3e51c60 100644 --- a/docker-compose.chirpstack.yml +++ b/docker-compose.chirpstack.yml @@ -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 diff --git a/docker-compose.ditto.yml b/docker-compose.ditto.yml index 41c65010..9eeeacfa 100644 --- a/docker-compose.ditto.yml +++ b/docker-compose.ditto.yml @@ -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: diff --git a/docker-compose.yml b/docker-compose.yml index 496a8cf7..18c51025 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/geojson-proxy/Dockerfile b/geojson-proxy/Dockerfile new file mode 100644 index 00000000..f03a723b --- /dev/null +++ b/geojson-proxy/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.11-slim +WORKDIR /app +COPY geojson_proxy.py . +EXPOSE 8080 +CMD ["python", "geojson_proxy.py"] diff --git a/geojson-proxy/geojson_proxy.py b/geojson-proxy/geojson_proxy.py new file mode 100644 index 00000000..13225e0d --- /dev/null +++ b/geojson-proxy/geojson_proxy.py @@ -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() diff --git a/scripts/create_or_agents.py b/scripts/create_or_agents.py new file mode 100644 index 00000000..5181a543 --- /dev/null +++ b/scripts/create_or_agents.py @@ -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() diff --git a/session_resume_2026-05-13.md b/session_resume_2026-05-13.md new file mode 100644 index 00000000..964530e4 --- /dev/null +++ b/session_resume_2026-05-13.md @@ -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 diff --git a/session_resume_2026-05-17.md b/session_resume_2026-05-17.md new file mode 100644 index 00000000..0aa2b575 --- /dev/null +++ b/session_resume_2026-05-17.md @@ -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 | diff --git a/simulator.py b/simulator.py index 34c96266..8550513b 100644 --- a/simulator.py +++ b/simulator.py @@ -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")) # =============================================================================