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:
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()
|
||||
Reference in New Issue
Block a user