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

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