"""Matter onboarding payloads: the "MT:..." QR string and the 11-digit manual code. Implements the Matter spec (5.1.3 QR payload, 5.1.4 manual pairing code): bit fields packed LSB-first, Base38 in 3-byte chunks, Verhoeff check digit. The sensorbox firmware uses the Matter SDK example commissionable data (CONFIG_EXAMPLE_COMMISSIONABLE_DATA_PROVIDER): every v1.0 box advertises VID 0xFFF1 / PID 0x8000 / discriminator 3840 with passcode 20202021, which is exactly the sticker (QR "MT:Y.K9042C00KA0648G00", manual 3497-011-2332). VID/PID/discriminator are taken from the box's own log when available; the passcode is never logged, so the known default is used. """ from __future__ import annotations import re from dataclasses import dataclass BASE38 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-." RENDEZVOUS_BLE = 1 << 1 DEFAULT_PASSCODE = 20202021 # Matter SDK test passcode used by the v1.0 firmware DEFAULT = dict(vendor_id=0xFFF1, product_id=0x8000, discriminator=3840) # chip[DIS]: Advertise commission parameter vendorID=65521 productID=32768 discriminator=3840/15 cm=1 cp=0 _ADVERTISE = re.compile(r"vendorID=(\d+) productID=(\d+) discriminator=(\d+)") @dataclass(frozen=True) class SetupPayload: vendor_id: int = DEFAULT["vendor_id"] product_id: int = DEFAULT["product_id"] discriminator: int = DEFAULT["discriminator"] passcode: int = DEFAULT_PASSCODE rendezvous: int = RENDEZVOUS_BLE flow: int = 0 # standard commissioning flow version: int = 0 from_log: bool = False # VID/PID/discriminator read from this box's log qr: str | None = None # exact payload reported by the box ("KOTA: matter qr=MT:...") manual: str | None = None # exact 11-digit manual code reported by the box @property def from_box(self) -> bool: return self.qr is not None def qr_text(self) -> str: if self.qr: return self.qr fields = [(self.version, 3), (self.vendor_id, 16), (self.product_id, 16), (self.flow, 2), (self.rendezvous, 8), (self.discriminator, 12), (self.passcode, 27), (0, 4)] value, shift = 0, 0 for v, bits in fields: if not 0 <= v < (1 << bits): raise ValueError(f"field value {v} does not fit in {bits} bits") value |= v << shift shift += bits return "MT:" + _base38(value.to_bytes(shift // 8, "little")) def manual_code(self) -> str: if self.manual: return self.manual short = self.discriminator >> 8 # upper 4 bits chunk1 = short >> 2 # vid_pid_present = 0 chunk2 = ((short & 0x3) << 14) | (self.passcode & 0x3FFF) chunk3 = self.passcode >> 14 digits = f"{chunk1}{chunk2:05d}{chunk3:04d}" return digits + str(verhoeff_check(digits)) def manual_code_pretty(self) -> str: c = self.manual_code() return f"{c[:4]}-{c[4:7]}-{c[7:]}" def _base38(data: bytes) -> str: out = [] for i in range(0, len(data), 3): chunk = data[i:i + 3] value = int.from_bytes(chunk, "little") for _ in range({3: 5, 2: 4, 1: 2}[len(chunk)]): value, rem = divmod(value, 38) out.append(BASE38[rem]) return "".join(out) _D = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 2, 3, 4, 0, 6, 7, 8, 9, 5], [2, 3, 4, 0, 1, 7, 8, 9, 5, 6], [3, 4, 0, 1, 2, 8, 9, 5, 6, 7], [4, 0, 1, 2, 3, 9, 5, 6, 7, 8], [5, 9, 8, 7, 6, 0, 4, 3, 2, 1], [6, 5, 9, 8, 7, 1, 0, 4, 3, 2], [7, 6, 5, 9, 8, 2, 1, 0, 4, 3], [8, 7, 6, 5, 9, 3, 2, 1, 0, 4], [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]] _P = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 5, 7, 6, 2, 8, 3, 0, 9, 4], [5, 8, 0, 3, 7, 9, 6, 1, 4, 2], [8, 9, 1, 6, 0, 4, 3, 5, 2, 7], [9, 4, 5, 3, 1, 2, 6, 8, 7, 0], [4, 2, 8, 6, 5, 7, 3, 9, 0, 1], [2, 7, 9, 3, 8, 0, 6, 4, 1, 5], [7, 0, 4, 6, 9, 1, 3, 2, 5, 8]] _INV = [0, 4, 3, 2, 1, 5, 6, 7, 8, 9] def verhoeff_check(digits: str) -> int: c = 0 for i, d in enumerate(reversed(digits)): c = _D[c][_P[(i + 1) % 8][int(d)]] return _INV[c] def payload_from_log(lines: list[str], qr: str | None = None, manual: str | None = None) -> SetupPayload: """Firmware 1.0.16+ reports its exact QR payload and manual code ("KOTA: matter ... qr=MT:... manual=..."); older firmware: use the advertised VID/PID/discriminator if seen; defaults otherwise.""" base = SetupPayload() for text in reversed(lines): if m := _ADVERTISE.search(text): base = SetupPayload(int(m.group(1)), int(m.group(2)), int(m.group(3)), from_log=True) break if qr: return SetupPayload(base.vendor_id, base.product_id, base.discriminator, from_log=True, qr=qr, manual=manual if manual and manual.isdigit() else None) return base