Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
f707caf
fix: enhance ZCL specification
Nerivec Sep 13, 2025
80cb58c
fix: convert all Clusters IDs to hex
Nerivec Sep 14, 2025
f7375f4
fix: cleanup scripts
Nerivec Sep 14, 2025
dc8e83b
fix: enhance with restrictions
Nerivec Sep 14, 2025
f7ccce1
fix: proper ref handling
Nerivec Sep 14, 2025
c807161
fix: skip min/max for ieee/sec key
Nerivec Sep 14, 2025
63c9b68
fix: support restrictions with cmd params
Nerivec Sep 14, 2025
234f6c9
fix: support restrictions overrides
Nerivec Sep 14, 2025
7e38ff7
fix: cleanup
Nerivec Sep 14, 2025
88103cf
fix: proper ref handling logic for cmd and fallback
Nerivec Sep 14, 2025
af82f43
fix: unify cmd logic to prevent disparities
Nerivec Sep 14, 2025
26ece96
fix: cleanup
Nerivec Sep 14, 2025
7438458
fix: enhance clusters typegen
Nerivec Sep 14, 2025
c0c5840
fix: missing properties
Nerivec Sep 14, 2025
574d363
fix: add script to update types from ZAP
Nerivec Sep 14, 2025
6989add
fix: add generated datatypes
Nerivec Sep 14, 2025
5689cea
fix: cleanup
Nerivec Sep 14, 2025
7e1309c
fix: add clusters auto update
Nerivec Sep 14, 2025
74cf08d
fix: add missing pulseWidthModulation cluster
Nerivec Sep 14, 2025
9faaa0d
fix: cleanup types (deduped/shorter)
Nerivec Sep 14, 2025
d730da3
fix: add ZclType invalid type gen
Nerivec Sep 15, 2025
dde4248
fix: implement attr/param meta checking
Nerivec Sep 15, 2025
c00705b
fix: manual check
Nerivec Sep 22, 2025
5cc0ee2
fix: missing `length` restriction handling
Nerivec Sep 23, 2025
659826f
fix: manual check
Nerivec Sep 23, 2025
f635c6d
fix: manual check
Nerivec Sep 23, 2025
1556ccc
fix: add missing `powerProfile` cluster
Nerivec Sep 23, 2025
f538661
fix: add missing `keepAlive` cluster
Nerivec Sep 23, 2025
3b70cf8
fix: add missing `genLevelCtrlForLighting` cluster
Nerivec Sep 23, 2025
c63d3c7
fix: cleanup
Nerivec Sep 23, 2025
57941c0
fix: manual check
Nerivec Sep 23, 2025
4d884b2
fix: manual check
Nerivec Sep 23, 2025
30fd50f
fix: manual check
Nerivec Sep 23, 2025
6752901
Merge branch 'master' into zap
Nerivec Sep 23, 2025
78d7dce
fix: merge mishap
Nerivec Sep 24, 2025
9566b43
fix: tests
Nerivec Sep 24, 2025
34b17d0
fix: manual check
Nerivec Sep 24, 2025
d03adfd
fix: manual check
Nerivec Sep 24, 2025
b86eec3
fix: manual check
Nerivec Sep 24, 2025
c0aa9bc
fix: manual check
Nerivec Sep 25, 2025
392809c
fix: manual check
Nerivec Sep 25, 2025
4b4a7e1
fix: manual check
Nerivec Sep 25, 2025
518dc2d
fix: improve check clusters changes script
Nerivec Sep 25, 2025
4be99fc
fix: manual check
Nerivec Sep 25, 2025
fb5cc5f
fix: manual check
Nerivec Sep 25, 2025
18027b5
fix: manual check
Nerivec Sep 25, 2025
7e014c6
fix: manual check
Nerivec Sep 25, 2025
34f1f5e
fix: use `DATA..` type for Foundation (no restriction)
Nerivec Sep 25, 2025
d4e892d
fix: manual check
Nerivec Sep 25, 2025
c21c4c6
fix: manual check
Nerivec Sep 26, 2025
6d85a91
fix: manual check
Nerivec Sep 26, 2025
cdb5e68
fix: manual check
Nerivec Sep 26, 2025
2ade75c
fix: manual check
Nerivec Sep 26, 2025
bb22556
chore: update clusters changelog
Nerivec Sep 26, 2025
c32dde6
fix: mark all remain. custom clusters writable/all range
Nerivec Sep 26, 2025
04244d7
fix: mark custom attr/params writable/all range
Nerivec Sep 26, 2025
8ff6bc6
chore: update clusters changelog
Nerivec Sep 26, 2025
9a692a1
chore: comment
Nerivec Sep 26, 2025
5fff2b7
fix: update restriction logic
Nerivec Sep 26, 2025
d1c3ee1
fix: cleanup
Nerivec Sep 26, 2025
5b88595
fix: non-value ignore logic with min/max
Nerivec Sep 26, 2025
775404b
fix: mistake
Nerivec Sep 26, 2025
27290fa
fix: more tests & cleanup
Nerivec Sep 26, 2025
41f06f5
fix: update clusters types
Nerivec Sep 26, 2025
b50fce4
Merge branch 'master' into zap
Nerivec Sep 29, 2025
83a0589
Merge remote-tracking branch 'origin/master' into zap
Nerivec Sep 29, 2025
74b1699
Merge remote-tracking branch 'upstream/master' into zap
Nerivec Oct 18, 2025
fbb68a8
fix: update cluster types
Nerivec Oct 18, 2025
99f0590
Merge branch 'master' into zap
Nerivec Oct 28, 2025
d32ba7b
Merge remote-tracking branch 'upstream/master' into zap
Nerivec Nov 11, 2025
66d736a
fix: options in control cmds are not always present
Nerivec Nov 11, 2025
cf752e2
fix: remove refs
Nerivec Nov 18, 2025
2be7486
Merge remote-tracking branch 'upstream/master' into zap
Nerivec Nov 18, 2025
3ed8988
Merge branch 'master' into zap
Nerivec Nov 26, 2025
23dd4f2
fix: comments gen
Nerivec Nov 26, 2025
28eca1e
chore: fix tests
Nerivec Nov 26, 2025
cc9b772
Merge branch 'master' into zap
Nerivec Nov 30, 2025
0d1b132
fix: cleanup, reduce size of `Clusters` obj
Nerivec Dec 7, 2025
e65835a
Merge remote-tracking branch 'mine/zap' into zap
Nerivec Dec 7, 2025
e5abfdf
fix: format
Nerivec Dec 7, 2025
a209d08
Merge branch 'master' into zap
Nerivec Dec 7, 2025
2966b6a
fix: migrate clusters from rename
Nerivec Dec 9, 2025
4a1496d
Merge remote-tracking branch 'upstream/master' into zap
Nerivec Dec 9, 2025
2554680
fix: coverage ignore
Nerivec Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 328 additions & 0 deletions scripts/check-clusters-changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
/**
* Usage:
* tsx scripts/check-zcl-clusters-changes.ts
*/

