Updated matter decoder to show more info, and added a homekit decoder.
This commit is contained in:
1 parent
548f2e701f
commit
27ebf13f6f
8 files changed
+831
-6
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
.DS_Store
|
||||||
@@ -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
|
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
|
embedded `<map>` block as an offline fallback. Reproduces the committed TSV
|
||||||
byte-for-byte.
|
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.
|
- `README.md` — user-facing docs; this file documents the internals.
|
||||||
|
|
||||||
## Commands
|
## 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
|
`parseInt(code, 16)`. It is exposed reactively through the `vendorName` `computed`, so
|
||||||
the lookup re-runs when the decoded vendor ID changes.
|
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
|
## Gotchas
|
||||||
|
|
||||||
- The `BarcodeDetector` scanner polls the camera via `setInterval` every 200ms and
|
- 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
|
- `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
|
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,
|
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`
|
mirror it in the script (and vice versa). The script's extras are the optional `manufacturers.tsv` (vendor name),
|
||||||
lookup for the vendor name; without that file it still decodes everything else.
|
`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
|
- `manufacturers.tsv` is regenerated with `gen-manufacturers.py` (Python 3
|
||||||
stdlib). Default source is the **upstream `manufacturers.xml` fetched over the
|
stdlib). Default source is the **upstream `manufacturers.xml` fetched over the
|
||||||
network** (via `curl`; a GitHub `blob/` URL is auto-rewritten to its
|
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
|
`<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
|
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.
|
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.
|
||||||
@@ -18,11 +18,12 @@ split into these fields:
|
|||||||
|---|---|
|
|---|---|
|
||||||
| **Version** | Encoding version (3 bits) |
|
| **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 |
|
| **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 |
|
| **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) |
|
| **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 |
|
| **Discriminator** | 12-bit discriminator |
|
||||||
| **Passcode** | 27-bit commissioning passcode |
|
| **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
|
## 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. |
|
| [`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. |
|
| [`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). |
|
| [`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
|
`CLAUDE.md` documents the internals for anyone (or Claude Code) working on the
|
||||||
code.
|
code.
|
||||||
@@ -89,6 +93,7 @@ Custom Flow: User Intent Commissioning Flow
|
|||||||
Discovery: BLE
|
Discovery: BLE
|
||||||
Discriminator:1723
|
Discriminator:1723
|
||||||
Passcode: 8126392
|
Passcode: 8126392
|
||||||
|
Pairing Code: 549080049513012506168
|
||||||
```
|
```
|
||||||
|
|
||||||
(The vendor ID `0x32D4` isn't in `manufacturers.tsv`, so no name is printed —
|
(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
|
directory to resolve the vendor name; without that file every field still decodes
|
||||||
and only the vendor name is omitted.
|
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
|
## 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
|
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.
|
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
|
## License
|
||||||
|
|
||||||
The vendor mapping is derived from Matter/CHIP's `manufacturers.xml`, which is
|
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.
|
||||||
Executable
+165
@@ -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()
|
||||||
Executable
+149
@@ -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
@@ -14,6 +14,14 @@
|
|||||||
# It is derived from project-chip/connectedhomeip's manufacturers.xml and is
|
# 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
|
# optional: without it, all fields still decode — the vendor name line is simply
|
||||||
# skipped. Regenerate it with: python3 gen-manufacturers.py (see that file).
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -94,6 +102,125 @@ get_vendor_name() {
|
|||||||
awk -F'\t' -v id="$1" '$1==id { print $2; exit }' "$tsv"
|
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() {
|
main() {
|
||||||
local uri payload
|
local uri payload
|
||||||
|
|
||||||
@@ -118,7 +245,7 @@ main() {
|
|||||||
base38_decode "$payload" || return 1
|
base38_decode "$payload" || return 1
|
||||||
|
|
||||||
local version vendor product customFlow discovery discriminator passcode
|
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)
|
version=$(extract_bits 0 3)
|
||||||
vendor=$(extract_bits 3 16)
|
vendor=$(extract_bits 3 16)
|
||||||
@@ -143,16 +270,44 @@ main() {
|
|||||||
[[ -z $caps ]] && caps="None"
|
[[ -z $caps ]] && caps="None"
|
||||||
|
|
||||||
vname=$(get_vendor_name "$vendor")
|
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 'Version: %s\n' "$version"
|
||||||
printf 'Vendor ID: %s (0x%04X)' "$vendor" "$vendor"
|
printf 'Vendor ID: %s (0x%04X)' "$vendor" "$vendor"
|
||||||
[[ -n $vname ]] && printf ' %s' "$vname"
|
[[ -n $vname ]] && printf ' %s' "$vname"
|
||||||
printf '\n'
|
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 'Custom Flow: %s\n' "$cf"
|
||||||
printf 'Discovery: %s\n' "$caps"
|
printf 'Discovery: %s\n' "$caps"
|
||||||
printf 'Discriminator: %s\n' "$discriminator"
|
printf 'Discriminator: %s\n' "$discriminator"
|
||||||
printf 'Passcode: %s\n' "$passcode"
|
printf 'Passcode: %s\n' "$passcode"
|
||||||
|
printf 'Pairing Code: %s\n' "$pairing"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
+173
@@ -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.
|
@@ -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.
|
Reference in new issue
Block a user