Repository navigation
Integration Behaviour
How the Neumann Connect integration talks to the speakers, finds and follows them, and what it does around standby, resets, the EQ and backups. The speakers' own behaviour and the protocol are described in the Neumann KH SSC Reference (Neumann Control wiki).
Source marks [measured], [code], [khtool]: see the legend on Home.
| Item | Behaviour | Source |
|---|---|---|
| Connection | One TCP connection per speaker, kept open and reused | [code] |
| Scope handling | Appends %<interface> automatically when the address is link-local, has no scope yet and an interface is configured; setup requires an interface for a link-local address. Detects link-local by the range fe80::/10, not by a fe80 string prefix |
[code] |
| Framing | Terminates requests with CR+LF; splits replies on LF, so CR+LF and LF are both accepted. Lines that are not valid JSON objects are ignored. Refuses to send NaN / Infinity
|
[code] |
| One leaf per message | A design choice, not a device limit: every reply belongs to exactly one path. Polling cost grows with the number of paths | [code] |
| Matching replies | Every value request carries an osc/xid, which the speaker echoes in the same line, error replies included; a line with another xid is a late answer and dropped, and the line with the own xid is the answer. Requests without an xid - osc methods (discovery schema/limits walk) and EQ sets the xid would push past the 29-token limit (20 bands) - are matched by their path; only after such an answer are complete lines still waiting discarded before the next request (stale drain, up to 10 ms). Requests on one connection are serialized behind a lock, handed over in FIFO order; a cancelled or timed-out request drops the connection |
[code] |
| Known limit | A late extra line for path X that arrives after the discard but before the real answer cannot be told apart from a fresh answer to X. The tested devices answer every leaf query with exactly one line [measured] | [code], [measured] |
| Reading an answer | Every request names the path it expects (expect_path); the client returns as soon as that path or an osc/error has arrived. While an answer is still incomplete, each new line restarts a 0.4 s settle window and lines are merged |
[code] |
| Timeouts and caps | Connect/response timeout 3.0 s; per answer 256 lines, 10 s, 4 MiB; a single line at most 1 MiB. Reaching a cap drops the connection | [code] |
| Errors | Since 1.19.1 the first [int, {...}] pair anywhere in the osc/error tree is taken (a flat pair as well); its position is ignored. Earlier versions expected a flat pair and read no code from real devices. Code 404 means "this model does not have the path" |
[code] |
| Not used | Address patterns, osc/ping, osc/version, subscriptions: the integration polls single leaves instead |
[code] |
Duplicate "osc" keys |
The integration uses a standard JSON parser; it is not affected: error replies carry the xid next to error in one osc object (measured on both models), and no subscriptions are used |
[code], [measured] |
| Item | Behaviour | Source |
|---|---|---|
| Poll interval | Every 30 s, cycle limit 25 s | [code] |
| Rates | fast = every cycle; fast, while enabled = every cycle while an entity reading the path is enabled, for the paths only entities disabled by default read (outputs 1 and 2, output meter and clip, standby countdown; the first refresh reads them once); slow = every 10th cycle (5 min), cached in between; setup = read once when the entry is set up; none = never polled. Per path: Entities-and-Polling | [code] |
| Cost | With the entities' default setting, about 45 % (KH 120 II) / 68 % (KH 750) fewer requests than polling everything every cycle, counted from the poll lists | [code] |
| A path the speaker does not have | Skipped during polling, logged at debug level | [code] |
| Item | Behaviour | Source |
|---|---|---|
product |
Decides which entities and poll paths exist; if a device reports no product, the model is shown as KH DSP
|
[code] |
vendor |
Device manufacturer. A device whose vendor does not contain "neumann" is not rejected: setup warns with the reported product and vendor and continues after confirmation. A device without vendor counts as Neumann |
[code] |
serial |
Config entry unique ID, device identifier, identity check | [code] |
version |
Device firmware (sw_version); re-read at every setup. A firmware update while Home Assistant keeps running shows up after the next reload or restart |
[code] |
hw_version |
Diagnostic sensor "Hardware version" | [code] |
| Unusable answers | An identity answer that is not plain text or a number (list, object, boolean, empty) counts as "no value" | [code] |
| Subwoofer names | Subwoofer features apply to KH 750 and KH 750 DSP
|
[code] |
| Masking | Serials in logs and exports show only the last 3 characters; addresses are masked (see Troubleshooting) | [code] |
| Path | Trigger | What it does | Source |
|---|---|---|---|
| Scan | "Search network automatically" in the setup dialog | Active 4 s mDNS search, then asks each candidate for its identity (at most 32 records resolved, 20 s budget) | [code] |
| Passive discovery | Home Assistant receives an announcement | Updates a known entry or offers a new speaker | [code] |
| Relocate | After a failed setup of an entry with a stored serial | Searches for the entry's speaker by serial (see below) | [code] |
- The service type
_ssc._tcp.local.is declared inmanifest.json; of the TXT keys onlyid(the serial) is used [code]. - Home Assistant's passive discovery re-reads a changed address only when the PTR record is refreshed at 75 % of its TTL (4500 s), i.e. after roughly 56 minutes (TTL [measured]; refresh timing derived from Home Assistant / python-zeroconf behaviour, not observed on hardware).
Address rules [code]:
- An IPv4 address is never picked; an announcement without any IPv6 address is rejected (
no_ipv6), and the scan skips such a record. - A link-local address is preferred; a global address is used only if no link-local address is announced (a global address dies with the next ISP prefix change [measured]).
- A discovered link-local address carries its scope ID (
%<scope>), so no separate interface is stored. - Manual entry: a link-local address requires the network interface (or
fe80::1%eth0notation in the address field). - Home Assistant and the speakers must share the same layer-2 segment.
The serial is the anchor. An address is only ever changed for the entry whose serial the device at the new address reports. [code]
| Situation | Behaviour | Source |
|---|---|---|
| Announcement for a known serial, address unchanged | Nothing happens, no network contact | [code] |
| Announcement for a known serial, new address or port | The device at the new address is asked for its serial; on a match the entry's host/port are updated and the entry reloads | [code] |
| Setup fails (speaker off, unreachable, wrong device) | Relocate: 4 s mDNS search over every candidate other than the current address. A candidate whose announcement carries its serial is taken by that serial without a connection; only candidates without one are asked (8 in parallel). The first exact match becomes the new address, and Home Assistant's next scheduled retry connects and checks the serial against the device | [code] |
| Relocate finds only the current address | Nothing is written | [code] |
| Entry has no stored serial | No relocate and no identity check; the address must be fixed via Reconfigure | [code] |
| Address changed manually via Reconfigure | Device must report the stored serial, otherwise aborted with "wrong device", nothing changed | [code] |
After an ISP prefix change, link-local preference, passive discovery and relocate together brought all affected entries back without user action. [measured]
Every setup of an entry with a stored serial reads the serial before the first poll. A different serial, an unusable answer or no answer at all fails the setup as "not ready"; nothing of the other device is published, and relocate searches for the real speaker. The check is fail-closed. The same exact-serial comparison is used for relocate, reconfigure and passive discovery. [code]
| Case | Behaviour | Source |
|---|---|---|
Announcement without TXT id
|
Ignored (no_serial) |
[code] |
| Unknown device that reports no serial itself | Not offered via passive discovery (no_serial); manual setup and scan still accept it, with host_port as unique ID |
[code] |
Unknown device whose reported serial differs from TXT id
|
The serial reported by the device wins | [code] |
| Known serial announced, but the device answers a different serial | Ignored (wrong_device), entry unchanged |
[code] |
Limitation: the serial check catches stale or wrong records, not a device on the same segment that deliberately claims another speaker's serial. [code]
"Run device discovery" asks one configured speaker for everything it can tell about itself. It is not the network search above. [code]
| Part | What it reads | Limits | Source |
|---|---|---|---|
| Known paths | Every path the integration knows for the model, one leaf per message; a path the device rejects is skipped | 120 s for the whole part; on timeout or lost connection nothing is recorded and the previous discovery is kept | [code] |
| Schema walk | The device's own tree via osc/schema, one level per request, then osc/limits for every leaf |
Best effort: 30 s, at most 500 nodes and 10 levels; if the device refuses, this part stays empty | [code] |
- Since 1.19.1 each schema and limits request returns as soon as its reply arrives: 3.4 s on a KH 120 II, 7.4 s on a KH 750 [measured]. Before, each reply waited the full 0.4 s and the walk stopped at its 30 s limit after 50 leaves [measured].
- The result is written as a JSON file to
<config>/neumann_kh/(not served over HTTP) and kept in a Home Assistant store per serial; the export carries the masked serial only. A run that read nothing is refused and the previous discovery is kept. [code] - The
writeableflags it records are not reliable (see the reference's Limits and Untested). [measured]
| Aspect | Behaviour | Source |
|---|---|---|
| Entities in standby | Become unavailable; this is expected, not an error | [code] |
| Log | One info line when the speaker drops out and one when it is back - no error entries | [code] |
| Recovery | A running entry keeps polling every 30 s and recovers on the first successful poll after wake-up | [code], [measured] |
| Reboot while Home Assistant runs | A few poll cycles fail, then polling picks up again; the config entry is not reloaded | [code] |
| Factory reset | Button Restore factory defaults writes FACTORY_DEFAULTS to device/restore (two presses, see Troubleshooting). A missing reply is treated as "reset may have run" and all cached values are discarded |
[code] |
| Control mode | Read-only diagnostic sensor Control mode (NETWORK / LOCAL), enabled by default; a backup restore does not write it. The select of earlier versions is removed from the entity registry at setup | [code] |
| Item | Behaviour | Source |
|---|---|---|
| Untested models | KH 80 / KH 80 DSP, KH 120 (logo table), KH 150 and the AES67 variants are listed in the model tables; untested models get the ranges of the tested model of the same kind (delay max 5760 samples for monitors, 1000 for the subwoofer) | [code] |
| KH 805 II / KH 810 II / KH 870 II | Not in the model tables, so no model-specific entities | [code] |
KH 750 audio/out5
|
At setup, level and mute switch to audio/out5 only if audio/out/level answers 404 and audio/out5/level answers a finite number; anything else keeps audio/out. Delay always stays in audio/out. Entities and backups always use the audio/out name, so a backup can be restored across both layouts |
[code] |
| KH 80 save |
device/save_settings is sent only to KH 80 / KH 80 DSP: after each accepted write except Identify, once per EQ reset and once per backup restore (not after a factory reset). A 404 means "nothing to save"; any other error is reported |
[code], [khtool] (issue tracker) |
| Identify | A switch, not a button, because the device keeps flashing for several minutes after a set [measured] | [code] |
Each eq2/eq3 container gets two entities, both in the entity category "Configuration" and enabled by default [code]. The crossover eq1 is not exposed [code].
| Entity | Name | Behaviour | Source |
|---|---|---|---|
| Switch | {container} enabled |
Writes the same value to every entry of the enabled array. Shows "on" when at least one band is enabled |
[code] |
| Button | {container}: reset to flat |
Sets gain and then boost of every band to 0.0; frequency, q, type and enabled stay unchanged. This makes the bands flat, because gain is a flat level and boost the filter height [measured] |
[code], [measured] |
- Container names all start with "EQ" (e.g. "EQ2 Main output", "EQ3 Output 1"), so they stay together in the configuration list. On the KH 750, "Subwoofer main output" (
audio/out) is the subwoofer's own woofer path, "Output 1" / "Output 2" are the output sockets A and B [measured], [code]. - Entity count: 4 on the KH 120 II (2 containers) and 12 on the KH 750 (6 containers). Mapping every band parameter to its own entity would create 360 entities on the KH 750 [code].
- The
enabledarrays are read on the slow cycle;gain,boost,frequency,qandtypeare not polled [code]. - During a reset, other device actions such as backup or restore are blocked, so a backup cannot capture a half-written EQ curve [code].
- A backup stores all known values except live measurements (meters, standby countdown), including all six leaves of every
eq2/eq3container; the crossovereq1is not part of it. A restore writes all six EQ leaves back [code]. - During a backup, paths the device rejects (for example with 404) are skipped; a lost connection or the time limit aborts it [code].
- A backup taken while the CONTROL (MODE) switch is on LOCAL is refused, and the previous backup is kept: on LOCAL the speaker reports its local settings, not the network ones (measured on the KH 750). A stored backup that holds LOCAL is refused at restore [code].
- A restore writes mute before the levels and unmute after them, so a backup "muted, high level" never plays unmuted at that level on the way [code].
- The restore notification counts values the speaker refused and values it did not confirm separately, and logs each of them (a refusal with its error code). If the speaker confirmed none of the values sent, the restore reports them as unconfirmed rather than unchanged, and still saves them on a KH 80 [code].
- A full backup reads 33 paths on a KH 120 II and 69 on a KH 750 [code]. With the larger path set of earlier versions (37 and 89 paths) it took about 1.0 s and 2.5 s [measured].
- Buttons, confirmation and refusals: Troubleshooting.
See also: Entities-and-Polling · Troubleshooting · Neumann KH SSC Reference
Neumann Connect Wiki