import fs from "node:fs/promises";
import path from "node:path";
import {fileURLToPath, pathToFileURL} from "node:url";
import {BuffaloZclDataType, DataType} from "../src/zspec/zcl/definition/enums";
import type {ClusterDefinition, ClusterName} from "../src/zspec/zcl/definition/tstype";

// #region Types

type Loggable = string | number | undefined;

type ChangeType = "added" | "removed" | "changed";

type Change = {
type: ChangeType;
path: Loggable[];
from?: Loggable;
to?: Loggable;
};

// #endregion

// #region Config

const SCRIPT_DIR = fileURLToPath(new URL(".", import.meta.url));
const LOG_FILE = path.resolve(SCRIPT_DIR, "clusters-changes.log");
const CURRENT_FILE = path.resolve(SCRIPT_DIR, "../src/zspec/zcl/definition/cluster.ts");

// #endregion

// #region Helpers

/**
* Creates a map from a name to an ID for a given record.
*/
function createIdMap<T extends {ID: number}>(record: Readonly<Record<string, T>>): Map<string, number> {
const map = new Map<string, number>();

for (const name in record) {
map.set(name, record[name].ID);
}

return map;
}

/**
* Creates a reverse map from an ID to a name for a given record.
*/
function createReverseIdMap<T extends {ID: number}>(record: Readonly<Record<string, T>>): Map<number, string> {
const map = new Map<number, string>();

for (const name in record) {
map.set(record[name].ID, name);
}

return map;
}

