mmrelay 1.0__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mmrelay might be problematic. Click here for more details.

mmrelay/__init__.py CHANGED
@@ -2,8 +2,12 @@
2
2
  Meshtastic Matrix Relay - Bridge between Meshtastic mesh networks and Matrix chat rooms.
3
3
  """
4
4
 
5
+ import importlib.metadata
5
6
  import os
6
7
 
7
- # Get version from environment variable if available (set by GitHub Actions)
8
- # Otherwise, use a default version
9
- __version__ = os.environ.get("GITHUB_REF_NAME", "1.0.0")
8
+ # Try to get version from package metadata (setup.cfg)
9
+ try:
10
+ __version__ = importlib.metadata.version("mmrelay")
11
+ except importlib.metadata.PackageNotFoundError:
12
+ # If package is not installed, fall back to environment variable or default
13
+ __version__ = os.environ.get("GITHUB_REF_NAME", "1.0.1")
mmrelay/cli.py CHANGED
@@ -94,6 +94,13 @@ def get_version():
94
94
  return __version__
95
95
 
96
96
 
97
+ def print_version():
98
+ """
99
+ Print the version in a simple format.
100
+ """
101
+ print(f"MMRelay v{__version__}")
102
+
103
+
97
104
  def check_config(args=None):
98
105
  """
99
106
  Check if the configuration file is valid.
@@ -262,7 +269,7 @@ def main():
262
269
 
263
270
  # Handle --version
264
271
  if args.version:
265
- print(f"mmrelay {get_version()}")
272
+ print_version()
266
273
  return 0
267
274
 
268
275
  # If no command was specified, run the main functionality
