Files
smart-city-digital-twin-mar…/geojson-proxy/geojson_proxy.py
Eric FELIXINE 47746b584c fix: OpenRemote PUT 403/409, MQTTv5 callback, geojson-proxy API REST
- simulator.py: Fix MQTTv5 callback crash (5th arg *args)
- simulator.py: Fix _or_put() - GET version+realm before PUT, inject version in payload
- simulator.py: Fix token TTL (min 30s cache)
- simulator.py: Round-robin OR updates (~5 assets/iteration instead of 60)
- geojson-proxy: Rewrite using REST API instead of psycopg2 (PG auth issue)
- geojson-proxy: Add sensorType + attributes in properties for map styling
- docker-compose.yml: Add openremote_default network + DB vars for proxy
- docker-compose.yml: Add OR_REALM=master for geojson-proxy

Resolves: OpenRemote 403 (wrong realm in payload), 409 (missing version),
MQTTv5 callback crash, geojson-proxy DB connection failure
2026-05-18 10:04:12 -04:00

156 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""GeoJSON proxy service for OpenRemote assets map display.
Fetches IoT sensor assets from OpenRemote REST API and serves them as GeoJSON.
"""
import json
import os
import urllib.request
import urllib.error
import urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler
OR_URL = os.environ.get("OR_URL", "http://openremote_manager_1:8080")
OR_ADMIN_USER = os.environ.get("OR_ADMIN_USER", "admin")
OR_ADMIN_PASS = os.environ.get("OR_ADMIN_PASS", "")
OR_REALM = os.environ.get("OR_REALM", "master")
OR_CLIENT_SECRET = os.environ.get("OR_CLIENT_SECRET", "0oQjzTfiEELYmj5jFwT4iIuWUDtQDvVa")
# All known IOTSensor asset IDs (from simulator ASSET_MAP + DB)
ASSET_IDS = [
"429858caca3341f56fbf65", "301218322f5aaca9d6d168", "bd35fe2a90133118b9b004",
"da59ec9301c4efd3fd55c4", "834f4b7b9df848f5c5c2d8",
"0f922351a9894bc0144c94", "4f83219bbee703b3e0a255", "381cc31ab83dd66ed4be37",
"808b73c22ecd19589a33be", "03c18679226329183b44b6",
"0ee6689f5c0499643d48eb", "8fb6b2d0601d98b47a4172", "0c00bda9e5075d12d59694",
"ae981dc9d155d1313b9acf", "96020cc5aef95c5fda7bb4",
"0be31930e45d2eb5c12ccd", "1802e76e3432d5eda1deb7", "08edb6518750d50644afe3",
"93d09bfac36d2ed95fc858", "7942726d84d2bd29de1e5d",
"9942f881ab6df375d8d9fa", "5400fdf5c51a4fe4f5a89c", "1a3bf32aa5208892e68965",
"d3725f922f96085f2df3f7", "13be192a8c23dd8fdceada",
"1f4302946b1a4a1ded23f6", "35e6ef027ed9a157ad8780", "526538589aa981bdc77ce9",
"d4a6ac7f34d64e581937c0", "40bbe989be2ae5b2a98b30",
# Additional assets from DB
"8b8f50aa8d13d65b2bafb7", "d642131a593c1cddcca3df",
"f4afeba492308772a9a1a4", "988232e4b779fd2cde2157",
"b75157bd68fde1577eda4d", "f388f67ec11e7860c352a3",
"ba30baf1fb3c69bdcc1b44",
]
_token_cache = {"token": "", "expires": 0}
def get_token():
"""Fetch an OpenRemote access token using admin credentials."""
import time
if _token_cache["token"] and _token_cache["expires"] > time.time() + 30:
return _token_cache["token"]
data = urllib.parse.urlencode({
"username": OR_ADMIN_USER,
"password": OR_ADMIN_PASS,
"grant_type": "password",
"client_id": "openremote",
"client_secret": OR_CLIENT_SECRET
}).encode()
req = urllib.request.Request(
f"http://openremote-keycloak-1:8080/auth/realms/{OR_REALM}/protocol/openid-connect/token",
data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
method="POST"
)
resp = urllib.request.urlopen(req, timeout=10)
body = json.loads(resp.read())
_token_cache["token"] = body["access_token"]
_token_cache["expires"] = time.time() + max(body.get("expires_in", 300) - 60, 30)
return _token_cache["token"]
def fetch_assets():
"""Fetch IoT sensor assets from OpenRemote REST API."""
token = get_token()
features = []
for asset_id in ASSET_IDS:
try:
req = urllib.request.Request(
f"{OR_URL}/api/{OR_REALM}/asset/{asset_id}",
headers={"Authorization": f"Bearer {token}", "Accept": "application/json"}
)
with urllib.request.urlopen(req, timeout=5) as r:
asset = json.loads(r.read().decode())
attrs = asset.get("attributes", {})
location = attrs.get("location", {})
value = location.get("value") if isinstance(location, dict) else None
coords = value.get("coordinates") if isinstance(value, dict) else None
if not coords or len(coords) < 2:
continue
props = {
"id": asset.get("id"),
"name": asset.get("name", ""),
"type": asset.get("type", ""),
"realm": asset.get("realm", ""),
}
# Add sensorType for color mapping
sensor_type = attrs.get("sensorType", {})
if isinstance(sensor_type, dict):
props["sensorType"] = sensor_type.get("value", "")
# Add scalar attribute values
for attr_name, attr_val in attrs.items():
if isinstance(attr_val, dict):
v = attr_val.get("value")
if v is not None and not isinstance(v, (dict, list)):
props[attr_name] = v
features.append({
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [coords[0], coords[1]]},
"properties": props
})
except Exception:
continue
return {"type": "FeatureCollection", "features": features}
class GeoJSONHandler(BaseHTTPRequestHandler):
def do_GET(self):
path = self.path.split("?")[0]
if path == "/geojson":
try:
result = fetch_assets()
body = json.dumps(result).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
except Exception as e:
error_body = json.dumps({"error": str(e)}).encode()
self.send_response(500)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(error_body)))
self.end_headers()
self.wfile.write(error_body)
elif path == "/health":
body = json.dumps({"status": "ok"}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
print(f"[geojson-proxy] {args[0]}")
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", 8080), GeoJSONHandler)
print("[geojson-proxy] Listening on 0.0.0.0:8080")
server.serve_forever()