- 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
261 lines
8.9 KiB
Python
261 lines
8.9 KiB
Python
#!/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()
|