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
302 lines
9.6 KiB
Python
302 lines
9.6 KiB
Python
"""
|
|
Base D-Bus service class for Venus OS integration.
|
|
|
|
This module provides a base class that wraps the VeDbusService from
|
|
Victron's velib_python library, following their standard patterns.
|
|
"""
|
|
|
|
import logging
|
|
import platform
|
|
import os
|
|
import sys
|
|
from abc import ABC, abstractmethod
|
|
from typing import Any, Dict, Optional, Callable
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Venus OS stores velib_python in /opt/victronenergy
|
|
VELIB_PATHS = [
|
|
'/opt/victronenergy/dbus-systemcalc-py/ext/velib_python',
|
|
'/opt/victronenergy/velib_python',
|
|
os.path.join(os.path.dirname(__file__), '../../ext/velib_python'),
|
|
]
|
|
|
|
# Try to import vedbus from velib_python
|
|
VeDbusService = None
|
|
dbusconnection = None
|
|
for path in VELIB_PATHS:
|
|
if os.path.exists(path):
|
|
if path not in sys.path:
|
|
sys.path.insert(0, path)
|
|
try:
|
|
from vedbus import VeDbusService
|
|
# Also import dbusconnection for creating separate connections
|
|
try:
|
|
from dbusmonitor import DbusMonitor
|
|
import dbus
|
|
except ImportError:
|
|
pass
|
|
logger.debug(f"Loaded VeDbusService from {path}")
|
|
break
|
|
except ImportError:
|
|
if path in sys.path:
|
|
sys.path.remove(path)
|
|
continue
|
|
|
|
if VeDbusService is None:
|
|
logger.warning(
|
|
"VeDbusService not available. Venus OS D-Bus publishing will be disabled. "
|
|
"This is expected when not running on Venus OS."
|
|
)
|
|
|
|
|
|
def get_dbus_connection():
|
|
"""Get a new private D-Bus connection.
|
|
|
|
Each service needs its own connection to avoid path conflicts.
|
|
"""
|
|
try:
|
|
import dbus
|
|
return dbus.SystemBus(private=True)
|
|
except Exception as e:
|
|
logger.error(f"Failed to create D-Bus connection: {e}")
|
|
return None
|
|
|
|
|
|
class VeDbusServiceBase(ABC):
|
|
"""Base class for Venus OS D-Bus services.
|
|
|
|
This class provides common functionality for creating D-Bus services
|
|
that follow Victron's standards for Venus OS integration.
|
|
|
|
Subclasses must implement:
|
|
- service_type: The service type (e.g., 'gps', 'tank', 'battery')
|
|
- product_name: Human-readable product name
|
|
- _get_paths(): Returns dict of D-Bus paths to register
|
|
- _update(): Called periodically to update values
|
|
|
|
Example:
|
|
class MyService(VeDbusServiceBase):
|
|
service_type = 'myservice'
|
|
product_name = 'My Custom Service'
|
|
|
|
def _get_paths(self):
|
|
return {
|
|
'/Value': {'initial': 0},
|
|
'/Status': {'initial': 'OK'},
|
|
}
|
|
|
|
def _update(self):
|
|
self._set_value('/Value', 42)
|
|
"""
|
|
|
|
# Subclasses must define these
|
|
service_type: str = ""
|
|
product_name: str = "Raymarine Sensor"
|
|
product_id: int = 0xFFFF # Custom product ID
|
|
|
|
def __init__(
|
|
self,
|
|
device_instance: int = 0,
|
|
connection: str = "Raymarine LightHouse",
|
|
custom_name: Optional[str] = None,
|
|
):
|
|
"""Initialize the D-Bus service.
|
|
|
|
Args:
|
|
device_instance: Unique instance number for this device type
|
|
connection: Connection description string
|
|
custom_name: Optional custom name override
|
|
"""
|
|
self.device_instance = device_instance
|
|
self.connection = connection
|
|
self.custom_name = custom_name
|
|
self._dbusservice = None
|
|
self._bus = None # Private D-Bus connection for this service
|
|
self._paths: Dict[str, Any] = {}
|
|
self._registered = False
|
|
|
|
@property
|
|
def service_name(self) -> str:
|
|
"""Get the full D-Bus service name."""
|
|
return f"com.victronenergy.{self.service_type}.raymarine_{self.device_instance}"
|
|
|
|
@abstractmethod
|
|
def _get_paths(self) -> Dict[str, Dict[str, Any]]:
|
|
"""Return the D-Bus paths to register.
|
|
|
|
Returns:
|
|
Dict mapping path names to their settings.
|
|
Each setting dict can contain:
|
|
- initial: Initial value
|
|
- writeable: Whether external writes are allowed (default: False)
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def _update(self) -> None:
|
|
"""Update the D-Bus values.
|
|
|
|
Called periodically to refresh values from sensor data.
|
|
Use _set_value() to update individual paths.
|
|
"""
|
|
pass
|
|
|
|
def register(self) -> bool:
|
|
"""Register the service with D-Bus.
|
|
|
|
Returns:
|
|
True if registration succeeded, False otherwise
|
|
"""
|
|
if VeDbusService is None:
|
|
logger.warning(f"Cannot register {self.service_name}: VeDbusService not available")
|
|
return False
|
|
|
|
if self._registered:
|
|
logger.warning(f"Service {self.service_name} already registered")
|
|
return True
|
|
|
|
try:
|
|
# Create a private D-Bus connection for this service
|
|
# Each service needs its own connection to avoid path conflicts
|
|
self._bus = get_dbus_connection()
|
|
if self._bus is None:
|
|
logger.error(f"Failed to get D-Bus connection for {self.service_name}")
|
|
return False
|
|
|
|
# Create service with register=False (new API requirement)
|
|
# This allows us to add all paths before registering
|
|
self._dbusservice = VeDbusService(
|
|
self.service_name,
|
|
bus=self._bus,
|
|
register=False
|
|
)
|
|
|
|
# Create management objects
|
|
self._dbusservice.add_path('/Mgmt/ProcessName', __file__)
|
|
self._dbusservice.add_path(
|
|
'/Mgmt/ProcessVersion',
|
|
f'1.0 (Python {platform.python_version()})'
|
|
)
|
|
self._dbusservice.add_path('/Mgmt/Connection', self.connection)
|
|
|
|
# Create mandatory objects
|
|
self._dbusservice.add_path('/DeviceInstance', self.device_instance)
|
|
self._dbusservice.add_path('/ProductId', self.product_id)
|
|
self._dbusservice.add_path(
|
|
'/ProductName',
|
|
self.custom_name or self.product_name
|
|
)
|
|
self._dbusservice.add_path('/FirmwareVersion', 1)
|
|
self._dbusservice.add_path('/HardwareVersion', 0)
|
|
self._dbusservice.add_path('/Connected', 1)
|
|
|
|
# Add custom name if supported
|
|
if self.custom_name:
|
|
self._dbusservice.add_path('/CustomName', self.custom_name)
|
|
|
|
# Register service-specific paths
|
|
self._paths = self._get_paths()
|
|
for path, settings in self._paths.items():
|
|
initial = settings.get('initial', None)
|
|
writeable = settings.get('writeable', False)
|
|
self._dbusservice.add_path(
|
|
path,
|
|
initial,
|
|
writeable=writeable,
|
|
onchangecallback=self._handle_changed_value if writeable else None
|
|
)
|
|
|
|
# Complete registration after all paths are added
|
|
self._dbusservice.register()
|
|
self._registered = True
|
|
|
|
logger.info(f"Registered D-Bus service: {self.service_name}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to register {self.service_name}: {e}")
|
|
return False
|
|
|
|
def unregister(self) -> None:
|
|
"""Unregister the service from D-Bus."""
|
|
if self._dbusservice and self._registered:
|
|
# VeDbusService doesn't have a clean unregister method,
|
|
# so we just mark ourselves as unregistered
|
|
self._registered = False
|
|
self._dbusservice = None
|
|
# Close the private D-Bus connection
|
|
if self._bus:
|
|
try:
|
|
self._bus.close()
|
|
except Exception:
|
|
pass
|
|
self._bus = None
|
|
logger.info(f"Unregistered D-Bus service: {self.service_name}")
|
|
|
|
def update(self) -> bool:
|
|
"""Update the service values.
|
|
|
|
Returns:
|
|
True to continue updates, False to stop
|
|
"""
|
|
if not self._registered:
|
|
return False
|
|
|
|
try:
|
|
self._update()
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error updating {self.service_name}: {e}")
|
|
return True # Keep trying
|
|
|
|
def _set_value(self, path: str, value: Any) -> None:
|
|
"""Set a D-Bus path value.
|
|
|
|
Args:
|
|
path: The D-Bus path (e.g., '/Position/Latitude')
|
|
value: The value to set
|
|
"""
|
|
if self._dbusservice and self._registered:
|
|
with self._dbusservice as s:
|
|
if path in s:
|
|
s[path] = value
|
|
|
|
def _get_value(self, path: str) -> Any:
|
|
"""Get a D-Bus path value.
|
|
|
|
Args:
|
|
path: The D-Bus path
|
|
|
|
Returns:
|
|
The current value, or None if not found
|
|
"""
|
|
if self._dbusservice and self._registered:
|
|
with self._dbusservice as s:
|
|
if path in s:
|
|
return s[path]
|
|
return None
|
|
|
|
def _handle_changed_value(self, path: str, value: Any) -> bool:
|
|
"""Handle external value changes.
|
|
|
|
Override this method to handle writes from other processes.
|
|
|
|
Args:
|
|
path: The D-Bus path that was changed
|
|
value: The new value
|
|
|
|
Returns:
|
|
True to accept the change, False to reject
|
|
"""
|
|
logger.debug(f"External change: {path} = {value}")
|
|
return True
|
|
|
|
def set_connected(self, connected: bool) -> None:
|
|
"""Set the connection status.
|
|
|
|
Args:
|
|
connected: True if data source is connected
|
|
"""
|
|
self._set_value('/Connected', 1 if connected else 0)
|