@@ -289,7 +296,7 @@ def handle_cli_commands(args):
289
296
  """
290
297
  # Handle --version
291
298
  if args.version:
292
- print(f"mmrelay {get_version()}")
299
+ print_version()
293
300
  return True
294
301
 
295
302
  # Handle --install-service
mmrelay/config.py CHANGED
@@ -187,7 +187,7 @@ def load_config(config_file=None, args=None):
187
187
 
188
188
  # If a specific config file was provided, use it
189
189
  if config_file and os.path.isfile(config_file):
190
- logger.info(f"Loading configuration from: {config_file}")
190
+ # Store the config path but don't log it yet - will be logged by main.py
191
191
  with open(config_file, "r") as f:
192
192
  relay_config = yaml.load(f, Loader=SafeLoader)
193
193
  config_path = config_file
@@ -200,10 +200,9 @@ def load_config(config_file=None, args=None):
200
200
  for path in config_paths:
201
201
  if os.path.isfile(path):
202
202
  config_path = path
203
- logger.info(f"Loading configuration from: {config_path}")
203
+ # Store the config path but don't log it yet - will be logged by main.py
204
204
  with open(config_path, "r") as f:
205
205
  relay_config = yaml.load(f, Loader=SafeLoader)
206
- logger.info(f"Loaded configuration with keys: {list(relay_config.keys())}")
207
206
  return relay_config
208
207
 
209
208
  # No config file found
mmrelay/log_utils.py CHANGED
@@ -2,44 +2,84 @@ import logging
2
2
  import os
3
3
  from logging.handlers import RotatingFileHandler
4
4
 
5
+ from rich.console import Console
6
+ from rich.logging import RichHandler
7
+
5
8
  from mmrelay.cli import parse_arguments
6
9
  from mmrelay.config import get_log_dir
7
10
 
11
+ # Initialize Rich console
12
+ console = Console()
13
+
14
+ # Define custom log level styles - not used directly but kept for reference
15
+ # Rich 14.0.0+ supports level_styles parameter, but we're using an approach
16
+ # that works with older versions too
17
+ LOG_LEVEL_STYLES = {
18
+ "DEBUG": "dim blue",
19
+ "INFO": "green",
20
+ "WARNING": "yellow",
21
+ "ERROR": "bold red",
22
+ "CRITICAL": "bold white on red",
23
+ }
24
+
8
25
  # Global config variable that will be set from main.py
9
26
  config = None
10
27
 
28
+ # Global variable to store the log file path
29
+ log_file_path = None
30
+
11
31
 
12
32
  def get_logger(name):
13
33
  logger = logging.getLogger(name=name)
14
34
 
15
35
  # Default to INFO level if config is not available
16
36
  log_level = logging.INFO
37
+ color_enabled = True # Default to using colors
17
38
 
18
- # Try to get log level from config
39
+ # Try to get log level and color settings from config
19
40
  global config
20
- if config is not None and "logging" in config and "level" in config["logging"]:
21
- log_level = getattr(logging, config["logging"]["level"].upper())
41
+ if config is not None and "logging" in config:
42
+ if "level" in config["logging"]:
43
+ log_level = getattr(logging, config["logging"]["level"].upper())
44
+ # Check if colors should be disabled
45
+ if "color_enabled" in config["logging"]:
46
+ color_enabled = config["logging"]["color_enabled"]
22
47
 
23
48
  logger.setLevel(log_level)
24
49
  logger.propagate = False
25
50
 
26
- # Add stream handler (console logging)
27
- stream_handler = logging.StreamHandler()
28
- stream_handler.setFormatter(
29
- logging.Formatter(
30
- fmt="%(asctime)s %(levelname)s:%(name)s:%(message)s",
31
- datefmt="%Y-%m-%d %H:%M:%S %z",
51
+ # Add handler for console logging (with or without colors)
52
+ if color_enabled:
53
+ # Use Rich handler with colors
54
+ console_handler = RichHandler(
55
+ rich_tracebacks=True,
56
+ console=console,
57
+ show_time=True,
58
+ show_level=True,
59
+ show_path=False,
60
+ markup=True,
61
+ log_time_format="%Y-%m-%d %H:%M:%S",
62
+ omit_repeated_times=False,
63
+ )
64
+ console_handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
65
+ else:
66
+ # Use standard handler without colors
67
+ console_handler = logging.StreamHandler()
68
+ console_handler.setFormatter(
69
+ logging.Formatter(
70
+ fmt="%(asctime)s %(levelname)s:%(name)s:%(message)s",
71
+ datefmt="%Y-%m-%d %H:%M:%S %z",
72
+ )
32
73
  )
33
- )
34
- logger.addHandler(stream_handler)
74
+ logger.addHandler(console_handler)
35
75
 
36
76
  # Check command line arguments for log file path
37
77
  args = parse_arguments()
38
78
 
39
- # Check if file logging is enabled
79
+ # Check if file logging is enabled (default to True for better user experience)
40
80
  if (
41
81
  config is not None
42
- and config.get("logging", {}).get("log_to_file", False)
82
+ and config.get("logging", {}).get("log_to_file", True)
43
83
  or args.logfile
44
84
  ):
45
85
  # Priority: 1. Command line arg, 2. Config file, 3. Default location (~/.mmrelay/logs)
@@ -64,21 +104,10 @@ def get_logger(name):
64
104
  if log_dir: # Ensure non-empty directory paths exist
65
105
  os.makedirs(log_dir, exist_ok=True)
66
106
 
67
- # Log which file we're using (only for the first logger)
107
+ # Store the log file path for later use
68
108
  if name == "M<>M Relay":
69
- # Create a basic logger to log the log file path
70
- # This is needed because we can't use the logger we're creating to log its own creation
71
- basic_logger = logging.getLogger("LogSetup")
72
- basic_logger.setLevel(logging.INFO)
73
- basic_handler = logging.StreamHandler()
74
- basic_handler.setFormatter(
75
- logging.Formatter(
76
- fmt="%(asctime)s %(levelname)s:%(name)s:%(message)s",
77
- datefmt="%Y-%m-%d %H:%M:%S %z",
78
- )
79
- )
80
- basic_logger.addHandler(basic_handler)
81
- basic_logger.info(f"Writing logs to: {log_file}")
109
+ global log_file_path
110
+ log_file_path = log_file
82
111
 
83
112
  # Create a file handler for logging
84
113
  try:
mmrelay/main.py CHANGED
@@ -10,8 +10,9 @@ import sys
10
10
 
11
11
  from nio import ReactionEvent, RoomMessageEmote, RoomMessageNotice, RoomMessageText
12
12
 
13
+ # Import version from package
13
14
  # Import meshtastic_utils as a module to set event_loop
14
- from mmrelay import meshtastic_utils
15
+ from mmrelay import __version__, meshtastic_utils
15
16
  from mmrelay.db_utils import (
16
17
  initialize_database,
17
18
  update_longnames,
@@ -33,6 +34,18 @@ logger = get_logger(name="M<>M Relay")
33
34
  logging.getLogger("nio").setLevel(logging.ERROR)
34
35
 
35
36
 
37
+ # Flag to track if banner has been printed
38
+ _banner_printed = False
39
+
40
+ def print_banner():
41
+ """Print a simple startup message with version information."""
42
+ global _banner_printed
43
+ # Only print the banner once
44
+ if not _banner_printed:
45
+ logger.info(f"Starting MMRelay v{__version__}")
46
+ _banner_printed = True
47
+
48
+
36
49
  async def main(config):
37
50
  """
38
51
  Main asynchronous function to set up and run the relay.
@@ -202,6 +215,9 @@ def run_main(args):
202
215
  Returns:
203
216
  int: Exit code (0 for success, non-zero for failure)
