Files
venus/axiom-nmea/raymarine_nmea/venus_dbus/service.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

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)