Skip to content

Commit 4f6d183

Browse files
committed
Show favorite nodes in --nodes
1 parent 2f44351 commit 4f6d183

2 files changed

Lines changed: 164 additions & 1 deletion

File tree

meshtastic/mesh_interface.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ def get_human_readable(name):
250250
"channel": "Channel",
251251
"lastHeard": "LastHeard",
252252
"since": "Since",
253+
"isFavorite": "Fav",
253254

254255
}
255256

@@ -297,7 +298,7 @@ def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any:
297298
showFields = ["N", "user.longName", "user.id", "user.shortName", "user.hwModel", "user.publicKey",
298299
"user.role", "position.latitude", "position.longitude", "position.altitude",
299300
"deviceMetrics.batteryLevel", "deviceMetrics.channelUtilization",
300-
"deviceMetrics.airUtilTx", "snr", "hopsAway", "channel", "lastHeard", "since"]
301+
"deviceMetrics.airUtilTx", "snr", "hopsAway", "channel", "isFavorite", "lastHeard", "since"]
301302
else:
302303
# Always at least include the row number.
303304
showFields.insert(0, "N")
@@ -339,6 +340,8 @@ def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any:
339340
formatted_value = "Powered"
340341
else:
341342
formatted_value = formatFloat(raw_value, 0, "%")
343+
elif field == "isFavorite":
344+
formatted_value = "*" if raw_value else ""
342345
elif field == "lastHeard":
343346
formatted_value = getLH(raw_value)
344347
elif field == "position.latitude":
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""Meshtastic unit tests for showNodes favorite column feature"""
2+
3+
import pytest
4+
5+
from ..mesh_interface import MeshInterface
6+
7+
8+
@pytest.fixture
9+
def iface_with_favorite_nodes():
10+
"""Fixture to setup nodes with favorite flags."""
11+
nodesById = {
12+
"!9388f81c": {
13+
"num": 2475227164,
14+
"user": {
15+
"id": "!9388f81c",
16+
"longName": "Favorite Node",
17+
"shortName": "FAV1",
18+
"macaddr": "RBeTiPgc",
19+
"hwModel": "TBEAM",
20+
},
21+
"position": {},
22+
"lastHeard": 1640204888,
23+
"isFavorite": True,
24+
},
25+
"!12345678": {
26+
"num": 305419896,
27+
"user": {
28+
"id": "!12345678",
29+
"longName": "Regular Node",
30+
"shortName": "REG1",
31+
"macaddr": "ABCDEFGH",
32+
"hwModel": "TLORA_V2",
33+
},
34+
"position": {},
35+
"lastHeard": 1640204999,
36+
"isFavorite": False,
37+
},
38+
}
39+
40+
nodesByNum = {
41+
2475227164: {
42+
"num": 2475227164,
43+
"user": {
44+
"id": "!9388f81c",
45+
"longName": "Favorite Node",
46+
"shortName": "FAV1",
47+
"macaddr": "RBeTiPgc",
48+
"hwModel": "TBEAM",
49+
},
50+
"position": {"time": 1640206266},
51+
"lastHeard": 1640206266,
52+
"isFavorite": True,
53+
},
54+
305419896: {
55+
"num": 305419896,
56+
"user": {
57+
"id": "!12345678",
58+
"longName": "Regular Node",
59+
"shortName": "REG1",
60+
"macaddr": "ABCDEFGH",
61+
"hwModel": "TLORA_V2",
62+
},
63+
"position": {"time": 1640206200},
64+
"lastHeard": 1640206200,
65+
"isFavorite": False,
66+
},
67+
}
68+
69+
iface = MeshInterface(noProto=True)
70+
iface.nodes = nodesById
71+
iface.nodesByNum = nodesByNum
72+
from unittest.mock import MagicMock
73+
myInfo = MagicMock()
74+
iface.myInfo = myInfo
75+
iface.myInfo.my_node_num = 2475227164
76+
return iface
77+
78+
79+
@pytest.mark.unit
80+
def test_showNodes_favorite_column_header(capsys, iface_with_favorite_nodes):
81+
"""Test that 'Fav' column header appears in showNodes output"""
82+
iface = iface_with_favorite_nodes
83+
iface.showNodes()
84+
out, err = capsys.readouterr()
85+
assert "Fav" in out
86+
assert err == ""
87+
88+
89+
@pytest.mark.unit
90+
def test_showNodes_favorite_asterisk_display(capsys, iface_with_favorite_nodes):
91+
"""Test that favorite nodes show asterisk and non-favorites show empty"""
92+
iface = iface_with_favorite_nodes
93+
iface.showNodes()
94+
out, err = capsys.readouterr()
95+
96+
# Check that the output contains the "Fav" column
97+
assert "Fav" in out
98+
99+
# The favorite node should have an asterisk in the output
100+
# We can't easily check the exact table cell, but we can verify
101+
# the asterisk appears somewhere in the output
102+
lines = out.split('\n')
103+
104+
# Find lines containing our nodes
105+
favorite_line = None
106+
regular_line = None
107+
for line in lines:
108+
if "Favorite Node" in line or "FAV1" in line:
109+
favorite_line = line
110+
if "Regular Node" in line or "REG1" in line:
111+
regular_line = line
112+
113+
# Basic sanity check - if we found the lines, they should be present
114+
assert favorite_line is not None or regular_line is not None
115+
assert err == ""
116+
117+
118+
@pytest.mark.unit
119+
def test_showNodes_favorite_field_formatting():
120+
"""Test the formatting logic for isFavorite field"""
121+
# Test favorite node
122+
raw_value = True
123+
formatted_value = "*" if raw_value else ""
124+
assert formatted_value == "*"
125+
126+
# Test non-favorite node
127+
raw_value = False
128+
formatted_value = "*" if raw_value else ""
129+
assert formatted_value == ""
130+
131+
# Test None/missing value
132+
raw_value = None
133+
formatted_value = "*" if raw_value else ""
134+
assert formatted_value == ""
135+
136+
137+
@pytest.mark.unit
138+
def test_showNodes_with_custom_fields_including_favorite(capsys, iface_with_favorite_nodes):
139+
"""Test that isFavorite can be specified in custom showFields"""
140+
iface = iface_with_favorite_nodes
141+
custom_fields = ["user.longName", "isFavorite"]
142+
iface.showNodes(showFields=custom_fields)
143+
out, err = capsys.readouterr()
144+
145+
# Should still show the Fav column when explicitly requested
146+
assert "Fav" in out
147+
assert err == ""
148+
149+
150+
@pytest.mark.unit
151+
def test_showNodes_default_fields_includes_favorite(iface_with_favorite_nodes):
152+
"""Test that isFavorite is included in default fields"""
153+
iface = iface_with_favorite_nodes
154+
155+
# Call showNodes which uses default fields
156+
result = iface.showNodes()
157+
158+
# The result should contain the formatted table as a string
159+
assert "Fav" in result
160+

0 commit comments

Comments
 (0)