- 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
156 lines
6.3 KiB
Python
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()
|