Skip to main content

Discovery

How a controller works out that a box on the network is yours, so nobody has to be told an IP address.

Getting this right is worth more than it looks. An installer commissioning a house has a list of devices and a limited amount of patience, and the difference between found, one click to add and find the address, type it in, hope it does not change is the difference between a driver people reach for and one they work around.

The four matchers​

All of them live in [discovery], all are optional, and any one hit is enough.

[discovery]
mdns = ["_hue._tcp"]
ssdp = ["urn:schemas-sony-com:service:ScalarWebAPI:1"]
sddp = ["sony:display"]
mac_oui = ["00:17:88", "EC:B5:FA"]
MatcherMatches againstUse it when
mdnsThe service type in a Bonjour/Avahi announcement, and optionally its TXTThe device advertises anything on .local
ssdpThe ST/NT of an SSDP announcementUPnP hardware — most TVs, most streamers
sddpAn SDDP announcement, across several of its fieldsAV rack gear. See below; this is the strong one.
mac_ouiThe first three octets of the MACA last resort, and a good one — a vendor's OUI is stable across every model they ship

A driver can declare all four. They are read as any of these means it might be mine, not as a conjunction.

mDNS, when the service type is not the whole story​

A service type is often the whole claim: _hue._tcp is a Hue bridge and nothing else advertises it. Sometimes it is nowhere near enough, and the failure is not subtle — on one ordinary home network, _airplay._tcp came back from an Apple TV, a VIZIO television, a Hisense Roku television, and the laptop the controller was running on. A driver that declared it was offered for all four.

What separates them is what they say about themselves, so a rule can ask:

mdns = [
# The whole service type is the claim.
"_viziocast._tcp",
# This one is shared, so the TXT record decides.
{ service = "_airplay._tcp", txt = { manufacturer = "VIZIO*" } },
]

A bare string is shorthand for the service on its own — every hint written before this existed still means what it meant. The table form adds TXT keys the announcement must carry, matched as globs, all of them required together. Keys are compared case-insensitively, and so are values, so md = "ecb*" matches the ECB701 an ecobee thermostat actually announces.

Alternatives are separate rules, because the keys within one rule are a conjunction:

mdns = [
{ service = "_hap._tcp", txt = { md = "ecobee*" } },
{ service = "_hap._tcp", txt = { md = "ecb*" } },
]

The same rule as SDDP applies, for the same reason: a key the device did not send cannot satisfy a rule that asks about it, * included. A HomeKit accessory that names no model is not "any model" — it is a device that has not told you what it is, and claiming it is how _hap._tcp came to mean "ecobee" and _airplay._tcp came to mean "Apple TV".

Whatever a device announced is shown on the controller's Discovery pane, on the row itself, so writing one of these is reading a table rather than running a packet capture.

SDDP, and why it is the useful one​

SDDP is what AV manufacturers implement to be found by control systems, and a great deal of hardware announces itself over it whether or not a control system is listening. An announcement carries several fields, so a rule can be as loose or as tight as you need:

# Any Sony display.
sddp = ["sony:display"]

# One exact model, by the driver filename the device asks for.
[[discovery.sddp]]
manufacturer = "Sony"
driver = "sony_display_XR*"

A bare string is shorthand for { type = "..." }. The table form matches on type, driver, manufacturer, model and primary_proxy, and every field you set must match. Values are globs, where * stands for any run of characters.

driver is the field worth knowing about. It is the filename the device says a control system should hold for it — which, on AV gear, names the exact model. It is an opaque label: nothing reads, ships, or derives anything from the file it names. Matching it is often the only way to recognize a device that would otherwise announce nothing distinctive.

Two rules that follow from how matching works, both of which have bitten someone:

  • A field the device did not send cannot satisfy a rule that asks about it — not even *. A rule reading "any model" will not claim a device that never mentioned a model, because that is the opposite of what it says.
  • A rule with no fields set matches nothing. An empty rule that matched everything would be one careless driver claiming every device in the house.

Probing, for hardware that announces nothing​

Plenty of devices sit on a port and say nothing until spoken to. Declare a probe on the transport and a controller running a network survey will try the port itself:

[[transport]]
kind = "tcp"
port = 23

[transport.probe]
send = "PWSTANDBY\r"
expect = "PW"

The point is to be sure. An open port says only that something is listening on the number this driver expects, which on a busy network is a coin toss. A reply that could only have come from the right software is an identification — so the best exchange is one that needs no credentials and grants nothing. Being told to go away in the right dialect is an excellent result.

expect is matched as a plain substring, not a pattern. A discovery rule gets read by whoever is wiring the house up, and a regex in a manifest is a second language to learn before answering "would this find my box".

Two things about probes that are deliberate:

  • Nothing is swept unless a driver asks. Leave probe out and the controller never opens a connection on your behalf. Scanning a home network is not a thing to do by default.
  • Probes only run for drivers that are already installed. A survey will not fetch and try every certified driver's probe — that would be the whole catalog knocking on every port in the house.

Being offered before you are installed​

The other three matchers do run against drivers nobody has installed, because their hints ride along in the registry index rather than only in the package.

That is the whole reason the index carries a discovery block per driver. A controller sees an unrecognized device announce itself, matches it against every certified driver, and offers the right one — turning the Discovery list from a dead end into one click. Without it, finding a device you have no driver for tells you nothing about which driver to go and get.

Nothing is downloaded to do this. The index is a small file the controller already has.

An unmatched device is still worth something​

A device that matches nothing shows up in the Discovery list anyway, unclaimed. That list is how an integrator finds out a driver does not exist yet, which is the most valuable thing it can tell them — and how you find out which fields a device actually announces when you are about to write one.

Practical advice​

  • Declare mac_oui even when you have something better. It costs one line and it catches the device whose announcement is turned off in a settings menu somebody found.
  • Do not match on a model number you have only seen once. A vendor ships forty models on one firmware; a rule tight enough to name a model claims one of them and leaves the other thirty-nine unclaimed.
  • Test against a real announcement, not the spec sheet. What a device actually sends and what its documentation says it sends are different documents. The Discovery list shows you the fields it really sent.