function toHex(value: number, pad: number): string {
return `0x${value.toString(16).padStart(pad, "0")}`;
}

/**
* Formats a log entry for display.
*/
function formatChange(change: Change): string {
const path = change.path.join(" > ");
const hexTo = typeof change.to === "number" ? ` (${toHex(change.to, 4)})` : "";
const hexFrom = typeof change.from === "number" ? ` (${toHex(change.from, 4)})` : "";

switch (change.type) {
case "added": {
return `[ADDED] ${path}${change.to ? `: ${change.to}${hexTo}` : ""}`;
}
case "removed": {
return `[REMOVED] ${path}: ${change.from}${hexFrom}`;
}
case "changed": {
return `[CHANGED] ${path}: ${change.from}${hexFrom} -> ${change.to}${hexTo}`;
}
}
}

// #endregion

// #region Comparison logic

class ClusterComparator {
readonly #changes: Change[] = [];

/**
* Compares two records (like `attributes`, `commands`, etc.)
*/
#compareRecords<T extends {ID: number; type?: unknown; parameters?: readonly {name: string; type: unknown}[]}>(
oldRecord: Readonly<Record<string, T>> | undefined,
newRecord: Readonly<Record<string, T>> | undefined,
currentPath: Loggable[],
): void {
oldRecord ??= {};
newRecord ??= {};

const oldIdMap = createIdMap(oldRecord);
const newIdMap = createIdMap(newRecord);
// const oldReverseIdMap = createReverseIdMap(oldRecord);
const newReverseIdMap = createReverseIdMap(newRecord);

// Check for removed and changed items
for (const oldName of oldIdMap.keys()) {
// biome-ignore lint/style/noNonNullAssertion: loop
const oldId = oldIdMap.get(oldName)!;
const oldItem = oldRecord[oldName];

if (!newIdMap.has(oldName)) {
const newNameForOldId = newReverseIdMap.get(oldId);

if (newNameForOldId) {
// Renamed item
this.#changes.push({
type: "changed",
path: [...currentPath, "name", oldId],
from: oldName,
to: newNameForOldId,
});

// Continue comparison with the new name
this.#compareItems(oldItem, newRecord[newNameForOldId], [...currentPath, newNameForOldId]);
} else {
// Removed item
this.#changes.push({type: "removed", path: [...currentPath, oldName], from: oldId});
}
} else {
// Item exists in both, compare details
this.#compareItems(oldItem, newRecord[oldName], [...currentPath, oldName]);
}
}

// Check for added items
for (const newName of newIdMap.keys()) {
if (!oldIdMap.has(newName)) {
// biome-ignore lint/style/noNonNullAssertion: checked above
const newId = newIdMap.get(newName)!;

// if (!oldReverseIdMap.has(newId)) {
// Added item
this.#changes.push({type: "added", path: [...currentPath, newName], to: newId});
// }
}
}
}

/**
* Compares two individual items (e.g., a specific attribute or command).
*/
// biome-ignore lint/suspicious/noExplicitAny: dynamic
#compareItems(oldItem: any, newItem: any, currentPath: Loggable[]): void {
// Check ID change
if (oldItem.ID !== newItem.ID) {
this.#changes.push({type: "changed", path: [...currentPath, "ID"], from: oldItem.ID, to: newItem.ID});
}

// Check type change (for attributes)
if ("type" in oldItem && "type" in newItem && oldItem.type !== newItem.type) {
this.#changes.push({
type: "changed",
path: [...currentPath, "type"],
from: DataType[oldItem.type] ?? BuffaloZclDataType[oldItem.type],
to: DataType[newItem.type] ?? BuffaloZclDataType[newItem.type],
});
}

// Check parameters (for commands)
if ("parameters" in oldItem || "parameters" in newItem) {
this.#compareParameters(oldItem.parameters, newItem.parameters, currentPath);
}
}