204
217
  """
218
+ # Print the banner at startup
219
+ print_banner()
220
+
205
221
  # Handle the --data-dir option
206
222
  if args and args.data_dir:
207
223
  import os
@@ -245,6 +261,19 @@ def run_main(args):
245
261
  set_config(db_utils, config)
246
262
  set_config(base_plugin, config)
247
263
 
264
+ # Get config path and log file path for logging
265
+ from mmrelay.config import config_path
266
+ from mmrelay.log_utils import log_file_path
267
+
268
+ # Create a logger with a different name to avoid conflicts with the one in config.py
269
+ config_rich_logger = get_logger("ConfigInfo")
270
+
271
+ # Now log the config file and log file locations with the properly formatted logger
272
+ if config_path:
273
+ config_rich_logger.info(f"Config file location: {config_path}")
274
+ if log_file_path:
275
+ config_rich_logger.info(f"Log file location: {log_file_path}")
276
+
248
277
  # Check if config exists and has the required keys
249
278
  required_keys = ["matrix", "meshtastic", "matrix_rooms"]
250
279
 
@@ -1,6 +1,8 @@
1
1
  import asyncio
2
2
  import contextlib
3
3
  import io
4
+ import os
5
+ import sys
4
6
  import threading
5
7
  import time
6
8
  from typing import List
@@ -46,6 +48,32 @@ shutting_down = False
46
48
  reconnect_task = None # To keep track of the reconnect task
47
49
 
48
50
 
51
+ def is_running_as_service():
52
+ """
53
+ Check if the application is running as a systemd service.
54
+ This is used to determine whether to show Rich progress indicators.
55
+
56
+ Returns:
57
+ bool: True if running as a service, False otherwise
58
+ """
59
+ # Check for INVOCATION_ID environment variable (set by systemd)
60
+ if os.environ.get("INVOCATION_ID"):
61
+ return True
62
+
63
+ # Check if parent process is systemd
64
+ try:
65
+ with open("/proc/self/status") as f:
66
+ for line in f:
67
+ if line.startswith("PPid:"):
68
+ ppid = int(line.split()[1])
69
+ with open(f"/proc/{ppid}/comm") as p:
70
+ return p.read().strip() == "systemd"
71
+ except (FileNotFoundError, PermissionError, ValueError):
72
+ pass
73
+
74
+ return False
75
+
76
+
49
77
  def serial_port_exists(port_name):
50
78
  """
51
79
  Check if the specified serial port exists.
@@ -118,7 +146,7 @@ def connect_meshtastic(passed_config=None, force_connect=False):
118
146
  if connection_type == "serial":
119
147
  # Serial connection
120
148
  serial_port = config["meshtastic"]["serial_port"]
121
- logger.info(f"Connecting to serial port {serial_port} ...")
149
+ logger.info(f"Connecting to serial port {serial_port}")
122
150
 
123
151
  # Check if serial port exists before connecting
124
152
  if not serial_port_exists(serial_port):
@@ -137,7 +165,9 @@ def connect_meshtastic(passed_config=None, force_connect=False):
137
165
  # BLE connection
138
166
  ble_address = config["meshtastic"].get("ble_address")
139
167
  if ble_address:
