Skip to content

Commit c272064

Browse files
authored
feat: improve bindings & reporting (#338)
1 parent c82b6ec commit c272064

24 files changed

+1848
-320
lines changed

src/Main.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const DashboardPage = lazy(async () => await import("./pages/Dashboard.js"));
1919
const NetworkPage = lazy(async () => await import("./pages/NetworkPage.js"));
2020
const GroupsPage = lazy(async () => await import("./pages/GroupsPage.js"));
2121
const GroupPage = lazy(async () => await import("./pages/GroupPage.js"));
22+
const ReportingPage = lazy(async () => await import("./pages/ReportingPage.js"));
23+
const BindingsPage = lazy(async () => await import("./pages/BindingsPage.js"));
2224
const OtaPage = lazy(async () => await import("./pages/OtaPage.js"));
2325
const TouchlinkPage = lazy(async () => await import("./pages/TouchlinkPage.js"));
2426
const LogsPage = lazy(async () => await import("./pages/LogsPage.js"));
@@ -56,8 +58,10 @@ function App() {
5658
<Route path="/device/:sourceIdx/:deviceId/:tab?" element={<DevicePage />} />
5759
<Route path="/groups" element={<GroupsPage />} />
5860
<Route path="/group/:sourceIdx/:groupId/:tab?" element={<GroupPage />} />
59-
<Route path="/touchlink" element={<TouchlinkPage />} />
61+
<Route path="/reporting" element={<ReportingPage />} />
62+
<Route path="/bindings" element={<BindingsPage />} />
6063
<Route path="/ota" element={<OtaPage />} />
64+
<Route path="/touchlink" element={<TouchlinkPage />} />
6165
<Route path="/network/:sourceIdx?" element={<NetworkPage />} />
6266
<Route path="/logs/:sourceIdx?" element={<LogsPage />} />
6367
<Route path="/activity" element={<ActivityPage />} />

src/components/binding/index.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import type { Device, Group } from "../../types.js";
2+
3+
export type BindingRuleTargetGroup = {
4+
type: "group";
5+
id: number;
6+
};
7+
8+
export type BindingRuleTargetDevice = {
9+
type: "endpoint";
10+
endpoint: string | number;
11+
ieee_address: string;
12+
};
13+
14+
export interface BindingRule {
15+
id?: number;
16+
isNew?: true;
17+
source: {
18+
ieee_address: string;
19+
endpoint: string | number;
20+
};
21+
target: BindingRuleTargetGroup | BindingRuleTargetDevice;
22+
clusters: string[];
23+
}
24+
25+
export type Action = "Bind" | "Unbind";
26+
27+
export interface BindingEndpoint {
28+
endpointId: string;
29+
rules: BindingRule[];
30+
}
31+
32+
export const makeDefaultBinding = (ieeeAddress: string, endpoint: string): BindingRule => ({
33+
isNew: true,
34+
target: { type: "endpoint", ieee_address: "", endpoint: "" },
35+
source: { ieee_address: ieeeAddress, endpoint },
36+
clusters: [],
37+
});
38+
39+
export const aggregateBindings = (device: Device): BindingRule[] => {
40+
const bindings: Record<string, BindingRule> = {};
41+
42+
for (const endpoint in device.endpoints) {
43+
const endpointDesc = device.endpoints[endpoint];
44+
45+
for (const binding of endpointDesc.bindings) {
46+
let targetId = "ieee_address" in binding.target ? `${binding.target.ieee_address}-${binding.target.endpoint}` : binding.target.id;
47+
48+
targetId = `${targetId}-${endpoint}`;
49+
50+
if (bindings[targetId]) {
51+
bindings[targetId].clusters.push(binding.cluster);
52+
} else {
53+
bindings[targetId] = {
54+
source: {
55+
ieee_address: device.ieee_address,
56+
endpoint,
57+
},
58+
target: { ...binding.target },
59+
clusters: [binding.cluster],
60+
};
61+
}
62+
}
63+
}
64+
65+
return Object.values(bindings);
66+
};
67+
68+
export const aggregateBindingsByEndpoints = (device: Device): BindingEndpoint[] => {
69+
const byEndpoints: BindingEndpoint[] = [];
70+
71+
for (const endpoint in device.endpoints) {
72+
const endpointDesc = device.endpoints[endpoint];
73+
const rulesByTarget: Record<string, BindingRule> = {};
74+
75+
for (const binding of endpointDesc.bindings) {
76+
const targetKey =
77+
"ieee_address" in binding.target ? `${binding.target.ieee_address}-${binding.target.endpoint}` : `group-${binding.target.id}`;
78+
79+
if (rulesByTarget[targetKey]) {
80+
rulesByTarget[targetKey].clusters.push(binding.cluster);
81+
} else {
82+
rulesByTarget[targetKey] = {
83+
source: {
84+
ieee_address: device.ieee_address,
85+
endpoint,
86+
},
87+
target: { ...binding.target },
88+
clusters: [binding.cluster],
89+
};
90+
}
91+
}
92+
93+
byEndpoints.push({ endpointId: endpoint, rules: Object.values(rulesByTarget) });
94+
}
95+
96+
return byEndpoints;
97+
};
98+
99+
export const findPossibleClusters = (rule: BindingRule, deviceEndpoints: Device["endpoints"], target?: Device | Group) => {
100+
const clusters: Set<string> = new Set(rule.clusters);
101+
const srcEndpoint = deviceEndpoints[rule.source.endpoint];
102+
const dstEndpoint =
103+
rule.target.type === "endpoint" && rule.target.endpoint != null ? (target as Device | undefined)?.endpoints[rule.target.endpoint] : undefined;
104+
const allClustersValid = rule.target.type === "group" || (target as Device | undefined)?.type === "Coordinator";
105+
106+
if (srcEndpoint && (dstEndpoint || allClustersValid)) {
107+
for (const cluster of [...srcEndpoint.clusters.input, ...srcEndpoint.clusters.output]) {
108+
if (allClustersValid) {
109+
clusters.add(cluster);
110+
} else {
111+
const supportedInputOutput = srcEndpoint.clusters.input.includes(cluster) && dstEndpoint?.clusters.output.includes(cluster);
112+
const supportedOutputInput = srcEndpoint.clusters.output.includes(cluster) && dstEndpoint?.clusters.input.includes(cluster);
113+
114+
if (supportedInputOutput || supportedOutputInput || allClustersValid) {
115+
clusters.add(cluster);
116+
}
117+
}
118+
}
119+
}
120+
121+
return clusters;
122+
};
123+
124+
export const getRuleDst = (
125+
target: BindingRule["target"],
126+
devices: Device[],
127+
groups: Group[],
128+
): { to: string | number; toEndpoint?: string | number } | undefined => {
129+
if (target.type === "group") {
130+
const targetGroup = groups.find((group) => group.id === target.id);
131+
132+
if (!targetGroup) {
133+
console.error("Target group does not exist:", target.id);
134+
return;
135+
}
136+
137+
return { to: targetGroup.id };
138+
}
139+
140+
const targetDevice = devices.find((device) => device.ieee_address === target.ieee_address);
141+
142+
if (!targetDevice) {
143+
console.error("Target device does not exist:", target.ieee_address);
144+
return;
145+
}
146+
147+
return { to: targetDevice.ieee_address, toEndpoint: targetDevice.type !== "Coordinator" ? target.endpoint : undefined };
148+
};
149+
150+
export const isValidBindingRuleEdit = (clusters: string[] | undefined): boolean => {
151+
if (!Array.isArray(clusters) || clusters.length === 0) {
152+
return false;
153+
}
154+
155+
return true;
156+
};
157+
158+
export const isValidBindingRule = (rule: BindingRule): boolean => {
159+
if (rule.source.endpoint === undefined || rule.source.endpoint === "" || Number.isNaN(rule.source.endpoint)) {
160+
return false;
161+
}
162+
163+
if (rule.target.type === "endpoint") {
164+
if (!rule.target.ieee_address) {
165+
return false;
166+
}
167+
168+
if (rule.target.endpoint === undefined || rule.target.endpoint === "" || Number.isNaN(rule.target.endpoint)) {
169+
return false;
170+
}
171+
} else if (rule.target.type === "group") {
172+
if (rule.target.id === undefined || Number.isNaN(rule.target.id)) {
173+
return false;
174+
}
175+
}
176+
177+
return isValidBindingRuleEdit(rule.clusters);
178+
};

0 commit comments

Comments
 (0)