- Investigated map display issues (agentLink, GeoJSON coords, realm config) - Cleaned up all dashboards and containers - Fresh Manager installation (PostgreSQL in recovery) - Updated TODO.md with current status - GeoJSON proxy: fixed coordinate order (lon/lat) - Session resume saved
168 lines
6.2 KiB
Python
168 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""GeoJSON proxy service for OpenRemote assets map display.
|
|
Fetches all assets with location 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", "Digitribe972")
|
|
OR_REALM = os.environ.get("OR_REALM", "master")
|
|
OR_CLIENT_SECRET = os.environ.get("OR_CLIENT_SECRET", "0oQjzTfiEELYmj5jFwT4iIuWUDtQDvVa")
|
|
|
|
_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 all assets with location from OpenRemote REST API."""
|
|
token = get_token()
|
|
features = []
|
|
|
|
# Query all assets with location attribute
|
|
try:
|
|
# Use the asset query API to get all assets with location
|
|
query = json.dumps({
|
|
"attributes": {
|
|
"location": {
|
|
"value": {"$exists": True}
|
|
}
|
|
}
|
|
}).encode()
|
|
req = urllib.request.Request(
|
|
f"{OR_URL}/api/{OR_REALM}/asset/query",
|
|
data=query,
|
|
headers={
|
|
"Authorization": f"Bearer {token}",
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json"
|
|
},
|
|
method="POST"
|
|
)
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
assets = json.loads(r.read().decode())
|
|
if not isinstance(assets, list):
|
|
assets = [assets]
|
|
except Exception as e:
|
|
# Fallback: try to get all assets and filter
|
|
try:
|
|
req = urllib.request.Request(
|
|
f"{OR_URL}/api/{OR_REALM}/asset?limit=100",
|
|
headers={"Authorization": f"Bearer {token}", "Accept": "application/json"}
|
|
)
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
assets = json.loads(r.read().decode())
|
|
if not isinstance(assets, list):
|
|
assets = [assets]
|
|
except Exception as e2:
|
|
return {"type": "FeatureCollection", "features": [], "error": str(e2)}
|
|
|
|
for asset in assets:
|
|
try:
|
|
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
|
|
|
|
# GeoJSON coordinates are [longitude, latitude]
|
|
features.append({
|
|
"type": "Feature",
|
|
"geometry": {"type": "Point", "coordinates": [coords[1], coords[0]]},
|
|
"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()
|