') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Downgrade getMetaInfo warning to debug for expected non-JSON responses by derekcentrico · Pull Request #129 · Velleman/python-linkplay · GitHub
Skip to content

Downgrade getMetaInfo warning to debug for expected non-JSON responses - #129

Open
derekcentrico wants to merge 2 commits into
Velleman:mainfrom
derekcentrico:fix/meta_info_warning_spam
Open

Downgrade getMetaInfo warning to debug for expected non-JSON responses#129
derekcentrico wants to merge 2 commits into
Velleman:mainfrom
derekcentrico:fix/meta_info_warning_spam

Conversation

@derekcentrico

Copy link
Copy Markdown

Problem

WiiM devices return "Failed" or an empty body (HTTP 200) from the getMetaInfo endpoint when nothing is playing. Both trigger json.JSONDecodeError in session_call_api_json, which logs a WARNING before raising LinkPlayInvalidDataException. On a 5-second poll cycle this produces hundreds of warning lines per day in Home Assistant logs:

WARNING [linkplay] Unexpected json for https://192.168.1.222/httpapi.asp?command=getMetaInfo: Expecting value: line 1 column 1 (char 0)

PR #93 added the data field to the exception and a catch in bridge.py for data == "Failed", but the warning was already emitted by that point. Empty-string responses were not handled at all and re-raised.

Reported in #128 and in Home Assistant core issue #175775.

Fix

Two changes:

  1. utils.py: Before logging, check whether the raw response is empty or "Failed". Log at DEBUG for those cases, keep WARNING for truly unexpected payloads.

  2. bridge.py: Broaden the catch to also treat empty-string responses as a non-error (same as "Failed"). Uses an isinstance check so exceptions raised without an explicit data string still re-raise correctly.

Tests

Adds test_meta_info_empty_response_handling alongside the existing test_meta_info_failed_handling. All 291 tests pass.

Closes#128

WiiM devices return "Failed" or an empty body (HTTP 200) from the
getMetaInfo endpoint when nothing is playing. Both trigger a
JSONDecodeError, and session_call_api_json logs a WARNING for every
occurrence. On a 5-second poll cycle this produces hundreds of
warning lines per day in Home Assistant logs.
The exception handler in bridge.py already treats "Failed" as a
non-error, but it only ran after the warning was already logged.
Empty responses were not handled at all and re-raised.
Log at DEBUG instead of WARNING when the raw response is empty or
"Failed". Broaden the bridge.py catch so empty responses are also
treated as a silent non-error.
ClosesVelleman#128
Only suppress LinkPlayInvalidDataException when exc.data is an actual
string whose stripped value is empty or "Failed". Exceptions raised
without a data field (data=None) now re-raise instead of being
silently swallowed.
@firstofjuly

Copy link
Copy Markdown

Independent reproduction on a different device model, in case it helps with review.

Device: WiiM Mini (project: Muzo_Mini, hardware: ALLWINNER-R328, firmware Linkplay.4.6.819436)
Stack: Home Assistant 2026.7.0, python-linkplay 0.2.14

Confirmed the endpoint returns a bare 6-byte non-JSON body when idle (status: none):

$ curl -sk "https://<device>/httpapi.asp?command=getMetaInfo"
Failed

Scale on a single idle device: 537,992 warnings over ~8 weeks (Jul 4 – Aug 30), which was 74% of all log entries on that instance. It buried several genuine faults — a dead sensor battery and an offline camera each went unnoticed for weeks because the log was unreadable.

Worth emphasising that the device is entirely healthy: 15/15 requests answered in ~0.08 s across getPlayerStatusEx, getStatusEx, getMetaInfo and getNewAudioOutputHardwareMode, from two separate hosts. So this is purely the log-and-raise in session_call_api_json emitting a warning for a condition bridge.py already treats as expected.

The stripped in ("", "Failed") approach looks right to me. Handling the empty-body case as well as Failed matters — I only found the Failed variant when diagnosing this, and would have missed the empty case.

One note for anyone landing here from search: this cannot be resolved by updating Home Assistant. Both 2026.7.0 and 2026.8.3 pin python-linkplay==0.2.14, so until a release ships, the only mitigation is silencing the logger:

logger:
logs:
linkplay: error

@derekcentrico

Copy link
Copy Markdown
Author

@Velleman Any chance you could take a look at this when you get a moment? We now have an independent reproduction from @firstofjuly on a different device model (WiiM Mini), logging over 500k warnings in 8 weeks from this single endpoint. The fix is minimal and scoped to the two known non-JSON responses ("" and "Failed").

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Don’t warn on expected WiiM getMetaInfo → Failed response

2 participants

@derekcentrico@firstofjuly