diff --git a/__pycache__/simulator.cpython-313.pyc b/__pycache__/simulator.cpython-313.pyc new file mode 100644 index 00000000..1700043f Binary files /dev/null and b/__pycache__/simulator.cpython-313.pyc differ diff --git a/simulator.py.backup_20260504_141747 b/simulator.py.backup_20260504_141747 new file mode 100644 index 00000000..392dd327 --- /dev/null +++ b/simulator.py.backup_20260504_141747 @@ -0,0 +1,501 @@ + 1|#!/usr/bin/env python3 + 2|""" + 3|Smart City IoT Simulator — Martinique (14.6°N, 61.2°W) + 4|======================================================= + 5|Publie vers MULTIPLES brokers MQTT + context brokers NGSI-LD. + 6| + 7|Brokers MQTT: + 8| - EMQX: emqx_emqx_1:1883 (sans auth) + 9| - Mosquitto: mosquitto-traefik:1883 (bunker/bunker) + 10| - BunkerM: bunkerm_bunkerm_1:1900 (TLS, bunker/bunker) + 11| - OpenRemote: openremote-manager-1:1883 (admin/Digitribe972) + 12| + 13|Context Brokers REST: + 14| - Orion-LD: fiware-gis-quickstart-orion-1:1026 (NGSI-LD) + 15| - Stellio: stellio-api-gateway:8080 (NGSI-LD) + 16| - FROST: frost_allinone-web-1:8080/FROST-Server/v1.1 (SensorThings) + 17| + 18|Variables d'environnement: + 19| PUBLISH_INTERVAL_SEC : intervalle de publication (défaut: 10s) + 20| BASE_LAT / BASE_LON : coordonnées de base (défaut: Fort-de-France) + 21| ENABLE_ORION=1 : activer Orion-LD (défaut: 1) + 22| ENABLE_STELLIO=1 : activer Stellio (défaut: 1) + 23| ENABLE_FROST=1 : activer FROST-Server (défaut: 1) + 24|""" + 25| + 26|import os, sys, json, time, random, signal, queue, threading, ssl, urllib.parse + 27|import paho.mqtt.client as mqtt + 28|import urllib.request, urllib.error + 29|from datetime import datetime, timezone + 30|from typing import Any + 31|import influxdb_client + 32|from influxdb_client.client.write_api import SYNCHRONOUS + 33| + 34|# ============================================================================= + 35|# Configuration + 36|# ============================================================================= + 37|BASE_LAT = float(os.environ.get("BASE_LAT", "14.6091")) + 38|BASE_LON = float(os.environ.get("BASE_LON", "-61.2155")) + 39|INTERVAL = int(os.environ.get("PUBLISH_INTERVAL_SEC", "10")) + 40|ENABLE_ORION = os.environ.get("ENABLE_ORION", "1") == "1" + 41|ENABLE_STELLIO = os.environ.get("ENABLE_STELLIO", "1") == "1" + 42|ENABLE_FROST = os.environ.get("ENABLE_FROST", "1") == "1" + 43|ENABLE_OPENREMOTE = os.environ.get("ENABLE_OPENREMOTE", "1") == "1" + 44|OR_ADMIN_USER = os.environ.get("OR_ADMIN_USER", "admin") + 45|OR_ADMIN_PASS = os.environ.get("OR_ADMIN_PASS", "Digitribe972") + 46|OR_REALM = os.environ.get("OR_REALM", "smartcity") + 47|OR_TOKEN_REALM = os.environ.get("OR_TOKEN_REALM", "master") # Realm pour obtention token + 48| + 49|# InfluxDB config + 50|ENABLE_INFLUX = os.environ.get("ENABLE_INFLUX", "1") == "1" + 51|INFLUX_URL = os.environ.get("INFLUX_URL", "http://digital-twin-influxdb:8086") + 52|INFLUX_ORG = os.environ.get("INFLUX_ORG", "digitribe") + 53|INFLUX_BUCKET = os.environ.get("INFLUX_BUCKET", "iot_data") + 54|INFLUX_TOKEN = os.environ.get("INFLUX_TOKEN", + 55| "my-super-secret-admin-token") + 56| + 57|# Initialize InfluxDB client + 58|_influx_client = None + 59|_influx_write_api = None + 60|if ENABLE_INFLUX: + 61| try: + 62| _influx_client = influxdb_client.InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG) + 63| _influx_write_api = _influx_client.write_api(write_options=SYNCHRONOUS) + 64| print(f"[INFLUX] ✅ Connected to {INFLUX_URL}") + 65| except Exception as e: + 66| print(f"[INFLUX] ❌ Connection failed: {e}") +FROST_URL = os.environ.get("FROST_URL", "http://frost_http-web-1:8080/FROST-Server/v1.1") + 68| + 69|SENSOR_COUNTS = { + 70| "traffic": int(os.environ.get("SENSOR_COUNT_traffic", "3")), + 71| "airquality": int(os.environ.get("SENSOR_COUNT_airquality", "2")), + 72| "parking": int(os.environ.get("SENSOR_COUNT_parking", "2")), + 73| "noise": int(os.environ.get("SENSOR_COUNT_noise", "1")), + 74| "weather": int(os.environ.get("SENSOR_COUNT_weather", "1")), + 75| "light": int(os.environ.get("SENSOR_COUNT_light", "1")), + 76|} + 77|# Si SENSOR_COUNT est défini, multiplier les counts de façon proportionnelle + 78|_total_default = sum(SENSOR_COUNTS.values()) + 79|if "SENSOR_COUNT" in os.environ: + 80| target = int(os.environ["SENSOR_COUNT"]) + 81| ratio = target / _total_default + 82| for k in SENSOR_COUNTS: + 83| SENSOR_COUNTS[k] = max(1, int(SENSOR_COUNTS[k] * ratio)) + 84| + 85|# ============================================================================= + 86|# Localisation des capteurs Martinique + 87|# ============================================================================= + 88|SENSOR_LOCATIONS: dict[str, list[dict]] = {} + 89|SENSOR_NAMES: dict[str, list[str]] = { + 90| "traffic": ["Carrefour Central", "Avenue des Caraïbes", "Boulevard Pasteur", + 91| "Rue des Flamboyants", "Place de la République"], + 92| "airquality": ["Quartier Bonde", "Port de Fort-de-France", "Château Denis", + 93| "Lamentin Aéroport", "Schoelcher Village"], + 94| "parking": ["Parking Rivière-Saleé", "Parking Cluny", "Parking Média", + 95| "Parking Grand-Camp", "Parking Dillon"], + 96| "noise": ["Rue des Arts", "Marché Central", "Université Fort-de-France", + 97| "Stade de Dillon", "Place du Champs de Mars"], + 98| "weather": ["Station Météo Lamentin", "Station Schoelcher", + 99| "Station Ajoupa-Bouillon", "Station Le François", "Station Le Robert"], + 100| "light": ["Eclairage Rue des Mouettes", "Candela Boulevard", + 101| "Lumiere Rue des Acacias", "Feux Signalisation Centre", "Eclairage Port"], + 102|} + 103| + 104|def _gen_locs(stype: str, count: int) -> list[dict]: + 105| locs = [] + 106| for i in range(count): + 107| lat = BASE_LAT + random.uniform(-0.05, 0.05) + 108| lon = BASE_LON + random.uniform(-0.05, 0.05) + 109| names = SENSOR_NAMES.get(stype, [stype]) + 110| locs.append({ + 111| "lat": round(lat, 6), + 112| "lon": round(lon, 6), + 113| "name": names[i % len(names)], + 114| }) + 115| return locs + 116| + 117|for stype, count in SENSOR_COUNTS.items(): + 118| SENSOR_LOCATIONS[stype] = _gen_locs(stype, count) + 119| + 120|# Ranges par type + 121|SENSOR_RANGES: dict[str, dict] = { + 122| "traffic": {"vehicle_count":(10,150),"average_speed_kmh":(10,80), + 123| "congestion_level":(0,5),"occupancy_percent":(0,100)}, + 124| "airquality": {"pm25_ugm3":(5,80),"pm10_ugm3":(10,150),"no2_ugm3":(5,60), + 125| "o3_ugm3":(20,120),"co_mgm3":(0.1,5.0), + 126| "temperature_celsius":(20,35),"humidity_percent":(40,95)}, + 127| "parking": {"total_spots":(50,500),"available_spots":(0,500), + 128| "occupancy_percent":(0,100),"turnover_per_hour":(5,50)}, + 129| "noise": {"noise_level_db":(40,95),"peak_db":(60,110)}, + 130| "weather": {"temperature_celsius":(22,34),"humidity_percent":(50,95), + 131| "wind_speed_kmh":(0,50),"pressure_hpa":(1005,1025), + 132| "rain_mm":(0,20),"uv_index":(0,11)}, + 133| "light": {"brightness_lux":(0,100000),"power_consumption_w":(0,500)}, + 134|} + 135| + 136|NOISE_CATEGORIES = ["quiet","moderate","loud","very_loud"] + 137|LIGHT_STATUSES = ["on","off","dimmed","auto"] + 138| + 139|# ============================================================================= + 140|# Capteurs déclarés + 141|# ============================================================================= + 142|SENSORS: dict[str, dict] = {} + 143|counter = 0 + 144|for stype, locs in SENSOR_LOCATIONS.items(): + 145| for loc in locs: + 146| sid = f"{stype}_{counter:03d}" + 147| SENSORS[sid] = {"type": stype, "lat": loc["lat"], "lon": loc["lon"], "name": loc["name"]} + 148| counter += 1 + 149| + 150|# ============================================================================= + 151|# Payload NGSI-LD pour Orion-LD / Stellio + 152|# ============================================================================= + 153|# Contextes NGSI-LD : core + Smart Data Models + 154|# https://smartdatamodels.org pour les @context officiels + 155|# Contexte NGSI-LD pur pour Orion-LD (vocabulaires standards uniquement) + 156|# Orion-LD ne peut pas résoudre raw.githubusercontent.com — utiliser uri.etsi.org uniquement + 157|ORION_CONTEXT = [ + 158| "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + 159|] + 160| + 161|# Mapping sensor type → Smart Data Model type NGSI-LD + 162|SMART_MODEL_MAPPING = { + 163| "airquality": "AirQualityObserved", + 164| "traffic": "TrafficFlowObserved", + 165| "parking": "OffStreetParking", + 166| "noise": "NoiseLevelObserved", + 167| "weather": "WeatherObserved", + 168| "light": "Device", + 169|} + 170|FROST_HEADERS = {"Accept": "application/json", "Content-Type": "application/json"} + 171| + 172|# Cache FROST : éviter de recréer Thing/Datastream + 173|_frost_cache: dict[str, tuple[str, str]] = {} # (sid, field) -> (thing_id, ds_id) + 174| + 175|# Contexte NGSI-LD pur pour Stellio et Orion-LD (vocabulaires standards uniquement) + 176|# Stellio et Orion-LD embarquent le contexte core NGSI-LD : https://uri.etsi.org/ngsi-ld/ + 177|# On n'utilise PAS les vocabulaires smartdatamodels.org distants (inaccessibles depuis les containers) + 178|# Les types d'entité Smart Data Models (AirQualityObserved, etc.) sont reconnus par leur nom + 179|# Les propriétés spécifiques sont stockées telles quelles (vocabulaire libre) + 180|STELLIO_INLINE_CONTEXT = [ + 181| "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + 182|] + 183| + 184|def _ngsi_payload(sid: str, sensor: dict, context: list | dict = ORION_CONTEXT) -> dict: + 185| """Construit un payload NGSI-LD avec Smart Data Models officiels.""" + 186| stype = sensor["type"] + 187| model_type = SMART_MODEL_MAPPING.get(stype, "Device") + 188| now = datetime.now(timezone.utc).isoformat() + 189| + 190| # Attributs communs à tous les modèles + 191| payload = { + 192| "@context": context, + 193| "id": f"urn:ngsi-ld:{model_type}:{sid}", + 194| "type": model_type, + 195| "dateObserved": {"type": "Property", "value": now}, + 196| "location": {"type": "GeoProperty", + 197| "value": {"type": "Point", + 198| "coordinates": [sensor["lon"], sensor["lat"]]}}, + 199| "name": {"type": "Property", "value": sensor["name"]}, + 200| "batteryLevel": {"type": "Property", "value": random.randint(60, 100)}, + 201| } + 202| + 203| # Attributs spécifiques par type de modèle + 204| ranges = SENSOR_RANGES.get(stype, {}) + 205| props = {} + 206| for field, val_range in ranges.items(): + 207| if isinstance(val_range, tuple) and len(val_range) == 2: + 208| lo, hi = val_range + 209| if isinstance(lo, (int, float)): + 210| props[field] = {"type": "Property", "value": round(random.uniform(lo, hi), 1)} + 211| elif isinstance(val_range, list): + 212| props[field] = {"type": "Property", "value": random.choice(val_range)} + 213| + 214| # Mapping vers les noms d'attributs Smart Data Models + 215| if stype == "airquality": + 216| if "pm25_ugm3" in props: payload["NO2"] = props.pop("pm25_ugm3") # Simplifié + 217| if "pm10_ugm3" in props: payload["PM10"] = props.pop("pm10_ugm3") + 218| if "no2_ugm3" in props: payload["NO2"] = props.pop("no2_ugm3") + 219| if "o3_ugm3" in props: payload["O3"] = props.pop("o3_ugm3") + 220| if "co_mgm3" in props: payload["CO"] = props.pop("co_mgm3") + 221| if "temperature_celsius" in props: payload["temperature"] = props.pop("temperature_celsius") + 222| if "humidity_percent" in props: payload["relativeHumidity"] = props.pop("humidity_percent") + 223| + 224| elif stype == "traffic": + 225| if "vehicle_count" in props: payload["vehicleCount"] = props.pop("vehicle_count") + 226| if "average_speed_kmh" in props: payload["averageVehicleSpeed"] = props.pop("average_speed_kmh") + 227| if "congestion_level" in props: payload["congestion"] = props.pop("congestion_level") + 228| if "occupancy_percent" in props: payload["occupancy"] = props.pop("occupancy_percent") + 229| + 230| elif stype == "parking": + 231| if "available_spots" in props: payload["availableSpotNumber"] = props.pop("available_spots") + 232| if "total_spots" in props: payload["totalSpotNumber"] = props.pop("total_spots") + 233| if "occupancy_percent" in props: payload["occupancy"] = props.pop("occupancy_percent") + 234| if "turnover_per_hour" in props: payload["turnover"] = props.pop("turnover_per_hour") + 235| + 236| elif stype == "noise": + 237| if "noise_level_db" in props: payload["noiseLevel"] = props.pop("noise_level_db") + 238| if "peak_db" in props: payload["noisePeak"] = props.pop("peak_db") + 239| payload["noiseCategory"] = {"type": "Property", "value": random.choice(NOISE_CATEGORIES)} + 240| + 241| elif stype == "weather": + 242| if "temperature_celsius" in props: payload["temperature"] = props.pop("temperature_celsius") + 243| if "humidity_percent" in props: payload["relativeHumidity"] = props.pop("humidity_percent") + 244| if "rain_mm" in props: payload["rainfall"] = props.pop("rain_mm") + 245| if "uv_index" in props: payload["uvIndex"] = props.pop("uv_index") + 246| if "wind_speed_kmh" in props: payload["windSpeed"] = props.pop("wind_speed_kmh") + 247| + 248| elif stype == "light": + 249| if "brightness_lux" in props: payload["illuminance"] = props.pop("brightness_lux") + 250| if "power_consumption_w" in props: payload["power"] = props.pop("power_consumption_w") + 251| payload["status"] = {"type": "Property", "value": random.choice(LIGHT_STATUSES)} + 252| + 253| return payload + 254| + 255|def _frost_payload(sid: str, sensor: dict) -> dict: + 256| """Construit un payload SensorThings pour FROST-Server.""" + 257| stype = sensor["type"] + 258| ranges = SENSOR_RANGES.get(stype, {}) + 259| datastreams = [] + 260| + 261| for field, val_range in ranges.items(): + 262| if isinstance(val_range, tuple) and len(val_range) == 2: + 263| lo, hi = val_range + 264| if isinstance(lo, (int, float)) and isinstance(hi, (int, float)): + 265| val = round(random.uniform(lo, hi), 1) + 266| unit = "http://www.qudt.org/vocab/unit#DegreeCelsius" + 267| obs_prop = { + 268| "name": f"{field} Observation", + 269| "description": f"Observation of {field}", + 270| "definition": unit, + 271| } + 272| sensor_data = { + 273| "name": f"Sensor {sid} {field}", + 274| "description": f"Sensor {sid} measuring {field}", + 275| "encodingType": "http://www.opengis.net/doc/IS/SensorML/2.0", + 276| "metadata": {"unit": unit}, + 277| } + 278| ds = { + 279| "name": f"Datastream {stype}/{field}", + 280| "description": f"Datastream for {stype} sensor {sid} - {field}", + 281| "observationType": "http://www.opengis.net/def/observationType/OGC-OM/2.0/OM_Measurement", + 282| "unitOfMeasurement": {"name": field, "symbol": "", "definition": unit}, + 283| "Sensor": sensor_data, + 284| "ObservedProperty": obs_prop, + 285| } + 286| datastreams.append((field, ds, val)) + 287| + 288| thing_payload = { + 289| "name": f"Thing_{sid}", + 290| "description": f"Smart City {stype} sensor in Martinique", + 291| "properties": {"sensorType": stype, "region": "Martinique"}, + 292| } + 293| return thing_payload, datastreams + 294| + 295|# ============================================================================= + 296|# HTTP helper + 297|# ============================================================================= + 298|def _http_post(url: str, data: dict, headers: dict) -> str: + 299| """POST et retourne 'ok' ou 'created' (ou '' si échec).""" + 300| try: + 301| body = json.dumps(data).encode() + 302| req = urllib.request.Request(url, data=body, headers=headers, method="POST") + 303| with urllib.request.urlopen(req, timeout=8) as resp: + 304| if resp.status == 204: + 305| return 'created' # No Content — succès + 306| if resp.status not in (200, 201): + 307| return '' + 308| # Lire le corps pour extraire l'ID (FROST) + 309| try: + 310| result = json.loads(resp.read()) + 311| if '@iot.selfLink' in result: + 312| link = result['@iot.selfLink'] + 313| return link.split('(')[1].rstrip(')') + 314| if '@iot.id' in result: + 315| return str(result['@iot.id']) + 316| except Exception: + 317| pass + 318| location = resp.headers.get('Location', '') + 319| if location: + 320| return location.split('(')[1].rstrip(')') if '(' in location else '' + 321| return 'created' + 322| except urllib.error.HTTPError as e: + 323| # Lire le corps de l'erreur pour debug + 324| try: + 325| err_body = e.read().decode()[:200] + 326| except Exception: + 327| err_body = str(e) + 328| print(f" ⚠️ HTTP POST {url} → {e.code}: {err_body}") + 329| return '' + 330| except Exception as e: + 331| print(f" ⚠️ HTTP POST {url} → {e}") + 332| return '' + 333| + 334|def _http_put(url: str, data: dict, headers: dict) -> bool: + 335| try: + 336| body = json.dumps(data).encode() + 337| req = urllib.request.Request(url, data=body, headers=headers, method="PUT") + 338| with urllib.request.urlopen(req, timeout=5) as resp: + 339| return resp.status in (200, 204) + 340| except urllib.error.HTTPError as e: + 341| if e.code == 409: + 342| return True # Already exists - that's fine + 343| print(f" ⚠️ HTTP PUT {url} → {e}") + 344| return False + 345| except Exception as e: + 346| print(f" ⚠️ HTTP PUT {url} → {e}") + 347| return False + 348| + 349|# ============================================================================= + 350|# MQTT Client multi-broker + 351|# ============================================================================= + 352|class MultiMQTT: + 353| def __init__(self): + 354| self.clients: dict[str, mqtt.Client] = {} + 355| self.ok: dict[str, bool] = {} + 356| self._lock = threading.Lock() + 357| self._setup() + 358| + 359| def _mk_client(self, name: str, host: str, port: int, + 360| tls: bool = False, user: str = "", pwd: str = "", + 361| ws: bool = False) -> mqtt.Client: + 362| cid = f"smartcity-sim-{name}-{os.getpid()}" + 363| c = mqtt.Client(client_id=cid, protocol=mqtt.MQTTv311) + 364| if user: + 365| c.username_pw_set(user, pwd) + 366| if tls: + 367| c.tls_set(cert_reqs=ssl.CERT_NONE) + 368| c.tls_insecure_set(True) + 369| if ws: + 370| c.ws_set(b"/mqtt") + 371| c.on_connect = lambda _c, _, __, rc: self._on_connect(name, rc) + 372| c.on_disconnect = lambda _c, _, __: self._on_disconnect(name) + 373| try: + 374| c.connect(host, port, keepalive=30) + 375| c.loop_start() + 376| except Exception as e: + 377| print(f"[MQTT] ❌ {name} @ {host}:{port} → {e}") + 378| self.ok[name] = False + 379| return c + 380| + 381| def _on_connect(self, name: str, rc: int): + 382| with self._lock: + 383| if rc == 0: + 384| self.ok[name] = True + 385| print(f"[MQTT] ✅ {name} connecté") + 386| else: + 387| self.ok[name] = False + 388| print(f"[MQTT] ❌ {name} rc={rc}") + 389| + 390| def _on_disconnect(self, name: str): + 391| with self._lock: + 392| self.ok[name] = False + 393| print(f"[MQTT] ⚠️ {name} déconnecté") + 394| + 395| def _setup(self): + 396| # Garder que EMQX et Mosquitto (MQTT fonctionnels) + 397| # BunkerM via HTTP API (port 2000) au lieu de MQTT/TLS + 398| brokers = [ + 399| ("EMQX", "emqx_emqx_1", 1883, False, "", ""), + 400| ("Mosquitto", "mosquitto-traefik", 1883, False, "bunker", "bunker"), + 401| ] + 402| print("[MQTT] 🔌 Connexion aux brokers...") + 403| for name, host, port, tls, user, pwd in brokers: + 404| c = self._mk_client(name, host, port, tls=tls, user=user, pwd=pwd) + 405| self.clients[name] = c + 406| self.ok[name] = False + 407| time.sleep(3) # Attend les connexions + 408| + 409| def publish(self, topic: str, payload: str) -> dict[str, bool]: + 410| results = {} + 411| with self._lock: + 412| for name, client in self.clients.items(): + 413| if self.ok.get(name, False): + 414| try: + 415| r = client.publish(topic, payload, qos=1) + 416| results[name] = (r.rc == mqtt.MQTT_ERR_SUCCESS) + 417| except Exception: + 418| results[name] = False + 419| else: + 420| results[name] = False + 421| return results + 422| + 423| def stop(self): + 424| for name, c in self.clients.items(): + 425| try: + 426| c.loop_stop() + 427| c.disconnect() + 428| except Exception: + 429| pass + 430| + 431|# ============================================================================= + 432|# URLs de base (résolues au démarrage) + 433|# ============================================================================= + 434|ORION_HOST = "fiware-gis-quickstart-orion-1" + 435|ORION_IP = "" + 436|try: + 437| import socket + 438| ORION_IP = socket.gethostbyname(ORION_HOST) + 439|except: + 440| pass + 441|ORION_URL = f"http://{ORION_IP or ORION_HOST}:1026" if ORION_IP else "http://fiware-gis-quickstart-orion-1:1026" + 442|STELLIO_URL = os.environ.get("STELLIO_URL", "http://stellio-api-gateway:8080") + 443|# Configuration OpenRemote (URLs dynamiques) + 444|OR_URL = os.environ.get("OR_URL", "http://openremote-manager-1:8080") # Hostname Docker interne + 445|OR_REALM = os.environ.get("OR_REALM", "smartcity") # Default: smartcity + 446|OR_TOKEN_URL = os.environ.get("OR_TOKEN_URL", f"http://openremote-keycloak-1:8080/auth/realms/{OR_TOKEN_REALM}/protocol/openid-connect/token") + 447|OR_TOKEN_TTL = int(os.environ.get("OR_TOKEN_TTL", "3600")) # Refresh token every hour + 448|STELLIO_TENANT = os.environ.get("STELLIO_TENANT", "urn:ngsi-ld:tenant:default") + 449| + 450|def publish_stellio(sid: str, sensor: dict) -> bool: + 451| """Publie sur Stellio via Traefik (gère le 409).""" + 452| entity = _ngsi_payload(sid, sensor, context=STELLIO_INLINE_CONTEXT) + 453| # Stellio a besoin du @context pour résoudre les vocabulaires NGSI-LD + 454| # (uri.etsi.org résolu depuis le JAR embarqué) + 455| url = f"{STELLIO_URL}/ngsi-ld/v1/entities" + 456| headers = { + 457| "Content-Type": "application/ld+json", + 458| "Accept": "application/ld+json", + 459| "NGSILD-Tenant": STELLIO_TENANT, + 460| } + 461| try: + 462| body = json.dumps(entity).encode() + 463| req = urllib.request.Request(url, data=body, headers=headers, method="POST") + 464| with urllib.request.urlopen(req, timeout=8) as resp: + 465| print(f" 🏢 Stellio: ✅ (HTTP {resp.status})") + 466| return True + 467| except urllib.error.HTTPError as e: + 468| if e.code == 409: # Already exists, do update with PUT + 469| try: + 470| entity_id = urllib.parse.quote(entity["id"], safe="") + 471| update_url = f"{STELLIO_URL}/ngsi-ld/v1/entities/{entity_id}" + 472| req2 = urllib.request.Request(update_url, data=body, headers=headers, method="PUT") + 473| with urllib.request.urlopen(req2, timeout=8) as resp2: + 474| print(f" 🏢 Stellio: ✅ (HTTP {resp2.status} updated)") + 475| return True + 476| except Exception as e2: + 477| print(f" ⚠️ Stellio update failed: {e2}") + 478| return False + 479| try: + 480| err = e.read().decode()[:300] + 481| except Exception: + 482| err = str(e) + 483| print(f" ⚠️ Stellio → {e.code}: {err}") + 484| return False + 485| except Exception as e: + 486| print(f" ⚠️ Stellio → {e}") + 487| return False + 488| + 489|def publish_orion(sid: str, sensor: dict) -> bool: + 490| """Publie sur Orion-LD (POST create, PATCH update).""" + 491| import socket + 492| entity = _ngsi_payload(sid, sensor) + 493| if not hasattr(publish_orion, "orion_ip"): + 494| try: + 495| publish_orion.orion_ip = socket.gethostbyname("fiware-gis-quickstart-orion-1") + 496| except Exception: + 497| publish_orion.orion_ip = "192.168.192.20" + 498| base = f"http://{publish_orion.orion_ip}:1026/ngsi-ld/v1" + 499| # 1. Essayer de créer (POST) + 500| try: + 501| \ No newline at end of file