Repository navigation
2008-era Circles (protocol 1.x) join the network but never answer any unicast request — discovery fails with "unknown NodeType not responding" #448
Replies: 2 comments 1 reply
|
@eltjor Can you share your detailed debug logs? Also, can you test if the Circles show up in Source? I probably will have some time later this week to look into this issue. |
Plugwise legacy-set koppelen aan HA — stick ↔ Circle+ pairing zonder SourceVastgelegd: 29 augustus 2026 — traject volledig doorlopen en geslaagd Systeemcontext
Het probleem en de diagnoseketen
Reset-procedures (belangrijk: er zijn er twee!)
Bronnen
Plugwise Toolkit — overzicht en gebruikToolkit voor het legacy Plugwise USB-netwerk (stick + Circle+ + Scripts op HA kopieren dmv Terminal
Voorwaarden (gelden voor élk script)
Hieronder is plugwise_pair_tool.py — het zwitsers zakmesVier subcommando's voor stick- en netwerkbeheer. #!/usr/bin/env python3
"""
plugwise_pair_tool.py — Plugwise USB-stick <-> Circle+ koppel- en diagnosetool
==============================================================================
Consolidatie van het pairing-traject (aug 2026). Werkt zonder Plugwise Source.
Gebruikt het reverse-engineered legacy-protocol; frames geverifieerd tegen
plugwise-usb v0.47.6 (de bibliotheek van de HA-integratie) en PlugwiseOnLinux.
Vereist: pip install pyserial
De Plugwise USB-integratie in Home Assistant moet UITGESCHAKELD zijn
(de stick kan maar door één programma tegelijk gebruikt worden).
Gebruik:
python3 plugwise_pair_tool.py status <poort>
python3 plugwise_pair_tool.py ping <poort> <mac>
python3 plugwise_pair_tool.py pair <poort> <circle+-mac>
python3 plugwise_pair_tool.py monitor <poort> <circle+-mac> [minuten]
python3 plugwise_pair_tool.py add <poort> <node-mac> # Circle/Stealth
# proactief joinen
Voorbeeld:
python3 plugwise_pair_tool.py pair \\
/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_AM01O9CC-if00-port0 \\
000D6F000278D49D
Poort altijd via /dev/serial/by-id/ opgeven (stabiel na reboot).
MAC = 16 tekens van de sticker achterop het apparaat, begint met 000D6F.
"""
import sys
import time
try:
import serial
except ImportError:
sys.exit("pyserial ontbreekt: pip install pyserial (op HAOS-terminal: "
"apk add python3 py3-pip && pip install pyserial --break-system-packages)")
HEADER = b"\x05\x05\x03\x03"
FOOTER = b"\r\n"
ACKS = {b"00C1": "ACCEPT", b"00C2": "FAILED", b"00C3": "NOT_ALLOWED",
b"00E1": "TIMEOUT (geen radio-antwoord)", b"00D9": "JOIN_ACCEPTED",
b"00DD": "CIRCLEPLUS_ACK", b"00CE": "STATUS_CE"}
# ---------- framing ----------
def crc16(d: bytes) -> bytes:
"""CRC16-XMODEM, identiek aan plugwise_usb.messages.calculate_checksum."""
c = 0
for b in d:
c ^= b << 8
for _ in range(8):
c = ((c << 1) ^ 0x1021) if c & 0x8000 else c << 1
c &= 0xFFFF
return f"{c:04X}".encode()
def frame(p: bytes) -> bytes:
return HEADER + p + crc16(p) + FOOTER
def selftest():
# hardcoded frames uit PlugwiseOnLinux + plugwise-usb 0.47.6
assert frame(b"000A") == HEADER + b"000AB43C" + FOOTER
assert frame(b"0001") == HEADER + b"0001CAAB" + FOOTER
# ---------- parsing ----------
def parse(raw: bytes) -> dict:
mid, seq, data = raw[0:4], raw[4:8], raw[8:-4]
m = {"id": mid, "seq": seq, "data": data, "raw": raw}
if mid == b"0000":
m["ack"] = bytes(data[0:4])
m["type"] = "ACK " + ACKS.get(m["ack"], data[0:4].decode(errors="replace"))
elif mid == b"0011": # StickInitResponse
m["type"] = "INIT"
m["stick"] = data[0:16].decode()
if len(data) >= 32: # volledige vorm: gekoppeld aan een netwerk
m["online"] = data[18:20] == b"01"
m["controller"] = "00" + data[22:36].decode()
m["netid"] = data[36:40].decode()
else: # korte vorm: stick kent geen netwerk
m["online"] = False
m["controller"] = "(geen netwerk)"
m["netid"] = "-"
elif mid == b"0002": # netwerkinfo van de Circle+
m["type"] = "NETWERKINFO"
m["channel"] = data[0:2].decode(errors="replace")
m["ext_pan"] = data[2:18].decode(errors="replace")
m["pan"] = data[66:70].decode(errors="replace") if len(data) >= 70 else "?"
m["idx"] = data[70:72].decode(errors="replace") if len(data) >= 72 else None
elif mid == b"0003":
m["type"] = f"STATUS code={data[0:4].decode(errors='replace')}"
elif mid == b"0005": # CirclePlusConnectResponse
m["type"] = "CONNECT-RESPONSE"
m["existing"] = data[0:2].decode(errors="replace")
m["allowed"] = data[2:4].decode(errors="replace")
elif mid == b"0024":
m["type"] = "NODE-INFO"
elif mid == b"000E":
m["type"] = "PING-RESPONSE (node antwoordt!)"
elif mid == b"0061":
m["type"] = f"REJOIN van {data[0:16].decode(errors='replace')}"
else:
m["type"] = f"bericht {mid.decode(errors='replace')}"
return m
def show(m: dict):
extra = ""
if m["id"] == b"0011":
extra = f" controller={m['controller']} online={m['online']} netid={m['netid']}"
if m["id"] == b"0002":
extra = f" channel={m['channel']} extpan={m['ext_pan']} pan={m['pan']} idx={m['idx']}"
if m["id"] == b"0005":
extra = f" existing={m['existing']} allowed={m['allowed']}"
print(f" <- {m['type']}{extra}")
print(f" raw: {m['raw'].decode(errors='replace')}")
def read_msgs(port, dur, quiet=False):
end, buf, out = time.time() + dur, b"", []
while time.time() < end:
buf += port.read(256)
while HEADER in buf:
s = buf.index(HEADER) + 4
try:
e = buf.index(FOOTER, s)
except ValueError:
break
raw = buf[s:e].strip(b"\x83")
buf = buf[e + 2:]
if len(raw) >= 12:
mm = parse(raw)
out.append(mm)
if not quiet:
show(mm)
return out
# ---------- bouwstenen ----------
def stick_init(port, quiet=False):
port.reset_input_buffer()
port.write(frame(b"000A"))
for m in read_msgs(port, 2.5, quiet):
if m["id"] == b"0011":
return m
return None
def ping_node(port, mac_b, quiet=False):
port.write(frame(b"000D" + mac_b))
return any(m["id"] == b"000E" for m in read_msgs(port, 6.0, quiet))
def check_mac(mac: str) -> bytes:
mac = mac.upper().strip()
if len(mac) != 16 or not mac.startswith("000D6F"):
sys.exit(f"'{mac}' is geen geldige Plugwise-MAC (16 tekens, start 000D6F)")
return mac.encode()
# ---------- subcommando's ----------
def cmd_status(port):
print("Stick-init ...")
st = stick_init(port)
if not st:
sys.exit("Stick antwoordt niet. Poort correct? HA-integratie gestopt?")
print(f"\nStick : {st['stick']}")
print(f"Netwerk : online={st['online']} controller={st['controller']} netid={st['netid']}")
def cmd_ping(port, mac_b):
print(f"Ping naar {mac_b.decode()} ...")
print("Antwoord!" if ping_node(port, mac_b) else "Geen antwoord (timeout).")
def cmd_pair(port, mac):
mac_b = mac.encode()
print("[1] Stick-init ...")
st = stick_init(port)
if not st:
sys.exit("Stick antwoordt niet.")
if st.get("controller") == mac and st.get("online"):
print(" Al gekoppeld — klaar!")
return
print(f"\n[2] Netwerkinfo van de Circle+ (0001+{mac}) ...")
print(" (Circle+ ingeplugd, <2 m van de stick. Reset vooraf is alleen")
print(" nodig als deze stap blijft time-outen.)")
idx = b"01"
for a in range(1, 7):
port.write(frame(b"0001" + mac_b))
msgs = read_msgs(port, 5.0)
ni = next((m for m in msgs if m["id"] == b"0002"), None)
if ni:
if ni.get("idx"):
idx = ni["idx"].encode()
break
print(f" poging {a}: geen 0002")
time.sleep(2)
else:
print("\n Geen netwerkinfo. Reset de Circle+ vers:")
print(" 2s in, uit, 2s in, uit, 8s in, uit, 2s wachten, erin (klik!)")
print(" en start het pair-commando direct opnieuw.")
return
print(f"\n[3] Fase 1: 0004 key=00 idx={idx.decode()} ...")
for a in range(1, 8):
port.write(frame(b"0004" + b"00" + idx + b"0000000000000000" + mac_b))
msgs = read_msgs(port, 3.0)
if any(m["id"] == b"0000" and m.get("ack") == b"00C1" for m in msgs):
print(f" fase 1 ACCEPT (poging {a}) — de Circle+ hoort nu te klikken")
break
time.sleep(2)
print(f"\n[4] Fase 2: 0004 key=01 idx={idx.decode()} — elke 5 s, max 60 s")
print(" (moet de stick raken vóór zijn interne herstart)")
t0 = time.time()
while time.time() - t0 < 60:
port.write(frame(b"0004" + b"01" + idx + b"0000000000000000" + mac_b))
msgs = read_msgs(port, 5.0)
if any(m["id"] == b"0000" and m.get("ack") == b"00C1" for m in msgs):
print(f" fase 2 ACCEPT na {int(time.time() - t0)} s")
break
print(f"\n[5] Afronding: NodeInfoRequest (0023+{mac}) 2x ...")
for _ in range(2):
port.write(frame(b"0023" + mac_b))
read_msgs(port, 4.0)
time.sleep(1)
print("\n[6] Controle-lus (10 rondes van 15 s) ...")
for r in range(1, 11):
st = stick_init(port, quiet=True)
ctrl = st.get("controller") if st else "?"
online = st.get("online") if st else "?"
print(f" ronde {r}: controller={ctrl} online={online}")
if st and ctrl == mac and online:
if ping_node(port, mac_b, quiet=True):
print(f"\n*** GELUKT: stick gekoppeld aan {mac}, ping bevestigd ***")
print("Stick terug naar HA en de integratie weer inschakelen.")
return
time.sleep(15)
print("\nNog niet bevestigd. Herhaal 'pair' (zonder reset), of monitor langer.")
def cmd_add(port, node_mac: str):
"""Registreer een Circle/Stealth proactief op MAC (PlugwiseOnLinux-methode).
Werkt zonder join-broadcast: 0007 schrijft de node in het geheugen van de
Circle+, die hem daarna zelf uitnodigt. Node moet vers gereset zijn en
binnen bereik van de Circle+ staan."""
mac_b = node_mac.encode()
print("[1] Stick-init ...")
st = stick_init(port, quiet=True)
if not st or not st.get("online"):
sys.exit("Stick niet gekoppeld aan een netwerk — eerst 'pair' draaien.")
print(f" netwerk OK: controller={st['controller']}")
print("[2] Join-acceptatie aan (0008 01) ...")
port.write(frame(b"000801"))
read_msgs(port, 3.0)
print(f"[3] Node registreren: 0007 01 {node_mac} (3x) ...")
for a in range(1, 4):
port.write(frame(b"0007" + b"01" + mac_b))
msgs = read_msgs(port, 4.0)
if any(m["id"] == b"0000" and m.get("ack") == b"00C1" for m in msgs):
print(f" registratie geaccepteerd (poging {a})")
break
time.sleep(2)
print("[4] 60 s wachten op toetreding (0061) — module mag nu ook even")
print(" uit/in het stopcontact om hem te laten opstarten ...")
joined = any(m["id"] == b"0061" and node_mac in m["raw"].decode(errors="replace")
for m in read_msgs(port, 60.0))
print("[5] Verificatie: node-info (0023) + ping ...")
port.write(frame(b"0023" + mac_b))
info = any(m["id"] == b"0024" for m in read_msgs(port, 5.0))
ok_ping = ping_node(port, mac_b, quiet=True)
if ok_ping or info:
print(f"\n*** {node_mac} doet mee (rejoin={'ja' if joined else 'stil'}, "
f"info={'ja' if info else 'nee'}, ping={'ja' if ok_ping else 'nee'}) ***")
else:
print("\nNode antwoordt (nog) niet. Registratie kan wel gelukt zijn:")
print("controleer met scan_network.py of hij in de tabel staat, en")
print("powercycle de module daarna nog eens dicht bij de Circle+.")
def cmd_monitor(port, mac, minutes):
mac_b = mac.encode()
rounds = max(1, int(minutes * 2))
for r in range(1, rounds + 1):
st = stick_init(port, quiet=True)
ctrl = st.get("controller") if st else "?"
online = st.get("online") if st else False
line = f"[{r:02d}] controller={ctrl} online={online}"
if st and ctrl == mac and online:
ok = ping_node(port, mac_b, quiet=True)
print(line + f" ping={'JA' if ok else 'nog niet'}")
if ok:
print("\n*** KOPPELING BEVESTIGD ***")
return
else:
print(line)
time.sleep(30)
print("Niet bevestigd binnen de opgegeven tijd.")
def main():
selftest()
if len(sys.argv) < 3:
sys.exit(__doc__)
cmd, dev = sys.argv[1].lower(), sys.argv[2]
port = serial.Serial(dev, 115200, timeout=0.2)
try:
if cmd == "status":
cmd_status(port)
elif cmd == "ping":
cmd_ping(port, check_mac(sys.argv[3]))
elif cmd == "pair":
cmd_pair(port, check_mac(sys.argv[3]).decode())
elif cmd == "add":
cmd_add(port, check_mac(sys.argv[3]).decode())
elif cmd == "monitor":
minutes = float(sys.argv[4]) if len(sys.argv) > 4 else 45
cmd_monitor(port, check_mac(sys.argv[3]).decode(), minutes)
else:
sys.exit(__doc__)
finally:
port.close()
if __name__ == "__main__":
main()Toont aan welke Circle+ de stick gekoppeld is en of het netwerk online is. Eén ping naar een node. "Antwoord!" = node actief in het mesh. Koppelt de stick aan een (andere) Circle+ — de vervanger van Plugwise Wacht (standaard 45 min) tot de stick de opgegeven controller online Registreert een Circle/Stealth proactief op MAC (0008 aan → 3× 0007 → join_monitor.py — live meekijken bij het aanmelden#!/usr/bin/env python3
"""
join_monitor.py — Live meekijken (en helpen) bij het joinen van Plugwise-modules
================================================================================
Zet join-acceptatie aan op het netwerk en toont in realtime wat er gebeurt
wanneer je een geresette Circle/Stealth insteekt (of een sensor wakker maakt):
0006 = module vraagt om te mogen joinen -> wordt automatisch geaccepteerd
0061 = module is (opnieuw) toegetreden
daarna: automatische ping + node-info als verificatie
Gebruik:
python3 join_monitor.py <poort> # auto-accepteren (aanrader)
python3 join_monitor.py <poort> --handmatig # alleen kijken, niets accepteren
Stoppen: Ctrl-C (zet joinen netjes weer uit en toont een samenvatting).
Vereist: pip install pyserial. HA-integratie moet UIT staan (poort is exclusief).
Workflow per module: reset (2s in, uit, 2s in, uit, 8s in, uit, 2s wachten,
erin — klik!) -> insteken dicht bij de Circle+ -> hier de melding afwachten.
Batterijsensoren (Scan/Sense): reset via gaatje achterop, daarna wake-knop.
"""
import sys
import time
from datetime import datetime
try:
import serial
except ImportError:
sys.exit("pyserial ontbreekt: pip install pyserial")
HEADER = b"\x05\x05\x03\x03"
FOOTER = b"\r\n"
ACKS = {b"00C1": "ACCEPT", b"00C2": "FAILED", b"00C3": "NOT_ALLOWED",
b"00E1": "TIMEOUT", b"00D9": "JOINEN AANGEZET (ack)",
b"00DD": "JOINEN UITGEZET (ack)"}
def crc16(d: bytes) -> bytes:
c = 0
for b in d:
c ^= b << 8
for _ in range(8):
c = ((c << 1) ^ 0x1021) if c & 0x8000 else c << 1
c &= 0xFFFF
return f"{c:04X}".encode()
def frame(p: bytes) -> bytes:
return HEADER + p + crc16(p) + FOOTER
def selftest():
assert frame(b"000A") == HEADER + b"000AB43C" + FOOTER
assert frame(b"000801") == HEADER + b"0008014068" + FOOTER # PlugwiseOnLinux
def ts() -> str:
return datetime.now().strftime("%H:%M:%S")
def parse(raw: bytes) -> dict:
mid, seq, data = raw[0:4], raw[4:8], raw[8:-4]
m = {"id": mid, "seq": seq, "data": data, "raw": raw}
if mid == b"0000":
m["ack"] = bytes(data[0:4])
m["type"] = "ACK " + ACKS.get(m["ack"], data[0:4].decode(errors="replace"))
elif mid == b"0011":
m["type"] = "INIT"
m["stick"] = data[0:16].decode()
if len(data) >= 32:
m["online"] = data[18:20] == b"01"
m["controller"] = "00" + data[22:36].decode()
else:
m["online"] = False
m["controller"] = "(geen netwerk)"
elif mid == b"0006":
m["type"] = "JOIN-VERZOEK"
m["mac"] = data[0:16].decode(errors="replace")
elif mid == b"0061":
m["type"] = "TOEGETREDEN"
m["mac"] = data[0:16].decode(errors="replace")
elif mid == b"000E":
m["type"] = "PING-OK"
elif mid == b"0024":
m["type"] = "NODE-INFO"
else:
m["type"] = f"bericht {mid.decode(errors='replace')}"
return m
def read_msgs(port, dur):
end, buf, out = time.time() + dur, b"", []
while time.time() < end:
buf += port.read(256)
while HEADER in buf:
s = buf.index(HEADER) + 4
try:
e = buf.index(FOOTER, s)
except ValueError:
break
raw = buf[s:e].strip(b"\x83")
buf = buf[e + 2:]
if len(raw) >= 12:
out.append(parse(raw))
return out
def verify_node(port, mac: str) -> str:
"""Ping + node-info naar een net toegetreden module."""
mac_b = mac.encode()
port.write(frame(b"000D" + mac_b))
ping = any(m["id"] == b"000E" for m in read_msgs(port, 5.0))
port.write(frame(b"0023" + mac_b))
info = any(m["id"] == b"0024" for m in read_msgs(port, 5.0))
if ping and info:
return "ping OK + node-info OK"
if ping:
return "ping OK (node-info nog stil)"
return "nog geen ping-antwoord (komt vaak later alsnog)"
def summary(nodes: dict):
if not nodes:
print(" (nog geen modules gezien)")
return
for mac, st in nodes.items():
print(f" {mac} laatste status: {st}")
def main():
selftest()
if len(sys.argv) < 2:
sys.exit(__doc__)
dev = sys.argv[1]
auto = "--handmatig" not in sys.argv
port = serial.Serial(dev, 115200, timeout=0.2)
print(f"[{ts()}] Stick-init ...")
port.reset_input_buffer()
port.write(frame(b"000A"))
st = next((m for m in read_msgs(port, 2.5) if m["id"] == b"0011"), None)
if not st:
sys.exit("Stick antwoordt niet. Poort correct? HA-integratie uit?")
print(f"[{ts()}] Netwerk: controller={st['controller']} online={st['online']}")
if not st["online"]:
sys.exit("Stick is aan geen enkel netwerk gekoppeld — eerst pairen "
"(plugwise_pair_tool.py pair).")
print(f"[{ts()}] Join-acceptatie AANzetten (0008 01) ...")
port.write(frame(b"000801"))
for m in read_msgs(port, 3.0):
print(f"[{ts()}] <- {m['type']}")
mode = "AUTO-accepteren" if auto else "HANDMATIG (alleen kijken)"
print(f"\n[{ts()}] Monitor actief — modus: {mode}")
print("=" * 62)
print("Steek nu een VERS geresette module in, dicht bij de Circle+.")
print("Reset-check: eruit/erin -> klik pas na enkele seconden = klaar om")
print("te joinen. Batterijsensor: wake-knop indrukken. Stop: Ctrl-C.")
print("=" * 62)
nodes: dict[str, str] = {}
last_beat = time.time()
try:
while True:
for m in read_msgs(port, 1.0):
if m["id"] == b"0006":
mac = m["mac"]
print(f"\n[{ts()}] >>> JOIN-VERZOEK van {mac}")
nodes[mac] = "join-verzoek gezien"
if auto:
print(f"[{ts()}] accepteren (0007 01 {mac}) ...")
port.write(frame(b"0007" + b"01" + mac.encode()))
nodes[mac] = "geaccepteerd, wacht op toetreding"
else:
print(f"[{ts()}] handmatige modus: NIET geaccepteerd")
elif m["id"] == b"0061":
mac = m["mac"]
print(f"\n[{ts()}] >>> TOEGETREDEN: {mac}")
nodes[mac] = "toegetreden, verifieren ..."
time.sleep(2)
result = verify_node(port, mac)
nodes[mac] = f"toegetreden — {result}"
print(f"[{ts()}] verificatie: {result}")
print(f"[{ts()}] Status tot nu toe:")
summary(nodes)
elif m["id"] == b"0000" and m.get("ack") not in (b"00C1",):
print(f"[{ts()}] <- {m['type']}")
if time.time() - last_beat > 60:
last_beat = time.time()
print(f"[{ts()}] ... monitor draait ({len(nodes)} module(s) gezien)")
except KeyboardInterrupt:
print(f"\n[{ts()}] Join-acceptatie UITzetten (0008 00) ...")
port.write(frame(b"000800"))
read_msgs(port, 2.0)
print(f"\n[{ts()}] Samenvatting van deze sessie:")
summary(nodes)
print("\nVergeet niet: HA-integratie weer inschakelen. Nieuwe modules")
print("verschijnen daar als kale MAC's — hernoem ze meteen, en zet bij")
print("meet-Circles het relais vast (lock).")
finally:
port.close()
if __name__ == "__main__":
main()Zet het join-venster open (0008) en toont realtime elk join-verzoek Workflow per module: resetten → klik-check → insteken bij de Circle+ →
scan_network.py — nodetabel van de Circle+ uitlezen#!/usr/bin/env python3
"""
scan_network.py — Lees de nodetabel van de Circle+ uit (adres 0 t/m 63)
=======================================================================
Toont welke modules geregistreerd zijn in het Plugwise-netwerk, ook als je
geen join-melding hebt gezien. Lege plekken (FFFF...) worden overgeslagen.
Gebruik:
python3 scan_network.py <poort> <circle+-mac>
Vereist: pip install pyserial. HA-integratie uit (poort is exclusief).
"""
import sys
import time
try:
import serial
except ImportError:
sys.exit("pyserial ontbreekt: pip install pyserial")
HEADER = b"\x05\x05\x03\x03"
FOOTER = b"\r\n"
def crc16(d: bytes) -> bytes:
c = 0
for b in d:
c ^= b << 8
for _ in range(8):
c = ((c << 1) ^ 0x1021) if c & 0x8000 else c << 1
c &= 0xFFFF
return f"{c:04X}".encode()
def frame(p: bytes) -> bytes:
return HEADER + p + crc16(p) + FOOTER
def read_msgs(port, dur):
end, buf, out = time.time() + dur, b"", []
while time.time() < end:
buf += port.read(256)
while HEADER in buf:
s = buf.index(HEADER) + 4
try:
e = buf.index(FOOTER, s)
except ValueError:
break
raw = buf[s:e].strip(b"\x83")
buf = buf[e + 2:]
if len(raw) >= 12:
out.append(raw)
return out
def main():
if len(sys.argv) != 3:
sys.exit(__doc__)
dev, mac = sys.argv[1], sys.argv[2].upper().strip()
mac_b = mac.encode()
port = serial.Serial(dev, 115200, timeout=0.2)
print(f"Nodetabel van Circle+ {mac} uitlezen (64 adressen) ...\n")
found, empty, timeouts = {}, 0, 0
for addr in range(64):
a = f"{addr:02X}".encode()
port.reset_input_buffer()
port.write(frame(b"0018" + mac_b + a))
got = False
for raw in read_msgs(port, 3.0):
if raw[0:4] != b"0019":
continue
data = raw[8:-4]
# twee mogelijke indelingen afvangen:
if len(data) >= 34 and data[0:16] == mac_b:
reg = data[16:32].decode(errors="replace")
else:
reg = data[0:16].decode(errors="replace")
got = True
if reg.strip("F") == "":
empty += 1
else:
found[addr] = reg
print(f" adres {addr:2d}: {reg}")
if not got:
timeouts += 1
print(f"\nResultaat: {len(found)} geregistreerde module(s), "
f"{empty} lege plekken, {timeouts} adressen zonder antwoord.")
if found:
print("\nGeregistreerd:")
for addr, reg in found.items():
print(f" {reg} (adres {addr})")
else:
print("Geen modules in de tabel — de joins zijn dus echt nog niet gelukt.")
port.close()
if __name__ == "__main__":
main()Leest alle 64 geheugenadressen van de Circle+ (0018/0019) en toont welke probe_node.py — wat spreekt deze module wél?#!/usr/bin/env python3
"""
probe_node.py — Test welke protocolberichten een (oude) Circle beantwoordt
==========================================================================
Stuurt achtereenvolgens: ping (000D), node-info (0023), kalibratie (0026),
klok-uitlezen (003E) en vermogen (0012), en rapporteert per bericht of er
een antwoord komt. Daarmee zie je precies wat 2008-firmware wel/niet spreekt
en of een kalibratie de node-info 'wakker maakt' (Source deed 0026 altijd
direct na het toevoegen).
Gebruik: python3 probe_node.py <poort> <node-mac>
Integratie moet UIT staan.
"""
import sys
import time
try:
import serial
except ImportError:
sys.exit("pyserial ontbreekt: pip install pyserial")
HEADER = b"\x05\x05\x03\x03"
FOOTER = b"\r\n"
def crc16(d: bytes) -> bytes:
c = 0
for b in d:
c ^= b << 8
for _ in range(8):
c = ((c << 1) ^ 0x1021) if c & 0x8000 else c << 1
c &= 0xFFFF
return f"{c:04X}".encode()
def frame(p: bytes) -> bytes:
return HEADER + p + crc16(p) + FOOTER
def read_all(port, dur):
"""Lees ALLES (ook debug-tekst van de stick) en rapporteer frames + ruis."""
end, buf = time.time() + dur, b""
frames, noise = [], []
while time.time() < end:
buf += port.read(256)
# frames eruit halen
work = buf
while HEADER in work:
s = work.index(HEADER) + 4
try:
e = work.index(FOOTER, s)
except ValueError:
break
raw = work[s:e].strip(b"\x83")
work = work[e + 2:]
if len(raw) >= 12:
frames.append(raw)
# debug-tekst ('# ...') zichtbaar maken
for line in buf.split(b"\r\n"):
if b"#" in line:
noise.append(line.decode(errors="replace"))
return frames, noise
def probe(port, naam, payload, verwacht, dur=6.0):
port.reset_input_buffer()
port.write(frame(payload))
frames, noise = read_all(port, dur)
hit = [f for f in frames if f[0:4] == verwacht]
to = any(f[0:4] == b"0000" and f[8:12] == b"00E1" for f in frames)
status = "ANTWOORD" if hit else ("radio-TIMEOUT" if to else "geen antwoord")
print(f" {naam:<28} -> {status}")
for f in hit:
print(f" {f.decode(errors='replace')}")
for n in noise:
print(f" stick-debug: {n.strip()}")
return bool(hit)
def main():
if len(sys.argv) != 3:
sys.exit(__doc__)
dev, mac = sys.argv[1], sys.argv[2].upper().strip()
mac_b = mac.encode()
port = serial.Serial(dev, 115200, timeout=0.2)
print(f"Probe van {mac}:\n")
ping = probe(port, "ping (000D)", b"000D" + mac_b, b"000E")
info1 = probe(port, "node-info (0023) poging 1", b"0023" + mac_b, b"0024")
cal = probe(port, "kalibratie (0026)", b"0026" + mac_b, b"0027")
info2 = probe(port, "node-info (0023) poging 2", b"0023" + mac_b, b"0024")
clock = probe(port, "klok uitlezen (003E)", b"003E" + mac_b, b"003F")
power = probe(port, "vermogen (0012)", b"0012" + mac_b, b"0013")
print("\nConclusie:")
if not ping:
print(" Node antwoordt op niets — niet actief in het mesh. Powercycle de")
print(" module en run de probe direct opnieuw; blijft het stil, dan is de")
print(" join niet afgerond en moet reset+join opnieuw.")
elif info1 or info2:
print(" Node-info werkt (evt. na kalibratie) — dit is bruikbaar voor een")
print(" workaround richting de integratie.")
else:
print(" Node leeft, maar 0023 blijft onbeantwoord — protocol-1.x-gedrag.")
print(" Noteer welke berichten WEL antwoorden; daarmee bepalen we de")
print(" workaround (issue bij plugwise_usb-beta met deze output).")
port.close()
if __name__ == "__main__":
main()Stuurt achtereenvolgens ping (000D), node-info (0023), kalibratie (0026),
find_and_add.py — joinen met een onvolledig MAC#!/usr/bin/env python3
"""
find_and_add.py — Circle joinen als je maar 5 van de 6 MAC-tekens hebt
======================================================================
De witte labels op deze set tonen 5 tekens (bv. "7A89E"), terwijl het MAC
er 6 heeft na de prefix. Bovendien verschilt het prefix per bouwjaar:
oudere Circles (2008-2011) gebruiken 000D6F0000/0001, nieuwere 0002/0003.
Dit script probeert daarom alle 128 kandidaten (4 prefixen x teken
ontbreekt vooraan of achteraan), registreert ze via 0007, wacht op de
toetreding van de echte module, en verwijdert daarna alle loze
registraties weer uit de Circle+ (001C).
Gebruik:
python3 find_and_add.py <poort> <circle+-mac> <5-tekens-van-label>
Voorbeeld:
python3 find_and_add.py /dev/serial/by-id/usb-FTDI_... 000D6F000278D49D 7A89E
Vooraf: module vers resetten en dicht bij de Circle+ insteken.
Tijdens de wachtfase mag hij even uit/in (opstart-moment = join-moment).
"""
import sys
import time
try:
import serial
except ImportError:
sys.exit("pyserial ontbreekt: pip install pyserial")
HEADER = b"\x05\x05\x03\x03"
FOOTER = b"\r\n"
PREFIXEN = ["000D6F0000", "000D6F0001", "000D6F0002", "000D6F0003"]
HEX = "0123456789ABCDEF"
def crc16(d: bytes) -> bytes:
c = 0
for b in d:
c ^= b << 8
for _ in range(8):
c = ((c << 1) ^ 0x1021) if c & 0x8000 else c << 1
c &= 0xFFFF
return f"{c:04X}".encode()
def frame(p: bytes) -> bytes:
return HEADER + p + crc16(p) + FOOTER
def read_msgs(port, dur):
end, buf, out = time.time() + dur, b"", []
while time.time() < end:
buf += port.read(256)
while HEADER in buf:
s = buf.index(HEADER) + 4
try:
e = buf.index(FOOTER, s)
except ValueError:
break
raw = buf[s:e].strip(b"\x83")
buf = buf[e + 2:]
if len(raw) >= 12:
out.append(raw)
return out
def ping(port, mac: str, dur: float = 2.0) -> bool:
port.write(frame(b"000D" + mac.encode()))
return any(r[0:4] == b"000E" for r in read_msgs(port, dur))
def main():
if len(sys.argv) != 4:
sys.exit(__doc__)
dev, cp_mac, label = sys.argv[1], sys.argv[2].upper(), sys.argv[3].upper().strip()
if len(label) != 5 or any(ch not in HEX for ch in label):
sys.exit(f"'{label}' is geen 5-teken hexcode")
cp_b = cp_mac.encode()
kandidaten = []
for pref in PREFIXEN:
kandidaten += [pref + label + x for x in HEX]
kandidaten += [pref + x + label for x in HEX]
port = serial.Serial(dev, 115200, timeout=0.2)
print("[1] Join-acceptatie aan ...")
port.write(frame(b"000801"))
read_msgs(port, 3.0)
print(f"[2] {len(kandidaten)} kandidaten registreren (0007) ...")
for i, mac in enumerate(kandidaten, 1):
port.write(frame(b"0007" + b"01" + mac.encode()))
read_msgs(port, 0.6)
print(f"\r {i}/{len(kandidaten)}", end="", flush=True)
print()
print("[3] Max 3 min wachten op toetreding — haal de module NU even uit")
print(" het stopcontact en steek hem terug (dicht bij de Circle+) ...")
echte = None
t0 = time.time()
while time.time() - t0 < 180 and not echte:
for raw in read_msgs(port, 2.0):
if raw[0:4] == b"0061":
mac = raw[8:24].decode(errors="replace")
print(f"\n >>> TOEGETREDEN: {mac}")
echte = mac
rest = 180 - int(time.time() - t0)
print(f"\r wachten ... nog {rest:3d}s", end="", flush=True)
print()
if not echte:
print("[3b] Geen toetredingsmelding; kandidaten afpingen als vangnet ...")
for i, mac in enumerate(kandidaten, 1):
print(f"\r ping {i}/{len(kandidaten)}", end="", flush=True)
if ping(port, mac):
echte = mac
print(f"\n >>> ANTWOORDT: {mac}")
break
print()
print("[4] Loze registraties opruimen (001C) ...")
for i, mac in enumerate(kandidaten, 1):
if mac == echte:
continue
port.write(frame(b"001C" + cp_b + mac.encode()))
read_msgs(port, 0.6)
print(f"\r {i}/{len(kandidaten)}", end="", flush=True)
print()
if echte:
print(f"\n*** Volledig MAC gevonden en gejoined: {echte} ***")
print(f" (label '{label}' -> noteer dit MAC bij het apparaat)")
okp = ping(port, echte)
print(f" controle-ping: {'OK' if okp else 'nog stil — komt meestal zo'}")
print(" Verifieer evt. met scan_network.py; daar hoort nu 1 module te staan.")
else:
print("\nGeen van de 32 kandidaten trad toe of antwoordde.")
print("Mogelijkheden: reset niet vers (herhaal ritueel, of probeer de")
print("oude methode: 3x 3s in/uit), module te ver van de Circle+, of")
print("het label wijkt af van het 000D6F0002-patroon. Run daarna opnieuw.")
port.close()
if __name__ == "__main__":
main()Voor als alleen de 5 labeltekens bekend zijn en de prefix onzeker is: Historische scripts (vervangen)
SnelreceptenNieuwe (nieuwere-generatie) Circle toevoegen en controleren: "HA ziet module X niet meer": Stick of Circle+ vervangen: Extra script I used for Research#!/usr/bin/env python3
"""
Plugwise Stick <-> Circle+ pairing tool -- v5
================================================
v5-strategie (gebaseerd op de v4-bevindingen):
- fase 1 (0004 key=00) werkt en de Circle+ klikt -> dat deel is goed
- de stick REBOOT binnen ~45 s na fase 1; fase 2 kwam daardoor te laat
- daarom: fase 2 (0004 key=01) nu ELKE 5 s direct na fase 1, 60 s lang
- afronden met 0023 (NodeInfoRequest) zoals PlugwiseOnLinux deed
- Circle+ NIET opnieuw resetten: die staat al goed (eigen netwerk 82E6)
Gebruik: python3 pair_circleplus_v5.py <poort> <circle+-mac>
"""
import sys
import time
try:
import serial
except ImportError:
sys.exit("pyserial ontbreekt: pip install pyserial")
HEADER = b"\x05\x05\x03\x03"
FOOTER = b"\r\n"
ACKS = {b"00C1": "ACCEPT", b"00C2": "FAILED", b"00C3": "NOT_ALLOWED",
b"00E1": "TIMEOUT", b"00D9": "JOIN_ACCEPTED", b"00DD": "CIRCLEPLUS_ACK"}
def crc16(d: bytes) -> bytes:
c = 0
for b in d:
c ^= b << 8
for _ in range(8):
c = ((c << 1) ^ 0x1021) if c & 0x8000 else c << 1
c &= 0xFFFF
return f"{c:04X}".encode()
def frame(p: bytes) -> bytes:
return HEADER + p + crc16(p) + FOOTER
def selftest():
assert frame(b"000A") == HEADER + b"000AB43C" + FOOTER
assert frame(b"0001") == HEADER + b"0001CAAB" + FOOTER
def parse(raw: bytes) -> dict:
mid, seq, data = raw[0:4], raw[4:8], raw[8:-4]
m = {"id": mid, "seq": seq, "data": data, "raw": raw}
if mid == b"0000":
m["ack"] = bytes(data[0:4])
m["type"] = "ACK " + ACKS.get(m["ack"], data[0:4].decode(errors="replace"))
elif mid == b"0011":
m["type"] = "INIT"
m["stick"] = data[0:16].decode()
if len(data) >= 32:
m["online"] = data[18:20] == b"01"
m["controller"] = "00" + data[22:36].decode()
m["netid"] = data[36:40].decode()
else:
m["online"] = False
m["controller"] = "(kort antwoord: niet gekoppeld)"
m["netid"] = "-"
elif mid == b"0002":
m["type"] = "NETWERKINFO"
m["channel"] = data[0:2].decode(errors="replace")
m["ext_pan"] = data[2:18].decode(errors="replace")
m["pan"] = data[66:70].decode(errors="replace") if len(data) >= 70 else "?"
m["idx"] = data[70:72].decode(errors="replace") if len(data) >= 72 else None
elif mid == b"0003":
m["type"] = f"STATUS code={data[0:4].decode(errors='replace')}"
elif mid == b"0005":
m["type"] = "CONNECT-RESPONSE"
m["existing"] = data[0:2].decode(errors="replace")
m["allowed"] = data[2:4].decode(errors="replace")
elif mid == b"0024":
m["type"] = "NODE-INFO (0024) — registratie-antwoord"
elif mid == b"000E":
m["type"] = "PING-RESPONSE (Circle+ antwoordt!)"
elif mid == b"0061":
m["type"] = f"REJOIN van {data[0:16].decode(errors='replace')}"
else:
m["type"] = f"bericht {mid.decode(errors='replace')}"
return m
def show(m: dict):
extra = ""
if m["id"] == b"0011":
extra = f" controller={m['controller']} online={m['online']} netid={m['netid']}"
if m["id"] == b"0002":
extra = f" channel={m['channel']} extpan={m['ext_pan']} pan={m['pan']} idx={m['idx']}"
if m["id"] == b"0005":
extra = f" existing={m['existing']} allowed={m['allowed']}"
print(f" <- {m['type']}{extra}")
print(f" raw: {m['raw'].decode(errors='replace')}")
def read_msgs(port, dur):
end, buf, out = time.time() + dur, b"", []
while time.time() < end:
buf += port.read(256)
while HEADER in buf:
s = buf.index(HEADER) + 4
try:
e = buf.index(FOOTER, s)
except ValueError:
break
raw = buf[s:e].strip(b"\x83")
buf = buf[e + 2:]
if len(raw) >= 12:
mm = parse(raw)
out.append(mm)
show(mm)
return out
def init(port):
port.reset_input_buffer()
port.write(frame(b"000A"))
for m in read_msgs(port, 2.5):
if m["id"] == b"0011":
return m
return None
def main():
selftest()
if len(sys.argv) != 3:
sys.exit(__doc__)
dev, mac = sys.argv[1], sys.argv[2].upper().strip()
mac_b = mac.encode()
port = serial.Serial(dev, 115200, timeout=0.2)
print("[1] Stick-init ...")
st = init(port)
if not st:
sys.exit("Stick antwoordt niet.")
if st.get("controller") == mac and st.get("online"):
print(" Al gekoppeld — klaar!")
return
print(f"\n[2] Netwerkinfo van de Circle+ (0001+{mac}) — GEEN reset vooraf nodig ...")
idx = b"01"
got_info = False
for a in range(1, 7):
port.write(frame(b"0001" + mac_b))
msgs = read_msgs(port, 5.0)
ni = next((m for m in msgs if m["id"] == b"0002"), None)
if ni:
got_info = True
if ni.get("idx"):
idx = ni["idx"].encode()
break
print(f" poging {a}: geen 0002")
time.sleep(2)
if not got_info:
print(" Circle+ geeft geen netwerkinfo. Reset hem alsnog vers")
print(" (2s in,uit,2s in,uit,8s in,uit,2s wachten,erin) en run opnieuw.")
print(f"\n[3] Fase 1: 0004 key=00 idx={idx.decode()} ...")
for a in range(1, 8):
port.write(frame(b"0004" + b"00" + idx + b"0000000000000000" + mac_b))
msgs = read_msgs(port, 3.0)
if any(m["id"] == b"0000" and m.get("ack") == b"00C1" for m in msgs):
print(f" fase 1 ACCEPT (poging {a}) — luister of de Circle+ klikt!")
break
time.sleep(2)
print(f"\n[4] Fase 2: 0004 key=01 idx={idx.decode()} — elke 5 s, 60 s lang ...")
fase2 = False
t0 = time.time()
while time.time() - t0 < 60:
port.write(frame(b"0004" + b"01" + idx + b"0000000000000000" + mac_b))
msgs = read_msgs(port, 5.0)
if any(m["id"] == b"0000" and m.get("ack") == b"00C1" for m in msgs):
print(f" fase 2 ACCEPT na {int(time.time()-t0)} s!")
fase2 = True
break
if not fase2:
print(" fase 2 kreeg geen ACCEPT binnen 60 s — we ronden toch af met 0023.")
print(f"\n[5] Afronding: NodeInfoRequest (0023+{mac}) 2x ...")
for _ in range(2):
port.write(frame(b"0023" + mac_b))
read_msgs(port, 4.0)
time.sleep(1)
print("\n[6] Controle-lus: init + ping, 10 rondes van 15 s ...")
for r in range(1, 11):
st = init(port)
ok = st and st.get("controller") == mac and st.get("online")
print(f" ronde {r}: controller={st.get('controller') if st else '?'} online={st.get('online') if st else '?'}")
if ok:
port.write(frame(b"000D" + mac_b))
if any(m["id"] == b"000E" for m in read_msgs(port, 6.0)):
print(f"\n*** GELUKT: gekoppeld aan {mac} + ping bevestigd ***")
print("Stick terug naar HA en integratie inschakelen!")
return
time.sleep(15)
print("\nNog niet bevestigd — stuur de volledige output door.")
port.close()
if __name__ == "__main__":
main() |
Uh oh!
There was an error while loading. Please reload this page.
Summary
Old (week 50/2008) Circles successfully join the network of a 2013 Circle+ — join request (
0006), NodeAdd (0007), rejoin notification (0061) all work, and the nodes appear in the Circle+ node table (verified via0018/0019scans, addresses 0–3). However, they never answer any unicast application request: NodeInfo (0023), ping (000D), calibration (0026), clock get (003E) and power usage (0012) all end in a stick-level00E1timeout, on freshly joined nodes as well. Discovery therefore logsNode <mac> with unknown NodeType not responding, is it offline?and the nodes never become HA devices.A 2013 Circle+ on the same network answers every one of those requests instantly, so the stick, wiring and RF environment are fine.
Evidence
1. Node table of the Circle+ (0018 scan) — registrations persist
2. Join flow works (live monitor, allow-join 0008 enabled)
3. Integration debug log — discovery of an old Circle
Note the stick emits plain-text debug lines (
# APSRequestNodeInfo: ...,# SENDING PING UNICAST: Macid: ...,# HANDLE: 0x..) interleaved with the framed protocol data. These appear for healthy nodes too, so they are probably harmless — mentioned for completeness because they do end up inside the receiver's buffer.4. Manual probe (raw serial, integration disabled)
Healthy 2013 Circle+
000D6F000278D49D— answers everything:2008 Circle
000D6F000038C383, freshly joined minutes earlier (0061 seen):Identical result for all four 2008 units, immediately after joining as well as later. So: network/MAC layer (join, registry) fine; APS/application layer never answers.
Questions
CIRCLE_FIRMWARE_SUPPORT) actually expected to answer0023? Or did these very early firmwares use a different request set that Source handled differently (e.g. required calibration0026/clock set0016first, or a different message id for node info)?0023, would you accept a documentation note (e.g. "Circles with firmware < 2009-09-08 require a firmware update via Plugwise Source, whose servers appear to be offline") and/or a friendlier log message than the generic "is it offline?" warning?Reproduction
000D/0023/0026/003E/0012) to the node times out at the stick with ack00E1, while the same requests to the 2013 Circle+ succeed.Full debug logs (integration + raw serial captures) available on request.
All reactions