From 59e00a95785f06efd1b9048d4b34075617b6ada6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Thu, 27 Nov 2025 13:57:11 +0100 Subject: [PATCH 1/5] prepare separation of showing/presenting information and retrieving it in a machine readable format. Actually, change gives the same result as before. --- meshtastic/Formatter.py | 35 ++++++++++++++++++ meshtastic/__main__.py | 34 +++++++++++------ meshtastic/mesh_interface.py | 72 ++++++++++++++++++++++++++---------- meshtastic/node.py | 59 ++++++++++++++++++++--------- 4 files changed, 152 insertions(+), 48 deletions(-) create mode 100644 meshtastic/Formatter.py diff --git a/meshtastic/Formatter.py b/meshtastic/Formatter.py new file mode 100644 index 00000000..c19c8884 --- /dev/null +++ b/meshtastic/Formatter.py @@ -0,0 +1,35 @@ +"""Defines the formatting of outputs using factories""" + +class FormatterFactory(): + """Factory of formatters""" + def __init__(self): + self.formatters = { + "json": FormatAsJson, + "default": FormatAsText + } + + def getFormatter(self, formatSpec: str): + """returns the formatter for info data. If no valid formatter is found, default to text""" + return self.formatters.get(formatSpec, self.formatters["default"]) + + +class InfoFormatter(): + """responsible to format info data""" + def format(self, data: dict, formatSpec: str) -> str: + """returns formatted string according to formatSpec for info data""" + formatter = FormatterFactory.getFormatter(formatSpec) + return formatter.formatInfo(data) + + +class FormatAsJson(): + """responsible to return the data as JSON string""" + def formatInfo(self, data: dict) -> str: + """Info as JSON""" + return "" + + +class FormatAsText(): + """responsible to print the data. No string return""" + def formatInfo(self, data: dict) -> str: + """Info printed""" + return "" diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 16854002..d93af4c5 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -978,12 +978,22 @@ def setSimpleConfig(modem_preset): ringtone = interface.getNode(args.dest, **getNode_kwargs).get_ringtone() print(f"ringtone:{ringtone}") + if args.get: + closeNow = True + node = interface.getNode(args.dest, False, **getNode_kwargs) + for pref in args.get: + found = getPref(node, pref[0]) + + if found: + print("Completed getting preferences") + if args.info: print("") # If we aren't trying to talk to our local node, don't show it if args.dest == BROADCAST_ADDR: + # infodata = interface.getInfo() + # infodata.update(interface.getNode(args.dest, **getNode_kwargs).getInfo()) interface.showInfo() - print("") interface.getNode(args.dest, **getNode_kwargs).showInfo() closeNow = True print("") @@ -999,26 +1009,21 @@ def setSimpleConfig(modem_preset): "Use the '--get' command for a specific configuration (e.g. 'lora') instead." ) - if args.get: - closeNow = True - node = interface.getNode(args.dest, False, **getNode_kwargs) - for pref in args.get: - found = getPref(node, pref[0]) - - if found: - print("Completed getting preferences") - if args.nodes: closeNow = True if args.dest != BROADCAST_ADDR: print("Showing node list of a remote node is not supported.") return - interface.showNodes(True, args.show_fields) + interface.showNodes(True, showFields=args.show_fields, printFmt=args.fmt) if args.show_fields and not args.nodes: print("--show-fields can only be used with --nodes") return + if args.fmt and not (args.nodes or args.info): + print("--fmt can only be used with --nodes or --info") + return + if args.qr or args.qr_all: closeNow = True url = interface.getNode(args.dest, True, **getNode_kwargs).getURL(includeAll=args.qr_all) @@ -1832,6 +1837,13 @@ def addLocalActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars default=None ) + group.add_argument( + "--fmt", + help="Specify format to show when using --nodes/--info", + type=str, + default=None + ) + return parser def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 7052bc5f..7d631dab 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -17,6 +17,7 @@ from typing import Any, Callable, Dict, List, Optional, Union import google.protobuf.json_format +from google.protobuf.json_format import MessageToDict try: import print_color # type: ignore[import-untyped] @@ -192,40 +193,62 @@ def _handleLogRecord(self, record: mesh_pb2.LogRecord) -> None: # For now we just try to format the line as if it had come in over the serial port self._handleLogLine(record.message) - def showInfo(self, file=sys.stdout) -> str: # pylint: disable=W0613 - """Show human readable summary about this object""" - owner = f"Owner: {self.getLongName()} ({self.getShortName()})" - myinfo = "" + def getInfo(self) -> dict: + """retrieve object data""" + objData: dict[str, Any] = { + "Owner": [self.getLongName(), self.getShortName()], + "My Info": {}, + "Metadata": {}, + "Nodes": {} + } + if self.myInfo: - myinfo = f"\nMy info: {message_to_json(self.myInfo)}" - metadata = "" + objData["My Info"] = MessageToDict(self.myInfo) if self.metadata: - metadata = f"\nMetadata: {message_to_json(self.metadata)}" - mesh = "\n\nNodes in mesh: " - nodes = {} + objData["Metadata"] = MessageToDict(self.metadata) + + keys_to_remove = ("raw", "decoded", "payload") if self.nodes: for n in self.nodes.values(): # when the TBeam is first booted, it sometimes shows the raw data # so, we will just remove any raw keys - keys_to_remove = ("raw", "decoded", "payload") n2 = remove_keys_from_dict(keys_to_remove, n) # if we have 'macaddr', re-format it - if "macaddr" in n2["user"]: - val = n2["user"]["macaddr"] - # decode the base64 value - addr = convert_mac_addr(val) - n2["user"]["macaddr"] = addr + self.reformatMAC(n2) # use id as dictionary key for correct json format in list of nodes nodeid = n2["user"]["id"] - nodes[nodeid] = n2 - infos = owner + myinfo + metadata + mesh + json.dumps(nodes, indent=2) + # nodes[nodeid] = n2 + objData["Nodes"][nodeid] = n2 + return objData + + @staticmethod + def reformatMAC(n2: dict): + """reformat MAC address to hex format""" + if "macaddr" in n2["user"]: + val = n2["user"]["macaddr"] + # decode the base64 value + addr = convert_mac_addr(val) + n2["user"]["macaddr"] = addr + + def showInfo(self, file=sys.stdout) -> str: # pylint: disable=W0613 + """Show human readable summary about this object""" + ifData = self.getInfo() + + owner = f"Owner: {ifData['Owner'][0]}({ifData['Owner'][1]})" + myinfo = f"My info: {json.dumps(ifData['My Info'])}" if ifData['My Info'] else "" + metadata = f"Metadata: {json.dumps(ifData['Metadata'])}" if ifData['Metadata'] else "" + mesh = f"\nNodes in mesh:{json.dumps(ifData['Nodes'], indent=2)}" + + infos = f"{owner}\n{myinfo}\n{metadata}\n{mesh}" print(infos) return infos - def showNodes( - self, includeSelf: bool = True, showFields: Optional[List[str]] = None + def showNodes(self, + includeSelf: bool = True, + showFields: Optional[List[str]] = None, + printFmt: Optional[str] = None ) -> str: # pylint: disable=W0613 """Show table summary of nodes in mesh @@ -372,7 +395,16 @@ def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any: for i, row in enumerate(rows): row["N"] = i + 1 - table = tabulate(rows, headers="keys", missingval="N/A", tablefmt="fancy_grid") + if not printFmt or len(printFmt) == 0: + printFmt = "fancy_grid" + if printFmt.lower() == 'json': + headers = [] + if len(rows) > 0: + headers = list(rows[0].keys()) + outDict = {'headers': headers, 'nodes': rows} + table = json.dumps(outDict) + else: + table = tabulate(rows, headers="keys", missingval="N/A", tablefmt=printFmt) print(table) return table diff --git a/meshtastic/node.py b/meshtastic/node.py index afb5611a..39cbc37d 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -2,11 +2,14 @@ """ import base64 +import json import logging import time from typing import Optional, Union, List +from google.protobuf.json_format import MessageToDict + from meshtastic.protobuf import admin_pb2, apponly_pb2, channel_pb2, config_pb2, localonly_pb2, mesh_pb2, portnums_pb2 from meshtastic.util import ( Timeout, @@ -77,33 +80,55 @@ def module_available(self, excluded_bit: int) -> bool: def showChannels(self): """Show human readable description of our channels.""" + chanCfg = self.getChannelInfo() print("Channels:") + for idx, c in enumerate(chanCfg['Channels']): + if channel_pb2.Channel.Role.Name(c['role'] )!= "DISABLED": + print(f" Index {idx}: {channel_pb2.Channel.Role.Name(c['role'])} psk={pskToString(c['psk'])} {json.dumps(c['settings'])}") + print("") + publicURL = chanCfg['publicURL'] + print(f"\nPrimary channel URL: {publicURL}") + adminURL = chanCfg['adminURL'] + if adminURL != publicURL: + print(f"Complete URL (includes all channels): {adminURL}") + + def getChannelInfo(self) -> dict: + """Return description of our channels as dict.""" + # print("Channels:") + chanConfig = {} if self.channels: logger.debug(f"self.channels:{self.channels}") - for c in self.channels: - cStr = message_to_json(c.settings) - # don't show disabled channels - if channel_pb2.Channel.Role.Name(c.role) != "DISABLED": - print( - f" Index {c.index}: {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}" - ) + chanConfig = [{"role": c.role, "psk": c.settings.psk, "settings": MessageToDict(c.settings, always_print_fields_with_no_presence=True)} for c in self.channels] + # for c in self.channels: + # cStr = MessageToDict(c.settings) + # # don't show disabled channels + # if channel_pb2.Channel.Role.Name(c.role) != "DISABLED": + # print( + # f" Index {c.index}: {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}" + # ) publicURL = self.getURL(includeAll=False) adminURL = self.getURL(includeAll=True) - print(f"\nPrimary channel URL: {publicURL}") - if adminURL != publicURL: - print(f"Complete URL (includes all channels): {adminURL}") + return {"Channels": chanConfig, "publicURL": publicURL, "adminURL": adminURL} def showInfo(self): """Show human readable description of our node""" - prefs = "" + cfgInfo = self.getInfo() + print(f"Preferences: {json.dumps(cfgInfo['Preferences'], indent=2)}") + print(f"Module preferences: {json.dumps(cfgInfo['Module preferences'], indent=2)}") + self.showChannels() + + def getInfo(self) ->dict: + """Return preferences of our node as dictionary""" + locConfig = {} if self.localConfig: - prefs = message_to_json(self.localConfig, multiline=True) - print(f"Preferences: {prefs}\n") - prefs = "" + locConfig = MessageToDict(self.localConfig) + modConfig = {} if self.moduleConfig: - prefs = message_to_json(self.moduleConfig, multiline=True) - print(f"Module preferences: {prefs}\n") - self.showChannels() + modConfig = MessageToDict(self.moduleConfig) + chanConfig = self.getChannelInfo() + info = {"Preferences": locConfig, "Module preferences": modConfig} + info.update(chanConfig) + return info def setChannels(self, channels): """Set the channels for this node""" From eb5f6aff104abe1365f851bb5eb6ca351837391b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Thu, 27 Nov 2025 16:12:17 +0100 Subject: [PATCH 2/5] put formatter factory in action --- meshtastic/Formatter.py | 35 --------------- meshtastic/__main__.py | 17 +++----- meshtastic/formatter.py | 83 ++++++++++++++++++++++++++++++++++++ meshtastic/mesh_interface.py | 13 ------ meshtastic/node.py | 28 ------------ 5 files changed, 88 insertions(+), 88 deletions(-) delete mode 100644 meshtastic/Formatter.py create mode 100644 meshtastic/formatter.py diff --git a/meshtastic/Formatter.py b/meshtastic/Formatter.py deleted file mode 100644 index c19c8884..00000000 --- a/meshtastic/Formatter.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Defines the formatting of outputs using factories""" - -class FormatterFactory(): - """Factory of formatters""" - def __init__(self): - self.formatters = { - "json": FormatAsJson, - "default": FormatAsText - } - - def getFormatter(self, formatSpec: str): - """returns the formatter for info data. If no valid formatter is found, default to text""" - return self.formatters.get(formatSpec, self.formatters["default"]) - - -class InfoFormatter(): - """responsible to format info data""" - def format(self, data: dict, formatSpec: str) -> str: - """returns formatted string according to formatSpec for info data""" - formatter = FormatterFactory.getFormatter(formatSpec) - return formatter.formatInfo(data) - - -class FormatAsJson(): - """responsible to return the data as JSON string""" - def formatInfo(self, data: dict) -> str: - """Info as JSON""" - return "" - - -class FormatAsText(): - """responsible to print the data. No string return""" - def formatInfo(self, data: dict) -> str: - """Info printed""" - return "" diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index d93af4c5..e8dc28d1 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -44,6 +44,8 @@ from meshtastic import BROADCAST_ADDR, mt_config, remote_hardware from meshtastic.ble_interface import BLEInterface from meshtastic.mesh_interface import MeshInterface +from meshtastic.formatter import InfoFormatter + try: from meshtastic.powermon import ( PowerMeter, @@ -988,21 +990,12 @@ def setSimpleConfig(modem_preset): print("Completed getting preferences") if args.info: - print("") # If we aren't trying to talk to our local node, don't show it if args.dest == BROADCAST_ADDR: - # infodata = interface.getInfo() - # infodata.update(interface.getNode(args.dest, **getNode_kwargs).getInfo()) - interface.showInfo() - interface.getNode(args.dest, **getNode_kwargs).showInfo() + infodata = interface.getInfo() + infodata.update(interface.getNode(args.dest, **getNode_kwargs).getInfo()) + InfoFormatter().format(infodata, args.fmt) closeNow = True - print("") - pypi_version = meshtastic.util.check_if_newer_version() - if pypi_version: - print( - f"*** A newer version v{pypi_version} is available!" - ' Consider running "pip install --upgrade meshtastic" ***\n' - ) else: print("Showing info of remote node is not supported.") print( diff --git a/meshtastic/formatter.py b/meshtastic/formatter.py new file mode 100644 index 00000000..654bcab1 --- /dev/null +++ b/meshtastic/formatter.py @@ -0,0 +1,83 @@ +import json + +from meshtastic.util import pskToString, check_if_newer_version +from meshtastic.protobuf import channel_pb2 + +"""Defines the formatting of outputs using factories""" + +class FormatterFactory(): + """Factory of formatters""" + def __init__(self): + self.formatters = { + "json": FormatAsJson, + "default": FormatAsText + } + + def getFormatter(self, formatSpec: str): + """returns the formatter for info data. If no valid formatter is found, default to text""" + return self.formatters.get(formatSpec.lower(), self.formatters["default"]) + + +class InfoFormatter(): + """responsible to format info data""" + def format(self, data: dict, formatSpec: str | None = None) -> str: + """returns formatted string according to formatSpec for info data""" + if not formatSpec: + formatSpec = 'default' + formatter = FormatterFactory().getFormatter(formatSpec) + return formatter().formatInfo(data) + + +class FormatAsJson(): + """responsible to return the data as JSON string""" + def formatInfo(self, data: dict) -> str: + """Info as JSON""" + + # Remove the bytes entry of PSK before serialization of JSON + for c in data['Channels']: + del c['psk'] + jsonData = json.dumps(data, indent=2) + print(jsonData) + return jsonData + + +class FormatAsText(): + """responsible to print the data. No string return""" + def formatInfo(self, data: dict) -> str: + """Info printed as plain text""" + print("") + self.showMeshInfo(data) + self.showNodeInfo(data) + print("") + pypi_version = check_if_newer_version() + if pypi_version: + print( + f"*** A newer version v{pypi_version} is available!" + ' Consider running "pip install --upgrade meshtastic" ***\n' + ) + return "" + + def showMeshInfo(self, data: dict): + """Show human-readable summary about mesh interface data""" + owner = f"Owner: {data['Owner'][0]}({data['Owner'][1]})" + myinfo = f"My info: {json.dumps(data['My Info'])}" if data['My Info'] else "" + metadata = f"Metadata: {json.dumps(data['Metadata'])}" if data['Metadata'] else "" + mesh = f"\nNodes in mesh:{json.dumps(data['Nodes'], indent=2)}" + + infos = f"{owner}\n{myinfo}\n{metadata}\n{mesh}" + print(infos) + + def showNodeInfo(self, data: dict): + """Show human-readable description of our node""" + print(f"Preferences: {json.dumps(data['Preferences'], indent=2)}") + print(f"Module preferences: {json.dumps(data['Module preferences'], indent=2)}") + print("Channels:") + for idx, c in enumerate(data['Channels']): + if channel_pb2.Channel.Role.Name(c['role'] )!= "DISABLED": + print(f" Index {idx}: {channel_pb2.Channel.Role.Name(c['role'])} psk={pskToString(c['psk'])} {json.dumps(c['settings'])}") + print("") + publicURL = data['publicURL'] + print(f"\nPrimary channel URL: {publicURL}") + adminURL = data['adminURL'] + if adminURL != publicURL: + print(f"Complete URL (includes all channels): {adminURL}") diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 7d631dab..3876d32d 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -232,19 +232,6 @@ def reformatMAC(n2: dict): addr = convert_mac_addr(val) n2["user"]["macaddr"] = addr - def showInfo(self, file=sys.stdout) -> str: # pylint: disable=W0613 - """Show human readable summary about this object""" - ifData = self.getInfo() - - owner = f"Owner: {ifData['Owner'][0]}({ifData['Owner'][1]})" - myinfo = f"My info: {json.dumps(ifData['My Info'])}" if ifData['My Info'] else "" - metadata = f"Metadata: {json.dumps(ifData['Metadata'])}" if ifData['Metadata'] else "" - mesh = f"\nNodes in mesh:{json.dumps(ifData['Nodes'], indent=2)}" - - infos = f"{owner}\n{myinfo}\n{metadata}\n{mesh}" - print(infos) - return infos - def showNodes(self, includeSelf: bool = True, showFields: Optional[List[str]] = None, diff --git a/meshtastic/node.py b/meshtastic/node.py index 39cbc37d..5f1e6fff 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -78,20 +78,6 @@ def module_available(self, excluded_bit: int) -> bool: except Exception: return True - def showChannels(self): - """Show human readable description of our channels.""" - chanCfg = self.getChannelInfo() - print("Channels:") - for idx, c in enumerate(chanCfg['Channels']): - if channel_pb2.Channel.Role.Name(c['role'] )!= "DISABLED": - print(f" Index {idx}: {channel_pb2.Channel.Role.Name(c['role'])} psk={pskToString(c['psk'])} {json.dumps(c['settings'])}") - print("") - publicURL = chanCfg['publicURL'] - print(f"\nPrimary channel URL: {publicURL}") - adminURL = chanCfg['adminURL'] - if adminURL != publicURL: - print(f"Complete URL (includes all channels): {adminURL}") - def getChannelInfo(self) -> dict: """Return description of our channels as dict.""" # print("Channels:") @@ -99,24 +85,10 @@ def getChannelInfo(self) -> dict: if self.channels: logger.debug(f"self.channels:{self.channels}") chanConfig = [{"role": c.role, "psk": c.settings.psk, "settings": MessageToDict(c.settings, always_print_fields_with_no_presence=True)} for c in self.channels] - # for c in self.channels: - # cStr = MessageToDict(c.settings) - # # don't show disabled channels - # if channel_pb2.Channel.Role.Name(c.role) != "DISABLED": - # print( - # f" Index {c.index}: {channel_pb2.Channel.Role.Name(c.role)} psk={pskToString(c.settings.psk)} {cStr}" - # ) publicURL = self.getURL(includeAll=False) adminURL = self.getURL(includeAll=True) return {"Channels": chanConfig, "publicURL": publicURL, "adminURL": adminURL} - def showInfo(self): - """Show human readable description of our node""" - cfgInfo = self.getInfo() - print(f"Preferences: {json.dumps(cfgInfo['Preferences'], indent=2)}") - print(f"Module preferences: {json.dumps(cfgInfo['Module preferences'], indent=2)}") - self.showChannels() - def getInfo(self) ->dict: """Return preferences of our node as dictionary""" locConfig = {} From 6551fe5d755bf3e7b943044965373c083119879c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Fri, 28 Nov 2025 10:51:25 +0100 Subject: [PATCH 3/5] make text formatter more robust against missing data elements fix unit tests related to introduction of formatter --- meshtastic/formatter.py | 42 +++++++++++------- meshtastic/tests/test_main.py | 54 ++++++++++++++--------- meshtastic/tests/test_mesh_interface.py | 32 +++++++++----- meshtastic/tests/test_node.py | 14 +++--- meshtastic/tests/test_serial_interface.py | 26 +++++++---- meshtastic/tests/test_tcp_interface.py | 28 ++++++++---- 6 files changed, 123 insertions(+), 73 deletions(-) diff --git a/meshtastic/formatter.py b/meshtastic/formatter.py index 654bcab1..e55c3379 100644 --- a/meshtastic/formatter.py +++ b/meshtastic/formatter.py @@ -60,24 +60,36 @@ def formatInfo(self, data: dict) -> str: def showMeshInfo(self, data: dict): """Show human-readable summary about mesh interface data""" owner = f"Owner: {data['Owner'][0]}({data['Owner'][1]})" - myinfo = f"My info: {json.dumps(data['My Info'])}" if data['My Info'] else "" - metadata = f"Metadata: {json.dumps(data['Metadata'])}" if data['Metadata'] else "" - mesh = f"\nNodes in mesh:{json.dumps(data['Nodes'], indent=2)}" + + myinfo = "" + if dx := data.get('My Info', None) is not None: + myinfo = f"My info: {json.dumps(dx)}" + + metadata = "" + if dx := data.get('Metadata', None) is not None: + metadata = f"Metadata: {json.dumps(dx)}" + + mesh = f"\nNodes in mesh:{json.dumps(data.get('Nodes', {}), indent=2)}" infos = f"{owner}\n{myinfo}\n{metadata}\n{mesh}" print(infos) def showNodeInfo(self, data: dict): """Show human-readable description of our node""" - print(f"Preferences: {json.dumps(data['Preferences'], indent=2)}") - print(f"Module preferences: {json.dumps(data['Module preferences'], indent=2)}") - print("Channels:") - for idx, c in enumerate(data['Channels']): - if channel_pb2.Channel.Role.Name(c['role'] )!= "DISABLED": - print(f" Index {idx}: {channel_pb2.Channel.Role.Name(c['role'])} psk={pskToString(c['psk'])} {json.dumps(c['settings'])}") - print("") - publicURL = data['publicURL'] - print(f"\nPrimary channel URL: {publicURL}") - adminURL = data['adminURL'] - if adminURL != publicURL: - print(f"Complete URL (includes all channels): {adminURL}") + if dx := data.get('Preferences', None) is not None: + print(f"Preferences: {json.dumps(dx, indent=2)}") + + if dx := data.get('Module preferences', None) is not None: + print(f"Module preferences: {json.dumps(dx, indent=2)}") + + if dx := data.get('Channels', None) is not None: + print("Channels:") + for idx, c in enumerate(dx): + if channel_pb2.Channel.Role.Name(c['role']) != "DISABLED": + print(f" Index {idx}: {channel_pb2.Channel.Role.Name(c['role'])} psk={pskToString(c['psk'])} {json.dumps(c['settings'])}") + print("") + publicURL = data['publicURL'] + print(f"\nPrimary channel URL: {publicURL}") + adminURL = data['adminURL'] + if adminURL != publicURL: + print(f"Complete URL (includes all channels): {adminURL}") diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 251de98f..7dc0efad 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -217,10 +217,12 @@ def test_main_info(capsys, caplog): iface = MagicMock(autospec=SerialInterface) - def mock_showInfo(): - print("inside mocked showInfo") + def mock_getInfo(): + print("inside mocked getInfo") + # return minimum data structure + return {'Owner': [None, None]} - iface.showInfo.side_effect = mock_showInfo + iface.getInfo.side_effect = mock_getInfo with caplog.at_level(logging.DEBUG): with patch( "meshtastic.serial_interface.SerialInterface", return_value=iface @@ -228,7 +230,7 @@ def mock_showInfo(): main() out, err = capsys.readouterr() assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked showInfo", out, re.MULTILINE) + assert re.search(r"inside mocked getInfo", out, re.MULTILINE) assert err == "" mo.assert_called() @@ -268,15 +270,17 @@ def test_main_info_with_tcp_interface(capsys): iface = MagicMock(autospec=TCPInterface) - def mock_showInfo(): - print("inside mocked showInfo") + def mock_getInfo(): + print("inside mocked getInfo") + # return minimum data structure + return {'Owner': [None, None]} - iface.showInfo.side_effect = mock_showInfo + iface.getInfo.side_effect = mock_getInfo with patch("meshtastic.tcp_interface.TCPInterface", return_value=iface) as mo: main() out, err = capsys.readouterr() assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked showInfo", out, re.MULTILINE) + assert re.search(r"inside mocked getInfo", out, re.MULTILINE) assert err == "" mo.assert_called() @@ -290,10 +294,12 @@ def test_main_no_proto(capsys): iface = MagicMock(autospec=SerialInterface) - def mock_showInfo(): - print("inside mocked showInfo") + def mock_getInfo(): + print("inside mocked getInfo") + # return minimum data structure + return {'Owner': [None, None]} - iface.showInfo.side_effect = mock_showInfo + iface.getInfo.side_effect = mock_getInfo # Override the time.sleep so there is no loop def my_sleep(amount): @@ -308,7 +314,7 @@ def my_sleep(amount): assert pytest_wrapped_e.value.code == 0 out, err = capsys.readouterr() assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked showInfo", out, re.MULTILINE) + assert re.search(r"inside mocked getInfo", out, re.MULTILINE) assert err == "" @@ -321,15 +327,17 @@ def test_main_info_with_seriallog_stdout(capsys): iface = MagicMock(autospec=SerialInterface) - def mock_showInfo(): - print("inside mocked showInfo") + def mock_getInfo(): + print("inside mocked getInfo") + # return minimum data structure + return {'Owner': [None, None]} - iface.showInfo.side_effect = mock_showInfo + iface.getInfo.side_effect = mock_getInfo with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: main() out, err = capsys.readouterr() assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked showInfo", out, re.MULTILINE) + assert re.search(r"inside mocked getInfo", out, re.MULTILINE) assert err == "" mo.assert_called() @@ -343,15 +351,17 @@ def test_main_info_with_seriallog_output_txt(capsys): iface = MagicMock(autospec=SerialInterface) - def mock_showInfo(): - print("inside mocked showInfo") + def mock_getInfo(): + print("inside mocked getInfo") + # return minimum data structure + return {'Owner': [None, None]} - iface.showInfo.side_effect = mock_showInfo + iface.getInfo.side_effect = mock_getInfo with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: main() out, err = capsys.readouterr() assert re.search(r"Connected to radio", out, re.MULTILINE) - assert re.search(r"inside mocked showInfo", out, re.MULTILINE) + assert re.search(r"inside mocked getInfo", out, re.MULTILINE) assert err == "" mo.assert_called() # do some cleanup @@ -408,8 +418,8 @@ def test_main_nodes(capsys): iface = MagicMock(autospec=SerialInterface) - def mock_showNodes(includeSelf, showFields): - print(f"inside mocked showNodes: {includeSelf} {showFields}") + def mock_showNodes(includeSelf, showFields, printFmt): + print(f"inside mocked showNodes: {includeSelf} {showFields} {printFmt}") iface.showNodes.side_effect = mock_showNodes with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: diff --git a/meshtastic/tests/test_mesh_interface.py b/meshtastic/tests/test_mesh_interface.py index 8d53628f..22ab7d7c 100644 --- a/meshtastic/tests/test_mesh_interface.py +++ b/meshtastic/tests/test_mesh_interface.py @@ -56,19 +56,29 @@ def test_MeshInterface(capsys): # Also get some coverage of the structured logging/power meter stuff by turning it on as well log_set = LogSet(iface, None, SimPowerSupply()) - iface.showInfo() - iface.localNode.showInfo() - iface.showNodes() - iface.sendText("hello") + ifData = iface.getInfo() + nodeData = iface.localNode.getInfo() + # iface.showNodes() + # iface.sendText("hello") iface.close() log_set.close() - out, err = capsys.readouterr() - assert re.search(r"Owner: None \(None\)", out, re.MULTILINE) - assert re.search(r"Nodes", out, re.MULTILINE) - assert re.search(r"Preferences", out, re.MULTILINE) - assert re.search(r"Channels", out, re.MULTILINE) - assert re.search(r"Primary channel URL", out, re.MULTILINE) - assert err == "" + + # test interface data + assert 'Owner' in ifData.keys() + assert len(ifData.get('Owner', [])) == 2 + assert ifData['Owner'][0] is None + assert ifData['Owner'][1] is None + assert 'My Info' in ifData.keys() + assert 'Metadata' in ifData.keys() + assert 'Nodes' in ifData.keys() + assert len(ifData['Nodes']) > 0 + + # test node data + assert 'Preferences' in nodeData.keys() + assert 'Module preferences' in nodeData.keys() + assert 'Channels' in nodeData.keys() + assert 'publicURL' in nodeData.keys() + assert 'adminURL' in nodeData.keys() @pytest.mark.unit diff --git a/meshtastic/tests/test_node.py b/meshtastic/tests/test_node.py index c5cb6b3f..c5c84f15 100644 --- a/meshtastic/tests/test_node.py +++ b/meshtastic/tests/test_node.py @@ -31,14 +31,12 @@ def test_node(capsys): anode.localConfig = lc lc.lora.CopyFrom(config_pb2.Config.LoRaConfig()) anode.moduleConfig = localonly_pb2.LocalModuleConfig() - anode.showInfo() - out, err = capsys.readouterr() - assert re.search(r'Preferences', out) - assert re.search(r'Module preferences', out) - assert re.search(r'Channels', out) - assert re.search(r'Primary channel URL', out) - assert not re.search(r'remote node', out) - assert err == '' + nodeData = anode.getInfo() + assert 'Preferences' in nodeData.keys() + assert 'Module preferences' in nodeData.keys() + assert 'Channels' in nodeData.keys() + assert 'publicURL' in nodeData.keys() + assert 'adminURL' in nodeData.keys() # TODO # @pytest.mark.unit diff --git a/meshtastic/tests/test_serial_interface.py b/meshtastic/tests/test_serial_interface.py index cb5b3e23..d1295fbb 100644 --- a/meshtastic/tests/test_serial_interface.py +++ b/meshtastic/tests/test_serial_interface.py @@ -22,8 +22,8 @@ def test_SerialInterface_single_port( """Test that we can instantiate a SerialInterface with a single port""" iface = SerialInterface(noProto=True) iface.localNode.localConfig.lora.CopyFrom(config_pb2.Config.LoRaConfig()) - iface.showInfo() - iface.localNode.showInfo() + ifData = iface.getInfo() + nodeData = iface.localNode.getInfo() iface.close() mocked_findPorts.assert_called() mocked_serial.assert_called() @@ -34,12 +34,22 @@ def test_SerialInterface_single_port( mock_hupcl.assert_called() mock_sleep.assert_called() - out, err = capsys.readouterr() - assert re.search(r"Nodes in mesh", out, re.MULTILINE) - assert re.search(r"Preferences", out, re.MULTILINE) - assert re.search(r"Channels", out, re.MULTILINE) - assert re.search(r"Primary channel", out, re.MULTILINE) - assert err == "" + + # test interface data + assert 'Owner' in ifData.keys() + assert len(ifData.get('Owner', [])) == 2 + assert ifData['Owner'][0] is None + assert ifData['Owner'][1] is None + assert 'My Info' in ifData.keys() + assert 'Metadata' in ifData.keys() + assert 'Nodes' in ifData.keys() + + # test node data + assert 'Preferences' in nodeData.keys() + assert 'Module preferences' in nodeData.keys() + assert 'Channels' in nodeData.keys() + assert 'publicURL' in nodeData.keys() + assert 'adminURL' in nodeData.keys() @pytest.mark.unit diff --git a/meshtastic/tests/test_tcp_interface.py b/meshtastic/tests/test_tcp_interface.py index 44e79de5..ad2acb72 100644 --- a/meshtastic/tests/test_tcp_interface.py +++ b/meshtastic/tests/test_tcp_interface.py @@ -16,18 +16,28 @@ def test_TCPInterface(capsys): iface = TCPInterface(hostname="localhost", noProto=True) iface.localNode.localConfig.lora.CopyFrom(config_pb2.Config.LoRaConfig()) iface.myConnect() - iface.showInfo() - iface.localNode.showInfo() - out, err = capsys.readouterr() - assert re.search(r"Owner: None \(None\)", out, re.MULTILINE) - assert re.search(r"Nodes", out, re.MULTILINE) - assert re.search(r"Preferences", out, re.MULTILINE) - assert re.search(r"Channels", out, re.MULTILINE) - assert re.search(r"Primary channel URL", out, re.MULTILINE) - assert err == "" + ifData = iface.getInfo() + nodeData = iface.localNode.getInfo() + assert mock_socket.called iface.close() + # test interface data + assert 'Owner' in ifData.keys() + assert len(ifData.get('Owner', [])) == 2 + assert ifData['Owner'][0] is None + assert ifData['Owner'][1] is None + assert 'My Info' in ifData.keys() + assert 'Metadata' in ifData.keys() + assert 'Nodes' in ifData.keys() + + # test node data + assert 'Preferences' in nodeData.keys() + assert 'Module preferences' in nodeData.keys() + assert 'Channels' in nodeData.keys() + assert 'publicURL' in nodeData.keys() + assert 'adminURL' in nodeData.keys() + @pytest.mark.unit def test_TCPInterface_exception(): From c0875aef7dd527eb0e1cf3ce00e4d71a8653d9dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Sat, 29 Nov 2025 11:46:37 +0100 Subject: [PATCH 4/5] create unit tests for formatter.py --- meshtastic/formatter.py | 55 +++-- meshtastic/node.py | 2 +- .../tests/formatter-test-input/cfg1.json | 197 ++++++++++++++++++ .../formatter-test-input/cfg2-OwnerOnly.json | 12 ++ .../formatter-test-input/cfg3-NoNodes.json | 166 +++++++++++++++ .../formatter-test-input/cfg4-NoPrefs.json | 120 +++++++++++ .../formatter-test-input/cfg5-NoChannels.json | 25 +++ meshtastic/tests/test_formatter.py | 147 +++++++++++++ 8 files changed, 703 insertions(+), 21 deletions(-) create mode 100644 meshtastic/tests/formatter-test-input/cfg1.json create mode 100644 meshtastic/tests/formatter-test-input/cfg2-OwnerOnly.json create mode 100644 meshtastic/tests/formatter-test-input/cfg3-NoNodes.json create mode 100644 meshtastic/tests/formatter-test-input/cfg4-NoPrefs.json create mode 100644 meshtastic/tests/formatter-test-input/cfg5-NoChannels.json create mode 100644 meshtastic/tests/test_formatter.py diff --git a/meshtastic/formatter.py b/meshtastic/formatter.py index e55c3379..1b531103 100644 --- a/meshtastic/formatter.py +++ b/meshtastic/formatter.py @@ -5,43 +5,57 @@ """Defines the formatting of outputs using factories""" -class FormatterFactory(): +class FormatterFactory: """Factory of formatters""" def __init__(self): - self.formatters = { + self.infoFormatters = { "json": FormatAsJson, "default": FormatAsText } - def getFormatter(self, formatSpec: str): + def getInfoFormatter(self, formatSpec: str = 'default'): """returns the formatter for info data. If no valid formatter is found, default to text""" - return self.formatters.get(formatSpec.lower(), self.formatters["default"]) + return self.infoFormatters.get(formatSpec.lower(), self.infoFormatters["default"]) -class InfoFormatter(): +class InfoFormatter: """responsible to format info data""" def format(self, data: dict, formatSpec: str | None = None) -> str: """returns formatted string according to formatSpec for info data""" if not formatSpec: formatSpec = 'default' - formatter = FormatterFactory().getFormatter(formatSpec) + formatter = FormatterFactory().getInfoFormatter(formatSpec) return formatter().formatInfo(data) -class FormatAsJson(): +class AbstractFormatter: + """Abstract base class for all derived formatters""" + @property + def getType(self) -> str: + return type(self).__name__ + + def formatInfo(self, data: dict): + """interface definition for formatting info data + Need to be implemented in the derived class""" + raise NotImplementedError + + +class FormatAsJson(AbstractFormatter): """responsible to return the data as JSON string""" def formatInfo(self, data: dict) -> str: """Info as JSON""" # Remove the bytes entry of PSK before serialization of JSON - for c in data['Channels']: - del c['psk'] + if 'Channels' in data: + for c in data['Channels']: + if '__psk__' in c: + del c['__psk__'] jsonData = json.dumps(data, indent=2) print(jsonData) return jsonData -class FormatAsText(): +class FormatAsText(AbstractFormatter): """responsible to print the data. No string return""" def formatInfo(self, data: dict) -> str: """Info printed as plain text""" @@ -62,11 +76,11 @@ def showMeshInfo(self, data: dict): owner = f"Owner: {data['Owner'][0]}({data['Owner'][1]})" myinfo = "" - if dx := data.get('My Info', None) is not None: + if (dx := data.get('My Info', None)) is not None: myinfo = f"My info: {json.dumps(dx)}" metadata = "" - if dx := data.get('Metadata', None) is not None: + if (dx := data.get('Metadata', None)) is not None: metadata = f"Metadata: {json.dumps(dx)}" mesh = f"\nNodes in mesh:{json.dumps(data.get('Nodes', {}), indent=2)}" @@ -76,20 +90,21 @@ def showMeshInfo(self, data: dict): def showNodeInfo(self, data: dict): """Show human-readable description of our node""" - if dx := data.get('Preferences', None) is not None: + if (dx := data.get('Preferences', None)) is not None: print(f"Preferences: {json.dumps(dx, indent=2)}") - if dx := data.get('Module preferences', None) is not None: + if (dx := data.get('Module preferences', None)) is not None: print(f"Module preferences: {json.dumps(dx, indent=2)}") - if dx := data.get('Channels', None) is not None: + if (dx := data.get('Channels', None)) is not None: print("Channels:") for idx, c in enumerate(dx): if channel_pb2.Channel.Role.Name(c['role']) != "DISABLED": - print(f" Index {idx}: {channel_pb2.Channel.Role.Name(c['role'])} psk={pskToString(c['psk'])} {json.dumps(c['settings'])}") + print(f" Index {idx}: {channel_pb2.Channel.Role.Name(c['role'])} psk={pskToString(c['__psk__'])} {json.dumps(c['settings'])}") print("") - publicURL = data['publicURL'] - print(f"\nPrimary channel URL: {publicURL}") - adminURL = data['adminURL'] - if adminURL != publicURL: + publicURL = data.get('publicURL', None) + if publicURL: + print(f"\nPrimary channel URL: {publicURL}") + adminURL = data.get('adminURL', None) + if adminURL and adminURL != publicURL: print(f"Complete URL (includes all channels): {adminURL}") diff --git a/meshtastic/node.py b/meshtastic/node.py index 5f1e6fff..97581a1e 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -84,7 +84,7 @@ def getChannelInfo(self) -> dict: chanConfig = {} if self.channels: logger.debug(f"self.channels:{self.channels}") - chanConfig = [{"role": c.role, "psk": c.settings.psk, "settings": MessageToDict(c.settings, always_print_fields_with_no_presence=True)} for c in self.channels] + chanConfig = [{"role": c.role, "__psk__": c.settings.psk, "settings": MessageToDict(c.settings, always_print_fields_with_no_presence=True)} for c in self.channels] publicURL = self.getURL(includeAll=False) adminURL = self.getURL(includeAll=True) return {"Channels": chanConfig, "publicURL": publicURL, "adminURL": adminURL} diff --git a/meshtastic/tests/formatter-test-input/cfg1.json b/meshtastic/tests/formatter-test-input/cfg1.json new file mode 100644 index 00000000..309240ef --- /dev/null +++ b/meshtastic/tests/formatter-test-input/cfg1.json @@ -0,0 +1,197 @@ +{ + "Owner": [ + "Meshtastic Test", + "MT" + ], + "My Info": { + "myNodeNum": 1234, + "minAppVersion": 30200, + "deviceId": "S==", + "pioEnv": "rak4631" + }, + "Metadata": { + "firmwareVersion": "2.6.11.60ec05e", + "deviceStateVersion": 24, + "canShutdown": true, + "hasBluetooth": true, + "hasEthernet": true, + "positionFlags": 811, + "hwModel": "RAK4631", + "hasPKC": true, + "excludedModules": 4480 + }, + "Nodes": { + "!11223344": { + "num": 381, + "user": { + "id": "!11223344", + "longName": "Meshtastic Test", + "shortName": "MT", + "macaddr": "11:22:33:44:55:66", + "hwModel": "RAK4631", + "publicKey": "he!", + "isUnmessagable": true + }, + "deviceMetrics": { + "batteryLevel": 101, + "voltage": 4.151, + "channelUtilization": 0.0, + "airUtilTx": 0.16225, + "uptimeSeconds": 192342 + }, + "position": { + "latitudeI": 470000000, + "longitudeI": 70000000, + "altitude": 2000, + "time": 1764344445, + "latitude": 47.0000000, + "longitude": 7.0000000 + }, + "lastHeard": 1764344445, + "isFavorite": true + } + }, + "Preferences": { + "position": { + "positionBroadcastSecs": 259200, + "fixedPosition": true, + "gpsUpdateInterval": 3600, + "gpsAttemptTime": 900, + "positionFlags": 811, + "broadcastSmartMinimumDistance": 100, + "broadcastSmartMinimumIntervalSecs": 30, + "gpsMode": "ENABLED" + }, + "lora": { + "usePreset": true, + "region": "EU_868", + "hopLimit": 3, + "txEnabled": true, + "txPower": 27, + "sx126xRxBoostedGain": true, + "ignoreMqtt": true + } + }, + "Module preferences": { + "mqtt": { + "address": "mqtt.meshtastic.org", + "username": "meshdev", + "password": "large4cats", + "encryptionEnabled": true, + "root": "msh/EU_868", + "mapReportSettings": { + "publishIntervalSecs": 3600 + } + }, + "cannedMessage": {}, + "audio": {}, + "remoteHardware": {}, + "neighborInfo": {}, + "ambientLighting": { + "current": 10, + "red": 219, + "green": 138, + "blue": 117 + }, + "detectionSensor": { + }, + "paxcounter": {} + }, + "Channels": [ + { + "role": 1, + "__psk__": "01", + "settings": { + "psk": "AQ==", + "moduleSettings": { + "positionPrecision": 13, + "isMuted": false + }, + "channelNum": 0, + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + } + ], + "publicURL": "https://meshtastic.org/e/#CgcSAQE6AggNEg8IATgDQANIAVAbaAHABgE", + "adminURL": "https://meshtastic.org/e/#CgcSAQE6AggNEg8IATgDQANIAVAbaAHABgE" +} diff --git a/meshtastic/tests/formatter-test-input/cfg2-OwnerOnly.json b/meshtastic/tests/formatter-test-input/cfg2-OwnerOnly.json new file mode 100644 index 00000000..4c557612 --- /dev/null +++ b/meshtastic/tests/formatter-test-input/cfg2-OwnerOnly.json @@ -0,0 +1,12 @@ +{ + "Owner": [ + "Meshtastic Test", + "MT" + ], + "My Info": {}, + "Metadata": {}, + "Nodes": {}, + "Preferences": {}, + "Module preferences": {}, + "Channels": [] +} diff --git a/meshtastic/tests/formatter-test-input/cfg3-NoNodes.json b/meshtastic/tests/formatter-test-input/cfg3-NoNodes.json new file mode 100644 index 00000000..6a644be5 --- /dev/null +++ b/meshtastic/tests/formatter-test-input/cfg3-NoNodes.json @@ -0,0 +1,166 @@ +{ + "Owner": [ + "Meshtastic Test", + "MT" + ], + "My Info": { + "myNodeNum": 1234, + "minAppVersion": 30200, + "deviceId": "S==", + "pioEnv": "rak4631" + }, + "Metadata": { + "firmwareVersion": "2.6.11.60ec05e", + "deviceStateVersion": 24, + "canShutdown": true, + "hasBluetooth": true, + "hasEthernet": true, + "positionFlags": 811, + "hwModel": "RAK4631", + "hasPKC": true, + "excludedModules": 4480 + }, + "Preferences": { + "position": { + "positionBroadcastSecs": 259200, + "fixedPosition": true, + "gpsUpdateInterval": 3600, + "gpsAttemptTime": 900, + "positionFlags": 811, + "broadcastSmartMinimumDistance": 100, + "broadcastSmartMinimumIntervalSecs": 30, + "gpsMode": "ENABLED" + }, + "lora": { + "usePreset": true, + "region": "EU_868", + "hopLimit": 3, + "txEnabled": true, + "txPower": 27, + "sx126xRxBoostedGain": true, + "ignoreMqtt": true + } + }, + "Module preferences": { + "mqtt": { + "address": "mqtt.meshtastic.org", + "username": "meshdev", + "password": "large4cats", + "encryptionEnabled": true, + "root": "msh/EU_868", + "mapReportSettings": { + "publishIntervalSecs": 3600 + } + }, + "cannedMessage": {}, + "audio": {}, + "remoteHardware": {}, + "neighborInfo": {}, + "ambientLighting": { + "current": 10, + "red": 219, + "green": 138, + "blue": 117 + }, + "detectionSensor": { + }, + "paxcounter": {} + }, + "Channels": [ + { + "role": 1, + "__psk__": "01", + "settings": { + "psk": "AQ==", + "moduleSettings": { + "positionPrecision": 13, + "isMuted": false + }, + "channelNum": 0, + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + } + ], + "publicURL": "https://meshtastic.org/e/#CgcSAQE6AggNEg8IATgDQANIAVAbaAHABgE", + "adminURL": "https://meshtastic.org/e/#CgcSAQE6AggNEg8IATgDQANIAVAbaAHABgE" +} diff --git a/meshtastic/tests/formatter-test-input/cfg4-NoPrefs.json b/meshtastic/tests/formatter-test-input/cfg4-NoPrefs.json new file mode 100644 index 00000000..bb478197 --- /dev/null +++ b/meshtastic/tests/formatter-test-input/cfg4-NoPrefs.json @@ -0,0 +1,120 @@ +{ + "Owner": [ + "Meshtastic Test", + "MT" + ], + "My Info": { + "myNodeNum": 1234, + "minAppVersion": 30200, + "deviceId": "S==", + "pioEnv": "rak4631" + }, + "Metadata": { + "firmwareVersion": "2.6.11.60ec05e", + "deviceStateVersion": 24, + "canShutdown": true, + "hasBluetooth": true, + "hasEthernet": true, + "positionFlags": 811, + "hwModel": "RAK4631", + "hasPKC": true, + "excludedModules": 4480 + }, + "Channels": [ + { + "role": 1, + "__psk__": "01", + "settings": { + "psk": "AQ==", + "moduleSettings": { + "positionPrecision": 13, + "isMuted": false + }, + "channelNum": 0, + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + }, + { + "role": 0, + "settings": { + "channelNum": 0, + "psk": "", + "name": "", + "id": 0, + "uplinkEnabled": false, + "downlinkEnabled": false + } + } + ], + "publicURL": "https://meshtastic.org/e/#CgcSAQE6AggNEg8IATgDQANIAVAbaAHABgE", + "adminURL": "https://meshtastic.org/e/#CgcSAQE6AggNEg8IATgDQANIAVAbaAHABgE" +} diff --git a/meshtastic/tests/formatter-test-input/cfg5-NoChannels.json b/meshtastic/tests/formatter-test-input/cfg5-NoChannels.json new file mode 100644 index 00000000..e7adf2b1 --- /dev/null +++ b/meshtastic/tests/formatter-test-input/cfg5-NoChannels.json @@ -0,0 +1,25 @@ +{ + "Owner": [ + "Meshtastic Test", + "MT" + ], + "My Info": { + "myNodeNum": 1234, + "minAppVersion": 30200, + "deviceId": "S==", + "pioEnv": "rak4631" + }, + "Metadata": { + "firmwareVersion": "2.6.11.60ec05e", + "deviceStateVersion": 24, + "canShutdown": true, + "hasBluetooth": true, + "hasEthernet": true, + "positionFlags": 811, + "hwModel": "RAK4631", + "hasPKC": true, + "excludedModules": 4480 + }, + "publicURL": "https://meshtastic.org/e/#CgcSAQE6AggNEg8IATgDQANIAVAbaAHABgE", + "adminURL": "https://meshtastic.org/e/#CgcSAQE6AggNEg8IATgDQANIAVAbaAHABgE" +} diff --git a/meshtastic/tests/test_formatter.py b/meshtastic/tests/test_formatter.py new file mode 100644 index 00000000..64d3c611 --- /dev/null +++ b/meshtastic/tests/test_formatter.py @@ -0,0 +1,147 @@ +"""Meshtastic unit tests for formatter.py""" + +import json +from pathlib import Path +import re +from unittest.mock import MagicMock, patch + +import pytest + +import meshtastic +import meshtastic.formatter +from meshtastic.formatter import FormatterFactory, AbstractFormatter, InfoFormatter + + +# from ..formatter import FormatterFactory, FormatAsText + +def pskBytes(d): + """Implement a hook to decode psk to byte as needed for the test""" + if '__psk__' in d: + d['__psk__'] = bytearray.fromhex(d['__psk__']) + return d + + +@pytest.mark.unit +def test_factory(): + ff = meshtastic.formatter.FormatterFactory() + assert len(ff.infoFormatters) > 0 + + # test for at least default formatter which should be returned when passed an empty string + f = ff.getInfoFormatter('')() + assert f.getType == 'FormatAsText' + + # test correct return when passing various fmt strings + f = ff.getInfoFormatter('json')() + assert f.getType == 'FormatAsJson' + f = ff.getInfoFormatter('JsOn')() + assert f.getType == 'FormatAsJson' + f = ff.getInfoFormatter('JSON')() + assert f.getType == 'FormatAsJson' + f = ff.getInfoFormatter('txt')() + assert f.getType == 'FormatAsText' + + # test behavior when passing 'None' as fmt + f = ff.getInfoFormatter()() + assert f.getType == 'FormatAsText' + +@pytest.mark.unit +@pytest.mark.parametrize("inFile, expected", [ + ("cfg1.json", { + "len": 4161, + "keys": ["Owner", "My Info", "Metadata", "Nodes", "Preferences", "Module preferences", "Channels", "publicURL"] + }), + ("cfg2-OwnerOnly.json", { + "len": 169, + "keys": ["Owner", "My Info", "Metadata", "Nodes", "Preferences", "Module preferences", "Channels"] + }), + ("cfg3-NoNodes.json", { + "len": 3413, + "keys": ["Owner", "My Info", "Metadata", "Preferences", "Module preferences", "Channels", "publicURL"] + }), + ("cfg4-NoPrefs.json", { + "len": 2354, + "keys": ["Owner", "My Info", "Metadata", "Channels", "publicURL"] + }), + ("cfg5-NoChannels.json", { + "len": 597, + "keys": ["Owner", "My Info", "Metadata", "publicURL"] + }) +]) +def test_jsonFormatter(inFile, expected): + """Load various test files and convert them to json without errors""" + formatter = FormatterFactory().getInfoFormatter('json') + + data = json.loads( + (Path('meshtastic/tests/formatter-test-input') / Path(inFile)).read_text(), + object_hook=pskBytes) + formattedData = formatter().formatInfo(data) + assert len(formattedData) == expected['len'] + + allKeysPresent = True + for k in expected['keys']: + allKeysPresent = allKeysPresent and re.search(k, formattedData) is not None + assert allKeysPresent + +@pytest.mark.unit +@pytest.mark.parametrize("inFile, expected", [ + ("cfg1.json", { + "len": 2390, + "keys": ["Owner", "My info", "Metadata", "Nodes in mesh", "Preferences", "Module preferences", "Channels", "Primary channel URL"] + }), + ("cfg2-OwnerOnly.json", { + "len": 220, + "keys": ["Owner", "My info", "Metadata", "Nodes in mesh", "Preferences", "Module preferences", "Channels"] + }), + ("cfg3-NoNodes.json", { + "len": 1717, + "keys": ["Owner", "My info", "Metadata", "Preferences", "Module preferences", "Channels", "Primary channel URL"] + }), + ("cfg4-NoPrefs.json", { + "len": 754, + "keys": ["Owner", "My info", "Metadata", "Channels", "Primary channel URL"] + }), + ("cfg5-NoChannels.json", { + "len": 461, + "keys": ["Owner", "My info", "Metadata", "Nodes in mesh"] + }) +]) +def test_txtFormatter(capsys, inFile, expected): + """Load various test files and convert them to json without errors""" + formatter = FormatterFactory().getInfoFormatter('') + + data = json.loads( + (Path('meshtastic/tests/formatter-test-input') / Path(inFile)).read_text(), + object_hook=pskBytes) + formattedData = formatter().formatInfo(data) + assert len(formattedData) == 0 + + out, _ = capsys.readouterr() + assert len(out) == expected['len'] + + allKeysPresent = True + for k in expected['keys']: + allKeysPresent = allKeysPresent and re.search(k, out) is not None + assert allKeysPresent + + +@pytest.mark.unit +def test_ExceptionAbstractClass(): + with pytest.raises(NotImplementedError): + AbstractFormatter().formatInfo({}) + + +@pytest.mark.unit +@pytest.mark.parametrize("inCfg, expected", [ + (("cfg1.json", 'json'), {"len": 4162}), + (("cfg1.json", 'txt'), {"len": 2390}), + (("cfg1.json", None), {"len": 2390}) + ]) +def test_InfoFormatter(capsys, inCfg, expected): + inFile = inCfg[0] + data = json.loads( + (Path('meshtastic/tests/formatter-test-input') / Path(inFile)).read_text(), + object_hook=pskBytes) + InfoFormatter().format(data, inCfg[1]) + + out, _ = capsys.readouterr() + assert len(out) == expected['len'] From 66ac40d91c4dcc81bacc44bad6904bef1f48776e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?j=C3=BCrgenRe?= Date: Sat, 29 Nov 2025 12:19:04 +0100 Subject: [PATCH 5/5] changing some keys, so they do not contain blanks any more adding fullSequence test to have a closed data flow from node/interface till formatted output --- meshtastic/formatter.py | 4 +- meshtastic/mesh_interface.py | 4 +- meshtastic/node.py | 2 +- .../tests/formatter-test-input/cfg1.json | 4 +- .../formatter-test-input/cfg2-OwnerOnly.json | 4 +- .../formatter-test-input/cfg3-NoNodes.json | 4 +- .../formatter-test-input/cfg4-NoPrefs.json | 2 +- .../formatter-test-input/cfg5-NoChannels.json | 2 +- meshtastic/tests/test_formatter.py | 89 +++++++++++++++---- meshtastic/tests/test_mesh_interface.py | 4 +- meshtastic/tests/test_node.py | 2 +- meshtastic/tests/test_serial_interface.py | 4 +- meshtastic/tests/test_tcp_interface.py | 4 +- 13 files changed, 92 insertions(+), 37 deletions(-) diff --git a/meshtastic/formatter.py b/meshtastic/formatter.py index 1b531103..017d2a75 100644 --- a/meshtastic/formatter.py +++ b/meshtastic/formatter.py @@ -76,7 +76,7 @@ def showMeshInfo(self, data: dict): owner = f"Owner: {data['Owner'][0]}({data['Owner'][1]})" myinfo = "" - if (dx := data.get('My Info', None)) is not None: + if (dx := data.get('MyInfo', None)) is not None: myinfo = f"My info: {json.dumps(dx)}" metadata = "" @@ -93,7 +93,7 @@ def showNodeInfo(self, data: dict): if (dx := data.get('Preferences', None)) is not None: print(f"Preferences: {json.dumps(dx, indent=2)}") - if (dx := data.get('Module preferences', None)) is not None: + if (dx := data.get('ModulePreferences', None)) is not None: print(f"Module preferences: {json.dumps(dx, indent=2)}") if (dx := data.get('Channels', None)) is not None: diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 3876d32d..1ca2e2a7 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -197,13 +197,13 @@ def getInfo(self) -> dict: """retrieve object data""" objData: dict[str, Any] = { "Owner": [self.getLongName(), self.getShortName()], - "My Info": {}, + "MyInfo": {}, "Metadata": {}, "Nodes": {} } if self.myInfo: - objData["My Info"] = MessageToDict(self.myInfo) + objData["MyInfo"] = MessageToDict(self.myInfo) if self.metadata: objData["Metadata"] = MessageToDict(self.metadata) diff --git a/meshtastic/node.py b/meshtastic/node.py index 97581a1e..c7097e41 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -98,7 +98,7 @@ def getInfo(self) ->dict: if self.moduleConfig: modConfig = MessageToDict(self.moduleConfig) chanConfig = self.getChannelInfo() - info = {"Preferences": locConfig, "Module preferences": modConfig} + info = {"Preferences": locConfig, "ModulePreferences": modConfig} info.update(chanConfig) return info diff --git a/meshtastic/tests/formatter-test-input/cfg1.json b/meshtastic/tests/formatter-test-input/cfg1.json index 309240ef..c70c87fe 100644 --- a/meshtastic/tests/formatter-test-input/cfg1.json +++ b/meshtastic/tests/formatter-test-input/cfg1.json @@ -3,7 +3,7 @@ "Meshtastic Test", "MT" ], - "My Info": { + "MyInfo": { "myNodeNum": 1234, "minAppVersion": 30200, "deviceId": "S==", @@ -72,7 +72,7 @@ "ignoreMqtt": true } }, - "Module preferences": { + "ModulePreferences": { "mqtt": { "address": "mqtt.meshtastic.org", "username": "meshdev", diff --git a/meshtastic/tests/formatter-test-input/cfg2-OwnerOnly.json b/meshtastic/tests/formatter-test-input/cfg2-OwnerOnly.json index 4c557612..a9a9f996 100644 --- a/meshtastic/tests/formatter-test-input/cfg2-OwnerOnly.json +++ b/meshtastic/tests/formatter-test-input/cfg2-OwnerOnly.json @@ -3,10 +3,10 @@ "Meshtastic Test", "MT" ], - "My Info": {}, + "MyInfo": {}, "Metadata": {}, "Nodes": {}, "Preferences": {}, - "Module preferences": {}, + "ModulePreferences": {}, "Channels": [] } diff --git a/meshtastic/tests/formatter-test-input/cfg3-NoNodes.json b/meshtastic/tests/formatter-test-input/cfg3-NoNodes.json index 6a644be5..83bde904 100644 --- a/meshtastic/tests/formatter-test-input/cfg3-NoNodes.json +++ b/meshtastic/tests/formatter-test-input/cfg3-NoNodes.json @@ -3,7 +3,7 @@ "Meshtastic Test", "MT" ], - "My Info": { + "MyInfo": { "myNodeNum": 1234, "minAppVersion": 30200, "deviceId": "S==", @@ -41,7 +41,7 @@ "ignoreMqtt": true } }, - "Module preferences": { + "ModulePreferences": { "mqtt": { "address": "mqtt.meshtastic.org", "username": "meshdev", diff --git a/meshtastic/tests/formatter-test-input/cfg4-NoPrefs.json b/meshtastic/tests/formatter-test-input/cfg4-NoPrefs.json index bb478197..00fd7428 100644 --- a/meshtastic/tests/formatter-test-input/cfg4-NoPrefs.json +++ b/meshtastic/tests/formatter-test-input/cfg4-NoPrefs.json @@ -3,7 +3,7 @@ "Meshtastic Test", "MT" ], - "My Info": { + "MyInfo": { "myNodeNum": 1234, "minAppVersion": 30200, "deviceId": "S==", diff --git a/meshtastic/tests/formatter-test-input/cfg5-NoChannels.json b/meshtastic/tests/formatter-test-input/cfg5-NoChannels.json index e7adf2b1..d14cc8ea 100644 --- a/meshtastic/tests/formatter-test-input/cfg5-NoChannels.json +++ b/meshtastic/tests/formatter-test-input/cfg5-NoChannels.json @@ -3,7 +3,7 @@ "Meshtastic Test", "MT" ], - "My Info": { + "MyInfo": { "myNodeNum": 1234, "minAppVersion": 30200, "deviceId": "S==", diff --git a/meshtastic/tests/test_formatter.py b/meshtastic/tests/test_formatter.py index 64d3c611..e484fd51 100644 --- a/meshtastic/tests/test_formatter.py +++ b/meshtastic/tests/test_formatter.py @@ -3,17 +3,23 @@ import json from pathlib import Path import re -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest import meshtastic -import meshtastic.formatter -from meshtastic.formatter import FormatterFactory, AbstractFormatter, InfoFormatter +# from meshtastic.formatter import FormatterFactory, AbstractFormatter, InfoFormatter +from ..protobuf import config_pb2 +from ..formatter import FormatterFactory, AbstractFormatter, InfoFormatter +from ..mesh_interface import MeshInterface +try: + # Depends upon the powermon group, not installed by default + from ..slog import LogSet + from ..powermon import SimPowerSupply +except ImportError: + pytest.skip("Can't import LogSet or SimPowerSupply", allow_module_level=True) -# from ..formatter import FormatterFactory, FormatAsText - def pskBytes(d): """Implement a hook to decode psk to byte as needed for the test""" if '__psk__' in d: @@ -23,7 +29,7 @@ def pskBytes(d): @pytest.mark.unit def test_factory(): - ff = meshtastic.formatter.FormatterFactory() + ff = FormatterFactory() assert len(ff.infoFormatters) > 0 # test for at least default formatter which should be returned when passed an empty string @@ -47,24 +53,24 @@ def test_factory(): @pytest.mark.unit @pytest.mark.parametrize("inFile, expected", [ ("cfg1.json", { - "len": 4161, - "keys": ["Owner", "My Info", "Metadata", "Nodes", "Preferences", "Module preferences", "Channels", "publicURL"] + "len": 4159, + "keys": ["Owner", "MyInfo", "Metadata", "Nodes", "Preferences", "ModulePreferences", "Channels", "publicURL"] }), ("cfg2-OwnerOnly.json", { - "len": 169, - "keys": ["Owner", "My Info", "Metadata", "Nodes", "Preferences", "Module preferences", "Channels"] + "len": 167, + "keys": ["Owner", "MyInfo", "Metadata", "Nodes", "Preferences", "ModulePreferences", "Channels"] }), ("cfg3-NoNodes.json", { - "len": 3413, - "keys": ["Owner", "My Info", "Metadata", "Preferences", "Module preferences", "Channels", "publicURL"] + "len": 3411, + "keys": ["Owner", "MyInfo", "Metadata", "Preferences", "ModulePreferences", "Channels", "publicURL"] }), ("cfg4-NoPrefs.json", { - "len": 2354, - "keys": ["Owner", "My Info", "Metadata", "Channels", "publicURL"] + "len": 2353, + "keys": ["Owner", "MyInfo", "Metadata", "Channels", "publicURL"] }), ("cfg5-NoChannels.json", { - "len": 597, - "keys": ["Owner", "My Info", "Metadata", "publicURL"] + "len": 596, + "keys": ["Owner", "MyInfo", "Metadata", "publicURL"] }) ]) def test_jsonFormatter(inFile, expected): @@ -132,7 +138,7 @@ def test_ExceptionAbstractClass(): @pytest.mark.unit @pytest.mark.parametrize("inCfg, expected", [ - (("cfg1.json", 'json'), {"len": 4162}), + (("cfg1.json", 'json'), {"len": 4160}), (("cfg1.json", 'txt'), {"len": 2390}), (("cfg1.json", None), {"len": 2390}) ]) @@ -145,3 +151,52 @@ def test_InfoFormatter(capsys, inCfg, expected): out, _ = capsys.readouterr() assert len(out) == expected['len'] + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +@pytest.mark.parametrize("inFmt, expected", [ + ('json', {"len": 556, "keys": ["!9388f81c", "Unknown f81c", "44:17:93:88:f8:1c", "https://meshtastic.org/e/#EgA"]}), + ('txt', {"len": 539, "keys": ["!9388f81c", "Unknown f81c", "44:17:93:88:f8:1c", "https://meshtastic.org/e/#EgA"]}), + (None, {"len": 539, "keys": ["!9388f81c", "Unknown f81c", "44:17:93:88:f8:1c", "https://meshtastic.org/e/#EgA"]}) + ]) +def test_fullSequenceTest(capsys, inFmt, expected): + """Test formatter when exporting data from an instantiated mesh interface + --> close the loop of data""" + iface = MeshInterface(noProto=True) + + NODE_ID = "!9388f81c" + NODE_NUM = 2475227164 + node = { + "num": NODE_NUM, + "user": { + "id": NODE_ID, + "longName": "Unknown f81c", + "shortName": "?1C", + "macaddr": "RBeTiPgc", + "hwModel": "TBEAM", + }, + "position": {}, + "lastHeard": 1640204888, + } + + iface.nodes = {NODE_ID: node} + iface.nodesByNum = {NODE_NUM: node} + + myInfo = MagicMock() + iface.myInfo = myInfo + + iface.localNode.localConfig.lora.CopyFrom(config_pb2.Config.LoRaConfig()) + + # Also get some coverage of the structured logging/power meter stuff by turning it on as well + log_set = LogSet(iface, None, SimPowerSupply()) + + ifData = iface.getInfo() + ifData.update(iface.localNode.getInfo()) + iface.close() + log_set.close() + + InfoFormatter().format(ifData, inFmt) + + out, _ = capsys.readouterr() + print(out) + assert len(out) == expected['len'] diff --git a/meshtastic/tests/test_mesh_interface.py b/meshtastic/tests/test_mesh_interface.py index 22ab7d7c..1efb09c8 100644 --- a/meshtastic/tests/test_mesh_interface.py +++ b/meshtastic/tests/test_mesh_interface.py @@ -68,14 +68,14 @@ def test_MeshInterface(capsys): assert len(ifData.get('Owner', [])) == 2 assert ifData['Owner'][0] is None assert ifData['Owner'][1] is None - assert 'My Info' in ifData.keys() + assert 'MyInfo' in ifData.keys() assert 'Metadata' in ifData.keys() assert 'Nodes' in ifData.keys() assert len(ifData['Nodes']) > 0 # test node data assert 'Preferences' in nodeData.keys() - assert 'Module preferences' in nodeData.keys() + assert 'ModulePreferences' in nodeData.keys() assert 'Channels' in nodeData.keys() assert 'publicURL' in nodeData.keys() assert 'adminURL' in nodeData.keys() diff --git a/meshtastic/tests/test_node.py b/meshtastic/tests/test_node.py index c5c84f15..456a834d 100644 --- a/meshtastic/tests/test_node.py +++ b/meshtastic/tests/test_node.py @@ -33,7 +33,7 @@ def test_node(capsys): anode.moduleConfig = localonly_pb2.LocalModuleConfig() nodeData = anode.getInfo() assert 'Preferences' in nodeData.keys() - assert 'Module preferences' in nodeData.keys() + assert 'ModulePreferences' in nodeData.keys() assert 'Channels' in nodeData.keys() assert 'publicURL' in nodeData.keys() assert 'adminURL' in nodeData.keys() diff --git a/meshtastic/tests/test_serial_interface.py b/meshtastic/tests/test_serial_interface.py index d1295fbb..9764fbea 100644 --- a/meshtastic/tests/test_serial_interface.py +++ b/meshtastic/tests/test_serial_interface.py @@ -40,13 +40,13 @@ def test_SerialInterface_single_port( assert len(ifData.get('Owner', [])) == 2 assert ifData['Owner'][0] is None assert ifData['Owner'][1] is None - assert 'My Info' in ifData.keys() + assert 'MyInfo' in ifData.keys() assert 'Metadata' in ifData.keys() assert 'Nodes' in ifData.keys() # test node data assert 'Preferences' in nodeData.keys() - assert 'Module preferences' in nodeData.keys() + assert 'ModulePreferences' in nodeData.keys() assert 'Channels' in nodeData.keys() assert 'publicURL' in nodeData.keys() assert 'adminURL' in nodeData.keys() diff --git a/meshtastic/tests/test_tcp_interface.py b/meshtastic/tests/test_tcp_interface.py index ad2acb72..71e45d4b 100644 --- a/meshtastic/tests/test_tcp_interface.py +++ b/meshtastic/tests/test_tcp_interface.py @@ -27,13 +27,13 @@ def test_TCPInterface(capsys): assert len(ifData.get('Owner', [])) == 2 assert ifData['Owner'][0] is None assert ifData['Owner'][1] is None - assert 'My Info' in ifData.keys() + assert 'MyInfo' in ifData.keys() assert 'Metadata' in ifData.keys() assert 'Nodes' in ifData.keys() # test node data assert 'Preferences' in nodeData.keys() - assert 'Module preferences' in nodeData.keys() + assert 'ModulePreferences' in nodeData.keys() assert 'Channels' in nodeData.keys() assert 'publicURL' in nodeData.keys() assert 'adminURL' in nodeData.keys()