Files
venus/dbus-tides/tides.py
Paul G 36a07dacb9 Extract shared signal-based D-Bus readers into lib/signal_reader.py
- Added lib/signal_reader.py with SignalGpsReader, SignalMeteoReader, and
  SignalDepthReader that use PropertiesChanged signal subscriptions instead
  of polling via GetValue(), reducing D-Bus overhead at steady state.
- Each reader discovers its service dynamically, seeds its cache with a
  one-shot GetValue, then relies on signals for all subsequent updates.
- Refactored dbus-tides, dbus-windy-station, dbus-no-foreign-land,
  dbus-lightning, and dbus-meteoblue-forecast to import from the shared
  library, removing ~600 lines of duplicated _unwrap() helpers and
  per-service GPS/meteo/depth reader classes.
- Updated install.sh for all five services to deploy signal_reader.py
  to /data/lib/ on the target device.
- Updated build-package.sh for all five services to bundle
  signal_reader.py into the .tar.gz package.
- Updated README.md with the new lib/ entry in the project table and
  documented the shared D-Bus readers pattern.
- Bumped version numbers in affected services (e.g. nfl_tracking 2.0.1).

Made-with: Cursor
2026-03-27 01:03:16 +00:00

698 lines
26 KiB
Python

#!/usr/bin/env python3
"""
dbus-tides -- Local Tide Prediction for Venus OS
Monitors vessel depth, detects tidal patterns from observations,
predicts future tides using a pure-Python harmonic engine, and
publishes merged data on D-Bus for the venus-html5-app dashboard.
"""
import json
import logging
import math
import os
import signal
import sys
import time
import threading
from collections import deque
sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'ext', 'velib_python'))
sys.path.insert(1, '/opt/victronenergy/velib_python')
sys.path.insert(1, os.path.join(os.path.dirname(__file__), '..', 'lib'))
sys.path.insert(1, '/data/lib')
try:
from gi.repository import GLib
except ImportError:
print("ERROR: GLib not available. This script must run on Venus OS.")
sys.exit(1)
try:
import dbus
from dbus.mainloop.glib import DBusGMainLoop
from vedbus import VeDbusService
from settingsdevice import SettingsDevice
except ImportError as e:
print("ERROR: Required module not available: %s" % e)
print("This script must run on Venus OS.")
sys.exit(1)
from config import (
SERVICE_NAME, VERSION,
DEPTH_SAMPLE_INTERVAL, DEPTH_HISTORY_HOURS, SPEED_THRESHOLD_MS,
PREDICTION_HOURS,
GPS_SAMPLE_INTERVAL, GPS_HISTORY_SIZE, GPS_HISTORY_SAMPLE_INTERVAL,
STATIONARY_RADIUS_METERS, MODEL_RERUN_RADIUS_METERS,
MODEL_RERUN_INTERVAL,
LOGGING_CONFIG,
)
from signal_reader import SignalGpsReader, SignalDepthReader
from depth_recorder import DepthRecorder
from tide_detector import TideDetector
from tide_predictor import TidePredictor
from tide_merger import TideMerger
from tide_adapter import TideAdapter
def haversine_distance(lat1, lon1, lat2, lon2):
"""Great-circle distance in meters."""
R = 6371000
phi1, phi2 = math.radians(lat1), math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlam = math.radians(lon2 - lon1)
a = (math.sin(dphi / 2) ** 2 +
math.cos(phi1) * math.cos(phi2) * math.sin(dlam / 2) ** 2)
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
class MovementDetector:
"""Determines whether the vessel has moved beyond a threshold."""
def __init__(self, history_size, threshold_meters):
self.history_size = history_size
self.threshold_meters = threshold_meters
self.positions = deque(maxlen=history_size)
self.last_sample_time = 0
def add_position(self, lat, lon, now=None):
if now is None:
now = time.time()
self.positions.append((now, lat, lon))
self.last_sample_time = now
def is_moving(self, current_lat, current_lon):
if not self.positions:
return False
for _, hist_lat, hist_lon in self.positions:
dist = haversine_distance(hist_lat, hist_lon,
current_lat, current_lon)
if dist >= self.threshold_meters:
return True
return False
class TidesController:
"""Coordinates all tide components and D-Bus publishing."""
def __init__(self):
self._setup_logging()
self.logger = logging.getLogger('Tides')
self.logger.info("Initializing dbus-tides v%s", VERSION)
self.bus = dbus.SystemBus()
self._create_dbus_service()
self._setup_settings()
self.gps = SignalGpsReader(self.bus)
self.depth_reader = SignalDepthReader(self.bus)
self.depth_recorder = DepthRecorder()
self.tide_detector = TideDetector()
self.tide_predictor = TidePredictor()
self.tide_merger = TideMerger()
self.tide_adapter = TideAdapter()
self.movement_stationary = MovementDetector(
GPS_HISTORY_SIZE, STATIONARY_RADIUS_METERS)
self.movement_model = MovementDetector(
GPS_HISTORY_SIZE, MODEL_RERUN_RADIUS_METERS)
self.current_lat = None
self.current_lon = None
self.model_lat = None
self.model_lon = None
self.last_depth_sample = 0
self.last_gps_check = 0
self.last_gps_history_sample = 0
self.last_prediction_run = 0
self.last_event_refresh = 0
self._predict_in_progress = False
GLib.timeout_add(1000, self._main_loop)
self.logger.info("Initialized -- sampling depth every %ds",
DEPTH_SAMPLE_INTERVAL)
def _setup_logging(self):
level = getattr(logging, LOGGING_CONFIG['level'], logging.INFO)
fmt = ('%(asctime)s %(levelname)s %(name)s: %(message)s'
if LOGGING_CONFIG['include_timestamp']
else '%(levelname)s %(name)s: %(message)s')
logging.basicConfig(level=level, format=fmt, stream=sys.stdout)
def _create_dbus_service(self):
self.logger = logging.getLogger('Tides')
self.logger.info("Creating D-Bus service: %s", SERVICE_NAME)
max_retries = 5
retry_delay = 1.0
for attempt in range(max_retries):
try:
self.dbus_service = VeDbusService(
SERVICE_NAME, self.bus, register=False)
break
except dbus.exceptions.NameExistsException:
if attempt < max_retries - 1:
self.logger.warning(
"D-Bus name exists, retrying in %.1fs (%d/%d)",
retry_delay, attempt + 1, max_retries)
time.sleep(retry_delay)
retry_delay *= 2
else:
raise
svc = self.dbus_service
svc.add_path('/Mgmt/ProcessName', 'dbus-tides')
svc.add_path('/Mgmt/ProcessVersion', VERSION)
svc.add_path('/Mgmt/Connection', 'local')
svc.add_path('/DeviceInstance', 0)
svc.add_path('/ProductId', 0xA162)
svc.add_path('/ProductName', 'Tide Prediction')
svc.add_path('/FirmwareVersion', VERSION)
svc.add_path('/Connected', 1)
def _status_text(p, v):
labels = {0: 'Idle', 1: 'Calibrating', 2: 'Ready', 3: 'Error'}
return labels.get(v, 'Unknown') if v is not None else 'Unknown'
svc.add_path('/Status', 0, gettextcallback=_status_text)
svc.add_path('/ErrorMessage', '')
svc.add_path('/IsStationary', 0)
svc.add_path('/Depth/Current', None)
svc.add_path('/Depth/History', '')
svc.add_path('/Depth/FullHistory', '')
svc.add_path('/Tide/Predictions', '')
svc.add_path('/Tide/Station/Predictions', '')
svc.add_path('/Tide/Local/Predictions', '')
svc.add_path('/Tide/Local/TimeOffset', 0.0)
svc.add_path('/Tide/Local/AmpScale', 1.0)
svc.add_path('/Tide/Local/MatchCount', 0)
svc.add_path('/Tide/Local/Status', 0)
for slot in ('NextHigh1', 'NextHigh2', 'NextLow1', 'NextLow2',
'PrevHigh1', 'PrevHigh2', 'PrevLow1', 'PrevLow2'):
svc.add_path(f'/Tide/{slot}/Time', None)
svc.add_path(f'/Tide/{slot}/Depth', None)
svc.add_path(f'/Tide/{slot}/ObsTime', None)
svc.add_path(f'/Tide/{slot}/ObsDepth', None)
svc.add_path('/Tide/LastModelRun', 0)
svc.add_path('/Tide/DataSource', '')
svc.add_path('/Tide/Station/Id', '')
svc.add_path('/Tide/Station/Name', '')
svc.add_path('/Tide/Station/Distance', None)
svc.add_path('/Tide/Station/Lat', None)
svc.add_path('/Tide/Station/Lon', None)
svc.add_path('/Tide/Station/Type', '')
svc.add_path('/Tide/Station/RefId', '')
svc.add_path('/Tide/NearbyStations', '[]')
svc.add_path('/Tide/ChartDepth', 0.0)
svc.add_path('/Tide/DatumOffset', 0.0)
svc.add_path('/Tide/ModelLocation/Lat', None)
svc.add_path('/Tide/ModelLocation/Lon', None)
svc.add_path('/Settings/Enabled', 1,
writeable=True,
onchangecallback=self._on_setting_changed)
svc.add_path('/Settings/MinTideAmplitude', 0.3,
writeable=True,
onchangecallback=self._on_setting_changed)
svc.add_path('/Settings/Units', 0,
writeable=True,
onchangecallback=self._on_setting_changed)
svc.add_path('/Settings/DatumOffset', -1.0,
writeable=True,
onchangecallback=self._on_setting_changed)
svc.add_path('/Settings/StationOverride', '',
writeable=True,
onchangecallback=self._on_setting_changed)
svc.register()
self.logger.info("D-Bus service created")
def _setup_settings(self):
self.settings = None
self.enabled = True
try:
path = '/Settings/Tides'
settings_def = {
'Enabled': [path + '/Enabled', 1, 0, 1],
'Units': [path + '/Units', 0, 0, 1],
}
self.settings = SettingsDevice(
self.bus, settings_def,
self._on_persistent_setting_changed)
if self.settings:
self.enabled = bool(self.settings['Enabled'])
self.dbus_service['/Settings/Enabled'] = (
1 if self.enabled else 0)
self.dbus_service['/Settings/Units'] = (
int(self.settings['Units']))
self.logger.info("Persistent settings initialized")
except Exception as e:
self.logger.warning("Could not init persistent settings: %s", e)
def _on_persistent_setting_changed(self, setting, old_value, new_value):
self.logger.info("Setting changed: %s = %s", setting, new_value)
if self.settings:
self.enabled = bool(self.settings['Enabled'])
def _on_setting_changed(self, path, value):
self.logger.info("Setting changed: %s = %s", path, value)
if path == '/Settings/Enabled':
self.enabled = bool(value)
if self.settings:
try:
self.settings['Enabled'] = 1 if self.enabled else 0
except Exception:
pass
elif path == '/Settings/MinTideAmplitude':
try:
self.tide_detector.min_amplitude = float(value)
except (TypeError, ValueError):
pass
elif path == '/Settings/Units':
if self.settings:
try:
self.settings['Units'] = int(value)
except Exception:
pass
elif path == '/Settings/StationOverride':
self.tide_predictor.station_override = str(value) if value else None
self.last_prediction_run = 0
return True
def _main_loop(self):
try:
if not self.enabled:
self.dbus_service['/Status'] = 0
return True
now = time.time()
self._check_gps(now)
self._check_depth(now)
self._check_predictions(now)
self._refresh_event_slots(now)
except dbus.exceptions.DBusException as e:
self.logger.warning("D-Bus error: %s", e)
except Exception as e:
self.logger.exception("Unexpected error: %s", e)
return True
def _check_gps(self, now):
if now - self.last_gps_check < GPS_SAMPLE_INTERVAL:
return
pos = self.gps.get_position()
if pos:
self.current_lat, self.current_lon = pos
if (now - self.last_gps_history_sample
>= GPS_HISTORY_SAMPLE_INTERVAL):
self.movement_stationary.add_position(
self.current_lat, self.current_lon, now)
self.movement_model.add_position(
self.current_lat, self.current_lon, now)
self.last_gps_history_sample = now
is_stationary = not self.movement_stationary.is_moving(
self.current_lat, self.current_lon)
self.dbus_service['/IsStationary'] = 1 if is_stationary else 0
self.last_gps_check = now
def _check_depth(self, now):
if now - self.last_depth_sample < DEPTH_SAMPLE_INTERVAL:
return
speed = self.gps.get_speed()
if speed is not None and speed > SPEED_THRESHOLD_MS:
self.last_depth_sample = now
return
depth = self.depth_reader.get_depth()
if depth is None:
self.last_depth_sample = now
return
result = self.depth_recorder.add_reading(
depth, self.current_lat, self.current_lon, now)
smoothed = self.depth_recorder.get_latest_depth()
if smoothed is not None:
self.dbus_service['/Depth/Current'] = round(smoothed, 2)
if result is not None:
history = self.depth_recorder.get_history(
lat=self.current_lat, lon=self.current_lon)
new_event = self.tide_detector.update(history)
self._recalibrate_if_moved(history)
now_ts = time.time()
display_cutoff = now_ts - 24 * 3600
obs_points = [
{'ts': int(ts), 'depth': round(d, 2)}
for ts, d in history if ts > display_cutoff
]
self.dbus_service['/Depth/History'] = json.dumps(obs_points)
full_obs = [
{'ts': int(ts), 'depth': round(d, 2)}
for ts, d in history
]
self.dbus_service['/Depth/FullHistory'] = json.dumps(full_obs)
if new_event is not None:
self._refresh_adaptation()
self.last_depth_sample = now
def _recalibrate_if_moved(self, history):
"""Recalibrate chart_depth if vessel has moved to a new position."""
if not self.tide_merger.needs_recalibration(
self.current_lat, self.current_lon):
return
pred_ts, pred_heights, _ = (
self.tide_predictor.get_last_prediction())
if pred_ts is None:
return
if self.tide_merger.calibrate(
history, pred_ts, pred_heights,
lat=self.current_lat, lon=self.current_lon):
self._publish_adaptation(pred_ts, pred_heights,
self.tide_predictor._last_extrema)
def _refresh_adaptation(self):
"""Recompute adaptation from observed events and last prediction."""
pred_ts, pred_heights, pred_extrema = (
self.tide_predictor.get_last_prediction())
if pred_extrema is None:
return
observed_events = self.tide_detector.get_recent_events(20)
if not observed_events:
return
if self.tide_adapter.update(observed_events, pred_extrema):
self._publish_adaptation(pred_ts, pred_heights, pred_extrema)
def _refresh_event_slots(self, now):
"""Re-classify next/prev tide events every 60s using cached extrema.
Keeps the dbus slots current as predicted event times pass,
without waiting for a new prediction run or observed event.
"""
if now - self.last_event_refresh < 60:
return
self.last_event_refresh = now
pred_ts, pred_heights, extrema = (
self.tide_predictor.get_last_prediction())
if extrema is None:
return
chart_depth = self.tide_merger.chart_depth or 0.0
best_extrema = extrema
if self.tide_adapter.status > 0:
best_extrema = self.tide_adapter.adapt_extrema(extrema)
calibrated_extrema = [
(ts, chart_depth + h, t) for ts, h, t in best_extrema]
next_tides = self.tide_merger.get_next_tides(
calibrated_extrema, count=2)
prev_tides = self.tide_merger.get_prev_tides(
calibrated_extrema, count=2)
observed_events = self.tide_detector.get_recent_events(20)
self._publish_tide_events(next_tides, prev_tides, observed_events)
def _check_predictions(self, now):
if self._predict_in_progress:
return
if self.current_lat is None or self.current_lon is None:
return
needs_model = False
if self.model_lat is None:
needs_model = True
elif now - self.last_prediction_run >= MODEL_RERUN_INTERVAL:
needs_model = True
elif self.movement_model.is_moving(
self.current_lat, self.current_lon):
needs_model = True
if needs_model:
self._predict_in_progress = True
thread = threading.Thread(
target=self._run_prediction,
args=(self.current_lat, self.current_lon, now),
daemon=True)
thread.start()
def _run_prediction(self, lat, lon, now):
"""Run in background thread."""
try:
self.logger.info(
"Running tide prediction for %.2f, %.2f", lat, lon)
self.dbus_service['/Status'] = 1
history = self.depth_recorder.get_history(lat=lat, lon=lon)
if not self.tide_predictor.update_location(
lat, lon, depth_history=history):
self.dbus_service['/Status'] = 3
self.dbus_service['/ErrorMessage'] = 'No constituent data'
return
pred_ts, pred_heights, extrema = (
self.tide_predictor.predict(
now - DEPTH_HISTORY_HOURS * 3600,
duration_hours=DEPTH_HISTORY_HOURS + PREDICTION_HOURS))
if pred_ts is None:
self.dbus_service['/Status'] = 3
self.dbus_service['/ErrorMessage'] = 'Prediction failed'
return
if history:
self.tide_merger.calibrate(
history, pred_ts, pred_heights, lat=lat, lon=lon)
if history:
self.tide_detector.update(
[(ts, d) for ts, d in history])
observed_events = self.tide_detector.get_recent_events(20)
if observed_events and extrema:
self.tide_adapter.update(observed_events, extrema)
self._publish_adaptation(pred_ts, pred_heights, extrema)
manual_offset = self.dbus_service['/Settings/DatumOffset']
if manual_offset is not None and manual_offset >= 0:
datum_offset = float(manual_offset)
else:
datum_offset = self.tide_predictor.estimate_datum_offset()
self.dbus_service['/Tide/DatumOffset'] = round(datum_offset, 3)
self.model_lat = lat
self.model_lon = lon
self.last_prediction_run = time.time()
self.dbus_service['/Tide/LastModelRun'] = int(
self.last_prediction_run)
self.dbus_service['/Tide/DataSource'] = (
self.tide_predictor.current_source or '')
self._publish_station_info()
self.dbus_service['/Tide/ModelLocation/Lat'] = lat
self.dbus_service['/Tide/ModelLocation/Lon'] = lon
self.dbus_service['/ErrorMessage'] = ''
self.dbus_service['/Status'] = 2
self.logger.info(
"Prediction complete, %d extrema found",
len(extrema) if extrema else 0)
except Exception as e:
self.logger.exception("Prediction error: %s", e)
self.dbus_service['/Status'] = 3
self.dbus_service['/ErrorMessage'] = str(e)[:200]
finally:
self._predict_in_progress = False
def _publish_station_info(self):
"""Publish selected station metadata and nearby stations list."""
svc = self.dbus_service
st = self.tide_predictor.current_station
if st:
svc['/Tide/Station/Id'] = st.get('id', '')
svc['/Tide/Station/Name'] = st.get('name', '')
svc['/Tide/Station/Distance'] = round(
st.get('distance', 0) / 1000, 1)
svc['/Tide/Station/Lat'] = st.get('lat')
svc['/Tide/Station/Lon'] = st.get('lon')
svc['/Tide/Station/Type'] = st.get('type', 'R')
svc['/Tide/Station/RefId'] = st.get('ref_id', '')
else:
svc['/Tide/Station/Id'] = ''
svc['/Tide/Station/Name'] = ''
svc['/Tide/Station/Distance'] = None
svc['/Tide/Station/Lat'] = None
svc['/Tide/Station/Lon'] = None
svc['/Tide/Station/Type'] = ''
svc['/Tide/Station/RefId'] = ''
nearby = self.tide_predictor.nearby_stations
svc['/Tide/NearbyStations'] = json.dumps(nearby)
def _publish_adaptation(self, pred_ts, pred_heights, extrema):
"""Build and publish station + local curves and next-tide data."""
history = self.depth_recorder.get_history(
lat=self.current_lat, lon=self.current_lon)
chart_depth = self.tide_merger.chart_depth or 0.0
self.dbus_service['/Tide/ChartDepth'] = round(chart_depth, 3)
station_depths = [chart_depth + h for h in pred_heights]
local_ts = None
local_depths = None
best_extrema = extrema
if self.tide_adapter.status > 0:
adapted_ts, adapted_h = self.tide_adapter.adapt_curve(
pred_ts, pred_heights)
local_depths = [chart_depth + h for h in adapted_h]
local_ts = adapted_ts
best_extrema = self.tide_adapter.adapt_extrema(extrema)
residual = self.tide_adapter.compute_residual_correction(
history, self.tide_predictor.predict_at, chart_depth)
if residual:
local_depths = self.tide_adapter.apply_residual_correction(
local_ts, local_depths, residual)
curves = self.tide_merger.build_dual_curves(
history, pred_ts, station_depths, local_ts, local_depths)
self.dbus_service['/Depth/History'] = json.dumps(
curves.get('observed', []))
full_obs = [
{'ts': int(ts), 'depth': round(d, 2)}
for ts, d in history
]
self.dbus_service['/Depth/FullHistory'] = json.dumps(full_obs)
self.dbus_service['/Tide/Station/Predictions'] = json.dumps(
curves.get('station', []))
self.dbus_service['/Tide/Local/Predictions'] = json.dumps(
curves.get('local', []))
best_curve = curves.get('local', []) or curves.get('station', [])
self.dbus_service['/Tide/Predictions'] = json.dumps(best_curve)
self.dbus_service['/Tide/Local/TimeOffset'] = round(
self.tide_adapter.time_offset, 1)
self.dbus_service['/Tide/Local/AmpScale'] = round(
self.tide_adapter.amp_scale, 3)
self.dbus_service['/Tide/Local/MatchCount'] = (
self.tide_adapter.match_count)
self.dbus_service['/Tide/Local/Status'] = self.tide_adapter.status
if best_extrema:
calibrated_extrema = [
(ts, chart_depth + h, t) for ts, h, t in best_extrema]
next_tides = self.tide_merger.get_next_tides(
calibrated_extrema, count=2)
prev_tides = self.tide_merger.get_prev_tides(
calibrated_extrema, count=2)
observed_events = self.tide_detector.get_recent_events(20)
self._publish_tide_events(
next_tides, prev_tides, observed_events)
def _publish_tide_events(self, next_tides, prev_tides,
observed_events=None):
svc = self.dbus_service
obs_by_type = {'high': [], 'low': []}
if observed_events:
for obs_ts, obs_depth, obs_type in observed_events:
obs_by_type.setdefault(obs_type, []).append(
(obs_ts, obs_depth))
def _find_obs(pred_ts, tide_type, max_window=10800):
"""Find the observed event closest to a predicted timestamp."""
best, best_dist = None, max_window
for obs_ts, obs_depth in obs_by_type.get(tide_type, []):
dist = abs(obs_ts - pred_ts)
if dist < best_dist:
best_dist = dist
best = (obs_ts, obs_depth)
return best
def _set_slot(slot, pred_list, idx, tide_type):
if idx < len(pred_list):
ts, depth = pred_list[idx]
svc[f'/Tide/{slot}/Time'] = int(ts)
svc[f'/Tide/{slot}/Depth'] = round(depth, 2)
obs = _find_obs(ts, tide_type)
if obs:
svc[f'/Tide/{slot}/ObsTime'] = int(obs[0])
svc[f'/Tide/{slot}/ObsDepth'] = round(obs[1], 2)
else:
svc[f'/Tide/{slot}/ObsTime'] = None
svc[f'/Tide/{slot}/ObsDepth'] = None
else:
for suffix in ('Time', 'Depth', 'ObsTime', 'ObsDepth'):
svc[f'/Tide/{slot}/{suffix}'] = None
highs = next_tides.get('high', [])
lows = next_tides.get('low', [])
_set_slot('NextHigh1', highs, 0, 'high')
_set_slot('NextHigh2', highs, 1, 'high')
_set_slot('NextLow1', lows, 0, 'low')
_set_slot('NextLow2', lows, 1, 'low')
prev_highs = prev_tides.get('high', [])
prev_lows = prev_tides.get('low', [])
_set_slot('PrevHigh1', prev_highs, 0, 'high')
_set_slot('PrevHigh2', prev_highs, 1, 'high')
_set_slot('PrevLow1', prev_lows, 0, 'low')
_set_slot('PrevLow2', prev_lows, 1, 'low')
def main():
DBusGMainLoop(set_as_default=True)
print("=" * 60)
print("dbus-tides v%s" % VERSION)
print("=" * 60)
mainloop = None
def signal_handler(signum, frame):
try:
sig_name = signal.Signals(signum).name
except ValueError:
sig_name = str(signum)
logging.info("Received %s, shutting down...", sig_name)
if mainloop is not None:
mainloop.quit()
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
try:
controller = TidesController()
mainloop = GLib.MainLoop()
mainloop.run()
except KeyboardInterrupt:
print("\nShutdown requested")
except Exception as e:
logging.error("Fatal error: %s", e, exc_info=True)
sys.exit(1)
finally:
logging.info("Service stopped")
if __name__ == '__main__':
main()