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"
|
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:
|
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:
|
smartcity-shared:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
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"))
|
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"))
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user