Skip to content

Commit adde8cd

Browse files
committed
fix: add TR-064 fallback for FritzBox device info
When data.lua?page=overview returns HTML instead of JSON, fall back to /tr064/tr64desc.xml to extract model name and firmware version.
1 parent 5ca1e20 commit adde8cd

File tree

2 files changed

+97
-1
lines changed

2 files changed

+97
-1
lines changed

app/fritzbox.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import requests
99

1010
log = logging.getLogger("docsis.fritzbox")
11+
_TR064_NS = {"tr64": "urn:dslforum-org:device-1-0"}
1112

1213

1314
def login(url: str, user: str, password: str) -> str:
@@ -95,7 +96,23 @@ def get_device_info(url: str, sid: str) -> dict:
9596
except (ValueError, TypeError):
9697
pass
9798
return result
98-
except Exception:
99+
except Exception as e:
100+
log.debug("FritzBox overview device info unavailable, trying TR-064 fallback: %s", e)
101+
102+
try:
103+
r = requests.get(f"{url}/tr064/tr64desc.xml", timeout=10)
104+
r.raise_for_status()
105+
root = ET.fromstring(r.text)
106+
model = (
107+
root.findtext(".//tr64:modelName", namespaces=_TR064_NS)
108+
or root.findtext(".//tr64:modelDescription", namespaces=_TR064_NS)
109+
or root.findtext(".//tr64:friendlyName", namespaces=_TR064_NS)
110+
or "FRITZ!Box"
111+
)
112+
sw_version = root.findtext(".//tr64:systemVersion/tr64:Display", namespaces=_TR064_NS) or ""
113+
return {"model": model, "sw_version": sw_version}
114+
except Exception as e:
115+
log.debug("FritzBox TR-064 device info fallback unavailable: %s", e)
99116
return {"model": "FRITZ!Box", "sw_version": ""}
100117

101118

tests/test_fritzbox_api.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Tests for the direct FritzBox API helpers in app.fritzbox."""
2+
3+
from unittest.mock import MagicMock, patch
4+
5+
from app import fritzbox as fb
6+
7+
8+
TR064_DESC_XML = """<?xml version="1.0"?>
9+
<root xmlns="urn:dslforum-org:device-1-0">
10+
<systemVersion>
11+
<Display>267.08.21</Display>
12+
</systemVersion>
13+
<device>
14+
<modelName>FRITZ!Box 6690 Cable</modelName>
15+
<modelDescription>FRITZ!Box 6690 Cable</modelDescription>
16+
<friendlyName>FRITZ!Box 6690 Cable</friendlyName>
17+
</device>
18+
</root>
19+
"""
20+
21+
22+
class TestGetDeviceInfo:
23+
@patch("app.fritzbox.requests.post")
24+
def test_uses_overview_json_when_available(self, mock_post):
25+
response = MagicMock()
26+
response.raise_for_status = MagicMock()
27+
response.json.return_value = {
28+
"data": {
29+
"fritzos": {
30+
"Productname": "FRITZ!Box 6660 Cable",
31+
"nspver": "8.02",
32+
"Uptime": "1234",
33+
}
34+
}
35+
}
36+
mock_post.return_value = response
37+
38+
info = fb.get_device_info("http://fritz.box", "sid123")
39+
40+
assert info == {
41+
"model": "FRITZ!Box 6660 Cable",
42+
"sw_version": "8.02",
43+
"uptime_seconds": 1234,
44+
}
45+
46+
@patch("app.fritzbox.requests.get")
47+
@patch("app.fritzbox.requests.post")
48+
def test_falls_back_to_tr064_when_overview_returns_html(self, mock_post, mock_get):
49+
html_response = MagicMock()
50+
html_response.raise_for_status = MagicMock()
51+
html_response.json.side_effect = ValueError("not json")
52+
html_response.text = "<html>login</html>"
53+
mock_post.return_value = html_response
54+
55+
tr064_response = MagicMock()
56+
tr064_response.raise_for_status = MagicMock()
57+
tr064_response.text = TR064_DESC_XML
58+
mock_get.return_value = tr064_response
59+
60+
info = fb.get_device_info("http://fritz.box", "sid123")
61+
62+
assert info == {
63+
"model": "FRITZ!Box 6690 Cable",
64+
"sw_version": "267.08.21",
65+
}
66+
67+
@patch("app.fritzbox.requests.get")
68+
@patch("app.fritzbox.requests.post")
69+
def test_returns_generic_fallback_when_overview_and_tr064_fail(self, mock_post, mock_get):
70+
post_response = MagicMock()
71+
post_response.raise_for_status = MagicMock()
72+
post_response.json.side_effect = ValueError("not json")
73+
mock_post.return_value = post_response
74+
75+
mock_get.side_effect = RuntimeError("network down")
76+
77+
info = fb.get_device_info("http://fritz.box", "sid123")
78+
79+
assert info == {"model": "FRITZ!Box", "sw_version": ""}

0 commit comments

Comments
 (0)