Files
venus/dbus-vrm-history/vrm_client.py
dev 9756538f16 Initial commit: Venus OS boat addons monorepo
Organizes 11 projects for Cerbo GX/Venus OS into a single repository:
- axiom-nmea: Raymarine LightHouse protocol decoder
- dbus-generator-ramp: Generator current ramp controller
- dbus-lightning: Blitzortung lightning monitor
- dbus-meteoblue-forecast: Meteoblue weather forecast
- dbus-no-foreign-land: noforeignland.com tracking
- dbus-tides: Tide prediction from depth + harmonics
- dbus-vrm-history: VRM cloud history proxy
- dbus-windy-station: Windy.com weather upload
- mfd-custom-app: MFD app deployment package
- venus-html5-app: Custom Victron HTML5 app fork
- watermaker: Watermaker PLC control UI

Adds root README, .gitignore, project template, and per-project
.gitignore files. Sensitive config files excluded via .gitignore
with .example templates provided.

Made-with: Cursor
2026-03-16 17:04:16 +00:00

93 lines
3.2 KiB
Python

"""
VRM API client using urllib (available on Venus OS without pip).
"""
import json
import logging
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
logger = logging.getLogger('VrmClient')
class VrmClient:
BASE_URL = "https://vrmapi.victronenergy.com/v2"
def __init__(self, token, site_id=None):
self.token = token
self.site_id = site_id
def _request(self, path, params=None):
url = f"{self.BASE_URL}{path}"
if params:
query = "&".join(f"{k}={v}" for k, v in params.items())
url = f"{url}?{query}"
req = Request(url)
req.add_header("x-authorization", f"Token {self.token}")
req.add_header("Content-Type", "application/json")
try:
with urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
except HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
logger.error(f"VRM API HTTP {e.code}: {body[:200]}")
raise
except URLError as e:
logger.error(f"VRM API connection error: {e.reason}")
raise
def discover_site_id(self, portal_id):
"""Auto-discover numeric siteId from the GX device portal ID.
1. GET /users/me -> idUser
2. GET /users/{idUser}/installations -> find matching identifier
"""
me = self._request("/users/me")
id_user = me.get("idUser") or me.get("user", {}).get("idUser")
if not id_user:
raise ValueError("Could not determine idUser from VRM API")
installs = self._request(f"/users/{id_user}/installations")
records = installs.get("records", [])
for inst in records:
if inst.get("identifier") == portal_id:
logger.info(f"Found VRM site: {inst.get('name')} (id={inst.get('idSite')})")
return inst["idSite"]
available = [f"{r.get('name')}({r.get('identifier')})" for r in records[:5]]
raise ValueError(
f"No installation matches portal ID '{portal_id}'. "
f"Available: {', '.join(available)}"
)
def get_stats(self, stat_type, attribute_codes, start, end, interval=None):
"""GET /installations/{siteId}/stats
stat_type: 'live_feed' or 'kwh'
attribute_codes: list of VRM attribute codes (e.g. ['bs'] for battery SOC)
"""
if not self.site_id:
raise ValueError("site_id not configured")
params = {"type": stat_type, "start": str(start), "end": str(end)}
for i, code in enumerate(attribute_codes):
params[f"attributeCodes[{i}]"] = code
if interval:
params["interval"] = interval
return self._request(f"/installations/{self.site_id}/stats", params)
def get_widget(self, widget_name, instance=None):
"""GET /installations/{siteId}/widgets/{widgetName}"""
if not self.site_id:
raise ValueError("site_id not configured")
params = {}
if instance is not None:
params["instance"] = str(instance)
return self._request(f"/installations/{self.site_id}/widgets/{widget_name}", params)