Skip to content

Commit a73f432

Browse files
authored
Merge pull request #888 from dandrzejewski/node-favorites
Show favorite nodes in --nodes
2 parents 9a72e36 + c3c5ce6 commit a73f432

File tree

2 files changed

+225
-1
lines changed

2 files changed

+225
-1
lines changed

meshtastic/mesh_interface.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ def get_human_readable(name):
253253
"channel": "Channel",
254254
"lastHeard": "LastHeard",
255255
"since": "Since",
256+
"isFavorite": "Fav",
256257

257258
}
258259

@@ -300,7 +301,7 @@ def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any:
300301
showFields = ["N", "user.longName", "user.id", "user.shortName", "user.hwModel", "user.publicKey",
301302
"user.role", "position.latitude", "position.longitude", "position.altitude",
302303
"deviceMetrics.batteryLevel", "deviceMetrics.channelUtilization",
303-
"deviceMetrics.airUtilTx", "snr", "hopsAway", "channel", "lastHeard", "since"]
304+
"deviceMetrics.airUtilTx", "snr", "hopsAway", "channel", "isFavorite", "lastHeard", "since"]
304305
else:
305306
# Always at least include the row number.
306307
showFields.insert(0, "N")
@@ -342,6 +343,8 @@ def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any:
342343
formatted_value = "Powered"
343344
else:
344345
formatted_value = formatFloat(raw_value, 0, "%")
346+
elif field == "isFavorite":
347+
formatted_value = "*" if raw_value else ""
345348
elif field == "lastHeard":
346349
formatted_value = getLH(raw_value)
347350
elif field == "position.latitude":
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
"""Meshtastic unit tests for showNodes favorite column feature"""
2+
3+
from unittest.mock import MagicMock
4+
5+
import pytest
6+
7+
from ..mesh_interface import MeshInterface
8+
9+
10+
@pytest.fixture
11+
def _iface_with_favorite_nodes():
12+
"""Fixture to setup nodes with favorite flags."""
13+
nodesById = {
14+
"!9388f81c": {
15+
"num": 2475227164,
16+
"user": {
17+
"id": "!9388f81c",
18+
"longName": "Favorite Node",
19+
"shortName": "FAV1",
20+
"macaddr": "RBeTiPgc",
21+
"hwModel": "TBEAM",
22+
},
23+
"position": {},
24+
"lastHeard": 1640204888,
25+
"isFavorite": True,
26+
},
27+
"!12345678": {
28+
"num": 305419896,
29+
"user": {
30+
"id": "!12345678",
31+
"longName": "Regular Node",
32+
"shortName": "REG1",
33+
"macaddr": "ABCDEFGH",
34+
"hwModel": "TLORA_V2",
35+
},
36+
"position": {},
37+
"lastHeard": 1640204999,
38+
"isFavorite": False,
39+
},
40+
"!abcdef00": {
41+
"num": 2882400000,
42+
"user": {
43+
"id": "!abcdef00",
44+
"longName": "Legacy Node",
45+
"shortName": "LEG1",
46+
"macaddr": "XYZABC00",
47+
"hwModel": "HELTEC_V3",
48+
},
49+
"position": {},
50+
"lastHeard": 1640205000,
51+
# Note: No isFavorite field - testing backward compatibility
52+
},
53+
}
54+
55+
nodesByNum = {
56+
2475227164: {
57+
"num": 2475227164,
58+
"user": {
59+
"id": "!9388f81c",
60+
"longName": "Favorite Node",
61+
"shortName": "FAV1",
62+
"macaddr": "RBeTiPgc",
63+
"hwModel": "TBEAM",
64+
},
65+
"position": {"time": 1640206266},
66+
"lastHeard": 1640206266,
67+
"isFavorite": True,
68+
},
69+
305419896: {
70+
"num": 305419896,
71+
"user": {
72+
"id": "!12345678",
73+
"longName": "Regular Node",
74+
"shortName": "REG1",
75+
"macaddr": "ABCDEFGH",
76+
"hwModel": "TLORA_V2",
77+
},
78+
"position": {"time": 1640206200},
79+
"lastHeard": 1640206200,
80+
"isFavorite": False,
81+
},
82+
2882400000: {
83+
"num": 2882400000,
84+
"user": {
85+
"id": "!abcdef00",
86+
"longName": "Legacy Node",
87+
"shortName": "LEG1",
88+
"macaddr": "XYZABC00",
89+
"hwModel": "HELTEC_V3",
90+
},
91+
"position": {"time": 1640206100},
92+
"lastHeard": 1640206100,
93+
# Note: No isFavorite field - testing backward compatibility
94+
},
95+
}
96+
97+
iface = MeshInterface(noProto=True)
98+
iface.nodes = nodesById
99+
iface.nodesByNum = nodesByNum
100+
myInfo = MagicMock()
101+
iface.myInfo = myInfo
102+
iface.myInfo.my_node_num = 2475227164
103+
return iface
104+
105+
106+
@pytest.mark.unit
107+
def test_showNodes_favorite_column_header(capsys, _iface_with_favorite_nodes):
108+
"""Test that 'Fav' column header appears in showNodes output"""
109+
iface = _iface_with_favorite_nodes
110+
iface.showNodes()
111+
out, err = capsys.readouterr()
112+
assert "Fav" in out
113+
assert err == ""
114+
115+
116+
@pytest.mark.unit
117+
def test_showNodes_favorite_asterisk_display(capsys, _iface_with_favorite_nodes):
118+
"""Test that favorite nodes show asterisk and non-favorites show empty"""
119+
iface = _iface_with_favorite_nodes
120+
iface.showNodes()
121+
out, err = capsys.readouterr()
122+
123+
# Check that the output contains the "Fav" column
124+
assert "Fav" in out
125+
126+
# Find lines containing our nodes
127+
lines = out.split('\n')
128+
favorite_line = None
129+
regular_line = None
130+
legacy_line = None
131+
for line in lines:
132+
if "Favorite Node" in line or "FAV1" in line:
133+
favorite_line = line
134+
if "Regular Node" in line or "REG1" in line:
135+
regular_line = line
136+
if "Legacy Node" in line or "LEG1" in line:
137+
legacy_line = line
138+
139+
# Verify all nodes are present in the output
140+
assert favorite_line is not None, "Favorite node should be in output"
141+
assert regular_line is not None, "Regular node should be in output"
142+
assert legacy_line is not None, "Legacy node should be in output"
143+
144+
# Verify the favorite node has an asterisk in its row
145+
assert "*" in favorite_line, "Favorite node should have an asterisk"
146+
147+
# Verify the regular (non-favorite) node does NOT have an asterisk
148+
assert regular_line.count("*") == 0, "Non-favorite node should not have an asterisk"
149+
150+
# Verify the legacy node (without isFavorite field) does NOT have an asterisk
151+
assert legacy_line.count("*") == 0, "Legacy node without isFavorite field should not have an asterisk"
152+
153+
assert err == ""
154+
155+
156+
@pytest.mark.unit
157+
def test_showNodes_favorite_field_formatting():
158+
"""Test the formatting logic for isFavorite field"""
159+
# Test favorite node
160+
raw_value = True
161+
formatted_value = "*" if raw_value else ""
162+
assert formatted_value == "*"
163+
164+
# Test non-favorite node
165+
raw_value = False
166+
formatted_value = "*" if raw_value else ""
167+
assert formatted_value == ""
168+
169+
# Test None/missing value
170+
raw_value = None
171+
formatted_value = "*" if raw_value else ""
172+
assert formatted_value == ""
173+
174+
175+
@pytest.mark.unit
176+
def test_showNodes_with_custom_fields_including_favorite(capsys, _iface_with_favorite_nodes):
177+
"""Test that isFavorite can be specified in custom showFields"""
178+
iface = _iface_with_favorite_nodes
179+
custom_fields = ["user.longName", "isFavorite"]
180+
iface.showNodes(showFields=custom_fields)
181+
out, err = capsys.readouterr()
182+
183+
# Should still show the Fav column when explicitly requested
184+
assert "Fav" in out
185+
assert err == ""
186+
187+
188+
@pytest.mark.unit
189+
def test_showNodes_default_fields_includes_favorite(_iface_with_favorite_nodes):
190+
"""Test that isFavorite is included in default fields"""
191+
iface = _iface_with_favorite_nodes
192+
193+
# Call showNodes which uses default fields
194+
result = iface.showNodes()
195+
196+
# The result should contain the formatted table as a string
197+
assert "Fav" in result
198+
199+
200+
@pytest.mark.unit
201+
def test_showNodes_backward_compatibility_missing_field(capsys, _iface_with_favorite_nodes):
202+
"""Test that nodes without isFavorite field are handled gracefully"""
203+
iface = _iface_with_favorite_nodes
204+
iface.showNodes()
205+
out, err = capsys.readouterr()
206+
207+
# Find the legacy node line
208+
lines = out.split('\n')
209+
legacy_line = None
210+
for line in lines:
211+
if "Legacy Node" in line or "LEG1" in line:
212+
legacy_line = line
213+
break
214+
215+
# Verify the legacy node appears in output
216+
assert legacy_line is not None, "Legacy node without isFavorite field should appear in output"
217+
218+
# Verify it doesn't have an asterisk (should be treated as non-favorite)
219+
assert legacy_line.count("*") == 0, "Legacy node should not have asterisk (treated as non-favorite)"
220+
221+
assert err == ""

0 commit comments

Comments
 (0)