diff --git a/axiom-nmea/examples/dbus-raymarine-publisher/build-package.sh b/axiom-nmea/examples/dbus-raymarine-publisher/build-package.sh index 63efb26..72c7a59 100755 --- a/axiom-nmea/examples/dbus-raymarine-publisher/build-package.sh +++ b/axiom-nmea/examples/dbus-raymarine-publisher/build-package.sh @@ -24,7 +24,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -VERSION="1.1.0" +VERSION="2.1.0" OUTPUT_DIR="$SCRIPT_DIR" PACKAGE_NAME="dbus-raymarine-publisher" diff --git a/axiom-nmea/examples/dbus-raymarine-publisher/venus_publisher.py b/axiom-nmea/examples/dbus-raymarine-publisher/venus_publisher.py index 49836bf..002c745 100644 --- a/axiom-nmea/examples/dbus-raymarine-publisher/venus_publisher.py +++ b/axiom-nmea/examples/dbus-raymarine-publisher/venus_publisher.py @@ -255,9 +255,10 @@ def main(): sensor_data=sensor_data, interface_ip=args.interface, on_decode=on_decode if args.debug else None, + min_decode_interval=0.05, ) listener.start() - logger.info("Multicast listener started") + logger.info("Multicast listener started (20Hz decode rate)") # Create NMEA TCP server (broadcasts all NMEA sentences, more than D-Bus) nmea_tcp_server = None diff --git a/dbus-anchor-alarm/build-package.sh b/dbus-anchor-alarm/build-package.sh index 3be8a4e..959d46d 100755 --- a/dbus-anchor-alarm/build-package.sh +++ b/dbus-anchor-alarm/build-package.sh @@ -11,7 +11,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -VERSION="2.1.0" +VERSION="2.2.0" OUTPUT_DIR="$SCRIPT_DIR" PACKAGE_NAME="dbus-anchor-alarm" diff --git a/dbus-anchor-alarm/config.py b/dbus-anchor-alarm/config.py index a5a2199..bbfc7c6 100644 --- a/dbus-anchor-alarm/config.py +++ b/dbus-anchor-alarm/config.py @@ -11,7 +11,7 @@ FIRMWARE_VERSION = 0 CONNECTED = 1 # Version -VERSION = '2.1.0' +VERSION = '2.2.0' # Timing MAIN_LOOP_INTERVAL_MS = 1000 diff --git a/dbus-anchor-alarm/sensor_reader.py b/dbus-anchor-alarm/sensor_reader.py index b57c85b..c10ee8e 100644 --- a/dbus-anchor-alarm/sensor_reader.py +++ b/dbus-anchor-alarm/sensor_reader.py @@ -53,13 +53,27 @@ def _unwrap(v): class SensorReader: - """Reads navigation sensor data from Venus OS D-Bus services.""" + """Reads navigation sensor data from Venus OS D-Bus services using signal subscriptions.""" def __init__(self, bus): self._bus = bus self._gps_available = False self._proxy_cache = {} - + + self._latitude = None + self._longitude = None + self._fix = None + self._speed_ms = None + self._course = None + self._heading = None + self._depth_m = None + self._wind_speed = None + self._wind_direction = None + self._last_update = time.time() + + self._poll_initial_values() + self._setup_signal_subscriptions() + def _get_proxy(self, service_name, path): """Return a cached D-Bus proxy, creating it only once per (service, path).""" key = (service_name, path) @@ -80,6 +94,93 @@ class SensorReader: self._proxy_cache.pop((service_name, path), None) logger.debug('D-Bus read failed: %s %s -- %s', service_name, path, e) return None + + def _poll_initial_values(self): + """Poll initial values once at startup.""" + self._latitude = self._read_dbus_value(GPS_SERVICE, '/Position/Latitude') + self._longitude = self._read_dbus_value(GPS_SERVICE, '/Position/Longitude') + self._fix = self._read_dbus_value(GPS_SERVICE, '/Fix') + self._speed_ms = self._read_dbus_value(GPS_SERVICE, '/Speed') + self._course = self._read_dbus_value(GPS_SERVICE, '/Course') + self._heading = self._read_dbus_value(NAVIGATION_SERVICE, '/Heading') + self._depth_m = self._read_dbus_value(NAVIGATION_SERVICE, '/Depth') + self._wind_speed = self._read_dbus_value(METEO_SERVICE, '/WindSpeed') + self._wind_direction = self._read_dbus_value(METEO_SERVICE, '/WindDirection') + self._update_gps_available() + + def _setup_signal_subscriptions(self): + """Subscribe to all sensor value changes.""" + subscriptions = [ + (GPS_SERVICE, '/Position/Latitude', '_on_latitude_changed'), + (GPS_SERVICE, '/Position/Longitude', '_on_longitude_changed'), + (GPS_SERVICE, '/Fix', '_on_fix_changed'), + (GPS_SERVICE, '/Speed', '_on_speed_changed'), + (GPS_SERVICE, '/Course', '_on_course_changed'), + (NAVIGATION_SERVICE, '/Heading', '_on_heading_changed'), + (NAVIGATION_SERVICE, '/Depth', '_on_depth_changed'), + (METEO_SERVICE, '/WindSpeed', '_on_wind_speed_changed'), + (METEO_SERVICE, '/WindDirection', '_on_wind_direction_changed'), + ] + + for service, path, handler_name in subscriptions: + try: + self._bus.add_signal_receiver( + getattr(self, handler_name), + signal_name='PropertiesChanged', + dbus_interface='com.victronenergy.BusItem', + bus_name=service, + path=path + ) + except dbus.exceptions.DBusException as e: + logger.debug(f'Failed to subscribe to {service}{path}: {e}') + + def _on_latitude_changed(self, changes): + if 'Value' in changes: + self._latitude = _unwrap(changes['Value']) + self._last_update = time.time() + + def _on_longitude_changed(self, changes): + if 'Value' in changes: + self._longitude = _unwrap(changes['Value']) + self._last_update = time.time() + + def _on_fix_changed(self, changes): + if 'Value' in changes: + self._fix = _unwrap(changes['Value']) + self._update_gps_available() + + def _on_speed_changed(self, changes): + if 'Value' in changes: + self._speed_ms = _unwrap(changes['Value']) + + def _on_course_changed(self, changes): + if 'Value' in changes: + self._course = _unwrap(changes['Value']) + + def _on_heading_changed(self, changes): + if 'Value' in changes: + self._heading = _unwrap(changes['Value']) + + def _on_depth_changed(self, changes): + if 'Value' in changes: + self._depth_m = _unwrap(changes['Value']) + + def _on_wind_speed_changed(self, changes): + if 'Value' in changes: + self._wind_speed = _unwrap(changes['Value']) + + def _on_wind_direction_changed(self, changes): + if 'Value' in changes: + self._wind_direction = _unwrap(changes['Value']) + + def _update_gps_available(self): + """Update GPS availability status.""" + self._gps_available = ( + self._latitude is not None and + self._longitude is not None and + self._fix is not None and + int(self._fix) >= 1 + ) @property def connected(self): @@ -87,45 +188,20 @@ class SensorReader: return self._gps_available def read(self): - """Read all sensors and return a SensorSnapshot. - - Each field is None if the corresponding D-Bus read fails. - Speed is converted from m/s to knots; depth from meters to feet. + """Return a SensorSnapshot from cached signal values. + + No D-Bus GetValue() calls - all values updated via signals. """ - lat = self._read_dbus_value(GPS_SERVICE, '/Position/Latitude') - lon = self._read_dbus_value(GPS_SERVICE, '/Position/Longitude') - fix = self._read_dbus_value(GPS_SERVICE, '/Fix') - - self._gps_available = ( - lat is not None and lon is not None - and fix is not None and int(fix) >= 1 - ) - - speed_ms = self._read_dbus_value(GPS_SERVICE, '/Speed') - speed = float(speed_ms) * MS_TO_KNOTS if speed_ms is not None else None - - course = self._read_dbus_value(GPS_SERVICE, '/Course') - if course is not None: - course = float(course) - - heading = self._read_dbus_value(NAVIGATION_SERVICE, '/Heading') - if heading is not None: - heading = float(heading) - - depth_m = self._read_dbus_value(NAVIGATION_SERVICE, '/Depth') - depth = float(depth_m) * METERS_TO_FEET if depth_m is not None else None - - wind_speed = self._read_dbus_value(METEO_SERVICE, '/WindSpeed') - if wind_speed is not None: - wind_speed = float(wind_speed) - - wind_direction = self._read_dbus_value(METEO_SERVICE, '/WindDirection') - if wind_direction is not None: - wind_direction = float(wind_direction) - + speed = float(self._speed_ms) * MS_TO_KNOTS if self._speed_ms is not None else None + course = float(self._course) if self._course is not None else None + heading = float(self._heading) if self._heading is not None else None + depth = float(self._depth_m) * METERS_TO_FEET if self._depth_m is not None else None + wind_speed = float(self._wind_speed) if self._wind_speed is not None else None + wind_direction = float(self._wind_direction) if self._wind_direction is not None else None + return SensorSnapshot( - latitude=float(lat) if lat is not None else None, - longitude=float(lon) if lon is not None else None, + latitude=float(self._latitude) if self._latitude is not None else None, + longitude=float(self._longitude) if self._longitude is not None else None, speed=speed, course=course, heading=heading, diff --git a/dbus-generator-ramp/build-package.sh b/dbus-generator-ramp/build-package.sh index 557ebc1..148110f 100755 --- a/dbus-generator-ramp/build-package.sh +++ b/dbus-generator-ramp/build-package.sh @@ -29,7 +29,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Default values -VERSION="2.1.0" +VERSION="2.2.0" OUTPUT_DIR="$SCRIPT_DIR" PACKAGE_NAME="dbus-generator-ramp" diff --git a/dbus-generator-ramp/dbus-generator-ramp.py b/dbus-generator-ramp/dbus-generator-ramp.py index 54d82d4..e104e75 100755 --- a/dbus-generator-ramp/dbus-generator-ramp.py +++ b/dbus-generator-ramp/dbus-generator-ramp.py @@ -55,7 +55,7 @@ from ramp_controller import RampController # Version -VERSION = '2.1.0' +VERSION = '2.2.0' # D-Bus service name for our addon SERVICE_NAME = 'com.victronenergy.generatorramp' @@ -122,6 +122,9 @@ class GeneratorRampController: self._last_run_energy_wh = 0.0 self._energy_last_time = None + # Signal subscription tracking + self._power_subscriptions_active = False + # D-Bus connection self.bus = dbus.SystemBus() @@ -595,9 +598,110 @@ class GeneratorRampController: self._read_ac_state() self._read_current_limit() + # Set up signal subscriptions + self._setup_signal_subscriptions() + except dbus.exceptions.DBusException as e: self.logger.error(f"D-Bus initialization failed: {e}") raise + + def _setup_signal_subscriptions(self): + """Subscribe to D-Bus property changes instead of polling.""" + try: + self.bus.add_signal_receiver( + self._on_generator_state_changed, + signal_name='PropertiesChanged', + dbus_interface='com.victronenergy.BusItem', + bus_name=self.generator_service, + path='/State' + ) + self.logger.debug(f"Subscribed to generator state changes") + + self.bus.add_signal_receiver( + self._on_ac_state_changed, + signal_name='PropertiesChanged', + dbus_interface='com.victronenergy.BusItem', + bus_name=self.vebus_service, + path='/Ac/ActiveIn/Connected' + ) + self.logger.debug(f"Subscribed to AC connection changes") + except dbus.exceptions.DBusException as e: + self.logger.warning(f"Failed to set up signal subscriptions: {e}") + + def _subscribe_power_readings(self): + """Subscribe to power readings when generator is running.""" + if self._power_subscriptions_active: + return + + paths = [ + '/Ac/ActiveIn/L1/P', + '/Ac/ActiveIn/L2/P', + '/Ac/Out/L1/P', + '/Ac/Out/L2/P', + '/Ac/In/1/CurrentLimit', + ] + + try: + for path in paths: + self.bus.add_signal_receiver( + lambda changes, p=path: self._on_power_changed(p, changes), + signal_name='PropertiesChanged', + dbus_interface='com.victronenergy.BusItem', + bus_name=self.vebus_service, + path=path + ) + self._power_subscriptions_active = True + self.logger.debug("Subscribed to power readings") + except dbus.exceptions.DBusException as e: + self.logger.warning(f"Failed to subscribe to power readings: {e}") + + def _unsubscribe_power_readings(self): + """Mark power readings as unsubscribed (signals ignored via flag).""" + if self._power_subscriptions_active: + self._power_subscriptions_active = False + self.logger.debug("Power reading signals now ignored") + + def _on_generator_state_changed(self, changes): + """Handle generator state change signal.""" + if 'Value' in changes: + self.generator_state = int(changes['Value']) + self.dbus_service['/Generator/State'] = self.generator_state + self.logger.debug(f"Generator state changed: {self.generator_state}") + + def _on_ac_state_changed(self, changes): + """Handle AC connection state change signal.""" + if 'Value' in changes: + self.ac_connected = bool(changes['Value']) + self.dbus_service['/AcInput/Connected'] = 1 if self.ac_connected else 0 + self.logger.debug(f"AC connected: {self.ac_connected}") + + def _on_power_changed(self, path, changes): + """Handle power reading change signal.""" + if not self._power_subscriptions_active: + return + + if 'Value' in changes: + value = float(changes['Value']) + + if path == '/Ac/ActiveIn/L1/P': + self.current_l1_power = value + self.dbus_service['/Power/L1'] = value + elif path == '/Ac/ActiveIn/L2/P': + self.current_l2_power = value + self.dbus_service['/Power/L2'] = value + self.dbus_service['/Power/Total'] = self.current_l1_power + self.current_l2_power + elif path == '/Ac/Out/L1/P': + self.output_l1_power = value + self.dbus_service['/OutputPower/L1'] = value + elif path == '/Ac/Out/L2/P': + self.output_l2_power = value + self.dbus_service['/OutputPower/L2'] = value + total = self.output_l1_power + self.output_l2_power + self.dbus_service['/OutputPower/Total'] = total + self.ramp_controller.set_output_power(total) + elif path == '/Ac/In/1/CurrentLimit': + self.current_limit_setting = value + self.dbus_service['/CurrentLimit'] = value def _get_proxy(self, service, path): """Return a cached D-Bus proxy, creating it only once per (service, path).""" @@ -752,12 +856,17 @@ class GeneratorRampController: if not self.enabled: return True - # Read current states from D-Bus - self._read_generator_state() - self._read_ac_state() - self._read_power() - self._read_output_power() - self._read_current_limit() + # Conditional D-Bus reading based on state + if self.state == self.STATE_IDLE: + self._read_generator_state() + self._unsubscribe_power_readings() + else: + self._subscribe_power_readings() + if self.state == self.STATE_WARMUP and (now - self.state_enter_time) < 0.1: + self._read_ac_state() + self._read_power() + self._read_output_power() + self._read_current_limit() # Accumulate energy while generator is providing AC power if self.ac_connected and self.generator_state in [