/**
* Compares the `parameters` array of two commands.
*/
#compareParameters(
oldParams: readonly {name: string; type: number}[] | undefined,
newParams: readonly {name: string; type: number}[] | undefined,
currentPath: Loggable[],
): void {
oldParams ??= [];
newParams ??= [];

const oldParamsMap = new Map(oldParams.map((p) => [p.name, p]));
const newParamsMap = new Map(newParams.map((p) => [p.name, p]));

for (const [oldName, oldParam] of oldParamsMap) {
const newParam = newParamsMap.get(oldName);
const paramPath = [...currentPath, "parameters", oldName];

if (newParam) {
// Parameter exists in both, check type
if (oldParam.type !== newParam.type) {
this.#changes.push({
type: "changed",
path: [...paramPath, "type"],
from: DataType[oldParam.type] ?? BuffaloZclDataType[oldParam.type],
to: DataType[newParam.type] ?? BuffaloZclDataType[newParam.type],
});
}
} else {
// Parameter removed
this.#changes.push({type: "removed", path: paramPath});
}
}

for (const newName of newParamsMap.keys()) {
if (!oldParamsMap.has(newName)) {
// Parameter added
this.#changes.push({type: "added", path: [...currentPath, "parameters", newName]});
}
}
}

/**
* Runs the full comparison between two `Clusters` definitions.
*/
public run(
oldClusters: Readonly<Record<ClusterName, Readonly<ClusterDefinition>>>,
newClusters: Readonly<Record<ClusterName, Readonly<ClusterDefinition>>>,
): Change[] {
this.#changes.length = 0;
const rootPath: Loggable[] = ["Clusters"];

this.#compareRecords(oldClusters, newClusters, rootPath);

// After the main comparison based on names, check for deeper changes in matching clusters
for (const clusterName in newClusters) {
if (clusterName in oldClusters) {
const oldCluster = oldClusters[clusterName as ClusterName];
const newCluster = newClusters[clusterName as ClusterName];
const clusterPath = [...rootPath, clusterName];

// Attributes
this.#compareRecords(oldCluster.attributes, newCluster.attributes, [...clusterPath, "attributes"]);

// Commands
this.#compareRecords(oldCluster.commands, newCluster.commands, [...clusterPath, "commands"]);

// Command Responses
this.#compareRecords(oldCluster.commandsResponse, newCluster.commandsResponse, [...clusterPath, "commandsResponse"]);
}
}

return this.#changes;
}
}

// #endregion

// #region Main

async function main(): Promise<void> {
console.log("Starting ZCL cluster definition comparison...");

const oldFilePathArg = process.argv[2];

if (!oldFilePathArg) {
console.error("ERROR: Missing required argument.");
console.error("Usage: tsx scripts/check-zcl-clusters-changes.ts <path-to-old-cluster-file>");
process.exit(1);
}

const oldFilePath = path.resolve(process.cwd(), oldFilePathArg);

try {
await fs.access(oldFilePath);
} catch {
console.error(`ERROR: Old file not found at: ${oldFilePath}`);
process.exit(1);
}

// Dynamically import both files using file:// URLs to ensure Windows compatibility
const {Clusters: newClusters} = await import(pathToFileURL(CURRENT_FILE).href);
const {Clusters: oldClusters} = await import(pathToFileURL(oldFilePath).href);

// Perform comparison
const comparator = new ClusterComparator();
const changes = comparator.run(oldClusters, newClusters);

// Output results
if (changes.length === 0) {
const message = "No changes detected between the two cluster definition files.";
console.log(message);
await fs.writeFile(LOG_FILE, `${message}\n`, "utf8");
return;
}

const logHeader = `Found ${changes.length} changes.\n`;

const formattedChanges: string[] = [];

for (const change of changes) {
if (change.type !== "added") {
formattedChanges.push(formatChange(change));
}
}

formattedChanges.push("");

for (const change of changes) {
if (change.type === "added") {
formattedChanges.push(formatChange(change));
}
}

const logContent = [logHeader, ...formattedChanges].join("\n");

await fs.writeFile(LOG_FILE, logContent, "utf8");

console.log(`\nComparison complete. Found ${changes.length} changes.`);
console.log(`See the full report in: ${LOG_FILE}`);
}

main().catch((error) => {
console.error("An unexpected error occurred:", error);
process.exit(1);
});

// #endregion
Loading