140
- logger.info(f"Connecting to BLE address {ble_address} ...")
168
+ logger.info(f"Connecting to BLE address {ble_address}")
169
+
170
+ # Connect without progress indicator
141
171
  meshtastic_client = meshtastic.ble_interface.BLEInterface(
142
172
  address=ble_address,
143
173
  noProto=False,
@@ -151,7 +181,9 @@ def connect_meshtastic(passed_config=None, force_connect=False):
151
181
  elif connection_type == "tcp":
152
182
  # TCP connection
153
183
  target_host = config["meshtastic"]["host"]
154
- logger.info(f"Connecting to host {target_host} ...")
184
+ logger.info(f"Connecting to host {target_host}")
185
+
186
+ # Connect without progress indicator
155
187
  meshtastic_client = meshtastic.tcp_interface.TCPInterface(
156
188
  hostname=target_host
157
189
  )
@@ -244,7 +276,25 @@ async def reconnect():
244
276
  logger.info(
245
277
  f"Reconnection attempt starting in {backoff_time} seconds..."
246
278
  )
247
- await asyncio.sleep(backoff_time)
279
+
280
+ # Show reconnection countdown with Rich (if not in a service)
281
+ if not is_running_as_service():
282
+ from rich.progress import Progress, TextColumn, BarColumn, TimeRemainingColumn
283
+ with Progress(
284
+ TextColumn("[cyan]Meshtastic: Reconnecting in"),
285
+ BarColumn(),
286
+ TextColumn("[cyan]{task.percentage:.0f}%"),
287
+ TimeRemainingColumn(),
288
+ transient=True,
289
+ ) as progress:
290
+ task = progress.add_task("Waiting", total=backoff_time)
291
+ for _ in range(backoff_time):
292
+ if shutting_down:
293
+ break
294
+ await asyncio.sleep(1)
295
+ progress.update(task, advance=1)
296
+ else:
297
+ await asyncio.sleep(backoff_time)
248
298
  if shutting_down:
249
299
  logger.debug(
250
300
  "Shutdown in progress. Aborting reconnection attempts."
mmrelay/setup_utils.py CHANGED
@@ -14,8 +14,20 @@ from pathlib import Path
14
14
 
15
15
 
16
16
  def get_executable_path():
17
- """Get the full path to the mmrelay executable."""
18
- return shutil.which("mmrelay") or sys.executable
17
+ """Get the full path to the mmrelay executable.
18
+
19
+ This function tries to find the mmrelay executable in the PATH,
20
+ which works for both pipx and pip installations.
21
+ """
22
+ mmrelay_path = shutil.which("mmrelay")
23
+ if mmrelay_path:
24
+ print(f"Found mmrelay executable at: {mmrelay_path}")
25
+ return mmrelay_path
26
+ else:
27
+ print(
28
+ "Warning: Could not find mmrelay executable in PATH. Using current Python interpreter."
29
+ )
30
+ return sys.executable
19
31
 
20
32
 
21
33
  def get_user_service_path():
@@ -31,7 +43,6 @@ def service_exists():
31
43
 
32
44
  def print_service_commands():
33
45
  """Print the commands for controlling the systemd user service."""
34
- print("\nUse these commands to control the mmrelay service:")
35
46
  print(" systemctl --user start mmrelay.service # Start the service")
36
47
  print(" systemctl --user stop mmrelay.service # Stop the service")
37
48
  print(" systemctl --user restart mmrelay.service # Restart the service")
@@ -39,24 +50,29 @@ def print_service_commands():
39
50
 
40
51
 
41
52
  def wait_for_service_start():
42
- """Wait for the service to start with a loading animation."""
43
- import sys
53
+ """Wait for the service to start with a Rich progress indicator."""
44
54
  import time
55
+ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
45
56
 
46
- print("\nStarting mmrelay service", end="")
47
- sys.stdout.flush()
48
-
49
- # Animation characters
50
- chars = ["-", "\\", "|", "/"]
57
+ # Create a Rich progress display with spinner and elapsed time
58
+ with Progress(
59
+ SpinnerColumn(),
60
+ TextColumn("[bold green]Starting mmrelay service..."),
61
+ TimeElapsedColumn(),
62
+ transient=True,
63
+ ) as progress:
64
+ # Add a task that will run for approximately 10 seconds
65
+ task = progress.add_task("Starting", total=100)
51
66
 
52
- # Wait for 10 seconds with animation
53
- for i in range(40): # 40 * 0.25s = 10s
54
- time.sleep(0.25)
55
- print(f"\rStarting mmrelay service {chars[i % len(chars)]}", end="")
56
- sys.stdout.flush()
67
+ # Update progress over 10 seconds
68
+ for i in range(10):
69
+ time.sleep(1)
70
+ progress.update(task, completed=10 * (i + 1))
57
71
 
58
- print("\rStarting mmrelay service... done!")
59
- sys.stdout.flush()
72
+ # Check if service is active after 5 seconds to potentially finish early
73
+ if i >= 5 and is_service_active():
74
+ progress.update(task, completed=100)
75
+ break
60
76
 
61
77
 
62
78
  def read_service_file():
@@ -67,45 +83,108 @@ def read_service_file():
67
83
  return None
68
84
 
69
85
 
70
- def create_service_file():
71
- """Create the systemd user service file."""
72
- executable_path = get_executable_path()
73
- if not executable_path:
74
- print("Error: Could not find mmrelay executable in PATH")
75
- return False
76
-
77
- # Create service directory if it doesn't exist
78
- service_dir = get_user_service_path().parent
79
- service_dir.mkdir(parents=True, exist_ok=True)
80
-
81
- # Create logs directory if it doesn't exist
82
- logs_dir = Path.home() / ".mmrelay" / "logs"
83
- logs_dir.mkdir(parents=True, exist_ok=True)
86
+ def get_template_service_path():
87
+ """Find the path to the template service file.
84
88
 
89
+ Returns:
90
+ str: The path to the template service file, or None if not found.
91
+ """
85
92
  # Try to find the service template file
86
- # First, check in the package directory
93
+ # First, check in the package directory (where it should be after installation)
87
94
  package_dir = os.path.dirname(__file__)
88
95
  template_path = os.path.join(
89
96
  os.path.dirname(os.path.dirname(package_dir)), "tools", "mmrelay.service"
90
97
  )
91
98
 
92
- # If not found, try the repository root
99
+ # If not found, try the repository root (for development)
93
100
  if not os.path.exists(template_path):
94
101
  repo_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
95
102
  template_path = os.path.join(repo_root, "tools", "mmrelay.service")
96
103
 
97
- # If still not found, try the current directory
104
+ # If still not found, try the current directory (fallback)
98
105
  if not os.path.exists(template_path):
99
106
  template_path = os.path.join(os.getcwd(), "tools", "mmrelay.service")
100
107
 
101
108
  if not os.path.exists(template_path):
102
- print(f"Error: Could not find service template at {template_path}")
103
- return False
109
+ return None
110
+
111
+ return template_path
112
+
113
+
114
+ def get_template_service_content():
115
+ """Get the content of the template service file.
116
+
117
+ Returns:
118
+ str: The content of the template service file, or None if not found.
119
+ """
120
+ template_path = get_template_service_path()
121
+ if not template_path:
122
+ return None
104
123
 
105
124
  # Read the template
106
125
  with open(template_path, "r") as f:
107
126
  service_template = f.read()
108
127
 
128
+ return service_template
129
+
130
+
131
+ def is_service_enabled():
132
+ """Check if the service is enabled.
133
+
134
+ Returns:
135
+ bool: True if the service is enabled, False otherwise.
136
+ """
137
+ try:
138
+ result = subprocess.run(
139
+ ["/usr/bin/systemctl", "--user", "is-enabled", "mmrelay.service"],
140
+ check=False, # Don't raise an exception if the service is not enabled
141
+ capture_output=True,
142
+ text=True,
143
+ )
144
+ return result.returncode == 0 and result.stdout.strip() == "enabled"
145
+ except Exception:
146
+ return False
147
+
148
+
149
+ def is_service_active():
150
+ """Check if the service is active (running).
151
+
152
+ Returns:
153
+ bool: True if the service is active, False otherwise.
154
+ """
155
+ try:
156
+ result = subprocess.run(
157
+ ["/usr/bin/systemctl", "--user", "is-active", "mmrelay.service"],
158
+ check=False, # Don't raise an exception if the service is not active
159
+ capture_output=True,
160
+ text=True,
161
+ )
162
+ return result.returncode == 0 and result.stdout.strip() == "active"
163
+ except Exception:
164
+ return False
165
+
166
+
167
+ def create_service_file():
168
+ """Create the systemd user service file."""
169
+ executable_path = get_executable_path()
170
+ if not executable_path:
171
+ print("Error: Could not find mmrelay executable in PATH")
172
+ return False
173
+
174
+ # Create service directory if it doesn't exist
175
+ service_dir = get_user_service_path().parent
176
+ service_dir.mkdir(parents=True, exist_ok=True)
177
+
178
+ # Create logs directory if it doesn't exist
179
+ logs_dir = Path.home() / ".mmrelay" / "logs"
180
+ logs_dir.mkdir(parents=True, exist_ok=True)
181
+
182
+ # Get the template service content
183
+ service_template = get_template_service_content()
184
+ if not service_template:
185
+ print("Error: Could not find service template file")
186
+ return False
187
+
109
188
  # Replace placeholders with actual values
110
189
  service_content = (
111
190
  service_template.replace(
@@ -147,74 +226,158 @@ def reload_daemon():
147
226
  return False
148
227
 
149
228
 
150
- def install_service():
151
- """Install or update the MMRelay user service."""
229
+ def service_needs_update():
230
+ """Check if the service file needs to be updated.
231
+
232
+ Returns:
233
+ tuple: (needs_update, reason) where needs_update is a boolean and reason is a string
234
+ """
152
235
  # Check if service already exists
153
236
  existing_service = read_service_file()
237
+ if not existing_service:
238
+ return True, "No existing service file found"
154
239
 
155
- if existing_service:
156
- print(f"A service file already exists at {get_user_service_path()}")
157
- if (
158
- not input("Do you want to reinstall/update the service? (y/n): ")
159
- .lower()
160
- .startswith("y")
161
- ):
162
- print("Service installation cancelled.")
163
- print_service_commands()
164
- return True
240
+ # Get the template service path
241
+ template_path = get_template_service_path()
242
+ if not template_path:
243
+ return False, "Could not find template service file"
165
244
 
166
- # Create or update service file
167
- if not create_service_file():
168
- return False
245
+ # Get the executable path
246
+ executable_path = get_executable_path()
247
+ if not executable_path:
248
+ return False, "Could not find mmrelay executable"
169
249
 
170
- # Reload daemon
171
- if not reload_daemon():
172
- return False
250
+ # Check if the ExecStart line in the existing service file contains the correct executable
251
+ if executable_path not in existing_service:
252
+ return (
253
+ True,
254
+ f"Service file does not use the current executable: {executable_path}",
255
+ )
256
+
257
+ # Check if the PATH environment includes pipx paths
258
+ if "%h/.local/pipx/venvs/mmrelay/bin" not in existing_service:
259
+ return True, "Service file does not include pipx paths in PATH environment"
260
+
261
+ # Check if the service file has been modified recently
262
+ template_mtime = os.path.getmtime(template_path)
263
+ service_path = get_user_service_path()
264
+ if os.path.exists(service_path):
265
+ service_mtime = os.path.getmtime(service_path)
266
+ if template_mtime > service_mtime:
267
+ return True, "Template service file is newer than installed service file"
268
+
269
+ return False, "Service file is up to date"
270
+
271
+
272
+ def install_service():
273
+ """Install or update the MMRelay user service."""
274
+ # Check if service already exists
275
+ existing_service = read_service_file()
276
+ service_path = get_user_service_path()
277
+
278
+ # Check if the service needs to be updated
279
+ update_needed, reason = service_needs_update()
173
280
 
281
+ # Check if the service is already installed and if it needs updating
174
282
  if existing_service:
175
- print("Service updated successfully")
283
+ print(f"A service file already exists at {service_path}")
284
+
285
+ if update_needed:
286
+ print(f"The service file needs to be updated: {reason}")
287
+ if (
288
+ not input("Do you want to update the service file? (y/n): ")
289
+ .lower()
290
+ .startswith("y")
291
+ ):
292
+ print("Service update cancelled.")
293
+ print_service_commands()
294
+ return True
295
+ else:
296
+ print(f"No update needed for the service file: {reason}")
176
297
  else:
177
- print("Service installed successfully")
298
+ print(f"No service file found at {service_path}")
299
+ print("A new service file will be created.")
178
300
 
179
- # Check if config is valid before starting the service
180
- from mmrelay.cli import check_config
301
+ # Create or update service file if needed
302
+ if not existing_service or update_needed:
303
+ if not create_service_file():
304
+ return False
181
305
 
182
- if not check_config():
183
- print(
184
- "\nWarning: Configuration is not valid. Service is installed but not started."
185
- )
186
- print("Please fix your configuration and then start the service manually.")
187
- print_service_commands()
188
- return True
306
+ # Reload daemon
307
+ if not reload_daemon():
308
+ return False
189
309
 
190
- # Ask if user wants to enable and start the service
191
- if (
192
- input("Do you want to enable the service to start at boot? (y/n): ")
193
- .lower()
194
- .startswith("y")
195
- ):
196
- try:
197
- subprocess.run(
198
- ["/usr/bin/systemctl", "--user", "enable", "mmrelay.service"],
199
- check=True,
200
- )
201
- print("Service enabled successfully")
202
- except subprocess.CalledProcessError as e:
203
- print(f"Error enabling service: {e}")
204
- except OSError as e:
205
- print(f"Error: {e}")
206
-
207
- if input("Do you want to start the service now? (y/n): ").lower().startswith("y"):
208
- if start_service():
209
- # Wait for the service to start
210
- wait_for_service_start()
211
-
212
- # Show service status
213
- show_service_status()
214
- print("Service started successfully")
310
+ if existing_service:
311
+ print("Service file updated successfully")
215
312
  else:
216
- print("\nWarning: Failed to start the service. Please check the logs.")
313
+ print("Service file created successfully")
314
+
315
+ # We don't need to validate the config here as it will be validated when the service starts
217
316
 
317
+ # Check if the service is already enabled
318
+ service_enabled = is_service_enabled()
319
+ if service_enabled:
320
+ print("The service is already enabled to start at boot.")
321
+ else:
322
+ print("The service is not currently enabled to start at boot.")
323
+ if (
324
+ input("Do you want to enable the service to start at boot? (y/n): ")
325
+ .lower()
326
+ .startswith("y")
327
+ ):
328
+ try:
329
+ subprocess.run(
330
+ ["/usr/bin/systemctl", "--user", "enable", "mmrelay.service"],
331
+ check=True,
332
+ )
333
+ print("Service enabled successfully")
334
+ service_enabled = True
335
+ except subprocess.CalledProcessError as e:
336
+ print(f"Error enabling service: {e}")
337
+ except OSError as e:
338
+ print(f"Error: {e}")
339
+
340
+ # Check if the service is already running
341
+ service_active = is_service_active()
342
+ if service_active:
343
+ print("The service is already running.")
344
+ if input("Do you want to restart the service? (y/n): ").lower().startswith("y"):
345
+ try:
346
+ subprocess.run(
347
+ ["/usr/bin/systemctl", "--user", "restart", "mmrelay.service"],
348
+ check=True,
349
+ )
350
+ print("Service restarted successfully")
351
+ # Wait for the service to restart
352
+ wait_for_service_start()
353
+ # Show service status
354
+ show_service_status()
355
+ except subprocess.CalledProcessError as e:
356
+ print(f"Error restarting service: {e}")
357
+ except OSError as e:
358
+ print(f"Error: {e}")
359
+ else:
360
+ print("The service is not currently running.")
361
+ if (
362
+ input("Do you want to start the service now? (y/n): ")
363
+ .lower()
364
+ .startswith("y")
365
+ ):
366
+ if start_service():
367
+ # Wait for the service to start
368
+ wait_for_service_start()
369
+ # Show service status
370
+ show_service_status()
371
+ print("Service started successfully")
372
+ else:
373
+ print("\nWarning: Failed to start the service. Please check the logs.")
374
+
375
+ # Print a summary of the service status
376
+ print("\nService Status Summary:")
377
+ print(f" Service File: {service_path}")
378
+ print(f" Enabled at Boot: {'Yes' if service_enabled else 'No'}")
379
+ print(f" Currently Running: {'Yes' if is_service_active() else 'No'}")
380
+ print("\nService Management Commands:")
218
381
  print_service_commands()
219
382
 
220
383
  return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmrelay
3
- Version: 1.0
3
+ Version: 1.0.1
4
4
  Summary: Bridge between Meshtastic mesh networks and Matrix chat rooms
5
5
  Home-page: https://github.com/geoffwhittington/meshtastic-matrix-relay
6
6
  Author: Geoff Whittington, Jeremiah K., and contributors
@@ -15,19 +15,32 @@ Requires-Python: >=3.8
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: meshtastic
18
- Requires-Dist: Pillow==11.1.0
18
+ Requires-Dist: Pillow==11.2.1
19
19
  Requires-Dist: matrix-nio==0.25.2
20
20
  Requires-Dist: matplotlib==3.10.1
21
21
  Requires-Dist: requests==2.32.3
22
- Requires-Dist: markdown==3.7
22
+ Requires-Dist: markdown==3.8
23
23
  Requires-Dist: haversine==2.9.0
24
24
  Requires-Dist: schedule==1.2.2
25
- Requires-Dist: platformdirs==4.2.0
25
+ Requires-Dist: platformdirs==4.3.7
26
26
  Requires-Dist: py-staticmaps>=0.4.0
27
+ Requires-Dist: rich==14.0.0
27
28
  Dynamic: license-file
28
29
 
29
30
  # M<>M Relay
30
31
 
32
+ ## ✨ Version 1.0 Released! ✨
33
+
34
+ **We're excited to announce MMRelay v1.0 with improved packaging, standardized directories, and enhanced CLI!**
35
+
36
+ **Existing users:** Version 1.0 requires a few quick migration steps:
37
+
38
+ 1. Follow the [UPGRADE_TO_V1.md](UPGRADE_TO_V1.md) guide for a smooth transition
39
+ 2. Move your configuration to the new standard location (`~/.mmrelay/config.yaml`)
40
+ 3. See [ANNOUNCEMENT.md](ANNOUNCEMENT.md) for all the exciting new features
41
+
42
+ Not ready to upgrade yet? No problem! Run `git checkout 0.10.1` to continue using the previous version.
43
+
31
44
  ## (Meshtastic <=> Matrix Relay)
32
45
 
33
46
  A powerful and easy-to-use relay between Meshtastic devices and Matrix chat rooms, allowing seamless communication across platforms. This opens the door for bridging Meshtastic devices to [many other platforms](https://matrix.org/bridges/).
@@ -41,17 +54,19 @@ MMRelay runs on Linux, macOS, and Windows.
41
54
  ### Quick Installation
42
55
 
43
56
  ```bash
44
- # Install using pip
45
- pip install mmrelay
46
-
47
- # Or use pipx for isolated installation (recommended)
57
+ # Install using pipx for isolated installation (recommended)
48
58
  pipx install mmrelay
59
+
60
+ # Pip will also work if you prefer
61
+ pip install mmrelay
49
62
  ```
50
63
 
64
+ For pipx installation instructions, see: [pipx installation guide](https://pipx.pypa.io/stable/installation/#on-linux)
65
+
51
66
  ### Resources
52
67
 
53
68
  - **New Users**: See [INSTRUCTIONS.md](INSTRUCTIONS.md) for setup and configuration
54
- - **Existing Users**: Check [UPGRADE_TO_V1.md](UPGRADE_TO_V1.md) for migration guidance
69
+ - **Existing Users**: See [UPGRADE_TO_V1.md](UPGRADE_TO_V1.md) for migration guidance
55
70
  - **Configuration**: Review [sample_config.yaml](sample_config.yaml) for examples
56
71
 
57
72
  ### Command-Line Options
@@ -1,14 +1,14 @@
1
- mmrelay/__init__.py,sha256=tY9iMcQ9e8IhGjiSjukqcNujnqqf7_OTORo7JvRDWvU,278
2
- mmrelay/cli.py,sha256=H-h-hvrFiOr9gTY5o21snBNiHJCDJx_HLR8JxTs1M-o,12273
3
- mmrelay/config.py,sha256=epH-xOOIMblG5w_fk86r8JgJK-70bLbX5CfMTWOfPmo,7064
1
+ mmrelay/__init__.py,sha256=rJqoGfYDKTqEyyzrpQDpwRPfFCDG90XnhGUSHzttjPo,440
2
+ mmrelay/cli.py,sha256=yBYOkCwGYDJ2gRKPoPltxB_PydX1CPNUerVJHcNuHTo,12355
3
+ mmrelay/config.py,sha256=OIDxQStuVHyWxsDfnKqt0rr26ETEoZPqHVXReEH3IG8,7006
4
4
  mmrelay/config_checker.py,sha256=UnoHVTXzfdTfFkbmXv9r_Si76v-sxXLb5FOaQSOM45E,4909
5
5
  mmrelay/db_utils.py,sha256=DP2YuKBZtV771wo9X-Z7Ww5txfaIR0inWh1K_oVZ7cA,11430
6
- mmrelay/log_utils.py,sha256=-ItwMnxa-t2ZKBIp6wzCkq19dk7A0MofsvjS_ilTSlQ,3808
7
- mmrelay/main.py,sha256=a_ZLDiGcBtrZgTlJq8gqGSW8YCduDgAOEDxzwy4AiXE,10097
6
+ mmrelay/log_utils.py,sha256=FXhaq4WSDHwlqiG3k1BbSxiOl5be4P4Kyr1gsIThOBw,4572
7
+ mmrelay/main.py,sha256=Okiat89uWiOrn5wq7lvJinYP3C6wzKdlQsyLxB8cCmI,11084
8
8
  mmrelay/matrix_utils.py,sha256=GkIVj2bbPHtx1emFMwhEhc1SWHcv4UvkuyZYdb-Wnwo,30511
9
- mmrelay/meshtastic_utils.py,sha256=7Q6mQ2gLLc_yjlGCmakPgGRoBB9znRWOCGMW0MgoZ-w,21414
9
+ mmrelay/meshtastic_utils.py,sha256=ha6wRYeU16wT8jkZlr7RjnsB1b9e276cgu0jw4vmdbw,23270
10
10
  mmrelay/plugin_loader.py,sha256=QfSXu4nxxBL_pGttLJGzGki8kRtWbFdtSoD_Qnx2iqU,12758
11
- mmrelay/setup_utils.py,sha256=iFHEGnrop3Lv2j-MDmOGT4oQYuAm_5YHAc16O2rpkew,8022
11
+ mmrelay/setup_utils.py,sha256=MWBrpf9lNlw21yOoyFhzz1DSJx2KMTzZIMcHS3MwcdM,14090
12
12
  mmrelay/plugins/__init__.py,sha256=KVMQIXRhe0wlGj4O3IZ0vOIQRKFkfPYejHXhJL17qrc,51
13
13
  mmrelay/plugins/base_plugin.py,sha256=7tBIARWp9Qc4iAEMcsKiUJh1cbqXHcXAk4dlu1b8SBg,7524
14
14
  mmrelay/plugins/debug_plugin.py,sha256=Jziht9Nj_bRO6Rmy7TjfBXaYo5eM3XsenbWFxPpyUs4,443
@@ -21,9 +21,9 @@ mmrelay/plugins/nodes_plugin.py,sha256=RDabzyG5hKG5aYWecsRUcLSjMCCv6Pngmq2Qpld1A
21
21
  mmrelay/plugins/ping_plugin.py,sha256=RTRdgDQUSO33lreDTmWsTlI0L1C3FJrXE0KYqfEWYO0,4017
22
22
  mmrelay/plugins/telemetry_plugin.py,sha256=8SxWv4BLXMUTbiVaD3MjlMMdQyS7S_1OfLlVNAUMSO0,6306
23
23
  mmrelay/plugins/weather_plugin.py,sha256=yoKA_HdFqFEhgYdXqLhvXatLphCyLJFuGUKCR7fILv0,8546
24
- mmrelay-1.0.dist-info/licenses/LICENSE,sha256=yceWauM1c0-FHxVplsD7W1-AbSeRaUNlmqT4UO1msBU,1073
25
- mmrelay-1.0.dist-info/METADATA,sha256=NCTfzxMnJjPpgBtOP0oGD7ZPYQBHA6CiUkvarmYzrGk,6219
26
- mmrelay-1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
27
- mmrelay-1.0.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
28
- mmrelay-1.0.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
29
- mmrelay-1.0.dist-info/RECORD,,
24
+ mmrelay-1.0.1.dist-info/licenses/LICENSE,sha256=yceWauM1c0-FHxVplsD7W1-AbSeRaUNlmqT4UO1msBU,1073
25
+ mmrelay-1.0.1.dist-info/METADATA,sha256=WziSMZeJluF69VwTLtCuxGvGtFxbAcumZV4iOAEWWTg,6953
26
+ mmrelay-1.0.1.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
27
+ mmrelay-1.0.1.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
28
+ mmrelay-1.0.1.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
29
+ mmrelay-1.0.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (79.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5