Skip to content

Commit 9edfa9c

Browse files
authored
feat: Generate definition for unsupported Green Power devices (#9020)
1 parent 0c24fcf commit 9edfa9c

File tree

2 files changed

+211
-72
lines changed

2 files changed

+211
-72
lines changed

src/lib/generateDefinition.ts

+102-72
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,74 @@ type Extender = [string[], ExtenderGenerator];
5252

5353
type DefinitionWithZigbeeModel = DefinitionWithExtend & {zigbeeModel: string[]};
5454

55+
// If generator will have endpoint argument - generator implementation
56+
// should not provide it if only the first device endpoint is passed in.
57+
// If multiple endpoints provided(maybe including the first device endpoint) -
58+
// they all should be passed as an argument, where possible, to be explicit.
59+
const INPUT_EXTENDERS: Extender[] = [
60+
[
61+
["msTemperatureMeasurement"],
62+
async (d, eps) => [new ExtendGenerator({extend: m.temperature, args: maybeEndpointArgs(d, eps), source: "temperature"})],
63+
],
64+
[["msPressureMeasurement"], async (d, eps) => [new ExtendGenerator({extend: m.pressure, args: maybeEndpointArgs(d, eps), source: "pressure"})]],
65+
[["msRelativeHumidity"], async (d, eps) => [new ExtendGenerator({extend: m.humidity, args: maybeEndpointArgs(d, eps), source: "humidity"})]],
66+
[["msCO2"], async (d, eps) => [new ExtendGenerator({extend: m.co2, args: maybeEndpointArgs(d, eps), source: "co2"})]],
67+
[["genPowerCfg"], async (d, eps) => [new ExtendGenerator({extend: m.battery, source: "battery"})]],
68+
[["genOnOff", "genLevelCtrl", "lightingColorCtrl"], extenderOnOffLight],
69+
[["seMetering", "haElectricalMeasurement"], extenderElectricityMeter],
70+
[["closuresDoorLock"], extenderLock],
71+
[
72+
["msIlluminanceMeasurement"],
73+
async (d, eps) => [new ExtendGenerator({extend: m.illuminance, args: maybeEndpointArgs(d, eps), source: "illuminance"})],
74+
],
75+
[["msOccupancySensing"], async (d, eps) => [new ExtendGenerator({extend: m.occupancy, source: "occupancy"})]],
76+
[
77+
["ssIasZone"],
78+
async (d, eps) => [
79+
new ExtendGenerator({
80+
extend: m.iasZoneAlarm,
81+
args: {
82+
zoneType: "generic",
83+
zoneAttributes: ["alarm_1", "alarm_2", "tamper", "battery_low"],
84+
},
85+
source: "iasZoneAlarm",
86+
}),
87+
],
88+
],
89+
[["ssIasWd"], async (d, eps) => [new ExtendGenerator({extend: m.iasWarning, source: "iasWarning"})]],
90+
[
91+
["genDeviceTempCfg"],
92+
async (d, eps) => [new ExtendGenerator({extend: m.deviceTemperature, args: maybeEndpointArgs(d, eps), source: "deviceTemperature"})],
93+
],
94+
[["pm25Measurement"], async (d, eps) => [new ExtendGenerator({extend: m.pm25, args: maybeEndpointArgs(d, eps), source: "pm25"})]],
95+
[["msFlowMeasurement"], async (d, eps) => [new ExtendGenerator({extend: m.flow, args: maybeEndpointArgs(d, eps), source: "flow"})]],
96+
[["msSoilMoisture"], async (d, eps) => [new ExtendGenerator({extend: m.soilMoisture, args: maybeEndpointArgs(d, eps), source: "soilMoisture"})]],
97+
[
98+
["closuresWindowCovering"],
99+
async (d, eps) => [new ExtendGenerator({extend: m.windowCovering, args: {controls: ["lift", "tilt"]}, source: "windowCovering"})],
100+
],
101+
[["genBinaryInput"], extenderBinaryInput],
102+
[["genBinaryOutput"], extenderBinaryOutput],
103+
];
104+
105+
const OUTPUT_EXTENDERS: Extender[] = [
106+
[["genOnOff"], async (d, eps) => [new ExtendGenerator({extend: m.commandsOnOff, args: maybeEndpointArgs(d, eps), source: "commandsOnOff"})]],
107+
[
108+
["genLevelCtrl"],
109+
async (d, eps) => [new ExtendGenerator({extend: m.commandsLevelCtrl, args: maybeEndpointArgs(d, eps), source: "commandsLevelCtrl"})],
110+
],
111+
[
112+
["lightingColorCtrl"],
113+
async (d, eps) => [new ExtendGenerator({extend: m.commandsColorCtrl, args: maybeEndpointArgs(d, eps), source: "commandsColorCtrl"})],
114+
],
115+
[
116+
["closuresWindowCovering"],
117+
async (d, eps) => [
118+
new ExtendGenerator({extend: m.commandsWindowCovering, args: maybeEndpointArgs(d, eps), source: "commandsWindowCovering"}),
119+
],
120+
],
121+
];
122+
55123
function generateSource(definition: DefinitionWithZigbeeModel, generatedExtend: GeneratedExtend[]): string {
56124
const imports = [...new Set(generatedExtend.map((e) => e.lib ?? "modernExtend"))];
57125
const importsStr = imports.map((e) => `const ${e === "modernExtend" ? "m" : e} = require('zigbee-herdsman-converters/lib/${e}');`).join("\n");
@@ -69,7 +137,23 @@ const definition = {
69137
module.exports = definition;`;
70138
}
71139

140+
function generateGreenPowerSource(definition: DefinitionWithExtend, ieeeAddr: string): string {
141+
return `import {genericGreenPower} from 'zigbee-herdsman-converters/lib/modernExtend';
142+
143+
export default {
144+
fingerprint: [{modelID: '${definition.model}', ieeeAddr: new RegExp('^${ieeeAddr}$')}],
145+
model: '${definition.model}',
146+
vendor: '${definition.vendor}',
147+
description: 'Automatically generated definition for Green Power',
148+
extend: [genericGreenPower()],
149+
};`;
150+
}
151+
72152
export async function generateDefinition(device: Zh.Device): Promise<{externalDefinitionSource: string; definition: DefinitionWithExtend}> {
153+
if (device.type === "GreenPower") {
154+
return generateGreenPowerDefinition(device);
155+
}
156+
73157
// Map cluster to all endpoints that have this cluster.
74158
const mapClusters = (endpoint: ZHModels.Endpoint, clusters: Cluster[], clusterMap: Map<string, ZHModels.Endpoint[]>) => {
75159
for (const cluster of clusters) {
@@ -82,8 +166,8 @@ export async function generateDefinition(device: Zh.Device): Promise<{externalDe
82166
}
83167
};
84168

85-
const knownInputClusters = inputExtenders.flatMap((ext) => ext[0]);
86-
const knownOutputClusters = outputExtenders.flatMap((ext) => ext[0]);
169+
const knownInputClusters = INPUT_EXTENDERS.flatMap((ext) => ext[0]);
170+
const knownOutputClusters = OUTPUT_EXTENDERS.flatMap((ext) => ext[0]);
87171

88172
const inputClusterMap = new Map<string, ZHModels.Endpoint[]>();
89173
const outputClusterMap = new Map<string, ZHModels.Endpoint[]>();
@@ -110,11 +194,11 @@ export async function generateDefinition(device: Zh.Device): Promise<{externalDe
110194
};
111195

112196
for (const [cluster, endpoints] of inputClusterMap) {
113-
await addGenerators(cluster, endpoints, inputExtenders);
197+
await addGenerators(cluster, endpoints, INPUT_EXTENDERS);
114198
}
115199

116200
for (const [cluster, endpoints] of outputClusterMap) {
117-
await addGenerators(cluster, endpoints, outputExtenders);
201+
await addGenerators(cluster, endpoints, OUTPUT_EXTENDERS);
118202
}
119203

120204
const extenders = generatedExtend.map((e) => e.getExtend());
@@ -158,6 +242,20 @@ export async function generateDefinition(device: Zh.Device): Promise<{externalDe
158242
return {externalDefinitionSource, definition};
159243
}
160244

245+
export function generateGreenPowerDefinition(device: Zh.Device): {externalDefinitionSource: string; definition: DefinitionWithExtend} {
246+
const definition: DefinitionWithExtend = {
247+
fingerprint: [{modelID: device.modelID, ieeeAddr: new RegExp(`^${device.ieeeAddr}$`)}],
248+
model: device.modelID ?? "",
249+
vendor: device.manufacturerName ?? "",
250+
description: "Automatically generated definition for Green Power",
251+
extend: [m.genericGreenPower()],
252+
generated: true,
253+
};
254+
255+
const externalDefinitionSource = generateGreenPowerSource(definition, device.ieeeAddr);
256+
return {externalDefinitionSource, definition};
257+
}
258+
161259
function stringifyEps(endpoints: ZHModels.Endpoint[]): string[] {
162260
return endpoints.map((e) => e.ID.toString());
163261
}
@@ -179,74 +277,6 @@ function maybeEndpointArgs<T>(device: Zh.Device, endpoints: Zh.Endpoint[], toExt
179277
return {endpointNames: stringifyEps(endpoints), ...toExtend};
180278
}
181279

182-
// If generator will have endpoint argument - generator implementation
183-
// should not provide it if only the first device endpoint is passed in.
184-
// If multiple endpoints provided(maybe including the first device endpoint) -
185-
// they all should be passed as an argument, where possible, to be explicit.
186-
const inputExtenders: Extender[] = [
187-
[
188-
["msTemperatureMeasurement"],
189-
async (d, eps) => [new ExtendGenerator({extend: m.temperature, args: maybeEndpointArgs(d, eps), source: "temperature"})],
190-
],
191-
[["msPressureMeasurement"], async (d, eps) => [new ExtendGenerator({extend: m.pressure, args: maybeEndpointArgs(d, eps), source: "pressure"})]],
192-
[["msRelativeHumidity"], async (d, eps) => [new ExtendGenerator({extend: m.humidity, args: maybeEndpointArgs(d, eps), source: "humidity"})]],
193-
[["msCO2"], async (d, eps) => [new ExtendGenerator({extend: m.co2, args: maybeEndpointArgs(d, eps), source: "co2"})]],
194-
[["genPowerCfg"], async (d, eps) => [new ExtendGenerator({extend: m.battery, source: "battery"})]],
195-
[["genOnOff", "genLevelCtrl", "lightingColorCtrl"], extenderOnOffLight],
196-
[["seMetering", "haElectricalMeasurement"], extenderElectricityMeter],
197-
[["closuresDoorLock"], extenderLock],
198-
[
199-
["msIlluminanceMeasurement"],
200-
async (d, eps) => [new ExtendGenerator({extend: m.illuminance, args: maybeEndpointArgs(d, eps), source: "illuminance"})],
201-
],
202-
[["msOccupancySensing"], async (d, eps) => [new ExtendGenerator({extend: m.occupancy, source: "occupancy"})]],
203-
[
204-
["ssIasZone"],
205-
async (d, eps) => [
206-
new ExtendGenerator({
207-
extend: m.iasZoneAlarm,
208-
args: {
209-
zoneType: "generic",
210-
zoneAttributes: ["alarm_1", "alarm_2", "tamper", "battery_low"],
211-
},
212-
source: "iasZoneAlarm",
213-
}),
214-
],
215-
],
216-
[["ssIasWd"], async (d, eps) => [new ExtendGenerator({extend: m.iasWarning, source: "iasWarning"})]],
217-
[
218-
["genDeviceTempCfg"],
219-
async (d, eps) => [new ExtendGenerator({extend: m.deviceTemperature, args: maybeEndpointArgs(d, eps), source: "deviceTemperature"})],
220-
],
221-
[["pm25Measurement"], async (d, eps) => [new ExtendGenerator({extend: m.pm25, args: maybeEndpointArgs(d, eps), source: "pm25"})]],
222-
[["msFlowMeasurement"], async (d, eps) => [new ExtendGenerator({extend: m.flow, args: maybeEndpointArgs(d, eps), source: "flow"})]],
223-
[["msSoilMoisture"], async (d, eps) => [new ExtendGenerator({extend: m.soilMoisture, args: maybeEndpointArgs(d, eps), source: "soilMoisture"})]],
224-
[
225-
["closuresWindowCovering"],
226-
async (d, eps) => [new ExtendGenerator({extend: m.windowCovering, args: {controls: ["lift", "tilt"]}, source: "windowCovering"})],
227-
],
228-
[["genBinaryInput"], extenderBinaryInput],
229-
[["genBinaryOutput"], extenderBinaryOutput],
230-
];
231-
232-
const outputExtenders: Extender[] = [
233-
[["genOnOff"], async (d, eps) => [new ExtendGenerator({extend: m.commandsOnOff, args: maybeEndpointArgs(d, eps), source: "commandsOnOff"})]],
234-
[
235-
["genLevelCtrl"],
236-
async (d, eps) => [new ExtendGenerator({extend: m.commandsLevelCtrl, args: maybeEndpointArgs(d, eps), source: "commandsLevelCtrl"})],
237-
],
238-
[
239-
["lightingColorCtrl"],
240-
async (d, eps) => [new ExtendGenerator({extend: m.commandsColorCtrl, args: maybeEndpointArgs(d, eps), source: "commandsColorCtrl"})],
241-
],
242-
[
243-
["closuresWindowCovering"],
244-
async (d, eps) => [
245-
new ExtendGenerator({extend: m.commandsWindowCovering, args: maybeEndpointArgs(d, eps), source: "commandsWindowCovering"}),
246-
],
247-
],
248-
];
249-
250280
async function extenderLock(device: Zh.Device, endpoints: Zh.Endpoint[]): Promise<GeneratedExtend[]> {
251281
// TODO: Support multiple endpoints
252282
if (endpoints.length > 1) {

src/lib/modernExtend.ts

+109
Original file line numberDiff line numberDiff line change
@@ -2138,6 +2138,115 @@ export function gasMeter(args?: GasMeterArgs): ModernExtend {
21382138

21392139
// #region Other extends
21402140

2141+
/**
2142+
* Version of the GP spec: 1.1.1
2143+
*/
2144+
export const GPDF_COMMANDS: Record<number, string> = {
2145+
/*0x00*/ 0: "identify",
2146+
/*0x10*/ 16: "recall_scene0",
2147+
/*0x11*/ 17: "recall_scene1",
2148+
/*0x12*/ 18: "recall_scene2",
2149+
/*0x13*/ 19: "recall_scene3",
2150+
/*0x14*/ 20: "recall_scene4",
2151+
/*0x15*/ 21: "recall_scene5",
2152+
/*0x16*/ 22: "recall_scene6",
2153+
/*0x17*/ 23: "recall_scene7",
2154+
/*0x18*/ 24: "store_scene0",
2155+
/*0x19*/ 25: "store_scene1",
2156+
/*0x1a*/ 26: "store_scene2",
2157+
/*0x1b*/ 27: "store_scene3",
2158+
/*0x1c*/ 28: "store_scene4",
2159+
/*0x1d*/ 29: "store_scene5",
2160+
/*0x1e*/ 30: "store_scene6",
2161+
/*0x1f*/ 31: "store_scene7",
2162+
/*0x20*/ 32: "off",
2163+
/*0x21*/ 33: "on",
2164+
/*0x22*/ 34: "toggle",
2165+
/*0x23*/ 35: "release",
2166+
/*0x30*/ 48: "move_up", // with payload
2167+
/*0x31*/ 49: "move_down", // with payload
2168+
/*0x32*/ 50: "step_up", // with payload
2169+
/*0x33*/ 51: "step_down", // with payload
2170+
/*0x34*/ 52: "level_control_stop",
2171+
/*0x35*/ 53: "move_up_with_on_off", // with payload
2172+
/*0x36*/ 54: "move_down_with_on_off", // with payload
2173+
/*0x37*/ 55: "step_up_with_on_off", // with payload
2174+
/*0x38*/ 56: "step_down_with_on_off", // with payload
2175+
/*0x40*/ 64: "move_hue_stop",
2176+
/*0x41*/ 65: "move_hue_up", // with payload
2177+
/*0x42*/ 66: "move_hue_down", // with payload
2178+
/*0x43*/ 67: "step_hue_up", // with payload
2179+
/*0x44*/ 68: "step_huw_down", // with payload
2180+
/*0x45*/ 69: "move_saturation_stop",
2181+
/*0x46*/ 70: "move_saturation_up", // with payload
2182+
/*0x47*/ 71: "move_saturation_down", // with payload
2183+
/*0x48*/ 72: "step_saturation_up", // with payload
2184+
/*0x49*/ 73: "step_saturation_down", // with payload
2185+
/*0x4a*/ 74: "move_color", // with payload
2186+
/*0x4b*/ 75: "step_color", // with payload
2187+
/*0x50*/ 80: "lock_door",
2188+
/*0x51*/ 81: "unlock_door",
2189+
/*0x60*/ 96: "press11",
2190+
/*0x61*/ 97: "release11",
2191+
/*0x62*/ 98: "press12",
2192+
/*0x63*/ 99: "release12",
2193+
/*0x64*/ 100: "press22",
2194+
/*0x65*/ 101: "release22",
2195+
/*0x66*/ 102: "short_press11",
2196+
/*0x67*/ 103: "short_press12",
2197+
/*0x68*/ 104: "short_press22",
2198+
/*0x69*/ 105: "press_8bit_vector", // with payload
2199+
/*0x6a*/ 106: "release_8bit_vector", // with payload
2200+
/*0xa0*/ 160: "attribute_reporting", // with payload
2201+
/*0xa1*/ 161: "manufacture_specific_attr_reporting", // with payload
2202+
/*0xa2*/ 162: "multi_cluster_reporting", // with payload
2203+
/*0xa3*/ 163: "manufacturer_specific_mcluster_reporting", // with payload
2204+
/*0xa4*/ 164: "request_attributes", // with payload
2205+
/*0xa5*/ 165: "read_attributes_response", // with payload
2206+
/*0xa6*/ 166: "zcl_tunneling", // with payload
2207+
/*0xa8*/ 168: "compact_attribute_reporting", // with payload
2208+
/*0xaf*/ 175: "any_sensor_command_a0_a3", // with payload
2209+
/*0xe0*/ 224: "commissioning", // with payload
2210+
/*0xe1*/ 225: "decommissioning",
2211+
/*0xe2*/ 226: "success",
2212+
/*0xe3*/ 227: "channel_request", // with payload
2213+
/*0xe4*/ 228: "application_description", // with payload
2214+
//-- sent to GPD
2215+
// /*0xf0*/ 240: "commissioning_reply",
2216+
// /*0xf1*/ 241: "write_attributes",
2217+
// /*0xf2*/ 242: "read_attributes",
2218+
// /*0xf3*/ 243: "channel_configuration",
2219+
// /*0xf6*/ 246: "zcl_tunneling",
2220+
};
2221+
2222+
export function genericGreenPower(): ModernExtend {
2223+
const exposes = [
2224+
e.action(Object.values(GPDF_COMMANDS)),
2225+
e.list("payload", ea.STATE, e.numeric("payload", ea.STATE).withDescription("Byte")).withDescription("Payload of the command"),
2226+
];
2227+
const fromZigbee: Fz.Converter[] = [
2228+
{
2229+
cluster: "greenPower",
2230+
type: ["commandNotification", "commandCommissioningNotification"],
2231+
convert: (model, msg, publish, options, meta) => {
2232+
const commandID = msg.data.commandID as number;
2233+
if (hasAlreadyProcessedMessage(msg, model, msg.data.frameCounter, `${msg.device.ieeeAddr}_${commandID}`)) return;
2234+
if (commandID >= 0xe0) return; // Skip op commands
2235+
2236+
const gpdfCommandStr = GPDF_COMMANDS[commandID];
2237+
const payloadBuf = msg.data.commandFrame.raw as Buffer | undefined;
2238+
2239+
return {
2240+
action: gpdfCommandStr ?? `unknown_${commandID}`,
2241+
payload: payloadBuf?.length > 0 ? Array.from(payloadBuf) : [],
2242+
};
2243+
},
2244+
},
2245+
];
2246+
2247+
return {exposes, fromZigbee, isModernExtend: true};
2248+
}
2249+
21412250
export interface CommandsScenesArgs {
21422251
commands?: string[];
21432252
bind?: boolean;

0 commit comments

Comments
 (0)