#!/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()