diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 22ec5d0..432e75c 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -335,6 +335,11 @@ def onConnected(interface): print("Writing modified preferences to device") getNode().writeConfig() + if args.export_config: + # export the configuration (the opposite of '--configure') + closeNow = True + export_config(interface) + if args.seturl: closeNow = True getNode().setURL(args.seturl) @@ -515,6 +520,44 @@ def subscribe(): # pub.subscribe(onNode, "meshtastic.node") +def export_config(interface): + """used in--export-config""" + owner = interface.getLongName() + channel_url = interface.localNode.getURL() + myinfo = interface.getMyNodeInfo() + pos = myinfo.get('position') + lat = None + lon = None + alt = None + if pos: + lat = pos.get('latitude') + lon = pos.get('longitude') + alt = pos.get('altitude') + + config = "# start of Meshtastic configure yaml\n" + if owner: + config += f"owner: {owner}\n\n" + if channel_url: + config += f"channel_url: {channel_url}\n\n" + if lat or lon or alt: + config += "location:\n" + if lat: + config += f" lat: {lat}\n" + if lon: + config += f" lon: {lon}\n" + if alt: + config += f" alt: {alt}\n" + config += "\n" + preferences = f'{interface.localNode.radioConfig.preferences}' + prefs = preferences.splitlines() + if prefs: + config += "user_prefs:\n" + for pref in prefs: + config += f" {meshtastic.util.quoteBooleans(pref)}\n" + print(config) + return config + + def common(): """Shared code for all of our command line wrappers""" our_globals = Globals.getInstance() @@ -605,6 +648,11 @@ def initParser(): help="Specify a path to a yaml(.yml) file containing the desired settings for the connected device.", action='append') + parser.add_argument( + "--export-config", + help="Export the configuration in yaml(.yml) format.", + action='store_true') + parser.add_argument( "--port", help="The port the Meshtastic device is connected to, i.e. /dev/ttyUSB0. If unspecified, we'll try to find it.", diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 2559e3b..edb5848 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -1,4 +1,5 @@ """Meshtastic unit tests for __main__.py""" +# pylint: disable=C0302 import sys import os @@ -7,7 +8,7 @@ import re from unittest.mock import patch, MagicMock import pytest -from meshtastic.__main__ import initParser, main, Globals, onReceive, onConnection +from meshtastic.__main__ import initParser, main, Globals, onReceive, onConnection, export_config import meshtastic.radioconfig_pb2 from ..serial_interface import SerialInterface from ..tcp_interface import TCPInterface @@ -1196,3 +1197,35 @@ def test_main_onConnection(reset_globals, capsys): out, err = capsys.readouterr() assert re.search(r'Connection changed: foo', out, re.MULTILINE) assert err == '' + + +@pytest.mark.unit +def test_main_export_config(reset_globals, capsys): + """Test export_config""" + iface = MagicMock(autospec=SerialInterface) + with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: + mo.getLongName.return_value = 'foo' + mo.localNode.getURL.return_value = 'bar' + mo.getMyNodeInfo().get.return_value = { 'latitudeI': 1100000000, 'longitudeI': 1200000000, + 'altitude': 100, 'batteryLevel': 34, 'latitude': 110.0, + 'longitude': 120.0} + mo.localNode.radioConfig.preferences = """phone_timeout_secs: 900 +ls_secs: 300 +position_broadcast_smart: true +fixed_position: true +position_flags: 35""" + export_config(mo) + out, err = capsys.readouterr() + assert re.search(r'owner: foo', out, re.MULTILINE) + assert re.search(r'channel_url: bar', out, re.MULTILINE) + assert re.search(r'location:', out, re.MULTILINE) + assert re.search(r'lat: 110.0', out, re.MULTILINE) + assert re.search(r'lon: 120.0', out, re.MULTILINE) + assert re.search(r'alt: 100', out, re.MULTILINE) + assert re.search(r'user_prefs:', out, re.MULTILINE) + assert re.search(r'phone_timeout_secs: 900', out, re.MULTILINE) + assert re.search(r'ls_secs: 300', out, re.MULTILINE) + assert re.search(r"position_broadcast_smart: 'true'", out, re.MULTILINE) + assert re.search(r"fixed_position: 'true'", out, re.MULTILINE) + assert re.search(r"position_flags: 35", out, re.MULTILINE) + assert err == '' diff --git a/meshtastic/tests/test_util.py b/meshtastic/tests/test_util.py index b03501d..b3c6d39 100644 --- a/meshtastic/tests/test_util.py +++ b/meshtastic/tests/test_util.py @@ -4,7 +4,7 @@ import re import pytest -from meshtastic.util import fixme, stripnl, pskToString, our_exit, support_info, genPSK256, fromStr, fromPSK +from meshtastic.util import fixme, stripnl, pskToString, our_exit, support_info, genPSK256, fromStr, fromPSK, quoteBooleans @pytest.mark.unit @@ -35,6 +35,16 @@ def test_fromStr(): assert fromStr('abc') == 'abc' +@pytest.mark.unit +def test_quoteBooleans(): + """Test quoteBooleans""" + assert quoteBooleans('') == '' + assert quoteBooleans('foo') == 'foo' + assert quoteBooleans('true') == 'true' + assert quoteBooleans('false') == 'false' + assert quoteBooleans(': true') == ": 'true'" + assert quoteBooleans(': false') == ": 'false'" + @pytest.mark.unit def test_fromPSK(): """Test fromPSK""" diff --git a/meshtastic/util.py b/meshtastic/util.py index f9f95d7..b4376f9 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -16,6 +16,14 @@ import pkg_resources blacklistVids = dict.fromkeys([0x1366]) +def quoteBooleans(a_string): + """Quote booleans + given a string that contains ": true", replace with ": 'true'" (or false) + """ + tmp = a_string.replace(": true", ": 'true'") + tmp = tmp.replace(": false", ": 'false'") + return tmp + def genPSK256(): """Generate a random preshared key""" return os.urandom(32)