From 4f6d183ed1d73cd9ced4db5a1fd62814a316b071 Mon Sep 17 00:00:00 2001 From: David Andrzejewski Date: Thu, 8 Jan 2026 18:13:03 -0500 Subject: [PATCH 1/3] Show favorite nodes in --nodes --- meshtastic/mesh_interface.py | 5 +- meshtastic/tests/test_showNodes_favorite.py | 160 ++++++++++++++++++++ 2 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 meshtastic/tests/test_showNodes_favorite.py diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index a3a67b794..d1b5219a3 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -250,6 +250,7 @@ def get_human_readable(name): "channel": "Channel", "lastHeard": "LastHeard", "since": "Since", + "isFavorite": "Fav", } @@ -297,7 +298,7 @@ def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any: showFields = ["N", "user.longName", "user.id", "user.shortName", "user.hwModel", "user.publicKey", "user.role", "position.latitude", "position.longitude", "position.altitude", "deviceMetrics.batteryLevel", "deviceMetrics.channelUtilization", - "deviceMetrics.airUtilTx", "snr", "hopsAway", "channel", "lastHeard", "since"] + "deviceMetrics.airUtilTx", "snr", "hopsAway", "channel", "isFavorite", "lastHeard", "since"] else: # Always at least include the row number. showFields.insert(0, "N") @@ -339,6 +340,8 @@ def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any: formatted_value = "Powered" else: formatted_value = formatFloat(raw_value, 0, "%") + elif field == "isFavorite": + formatted_value = "*" if raw_value else "" elif field == "lastHeard": formatted_value = getLH(raw_value) elif field == "position.latitude": diff --git a/meshtastic/tests/test_showNodes_favorite.py b/meshtastic/tests/test_showNodes_favorite.py new file mode 100644 index 000000000..287fa784b --- /dev/null +++ b/meshtastic/tests/test_showNodes_favorite.py @@ -0,0 +1,160 @@ +"""Meshtastic unit tests for showNodes favorite column feature""" + +import pytest + +from ..mesh_interface import MeshInterface + + +@pytest.fixture +def iface_with_favorite_nodes(): + """Fixture to setup nodes with favorite flags.""" + nodesById = { + "!9388f81c": { + "num": 2475227164, + "user": { + "id": "!9388f81c", + "longName": "Favorite Node", + "shortName": "FAV1", + "macaddr": "RBeTiPgc", + "hwModel": "TBEAM", + }, + "position": {}, + "lastHeard": 1640204888, + "isFavorite": True, + }, + "!12345678": { + "num": 305419896, + "user": { + "id": "!12345678", + "longName": "Regular Node", + "shortName": "REG1", + "macaddr": "ABCDEFGH", + "hwModel": "TLORA_V2", + }, + "position": {}, + "lastHeard": 1640204999, + "isFavorite": False, + }, + } + + nodesByNum = { + 2475227164: { + "num": 2475227164, + "user": { + "id": "!9388f81c", + "longName": "Favorite Node", + "shortName": "FAV1", + "macaddr": "RBeTiPgc", + "hwModel": "TBEAM", + }, + "position": {"time": 1640206266}, + "lastHeard": 1640206266, + "isFavorite": True, + }, + 305419896: { + "num": 305419896, + "user": { + "id": "!12345678", + "longName": "Regular Node", + "shortName": "REG1", + "macaddr": "ABCDEFGH", + "hwModel": "TLORA_V2", + }, + "position": {"time": 1640206200}, + "lastHeard": 1640206200, + "isFavorite": False, + }, + } + + iface = MeshInterface(noProto=True) + iface.nodes = nodesById + iface.nodesByNum = nodesByNum + from unittest.mock import MagicMock + myInfo = MagicMock() + iface.myInfo = myInfo + iface.myInfo.my_node_num = 2475227164 + return iface + + +@pytest.mark.unit +def test_showNodes_favorite_column_header(capsys, iface_with_favorite_nodes): + """Test that 'Fav' column header appears in showNodes output""" + iface = iface_with_favorite_nodes + iface.showNodes() + out, err = capsys.readouterr() + assert "Fav" in out + assert err == "" + + +@pytest.mark.unit +def test_showNodes_favorite_asterisk_display(capsys, iface_with_favorite_nodes): + """Test that favorite nodes show asterisk and non-favorites show empty""" + iface = iface_with_favorite_nodes + iface.showNodes() + out, err = capsys.readouterr() + + # Check that the output contains the "Fav" column + assert "Fav" in out + + # The favorite node should have an asterisk in the output + # We can't easily check the exact table cell, but we can verify + # the asterisk appears somewhere in the output + lines = out.split('\n') + + # Find lines containing our nodes + favorite_line = None + regular_line = None + for line in lines: + if "Favorite Node" in line or "FAV1" in line: + favorite_line = line + if "Regular Node" in line or "REG1" in line: + regular_line = line + + # Basic sanity check - if we found the lines, they should be present + assert favorite_line is not None or regular_line is not None + assert err == "" + + +@pytest.mark.unit +def test_showNodes_favorite_field_formatting(): + """Test the formatting logic for isFavorite field""" + # Test favorite node + raw_value = True + formatted_value = "*" if raw_value else "" + assert formatted_value == "*" + + # Test non-favorite node + raw_value = False + formatted_value = "*" if raw_value else "" + assert formatted_value == "" + + # Test None/missing value + raw_value = None + formatted_value = "*" if raw_value else "" + assert formatted_value == "" + + +@pytest.mark.unit +def test_showNodes_with_custom_fields_including_favorite(capsys, iface_with_favorite_nodes): + """Test that isFavorite can be specified in custom showFields""" + iface = iface_with_favorite_nodes + custom_fields = ["user.longName", "isFavorite"] + iface.showNodes(showFields=custom_fields) + out, err = capsys.readouterr() + + # Should still show the Fav column when explicitly requested + assert "Fav" in out + assert err == "" + + +@pytest.mark.unit +def test_showNodes_default_fields_includes_favorite(iface_with_favorite_nodes): + """Test that isFavorite is included in default fields""" + iface = iface_with_favorite_nodes + + # Call showNodes which uses default fields + result = iface.showNodes() + + # The result should contain the formatted table as a string + assert "Fav" in result + From 683dd23d63f11eb48673d1385012feebea145cad Mon Sep 17 00:00:00 2001 From: David Andrzejewski Date: Thu, 8 Jan 2026 18:53:55 -0500 Subject: [PATCH 2/3] Fix a few pylint things. --- meshtastic/tests/test_showNodes_favorite.py | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/meshtastic/tests/test_showNodes_favorite.py b/meshtastic/tests/test_showNodes_favorite.py index 287fa784b..dddacfa33 100644 --- a/meshtastic/tests/test_showNodes_favorite.py +++ b/meshtastic/tests/test_showNodes_favorite.py @@ -1,12 +1,14 @@ """Meshtastic unit tests for showNodes favorite column feature""" +from unittest.mock import MagicMock + import pytest from ..mesh_interface import MeshInterface @pytest.fixture -def iface_with_favorite_nodes(): +def _iface_with_favorite_nodes(): """Fixture to setup nodes with favorite flags.""" nodesById = { "!9388f81c": { @@ -69,7 +71,6 @@ def iface_with_favorite_nodes(): iface = MeshInterface(noProto=True) iface.nodes = nodesById iface.nodesByNum = nodesByNum - from unittest.mock import MagicMock myInfo = MagicMock() iface.myInfo = myInfo iface.myInfo.my_node_num = 2475227164 @@ -77,9 +78,9 @@ def iface_with_favorite_nodes(): @pytest.mark.unit -def test_showNodes_favorite_column_header(capsys, iface_with_favorite_nodes): +def test_showNodes_favorite_column_header(capsys, _iface_with_favorite_nodes): """Test that 'Fav' column header appears in showNodes output""" - iface = iface_with_favorite_nodes + iface = _iface_with_favorite_nodes iface.showNodes() out, err = capsys.readouterr() assert "Fav" in out @@ -87,9 +88,9 @@ def test_showNodes_favorite_column_header(capsys, iface_with_favorite_nodes): @pytest.mark.unit -def test_showNodes_favorite_asterisk_display(capsys, iface_with_favorite_nodes): +def test_showNodes_favorite_asterisk_display(capsys, _iface_with_favorite_nodes): """Test that favorite nodes show asterisk and non-favorites show empty""" - iface = iface_with_favorite_nodes + iface = _iface_with_favorite_nodes iface.showNodes() out, err = capsys.readouterr() @@ -135,9 +136,9 @@ def test_showNodes_favorite_field_formatting(): @pytest.mark.unit -def test_showNodes_with_custom_fields_including_favorite(capsys, iface_with_favorite_nodes): +def test_showNodes_with_custom_fields_including_favorite(capsys, _iface_with_favorite_nodes): """Test that isFavorite can be specified in custom showFields""" - iface = iface_with_favorite_nodes + iface = _iface_with_favorite_nodes custom_fields = ["user.longName", "isFavorite"] iface.showNodes(showFields=custom_fields) out, err = capsys.readouterr() @@ -148,13 +149,12 @@ def test_showNodes_with_custom_fields_including_favorite(capsys, iface_with_favo @pytest.mark.unit -def test_showNodes_default_fields_includes_favorite(iface_with_favorite_nodes): +def test_showNodes_default_fields_includes_favorite(_iface_with_favorite_nodes): """Test that isFavorite is included in default fields""" - iface = iface_with_favorite_nodes + iface = _iface_with_favorite_nodes # Call showNodes which uses default fields result = iface.showNodes() # The result should contain the formatted table as a string assert "Fav" in result - From c3c5ce64ddb575756d088876dcb598e4728152a1 Mon Sep 17 00:00:00 2001 From: David Andrzejewski Date: Thu, 8 Jan 2026 19:08:18 -0500 Subject: [PATCH 3/3] Copilot had a few suggestions on code review, implemented them. --- meshtastic/tests/test_showNodes_favorite.py | 77 ++++++++++++++++++--- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/meshtastic/tests/test_showNodes_favorite.py b/meshtastic/tests/test_showNodes_favorite.py index dddacfa33..1ddd47b25 100644 --- a/meshtastic/tests/test_showNodes_favorite.py +++ b/meshtastic/tests/test_showNodes_favorite.py @@ -1,4 +1,4 @@ -"""Meshtastic unit tests for showNodes favorite column feature""" +"""Meshtastic unit tests for showNodes favorite column feature""" from unittest.mock import MagicMock @@ -37,6 +37,19 @@ def _iface_with_favorite_nodes(): "lastHeard": 1640204999, "isFavorite": False, }, + "!abcdef00": { + "num": 2882400000, + "user": { + "id": "!abcdef00", + "longName": "Legacy Node", + "shortName": "LEG1", + "macaddr": "XYZABC00", + "hwModel": "HELTEC_V3", + }, + "position": {}, + "lastHeard": 1640205000, + # Note: No isFavorite field - testing backward compatibility + }, } nodesByNum = { @@ -66,6 +79,19 @@ def _iface_with_favorite_nodes(): "lastHeard": 1640206200, "isFavorite": False, }, + 2882400000: { + "num": 2882400000, + "user": { + "id": "!abcdef00", + "longName": "Legacy Node", + "shortName": "LEG1", + "macaddr": "XYZABC00", + "hwModel": "HELTEC_V3", + }, + "position": {"time": 1640206100}, + "lastHeard": 1640206100, + # Note: No isFavorite field - testing backward compatibility + }, } iface = MeshInterface(noProto=True) @@ -97,22 +123,33 @@ def test_showNodes_favorite_asterisk_display(capsys, _iface_with_favorite_nodes) # Check that the output contains the "Fav" column assert "Fav" in out - # The favorite node should have an asterisk in the output - # We can't easily check the exact table cell, but we can verify - # the asterisk appears somewhere in the output - lines = out.split('\n') - # Find lines containing our nodes + lines = out.split('\n') favorite_line = None regular_line = None + legacy_line = None for line in lines: if "Favorite Node" in line or "FAV1" in line: favorite_line = line if "Regular Node" in line or "REG1" in line: regular_line = line + if "Legacy Node" in line or "LEG1" in line: + legacy_line = line + + # Verify all nodes are present in the output + assert favorite_line is not None, "Favorite node should be in output" + assert regular_line is not None, "Regular node should be in output" + assert legacy_line is not None, "Legacy node should be in output" + + # Verify the favorite node has an asterisk in its row + assert "*" in favorite_line, "Favorite node should have an asterisk" + + # Verify the regular (non-favorite) node does NOT have an asterisk + assert regular_line.count("*") == 0, "Non-favorite node should not have an asterisk" + + # Verify the legacy node (without isFavorite field) does NOT have an asterisk + assert legacy_line.count("*") == 0, "Legacy node without isFavorite field should not have an asterisk" - # Basic sanity check - if we found the lines, they should be present - assert favorite_line is not None or regular_line is not None assert err == "" @@ -158,3 +195,27 @@ def test_showNodes_default_fields_includes_favorite(_iface_with_favorite_nodes): # The result should contain the formatted table as a string assert "Fav" in result + + +@pytest.mark.unit +def test_showNodes_backward_compatibility_missing_field(capsys, _iface_with_favorite_nodes): + """Test that nodes without isFavorite field are handled gracefully""" + iface = _iface_with_favorite_nodes + iface.showNodes() + out, err = capsys.readouterr() + + # Find the legacy node line + lines = out.split('\n') + legacy_line = None + for line in lines: + if "Legacy Node" in line or "LEG1" in line: + legacy_line = line + break + + # Verify the legacy node appears in output + assert legacy_line is not None, "Legacy node without isFavorite field should appear in output" + + # Verify it doesn't have an asterisk (should be treated as non-favorite) + assert legacy_line.count("*") == 0, "Legacy node should not have asterisk (treated as non-favorite)" + + assert err == ""