Checklist
Describe the feature you'd like
In Vantage InFusion installations with multiple Master controllers sharing one IP, aiovantage only returns objects from the primary master. Objects on the second master are not enumerated, even though they are reachable from the same IP and queryable from a single TCP connection.
My setup
- 1 Vantage InFusion IP (
192.168.1.98:2001)
- 2 Master controllers (internal IDs
58 and 88), cross-linked
- Total ~200 lights across both controllers in Design Center
Result with home-assistant-vantage / aiovantage 0.22.8
- 93 lights imported (matches just one master's count)
- 116 entities total, 161 devices in HA
- Debug logs show
OpenFilter queries but no per-master targeting
Result with homebridge-vantage
Same IP, same controllers — all ~200 lights imported, plus other objects from both masters.
Root cause
The HomeBridge plugin (source) prefixes every config query with <?Master N?> and iterates controller = 1, 2, 3, ... until a master fails to respond:
configuration.write("<?Master " + controller.toString() + "?><IConfiguration><OpenFilter>…")
// after each pass:
if (writeCount >= objecTypes.length) { controller++; writeCount = 0 }
// terminates when a master doesn't respond:
else shouldbreak = true
aiovantage's _config_client/connection.py has no <?Master N?> prefix — every query is sent untargeted. The InFusion routes those to the primary master only.
Reproduction / proof of concept
I wrote a standalone async Python client that queries the same InFusion using the master-prefixed protocol. Zero overlap between the two masters' VID sets and the total matches what HomeBridge sees:
| Query |
Loads returned |
<Load Master="…"> attribute |
<?Master 1?> |
105 |
controller 58 |
<?Master 2?> |
95 |
controller 88 |
| Combined |
200 |
both — zero VID overlap |
Minimal working snippet that demonstrates the protocol — feel free to adapt:
import asyncio, re, xml.etree.ElementTree as ET
HOST, PORT = "192.168.1.98", 2001
async def fetch_loads(master: int) -> list[ET.Element]:
reader, writer = await asyncio.open_connection(HOST, PORT)
# 1) OpenFilter — note the <?Master N?> prefix
writer.write(
f"<?Master {master}?><IConfiguration><OpenFilter>"
f"<call><Objects><ObjectType>Load</ObjectType></Objects></call>"
f"</OpenFilter></IConfiguration>\n".encode()
)
await writer.drain()
resp = (await reader.readuntil(b"</IConfiguration>\n")).decode()
handle = re.search(r"<return>\s*(\d+)\s*</return>", resp).group(1)
# 2) Page through results — also prefixed
all_objs: list[ET.Element] = []
while True:
writer.write(
f"<?Master {master}?><IConfiguration><GetFilterResults>"
f"<call><hFilter>{handle}</hFilter><Count>100</Count>"
f"<WholeObject>true</WholeObject></call><return/>"
f"</GetFilterResults></IConfiguration>\n".encode()
)
await writer.drain()
data = (await reader.readuntil(b"</IConfiguration>\n")).decode()
data = re.sub(r"<\?Master\s+\d+\?>", "", data, count=1)
page = ET.fromstring(data.strip()).findall(".//Object")
if not page:
break
all_objs.extend(page)
if len(page) < 100:
break
writer.write(
f"<?Master {master}?><IConfiguration><CloseFilter>"
f"<call>{handle}</call></CloseFilter></IConfiguration>\n".encode()
)
await writer.drain()
writer.close()
return all_objs
async def main():
seen = set()
for m in (1, 2, 3, 4):
try:
objs = await asyncio.wait_for(fetch_loads(m), timeout=10)
except asyncio.TimeoutError:
break
new = {o.find("./Load").attrib["VID"] for o in objs if o.find("./Load") is not None} - seen
if not new:
break # no master at that number / already covered
seen |= new
print(f"Master {m}: +{len(new)} new loads (total {len(seen)})")
asyncio.run(main())
Output on my system:
Master 1: +105 new loads (total 105)
Master 2: +95 new loads (total 200)
A control session (port 3001) also accepts the <?Master N?> prefix on commands like STATUS LOAD / LOAD <vid> <level> / GETLOAD <vid>, so live state and writes work across masters too — I have a custom HA integration doing this in production.
Proposed solution
Either:
- Auto-discovery: enumerate
Master objects first, then re-issue each OpenFilter/GetFilterResults/CloseFilter once per master with the <?Master N?> prefix and merge the results.
- Configurable list of master IDs in
Vantage() constructor (masters=[1, 2, 3]), defaulting to "all discovered" for backward compatibility.
Either way the controllers module would need a per-master pass; the command-client similarly needs the prefix when sending writes to a load that lives on a non-primary master.
Verification data available
Happy to share full debug logs (aiovantage: debug) or the PoC script above if useful.
Additional context
No response
Checklist
Describe the feature you'd like
In Vantage InFusion installations with multiple Master controllers sharing one IP,
aiovantageonly returns objects from the primary master. Objects on the second master are not enumerated, even though they are reachable from the same IP and queryable from a single TCP connection.My setup
192.168.1.98:2001)58and88), cross-linkedResult with home-assistant-vantage / aiovantage 0.22.8
OpenFilterqueries but no per-master targetingResult with
homebridge-vantageSame IP, same controllers — all ~200 lights imported, plus other objects from both masters.
Root cause
The HomeBridge plugin (source) prefixes every config query with
<?Master N?>and iteratescontroller = 1, 2, 3, ...until a master fails to respond:aiovantage's_config_client/connection.pyhas no<?Master N?>prefix — every query is sent untargeted. The InFusion routes those to the primary master only.Reproduction / proof of concept
I wrote a standalone async Python client that queries the same InFusion using the master-prefixed protocol. Zero overlap between the two masters' VID sets and the total matches what HomeBridge sees:
<Load Master="…">attribute<?Master 1?><?Master 2?>Minimal working snippet that demonstrates the protocol — feel free to adapt:
Output on my system:
A control session (port 3001) also accepts the
<?Master N?>prefix on commands likeSTATUS LOAD/LOAD <vid> <level>/GETLOAD <vid>, so live state and writes work across masters too — I have a custom HA integration doing this in production.Proposed solution
Either:
Masterobjects first, then re-issue eachOpenFilter/GetFilterResults/CloseFilteronce per master with the<?Master N?>prefix and merge the results.Vantage()constructor (masters=[1, 2, 3]), defaulting to "all discovered" for backward compatibility.Either way the controllers module would need a per-master pass; the command-client similarly needs the prefix when sending writes to a load that lives on a non-primary master.
Verification data available
Happy to share full debug logs (
aiovantage: debug) or the PoC script above if useful.Additional context
No response