From 3e302b0732948344a0d6d9b079993bccc40ff4a6 Mon Sep 17 00:00:00 2001 From: Eric FELIXINE Date: Mon, 4 May 2026 19:00:23 -0400 Subject: [PATCH] WIP: Orion Grafana blocked (readOnly) + move to OpenRemote MQTT --- __pycache__/simulator.cpython-313.pyc | Bin 0 -> 48822 bytes simulator.py.backup_20260504_141747 | 501 ++++++++++++++++++++++++++ 2 files changed, 501 insertions(+) create mode 100644 __pycache__/simulator.cpython-313.pyc create mode 100644 simulator.py.backup_20260504_141747 diff --git a/__pycache__/simulator.cpython-313.pyc b/__pycache__/simulator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1700043f16d56d3d892942a93cff3c298e003c35 GIT binary patch literal 48822 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 Xj 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