Updated matter decoder to show more info, and added a homekit decoder.

This commit is contained in:
Scott Garrett committed 2026-08-31 19:46:47 -04:00
1 parent 548f2e701f
commit 27ebf13f6f
8 files changed
+831 -6

No files matched your search

+1
View File
@@ -0,0 +1 @@
.DS_Store
+89 -2
View File
@@ -21,6 +21,25 @@ flow, discovery capabilities, discriminator, passcode). Three artifacts:
another URL or a local XML file; passing `matter-qr-decoder.html` uses the
embedded `<map>` block as an offline fallback. Reproduces the committed TSV
byte-for-byte.
- `products.tsv` — 3-column `vendor ID → product ID → name` table, consumed by
`matter-decode.sh`'s `get_product_name()`: when the decoded vendor ID (or an
equivalent, see `vendor-equivalents.tsv`) is present, the product ID is
resolved to a name and printed on the Product ID line. LIFX's rows are keyed
under `4870`. Optional — a missing file only drops the product-name lookup.
- `vendor-equivalents.tsv` — 2-column `alias vendor ID → canonical vendor ID`
table, hand-edited, recording companies that are the same after a
merger/acquisition (e.g. Feit `5155` → LIFX `4870`). Used by
`matter-decode.sh` so a product resolves under either ID and the vendor name
is annotated `(a.k.a. ...)`. `#`-comment lines are ignored. Optional.
- `gen-lifx-products.py` — Python 3 stdlib script that regenerates LIFX's rows
(vendor `4870`) in `products.tsv`. Default source is LIFX's upstream
`products.json` **fetched over the network** (via `curl`, falling back to
`urllib`); a positional arg overrides with another URL or a local `.json`
file. It **merges** LIFX's rows into `products.tsv` (dropping LIFX's old
rows, preserving every other vendor's), so re-running it is safe alongside
other vendors' generators. Caveat: `products.json` carries only a `pid` per
product (no separate Matter/CHIP product-id field), so the lookup assumes
LIFX's Matter product ID equals their `pid`.
- `README.md` — user-facing docs; this file documents the internals.
## Commands
@@ -74,6 +93,64 @@ Everything lives in `matter-qr-decoder.html` (~1500 lines). Two regions matter:
`parseInt(code, 16)`. It is exposed reactively through the `vendorName` `computed`, so
the lookup re-runs when the decoded vendor ID changes.
### Product name lookup + vendor equivalences (bash only)
Unlike the vendor-name lookup, the product-name lookup and the vendor-equivalence
table live **only in `matter-decode.sh`** — the browser app has neither. This is
a deliberate, documented divergence from the usual "keep the two in lockstep"
rule, chosen because the product catalog and the equivalence list change more
often than the vendor map and are not yet worth embedding in the static HTML.
`matter-decode.sh` defines two helpers:
- `equivalent_vendor_ids VENDOR` — resolves VENDOR to its whole equivalence class
via `vendor-equivalents.tsv` (2 cols `alias \t canonical`, `#` lines ignored,
canonical-form: the canonical id never appears in col 1). It prints the vendor
itself plus every alias sharing its canonical id, sorted. A missing
`vendor-equivalents.tsv` yields just the vendor.
- `get_product_name VENDOR PRODUCT` — checks `products.tsv` (3 cols `vendor \t
product \t name`) for the exact vendor first, then every equivalent; first
match wins. A missing `products.tsv` yields no name. It must `return 0` on a
miss: under `set -e`, a non-zero status in the `pname=$(...)` substitution
would abort the script before anything prints.
In `main()`, the product name (`pname`) is printed on the Product ID line via the
same `[[ -n $pname ]] && printf` pattern the vendor name uses. The vendor
name is annotated with its equivalents: the *other* class members' names (from
`manufacturers.tsv`) are joined and appended as `(a.k.a. X, ...)`, so a Feit
(`5155`) device reads `Feit Electric Company, Inc. (a.k.a. Lifi Labs INC. dba
LIFX)` and a LIFX (`4870`) device reads the mirror image. Both `products.tsv` and
`vendor-equivalents.tsv` are optional; a missing file only drops its extra.
`gen-lifx-products.py` regenerates LIFX's rows into `products.tsv` by merging
(dropping only vendor `4870`'s rows, preserving others). To add another vendor,
write a sibling `gen-<vendor>-products.py` that parses that vendor's catalog and
merges its rows — keyed under its own vendor ID — into the same `products.tsv`;
then add that vendor's equivalences (if any) to `vendor-equivalents.tsv`.
Mirroring this into the HTML app is the natural next step if it is wanted.
### Manual pairing code (bash only)
`matter-decode.sh` also prints a **Pairing Code:** line: the Matter *manual
pairing code* — the PIN a commissioning app (Apple Home, Google Home, the Matter
app) asks you to type. It is **not** a raw payload field; it is *derived* from
the passcode and discriminator and, for non-standard commissioning flows, the
vendor/product IDs, then guarded by a Verhoeff check digit. This is a port of
`generateManualPairingCode()` from the standalone "Matter QR Code Tool"
(`chill-uk/matter-qr-app`, `web/app.js`); the browser app in this repo has
neither this nor the product/equivalence lookups, so like them it is bash-only.
Two helpers, defined just above `main()`:
- `verhoeff_check_digit DIGITS` — the Verhoeff check digit over a decimal string
(the three standard Verhoeff tables are inlined; digits are processed
right-to-left, as the reference does).
- `compute_pairing_code VENDOR PRODUCT FLOW DISCRIMINATOR PASSCODE` — assembles
the code. The 12-bit discriminator contributes its top 4 bits; the 27-bit
passcode is split into its low 14 and high 13 bits; non-standard flows
(user-intent `1`, custom `2`) append 5-digit vendor and product groups. Result:
11 digits for the standard flow, 21 otherwise, plus the check digit.
## Gotchas
- The `BarcodeDetector` scanner polls the camera via `setInterval` every 200ms and
@@ -86,8 +163,9 @@ the lookup re-runs when the decoded vendor ID changes.
- `matter-decode.sh` is a bash port of the HTML app's decode pipeline, kept in lockstep
with it — the base38 charset/chunking, little-endian byte extraction, and the 84-bit
LSB-first field layout are identical. When you change the decode logic in the HTML app,
mirror it in the script (and vice versa). The script's only extra is a `manufacturers.tsv`
lookup for the vendor name; without that file it still decodes everything else.
mirror it in the script (and vice versa). The script's extras are the optional `manufacturers.tsv` (vendor name),
`products.tsv` (product name), and `vendor-equivalents.tsv` (vendor
equivalences); without any of them it still decodes everything else.
- `manufacturers.tsv` is regenerated with `gen-manufacturers.py` (Python 3
stdlib). Default source is the **upstream `manufacturers.xml` fetched over the
network** (via `curl`; a GitHub `blob/` URL is auto-rewritten to its
@@ -99,3 +177,12 @@ the lookup re-runs when the decoded vendor ID changes.
`<script type="text/xml">` block in the HTML app has the same upstream
provenance and a "do not edit manually" note, so refreshing the TSV from new
upstream XML leaves the HTML copy stale. Refresh both from the same source.
- The product-name lookup and vendor equivalences are **bash-only** —
`matter-decode.sh` has them, the browser app does not, so this is one place the
two are *not* in lockstep (deliberate, for now). `products.tsv` and
`vendor-equivalents.tsv` are optional and degrade silently (a missing file means
no name / no equivalent, never an error). `vendor-equivalents.tsv` is
canonical-form, one hop: the canonical id must not itself appear in col 1.
`get_product_name` must `return 0` on a miss or `set -e` aborts the decode.
Mirror these into the HTML app (a second embedded map plus a `productName` /
`equivalentVendorName` computed) only if that is wanted.
+77 -2
View File
@@ -18,11 +18,12 @@ split into these fields:
|---|---|
| **Version** | Encoding version (3 bits) |
| **Vendor ID** | 16-bit manufacturer identifier, shown as decimal + `0x`-hex, with a name looked up from `manufacturers.tsv` when available |
| **Product ID** | 16-bit vendor-assigned product identifier, decimal + `0x`-hex |
| **Product ID** | 16-bit vendor-assigned product identifier, decimal + `0x`-hex; for any vendor with a catalog the product ID is also resolved to a name from `products.tsv` (and, via `vendor-equivalents.tsv`, an equivalent vendor's catalog) when available |
| **Custom Flow** | Commissioning flow type: 0 = Standard, 1 = User Intent, 2 = Custom, 3 = Reserved |
| **Discovery** | How the device can be found: **SoftAP** / **BLE** / **On IP Network** (a bitset — a device may show more than one) |
| **Discriminator** | 12-bit discriminator |
| **Passcode** | 27-bit commissioning passcode |
| **Pairing Code** | The Matter *manual pairing code* — the PIN a commissioning app (Apple Home, Google Home, the Matter app) asks you to type. Not a raw field: derived from the passcode, discriminator and (for non-standard flows) the vendor/product IDs, then guarded by a Verhoeff check digit. 11 digits for the standard flow, 21 for user-intent/custom |
## Components
@@ -32,6 +33,9 @@ split into these fields:
| [`matter-decode.sh`](matter-decode.sh) | A pure-bash CLI port of the same decode logic. No camera scanning. |
| [`manufacturers.tsv`](manufacturers.tsv) | Tab-separated `vendor ID → name` table (~900 rows) used to turn a numeric vendor ID into a name. Shared by both tools; optional. |
| [`gen-manufacturers.py`](gen-manufacturers.py) | Python 3 script that regenerates `manufacturers.tsv`; pulls the upstream `manufacturers.xml` over the network by default (or a URL/local file/HTML block you pass in). |
| [`products.tsv`](products.tsv) | Tab-separated `vendor ID → product ID → name` table (3 columns) used by `matter-decode.sh` to turn a product ID into a name, for any vendor that has a catalog (e.g. **LIFX**, vendor `0x1306` / `4870`). Optional. |
| [`vendor-equivalents.tsv`](vendor-equivalents.tsv) | Tab-separated `alias vendor ID → canonical vendor ID` table recording companies that are the same after a merger/acquisition (e.g. Feit `5155` = LIFX `4870`), so a product resolves under either ID. Hand-edited; optional. |
| [`gen-lifx-products.py`](gen-lifx-products.py) | Python 3 script that (re)generates LIFX's rows in `products.tsv`, merging them in from LIFX's upstream `products.json` over the network by default (or a URL/local file); preserves other vendors' rows. |
`CLAUDE.md` documents the internals for anyone (or Claude Code) working on the
code.
@@ -89,6 +93,7 @@ Custom Flow: User Intent Commissioning Flow
Discovery: BLE
Discriminator:1723
Passcode: 8126392
Pairing Code: 549080049513012506168
```
(The vendor ID `0x32D4` isn't in `manufacturers.tsv`, so no name is printed —
@@ -98,6 +103,25 @@ The script is pure bash (3.x+). It reads `manufacturers.tsv` from its own
directory to resolve the vendor name; without that file every field still decodes
and only the vendor name is omitted.
For any vendor that has a catalog in `products.tsv` (e.g. **LIFX**, vendor ID
`4870` / `0x1306`) the script additionally resolves the **product ID** to a name,
printed on the Product ID line just like the vendor name on the Vendor ID line.
When a vendor has an **equivalent** in `vendor-equivalents.tsv` (a company merged
with or acquired — e.g. Feit `5155` = LIFX `4870`), the lookup also checks the
equivalent's catalog and the Vendor ID line is annotated, so either company's
device resolves to the shared product name:
```
$ echo "MT:R5QU6X.D01M3QG5.000" | ./matter-decode.sh
Version: 1
Vendor ID: 4870 (0x1306) Lifi Labs INC. dba LIFX (a.k.a. Feit Electric Company, Inc.)
Product ID: 123 (0x007B) LIFX Mini 3.1 Color US
```
Both files are optional, like `manufacturers.tsv`: without `products.tsv` the
product name is omitted, and without `vendor-equivalents.tsv` no equivalent is
considered — but every field still decodes.
---
## Data format
@@ -154,7 +178,58 @@ The same table is also embedded as a `<map>` XML block near the end of
only the TSV, so if you refresh the TSV from new upstream XML, also paste that XML
into the HTML's `<script type="text/xml">` block to keep the two copies in sync.
## Product name lookup
`matter-decode.sh` also maps a **Product ID** to a name, for any vendor that has
a catalog. The product map is `products.tsv` (three columns: `vendor ID \t
product ID \t name`); when the decoded vendor ID — or any of its equivalents, see
below — is present, the product ID is resolved to a name and printed on the
Product ID line. The table is optional — a missing file only drops the product
name, like `manufacturers.tsv` for the vendor name.
LIFX's rows (vendor `4870`) are derived from LIFX's upstream
`LIFX/products .../products.json` — **refresh them from there rather than
hand-editing.** One caveat: that file carries only a `pid` per product, with no
separate Matter/CHIP product-id field, so the lookup assumes LIFX's Matter
product ID equals their `pid`.
To refresh LIFX's rows, run **`gen-lifx-products.py`** (Python 3 standard
library only). By default it **pulls LIFX's `products.json` over the network**
(via `curl`, falling back to `urllib`) and **merges** the rows into
`products.tsv` — replacing LIFX's (vendor `4870`) rows in place and leaving any
other vendors' rows untouched:
```sh
python3 gen-lifx-products.py
```
To refresh from a different source, pass a URL or a local `.json` file:
```sh
python3 gen-lifx-products.py https://.../products.json
python3 gen-lifx-products.py ./products.json
```
Add another vendor the same way: a `gen-<vendor>-products.py` that parses that
vendor's catalog and merges its rows — keyed under its own vendor ID — into the
same `products.tsv`.
## Vendor equivalences
Some vendor IDs belong to the same company after a merger or acquisition — e.g.
Feit Electric (`5155`) acquired LIFX (`4870`). Record such equivalences in
`vendor-equivalents.tsv` (two columns: `alias vendor ID \t canonical vendor ID`;
lines starting with `#` are ignored). When a decoded vendor has an equivalent,
the product lookup also checks the equivalent's catalog and the Vendor ID line is
annotated `(a.k.a. <equivalent name>)`, so a Feit device resolves to a LIFX
product name and vice-versa.
The table is hand-edited and optional: a missing file simply means no equivalent
is considered. The canonical (col-2) id should not itself appear in col-1.
## License
The vendor mapping is derived from Matter/CHIP's `manufacturers.xml`, which is
Apache-2.0. The app itself is a small single-file utility.
Apache-2.0. The LIFX product mapping is derived from LIFX's `products.json`.
Vendor equivalences (`vendor-equivalents.tsv`) are maintained locally. The app
itself is a small single-file utility.
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
# Regenerate LIFX's rows in products.tsv from LIFX's upstream product catalog.
#
# products.tsv has three columns, "<vendor id>\t<product id>\t<name>", sorted by
# (vendor id, product id). It is consumed by matter-decode.sh's get_product_name():
# when a decoded Matter QR's vendor ID has an equivalent in
# vendor-equivalents.tsv, the product ID is looked up here (under any equivalent
# id) to print a human name alongside it. LIFX's Matter vendor id is 4870; the
# rows below are LIFX's, keyed under 4870.
#
# The table is derived from upstream LIFX's products.json -- refresh from there
# rather than hand-editing.
#
# https://raw.githubusercontent.com/LIFX/products/refs/heads/master/products.json
#
# By default the script pulls the upstream file over the network (via curl,
# falling back to urllib) and MERGES LIFX's rows into products.tsv, preserving
# any other vendors' rows already there:
#
# python3 gen-lifx-products.py
#
# A positional argument overrides the source with another URL or a local .json
# file; --output (default: products.tsv) overrides the target. Merging is
# idempotent: re-running replaces LIFX's (vendor 4870) rows in place and leaves
# every other vendor's rows untouched. Requires only the Python 3 standard
# library.
#
# NOTE: the upstream file carries only "pid" per product -- there is no separate
# Matter/CHIP product-id field, so the lookup assumes LIFX's Matter product ID
# equals their "pid". If that ever changes, adjust the mapping here.
import argparse
import json
import shutil
import subprocess
import sys
import urllib.request
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
# LIFX's Matter/CHIP manufacturer id.
LIFX_VID = 4870
# Pinned upstream source -- LIFX's product catalog.
UPSTREAM = ("https://raw.githubusercontent.com/LIFX/products/"
"refs/heads/master/products.json")
# fetch a URL to text via curl (preferred) or urllib (fallback).
def fetch_url(url):
if shutil.which("curl"):
try:
out = subprocess.run(["curl", "-fsSL", url],
capture_output=True, check=True).stdout
return out.decode("utf-8")
except subprocess.CalledProcessError as e:
sys.stderr.write(e.stderr.decode("utf-8", "replace"))
sys.exit(f"error: failed to fetch {url}")
req = urllib.request.Request(url, headers={"User-Agent": "gen-lifx-products.py"})
try:
with urllib.request.urlopen(req) as resp:
return resp.read().decode("utf-8")
except Exception as e:
sys.exit(f"error: failed to fetch {url}: {e}")
# return JSON text for a URL or a local .json path.
def read_source(source):
if source.startswith(("http://", "https://")):
return fetch_url(source)
path = Path(source)
if not path.exists():
sys.exit(f"error: no such file: {path}")
return path.read_text(encoding="utf-8")
# return [(product_id, name), ...] from LIFX's products.json. The file is a list
# of vendor entries, each with a "products" list of {"pid", "name"}; flatten all
# and warn on a duplicate pid (later entry wins).
def rows_from_json(json_text):
data = json.loads(json_text)
rows = []
seen = {}
for vendor in data:
for prod in vendor.get("products", []):
pid = prod.get("pid")
name = prod.get("name", "")
if pid is None:
continue
if pid in seen:
sys.stderr.write(
f"warning: duplicate pid {pid} "
f"({seen[pid]!r} vs {name!r}); later entry wins\n")
seen[pid] = name
rows.append((int(pid), name))
return rows
# keep the (vendor, product, name) rows in `path` whose vendor is not `vendor`,
# skipping blank and '#' comment lines. Used so a re-run replaces only this
# vendor's rows and leaves the others' intact.
def load_other_rows(path, vendor):
kept = []
p = Path(path)
if not p.exists():
return kept
for line in p.read_text(encoding="utf-8").splitlines():
if not line.strip() or line.lstrip().startswith("#"):
continue
cols = line.split("\t")
if len(cols) < 3 or cols[0].strip() == "":
sys.stderr.write(f"warning: skipping malformed row: {line!r}\n")
continue
try:
v = int(cols[0])
pid = int(cols[1])
except ValueError:
sys.stderr.write(f"warning: skipping malformed row: {line!r}\n")
continue
if v != vendor:
kept.append((v, pid, cols[2]))
return kept
# merge this vendor's freshly parsed rows into the shared products.tsv: drop the
# vendor's existing rows, add the new ones, re-sort by (vendor, product), write
# back. Returns the number of rows written for this vendor.
def merge_products(path, new_rows, vendor):
kept = load_other_rows(path, vendor)
combined = kept + [(vendor, pid, name) for (pid, name) in new_rows]
combined.sort(key=lambda r: (r[0], r[1]))
with open(path, "w", encoding="utf-8", newline="") as f:
for v, pid, name in combined:
f.write(f"{v}\t{pid}\t{name}\n")
return len(new_rows)
def main():
ap = argparse.ArgumentParser(
description="Regenerate LIFX's rows (vendor 4870) in products.tsv from "
"LIFX's upstream products.json, merged with other vendors "
"(fetches upstream by default).")
ap.add_argument(
"source", nargs="?", default=UPSTREAM,
help="a URL or local .json file (default: LIFX's upstream products.json)")
ap.add_argument(
"-o", "--output", default=str(SCRIPT_DIR / "products.tsv"),
help="output path (default: products.tsv next to this script); "
"LIFX's rows are merged in, other vendors preserved")
args = ap.parse_args()
json_text = read_source(args.source)
rows = rows_from_json(json_text)
if not rows:
sys.exit("error: no products found in the source")
out = Path(args.output)
n = merge_products(out, rows, LIFX_VID)
sys.stderr.write(
f"merged {n} LIFX products into {out} (from {args.source})\n")
if __name__ == "__main__":
main()
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env bash
#
# homekit-decode.fixed.sh — corrected decoder for a HomeKit setup payload.
#
# This is a fixed copy of homekit-decode.sh. It decodes using the *correct*
# HomeKit bit layout (the one composeSetupUri in the homekit-code package
# actually encodes), instead of the misaligned bit positions in the original.
#
# Correct layout, least-significant bit first:
# bits 0-26 pin (27 bits)
# bits 27-30 flag (4 bits: 1=NFC 2=IP 4=BLE 8=WAC/MFi)
# bits 31-38 categoryId (8 bits)
# bits 39-42 reserved (4 bits)
# bits 43-45 version (3 bits)
#
# Fixes vs homekit-decode.sh:
# - categoryId was (val >> 27) & 0xFF, which mixed in the flag field; the
# categoryId field starts at bit 31, so it is now (val >> 31) & 0xFF.
# - The original "IP/BLE/Thread" flags read bits 35/36/37, which live inside
# the categoryId field. The real flag field is bits 27-30 with sub-flags
# NFC(1)/IP(2)/BLE(4)/WAC(8); "Thread" was a misread and is dropped.
# - version was (val >> 38) & 0xFF, overlapping reserved; it is a 3-bit field
# at bits 43-45, so it is now (val >> 43) & 0x7.
# - reserved (bits 39-42) is reported instead of being swallowed by version.
# - the category id is resolved to its HAP name (e.g. 8 -> "Switch"); ids not
# in the known table print as "unknown".
#
# Usage: ./homekit-decode.fixed.sh [PAYLOAD]
# Defaults to the demo payload "005KEOUCE" when no argument is given, matching
# the original so the two scripts decode the same thing by default.
# The output also prints the setup id and a reconstructed "X-HM://" payload
# rebuilt from the decoded fields: a complete record for recreating the barcode.
set -euo pipefail
# Strip the "X-HM://" prefix if present. The first 9 chars are the fixed
# parameter block; the rest is the (variable-length) setup id, appended verbatim
# and not part of the numeric block.
body="${1:-005KEOUCE}"
body="${body#X-HM://}"
clean_str="${body:0:9}"
setup_id="${body:9}"
# Step 1: Base36 -> integer (same as homekit-decode.sh).
val=0
for (( i = 0; i < ${#clean_str}; i++ )); do
code=$(printf '%d' "'${clean_str:i:1}")
if (( code >= 48 && code <= 57 )); then digit=$(( code - 48 ))
elif (( code >= 65 && code <= 90 )); then digit=$(( code - 65 + 10 ))
elif (( code >= 97 && code <= 122 )); then digit=$(( code - 97 + 10 ))
else
printf 'error: invalid Base36 character %q\n' "${clean_str:i:1}" >&2
exit 1
fi
val=$(( val * 36 + digit ))
done
# Step 2: Bitmask extraction, corrected to the HomeKit layout.
pin=$(( val & 0x7FFFFFF )) # bits 0-26
flag=$(( (val >> 27) & 0xF )) # bits 27-30
category=$(( (val >> 31) & 0xFF )) # bits 31-38
reserved=$(( (val >> 39) & 0xF )) # bits 39-42
version=$(( (val >> 43) & 0x7 )) # bits 43-45
# Resolve the numeric category id to its HAP name (homekit_accessory_category_*).
# https://developer.apple.com/documentation/homekit/hmaccessorycategory
# A case statement (not a `declare -A` map) keeps this working on the stock
# macOS bash 3.2 that `#!/usr/bin/env bash` resolves to. "Heater" corrects a
# "Header" typo in the source table; id 25 is reserved by HomeKit, so unknown.
cat_name="unknown"
case "$category" in
1) cat_name="Other" ;;
2) cat_name="Bridge" ;;
3) cat_name="Fan" ;;
4) cat_name="Garage" ;;
5) cat_name="Lightbulb" ;;
6) cat_name="Door Lock" ;;
7) cat_name="Outlet" ;;
8) cat_name="Switch" ;;
9) cat_name="Thermostat" ;;
10) cat_name="Sensor" ;;
11) cat_name="Security System" ;;
12) cat_name="Door" ;;
13) cat_name="Window" ;;
14) cat_name="Window Covering" ;;
15) cat_name="Programmable Switch" ;;
16) cat_name="Range Extender" ;;
17) cat_name="IP Camera" ;;
18) cat_name="Video Door Bell" ;;
19) cat_name="Air Purifier" ;;
20) cat_name="Heater" ;;
21) cat_name="Air Conditioner" ;;
22) cat_name="Humidifier" ;;
23) cat_name="Dehumidifier" ;;
24) cat_name="Apple TV" ;;
26) cat_name="Speaker" ;;
27) cat_name="AirPort" ;;
28) cat_name="Sprinkler" ;;
29) cat_name="Faucet" ;;
30) cat_name="Shower Head" ;;
31) cat_name="Television" ;;
32) cat_name="Target Controller" ;;
esac
# Python's bool() prints "True"/"False"; treat any non-zero bit as true.
to_bool() {
if (( $1 != 0 )); then
printf 'True'
else
printf 'False'
fi
}
# Base36 integer -> uppercase string (mirrors composeSetupUri's toString(36).toUpperCase()).
to_base36() {
local n=$1 digits='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' out=''
if [ "$n" -eq 0 ]; then
printf '0'
return
fi
while [ "$n" -gt 0 ]; do
out="${digits:n%36:1}${out}"
n=$(( n / 36 ))
done
printf '%s' "$out"
}
# Reconstruct the 9-char parameter block from the decoded fields (mirrors
# composeSetupUri's stepwise packing) so the output is a complete record the
# barcode can be rebuilt from.
block=$(( version & 0x7 ))
block=$(( (block << 4) | (reserved & 0xF) ))
block=$(( (block << 8) | (category & 0xFF) ))
block=$(( (block << 4) | (flag & 0xF) ))
block=$(( (block << 27) | (pin & 0x7FFFFFF) ))
block36="$(to_base36 "$block")"
while [ ${#block36} -lt 9 ]; do block36="0${block36}"; done
printf 'Setup PIN: %08d\n' "$pin"
printf 'Flag: %d\n' "$flag"
printf ' Supports NFC: %s\n' "$(to_bool $(( flag & 1 )))"
printf ' Supports IP: %s\n' "$(to_bool $(( flag & 2 )))"
printf ' Supports BLE: %s\n' "$(to_bool $(( flag & 4 )))"
printf ' Supports WAC/MFi: %s\n' "$(to_bool $(( flag & 8 )))"
printf 'Category ID: %d (%s)\n' "$category" "$cat_name"
printf 'Reserved: %d\n' "$reserved"
printf 'Version: %d\n' "$version"
printf 'Setup ID: %s\n' "${setup_id:-<none>}"
printf 'Setup Payload: X-HM://%s%s\n' "$block36" "$setup_id"
+157 -2
View File
@@ -14,6 +14,14 @@
# It is derived from project-chip/connectedhomeip's manufacturers.xml and is
# optional: without it, all fields still decode — the vendor name line is simply
# skipped. Regenerate it with: python3 gen-manufacturers.py (see that file).
#
# Two more optional companions, same "missing file = skip the extra" rule:
# products.tsv 3 cols: vendor id \t product id \t name (any vendor),
# regenerated by the gen-*-products.py scripts.
# vendor-equivalents.tsv 2 cols: alias vendor id \t canonical vendor id;
# hand-edited to record mergers/acquisitions (e.g.
# Feit 5155 == LIFX 4870) so a product resolves under
# either id and the vendor name gets annotated.
set -euo pipefail
@@ -94,6 +102,125 @@ get_vendor_name() {
awk -F'\t' -v id="$1" '$1==id { print $2; exit }' "$tsv"
}
# vendor-equivalents.tsv is 2 columns "alias vendor id \t canonical vendor id"
# (see that file's header). It records that two vendor IDs are the same company
# -- a merger or acquisition (e.g. Feit Electric 5155 == LIFX 4870).
#
# equivalent_vendor_ids VENDOR -> prints VENDOR's whole equivalence class: the
# vendor itself plus every alias that shares its canonical id, one per line,
# unique and sorted. It is the single source of truth for "these vendor IDs are
# the same company", used by both the product lookup and the vendor-name
# annotation. A missing vendor-equivalents.tsv yields just the vendor itself.
equivalent_vendor_ids() {
local eq="$SCRIPT_DIR/vendor-equivalents.tsv"
local v=$1 root
[[ -f $eq ]] || { printf '%s\n' "$v"; return 0; }
# canonical id of v: col-2 where v is an alias, else v itself.
root=$(awk -F'\t' -v id="$v" '$1 !~ /^#/ && $1==id { print $2; exit }' "$eq")
[[ -n $root ]] || root=$v
# every alias of that canonical, plus the canonical itself; unique+sorted.
{
awk -F'\t' -v r="$root" '$1 !~ /^#/ && $2==r { print $1 }' "$eq"
printf '%s\n' "$root"
} | sort -un
}
# products.tsv is 3 columns "vendor id \t product id \t name" (regenerated by the
# gen-*-products.py scripts; see the "do not edit" note there).
#
# get_product_name VENDOR PRODUCT -> prints the product name if that pair -- or,
# via equivalent_vendor_ids, any equivalent vendor's -- is in products.tsv. The
# exact vendor is checked before its equivalents. A missing products.tsv, or an
# id not present, yields no name.
get_product_name() {
local products="$SCRIPT_DIR/products.tsv"
[[ -f $products ]] || return 0
local id hit
# exact vendor first
hit=$(awk -F'\t' -v v="$1" -v p="$2" '$1==v && $2==p { print $3; exit }' "$products")
[[ -n $hit ]] && { printf '%s' "$hit"; return 0; }
# then any equivalent vendor id, skipping the exact vendor already checked
for id in $(equivalent_vendor_ids "$1"); do
[[ "$id" == "$1" ]] && continue
hit=$(awk -F'\t' -v v="$id" -v p="$2" '$1==v && $2==p { print $3; exit }' "$products")
[[ -n $hit ]] && { printf '%s' "$hit"; return 0; }
done
return 0
}
# The Matter "manual pairing code" is the PIN a commissioning app (Apple Home,
# Google Home, the Matter app) asks you to type -- it is NOT one of the raw
# payload fields, it is derived from the passcode and discriminator and, for
# non-standard commissioning flows, the vendor/product ids, then guarded by a
# Verhoeff check digit. A port of generateManualPairingCode() from the Matter
# QR tool (chill-uk/matter-qr-app, web/app.js).
# verhoeff_check_digit DIGITS -> prints the single Verhoeff check digit of the
# decimal string DIGITS. The three tables are the fixed standard Verhoeff
# tables; the digit string is processed right-to-left, as the reference does.
verhoeff_check_digit() {
local value=$1
local -a mul=(
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
)
local -a perm=(
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
)
local -a inv=( 0 4 3 2 1 5 6 7 8 9 )
local len=${#value}
local i p col checksum=0 digit
for (( i=0; i<len; i++ )); do
# The reference reverses the digit string; the i-th processed digit is
# therefore the (len-1-i)-th character of the original.
digit=${value:len-1-i:1}
p=$(( (i + 1) % 8 ))
col=${perm[p * 10 + digit]}
checksum=${mul[checksum * 10 + col]}
done
printf '%s' "${inv[checksum]}"
}
# compute_pairing_code VENDOR PRODUCT FLOW DISCRIMINATOR PASSCODE -> prints the
# Matter manual pairing code. For standard flow (0) it is 10 digits + a Verhoeff
# check digit; user-intent (1) and custom (2) flows append 5-digit vendor and
# product groups, so they are 20 digits + check.
compute_pairing_code() {
local vendor=$1 product=$2 flow=$3 discriminator=$4 passcode=$5
local short has chunk1 chunk2 chunk3 code
# 4-bit manual discriminator: the top 4 bits of the 12-bit QR discriminator.
short=$(( (discriminator >> 8) & 0x0f ))
# Only non-standard flows carry the vendor/product ids in the pairing code.
has=$(( flow != 0 ? 1 : 0 ))
chunk1=$(( ((short >> 2) & 0x03) | (has << 2) ))
chunk2=$(( (passcode & 0x3FFF) | ((short & 0x03) << 14) ))
chunk3=$(( (passcode >> 14) & 0x1FFF ))
code=$(printf '%1d%05d%04d' "$chunk1" "$chunk2" "$chunk3")
if (( has )); then
code+=$(printf '%05d%05d' "$vendor" "$product")
fi
code+=$(verhoeff_check_digit "$code")
printf '%s' "$code"
}
main() {
local uri payload
@@ -118,7 +245,7 @@ main() {
base38_decode "$payload" || return 1
local version vendor product customFlow discovery discriminator passcode
local cf caps="" vname
local cf caps="" vname pname="" others="" on id pairing=""
version=$(extract_bits 0 3)
vendor=$(extract_bits 3 16)
@@ -143,16 +270,44 @@ main() {
[[ -z $caps ]] && caps="None"
vname=$(get_vendor_name "$vendor")
# If the decoded vendor has an equivalent (e.g. Feit 5155 <-> LIFX 4870),
# annotate its name so the two read as one company.
for id in $(equivalent_vendor_ids "$vendor"); do
[[ "$id" == "$vendor" ]] && continue
on=$(get_vendor_name "$id")
[[ -n $on ]] && others="${others:+$others, }$on"
done
if [[ -n $others ]]; then
if [[ -n $vname ]]; then
vname="$vname (a.k.a. $others)"
else
vname="a.k.a. $others"
fi
fi
# Resolve the product ID to a human name, for any vendor (and equivalents).
pname=$(get_product_name "$vendor" "$product")
# The pairing code is the PIN a commissioning app asks you to type; it is
# derived from the passcode, discriminator, and (for non-standard flows) the
# vendor/product ids, with a Verhoeff check digit appended.
pairing=$(compute_pairing_code "$vendor" "$product" "$customFlow" "$discriminator" "$passcode")
if [ ${#pairing} -eq 11 ]
then
pairing="${pairing:0:4}-${pairing:4:3}-${pairing:7:4}"
fi
printf 'Version: %s\n' "$version"
printf 'Vendor ID: %s (0x%04X)' "$vendor" "$vendor"
[[ -n $vname ]] && printf ' %s' "$vname"
printf '\n'
printf 'Product ID: %s (0x%04X)\n' "$product" "$product"
printf 'Product ID: %s (0x%04X)' "$product" "$product"
[[ -n $pname ]] && printf ' %s' "$pname"
printf '\n'
printf 'Custom Flow: %s\n' "$cf"
printf 'Discovery: %s\n' "$caps"
printf 'Discriminator: %s\n' "$discriminator"
printf 'Passcode: %s\n' "$passcode"
printf 'Pairing Code: %s\n' "$pairing"
}
main "$@"
+173
View File
@@ -0,0 +1,173 @@
4870 1 Original
4870 2 Original 1000
4870 3 Color 650
4870 10 LIFX White 800 (LV)
4870 11 LIFX White 800 (HV)
4870 12 LIFX LCMv4 White OEM (A19 LV)
4870 13 LIFX LCMv4 White OEM (A19 HV)
4870 14 LIFX Carbon Prototype
4870 15 LIFX LCMv4 Color (A21)
4870 18 LIFX White 900 (BR30)
4870 19 LIFX White 900 (BR30)
4870 20 LIFX Color 1000 (BR30)
4870 21 LIFX LCMv4 Color (GU10)
4870 22 LIFX Color 1000
4870 23 LIFX (A19)
4870 24 LIFX (BR30)
4870 25 LIFX+ (A19)
4870 26 LIFX+ (BR30)
4870 27 LIFX (A19)
4870 28 LIFX (BR30)
4870 29 LIFX+ (A19)
4870 30 LIFX+ (BR30)
4870 31 LIFX Z
4870 32 LIFX Z
4870 33 LIFX Downlight
4870 36 LIFX DL
4870 37 LIFX DL
4870 38 LIFX Beam
4870 39 LIFX DL WW 700lm
4870 40 LIFX DL Colour 700lm
4870 43 LIFX (A19)
4870 44 LIFX (BR30)
4870 45 LIFX+ (A19)
4870 46 LIFX+ (BR30)
4870 49 LIFX Mini C
4870 50 LIFX Mini DD
4870 51 LIFX Mini W
4870 52 LIFX GU10
4870 53 LIFX GU10
4870 54 LIFX GU10 White
4870 55 LIFX Tile
4870 56 LIFX Beam
4870 57 LIFX Candle C
4870 59 LIFX Mini C
4870 60 LIFX Mini DD
4870 61 LIFX Mini W
4870 62 LIFX (A19) LCM3
4870 63 LIFX (BR30) LCM3
4870 64 LIFX+ (A19) LCM3
4870 65 LIFX+ (BR30) LCM3
4870 66 LIFX Mini W
4870 67 LIFX Candle WW
4870 68 LIFX Candle C
4870 70 LIFX Switch
4870 71 LIFX Switch
4870 72 LIFX A19
4870 81 LIFX Candle WW
4870 82 LIFX Filament Clear
4870 84 LIFX Switch
4870 85 LIFX Filament Amber
4870 86 LIFX Filament Smokey
4870 87 LIFX White
4870 88 LIFX White
4870 89 LIFX Switch
4870 90 LIFX Clean A19 1100lm
4870 91 LIFX Color 800lm
4870 92 LIFX Colour 1000lm
4870 93 LIFX Color A19 1100lm
4870 94 LIFX Color BR30 1100lm
4870 96 LIFX Candle WW
4870 97 LIFX Colour A19 1200lm
4870 98 LIFX Colour BR30 1100lm
4870 99 LIFX Clean A19 1200lm
4870 100 LIFX Filament Clear
4870 101 LIFX Filament Amber
4870 102 LIFX Filament Smokey
4870 109 LIFX NV A19 1100lm
4870 110 LIFX NV BR30 1100lm
4870 111 LIFX NV A19 1200lm
4870 112 LIFX NV BR30 1100lm
4870 113 LIFX WW 800lm
4870 114 LIFX WW 1000lm
4870 115 LIFX Switch
4870 116 LIFX Switch US
4870 117 LIFX Z
4870 118 LIFX Z
4870 119 LIFX Beam
4870 120 LIFX Beam
4870 121 LIFX DL
4870 122 LIFX DL
4870 123 LIFX Mini 3.1 Color US
4870 124 LIFX Mini 3.1 Color Intl
4870 125 LIFX Mini 3.1 WW US
4870 126 LIFX Mini 3.1 WW Intl
4870 127 LIFX Mini 3.1 W US
4870 128 LIFX Mini 3.1 W Intl
4870 129 LIFX Color 800
4870 130 LIFX Colour 1000
4870 131 LIFX White to Warm 800
4870 132 LIFX White to Warm 1000
4870 133 LIFX White 650
4870 134 LIFX White 800
4870 135 LIFX GU10 Color US
4870 136 LIFX GU10 Color Intl
4870 137 LIFX Candle C
4870 138 LIFX Candle C
4870 141 LIFX Neon
4870 142 LIFX Neon
4870 143 LIFX String
4870 144 LIFX String
4870 151 LIFX Neon Outdoor
4870 152 LIFX Neon Outdoor
4870 153 LIFX PAR38 US
4870 154 LIFX PAR38 Intl
4870 155 LIFX Sunshine
4870 156 LIFX A21 1000lm Intl
4870 161 LIFX Neon Outdoor
4870 162 LIFX Neon Outdoor
4870 163 LIFX A19
4870 164 LIFX BR30
4870 165 LIFX A19 Intl
4870 166 LIFX BR30 Intl
4870 167 LIFX DL AU
4870 168 LIFX DL US
4870 169 LIFX A21
4870 170 LIFX A21
4870 171 LIFX Spot
4870 172 LIFX Spot
4870 173 LIFX Path
4870 174 LIFX Path
4870 175 LIFX PAR38
4870 176 LIFX Ceiling
4870 177 LIFX Ceiling
4870 178 LIFX Downlight US
4870 179 LIFX Downlight US
4870 180 LIFX Downlight US
4870 181 LIFX Mini
4870 182 LIFX Mini
4870 185 LIFX Candle C
4870 186 LIFX Candle C Intl
4870 187 LIFX Candle
4870 188 LIFX Candle Intl
4870 191 LIFX Everyday A19
4870 192 LIFX Everyday A19 Intl
4870 201 LIFX Ceiling 13x26
4870 202 LIFX Ceiling 13x26 Intl
4870 203 LIFX String
4870 204 LIFX String
4870 205 LIFX Neon
4870 206 LIFX Neon
4870 207 LIFX Everyday Lightstrip
4870 208 LIFX Everyday Lightstrip Intl
4870 211 LIFX Everyday Permanent Outdoor US
4870 213 LIFX Permanent Outdoor
4870 214 LIFX Permanent Outdoor Intl
4870 215 LIFX Candle C
4870 216 LIFX Candle C Intl
4870 217 LIFX Tube
4870 218 LIFX Tube Intl
4870 219 LIFX Luna
4870 220 LIFX Luna Intl
4870 221 LIFX Spot Intl
4870 222 LIFX Path Intl
4870 223 LIFX DL
4870 224 LIFX DL Intl
4870 225 LIFX PAR38 Intl
4870 226 LIFX Dimmer Switch
4870 229 LIFX Path Intl
4870 265 LIFX Ceiling 13"
4870 266 LIFX Ceiling 13" Intl
4870 267 LIFX Mirror
4870 268 LIFX Mirror Intl
4870 300 LIFX Everyday Permanent Outdoor US
Can't render this file because it contains an unexpected character in line 169 and column 25.
+20
View File
@@ -0,0 +1,20 @@
# vendor-equivalents.tsv -- vendor IDs that mean the same company (mergers,
# acquisitions, rebrands). matter-decode.sh uses this to widen a product-name
# lookup and to annotate a vendor name when the decoded vendor has an equivalent.
#
# Format (tab-separated, two columns):
#
# <alias vendor id> <canonical vendor id>
#
# The canonical id is the "surviving" vendor; it must NOT itself appear in
# column 1 (one-hop / canonical form). Every equivalent in a group points at the
# same canonical id. Lines beginning with '#' are ignored.
#
# This file is hand-edited: add a row here when one company acquires or merges
# with another, so that products registered under either ID resolve together.
# Vendor IDs are decimal, as shown on the Vendor ID line (the 0x-hex in
# parentheses is for reference only).
#
# Feit Electric Company, Inc. acquired LIFX, so a Feit (5155) device and a
# LIFX (4870) device are the same company. LIFX is the canonical id here.
5155 4870
Can't render this file because it contains an unexpected character in line 9 and column 27.