Skip to content

Instantly share code, notes, and snippets.

@waterlou
Created February 7, 2024 03:09
Show Gist options
  • Save waterlou/8ba3767a339b48a9d75741f5e664894e to your computer and use it in GitHub Desktop.
Save waterlou/8ba3767a339b48a9d75741f5e664894e to your computer and use it in GitHub Desktop.
Aqara_z1.js
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const legacy = require('zigbee-herdsman-converters/lib/legacy');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const constants = require('zigbee-herdsman-converters/lib/constants');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const utils = require('zigbee-herdsman-converters/lib/utils');
const globalStore = require('zigbee-herdsman-converters/lib/store');
const lumi = require('zigbee-herdsman-converters/lib/lumi');
const { lumiZigbeeOTA } = lumi.modernExtend;
//. PLEASE READ: These are present from device/xiaomi.ts, only needed for external converter
const herdsman = require('zigbee-herdsman');
const e = exposes.presets;
const ea = exposes.access;
const manufacturerOptions = {
xiaomi: { manufacturerCode: herdsman.Zcl.ManufacturerCode.LUMI_UNITED_TECH, disableDefaultResponse: true },
};
const { hasAlreadyProcessedMessage } = require('zigbee-herdsman-converters/lib/utils');
const preventReset = async (type, data, device) => {
if (
// options.allow_reset ||
type !== 'message' ||
data.type !== 'attributeReport' ||
data.cluster !== 'genBasic' ||
!data.data[0xfff0] ||
// eg: [0xaa, 0x10, 0x05, 0x41, 0x87, 0x01, 0x01, 0x10, 0x00]
!data.data[0xFFF0].slice(0, 5).equals(Buffer.from([0xaa, 0x10, 0x05, 0x41, 0x87]))
) {
return;
}
const payload = {
[0xfff0]: {
value: [0xaa, 0x10, 0x05, 0x41, 0x47, 0x01, 0x01, 0x10, 0x01],
type: 0x41,
}
};
await device.getEndpoint(1).write('genBasic', payload, { manufacturerCode });
};
const fzLocal = {
lumi_multistate_action: {
cluster: 'genMultistateInput',
type: ['attributeReport'],
convert: (model, msg, publish, options, meta) => {
if (hasAlreadyProcessedMessage(msg, model)) return;
let actionLookup = { 0: 'hold', 1: 'single', 2: 'double', 3: 'triple', 255: 'release' };
if (model.model === 'WXKG12LM') {
actionLookup = { ...actionLookup, 16: 'hold', 17: 'release', 18: 'shake' };
}
let buttonLookup = null;
if (['WXKG02LM_rev2', 'WXKG07LM', 'WXKG15LM', 'WXKG17LM', 'WXKG22LM'].includes(model.model)) {
buttonLookup = { 1: 'left', 2: 'right', 3: 'both' };
}
if (['QBKG12LM', 'QBKG24LM'].includes(model.model)) buttonLookup = { 5: 'left', 6: 'right', 7: 'both' };
if (['QBKG39LM', 'QBKG41LM', 'WS-EUK02', 'WS-EUK04', 'QBKG20LM', 'QBKG28LM', 'QBKG31LM', 'ZNQBKG25LM'].includes(model.model)) {
buttonLookup = { 41: 'left', 42: 'right', 51: 'both' };
}
if (['QBKG25LM', 'QBKG26LM', 'QBKG29LM', 'QBKG32LM', 'QBKG34LM', 'ZNQBKG31LM', 'ZNQBKG26LM'].includes(model.model)) {
buttonLookup = {
41: 'left', 42: 'center', 43: 'right',
51: 'left_center', 52: 'left_right', 53: 'center_right',
61: 'all',
};
}
if (['ZNQBKG39LM'].includes(model.model)) {
buttonLookup = { 1: 'top', 2: 'bottom' };
}
if (['ZNQBKG40LM'].includes(model.model)) {
buttonLookup = { 1: 'top', 2: 'middle', 3: 'bottom' };
}
if (['ZNQBKG41LM'].includes(model.model)) {
buttonLookup = { 1: 'top', 2: 'middle', 3: 'bottom', 4: 'wireless' };
}
if (['WS-USC02', 'WS-USC04'].includes(model.model)) {
buttonLookup = { 41: 'top', 42: 'bottom', 51: 'both' };
}
const action = actionLookup[msg.data['presentValue']];
if (buttonLookup) {
const button = buttonLookup[msg.endpoint.ID];
if (button) {
return { action: `${action}_${button}` };
}
} else {
return { action };
}
},
}
};
const tzLocal = {
lumi_switch_lock_relay_opple: {
key: ['lock_relay'],
convertSet: async (entity, key, value, meta) => {
await entity.write('manuSpecificLumi', { 0x0285: { value: (value ? 1 : 0), type: 0x20 } },
manufacturerOptions.lumi);
return { state: { lock_relay: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificLumi', [0x0285], manufacturerOptions.lumi);
},
},
lumi_switch_click_mode: {
key: ['click_mode'],
convertSet: async (entity, key, value, meta) => {
await entity.write('manuSpecificLumi',
{ 0x0286: { value: utils.getFromLookup(value, { 'fast': 0x1, 'multi': 0x02 }), type: 0x20 } }, manufacturerOptions.lumi);
return { state: { click_mode: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificLumi', [0x0286], manufacturerOptions.lumi);
},
},
lumi_switch_power_outage_memory: {
key: ['power_outage_memory'],
convertSet: async (entity, key, value, meta) => {
if (Array.isArray(meta.mapped)) throw new Error(`Not supported for groups`);
if (['SP-EUC01', 'ZNCZ04LM', 'ZNCZ15LM', 'QBCZ14LM', 'QBCZ15LM', 'SSM-U01', 'SSM-U02', 'DLKZMK11LM', 'DLKZMK12LM',
'WS-EUK01', 'WS-EUK02', 'WS-EUK03', 'WS-EUK04', 'QBKG19LM', 'QBKG18LM', 'QBKG20LM', 'QBKG25LM', 'QBKG26LM', 'QBKG28LM', 'QBKG29LM',
'QBKG30LM', 'QBKG31LM', 'QBKG32LM', 'QBKG34LM', 'QBKG38LM', 'QBKG39LM', 'QBKG40LM', 'QBKG41LM', 'ZNDDMK11LM', 'ZNLDP13LM',
'ZNQBKG31LM', 'WS-USC02', 'WS-USC03', 'WS-USC04', 'ZNQBKG24LM', 'ZNQBKG25LM', 'JWDL001A', 'SSWQD02LM', 'SSWQD03LM',
'XDD11LM', 'XDD12LM', 'XDD13LM', 'ZNLDP12LM', 'ZNLDP13LM', 'ZNXDD01LM', 'WS-USC01',
].includes(meta.mapped.model)) {
await entity.write('manuSpecificLumi', { 0x0201: { value: value ? 1 : 0, type: 0x10 } }, manufacturerOptions.lumi);
} else if (['ZNCZ02LM', 'QBCZ11LM', 'LLKZMK11LM'].includes(meta.mapped.model)) {
const payload = value ?
[[0xaa, 0x80, 0x05, 0xd1, 0x47, 0x07, 0x01, 0x10, 0x01], [0xaa, 0x80, 0x03, 0xd3, 0x07, 0x08, 0x01]] :
[[0xaa, 0x80, 0x05, 0xd1, 0x47, 0x09, 0x01, 0x10, 0x00], [0xaa, 0x80, 0x03, 0xd3, 0x07, 0x0a, 0x01]];
await entity.write('genBasic', { 0xFFF0: { value: payload[0], type: 0x41 } }, manufacturerOptions.lumi);
await entity.write('genBasic', { 0xFFF0: { value: payload[1], type: 0x41 } }, manufacturerOptions.lumi);
} else if (['ZNCZ11LM', 'ZNCZ12LM'].includes(meta.mapped.model)) {
const payload = value ?
[0xaa, 0x80, 0x05, 0xd1, 0x47, 0x00, 0x01, 0x10, 0x01] :
[0xaa, 0x80, 0x05, 0xd1, 0x47, 0x01, 0x01, 0x10, 0x00];
await entity.write('genBasic', { 0xFFF0: { value: payload, type: 0x41 } }, manufacturerOptions.lumi);
} else if (['ZNQBKG38LM', 'ZNQBKG39LM', 'ZNQBKG40LM', 'ZNQBKG41LM'].includes(meta.mapped.model)) {
// Support existing syntax of a nested object just for the state field. Though it's quite silly IMO.
const targetValue = utils.isObject(value) && value.hasOwnProperty('state') ? value.state : value;
const lookupState = { on: 0x01, electric_appliances_on: 0x00, electric_appliances_off: 0x02, inverted: 0x03 };
await entity.write('manuSpecificLumi', { 0x0517: { value: utils.getFromLookup(targetValue, lookupState), type: 0x20 } }, manufacturerOptions.lumi);
} else {
throw new Error('Not supported');
}
return { state: { power_outage_memory: value } };
},
convertGet: async (entity, key, meta) => {
if (Array.isArray(meta.mapped)) throw new Error(`Not supported for groups`);
if (['SP-EUC01', 'ZNCZ04LM', 'ZNCZ15LM', 'QBCZ14LM', 'QBCZ15LM', 'SSM-U01', 'SSM-U02', 'DLKZMK11LM', 'DLKZMK12LM',
'WS-EUK01', 'WS-EUK02', 'WS-EUK03', 'WS-EUK04', 'QBKG19LM', 'QBKG18LM', 'QBKG20LM', 'QBKG25LM', 'QBKG26LM', 'QBKG28LM', 'QBKG29LM',
'QBKG30LM', 'QBKG31LM', 'QBKG32LM', 'QBKG34LM', 'QBKG38LM', 'QBKG39LM', 'QBKG40LM', 'QBKG41LM', 'ZNDDMK11LM', 'ZNLDP13LM',
'ZNQBKG31LM', 'WS-USC02', 'WS-USC03', 'WS-USC04', 'ZNQBKG24LM', 'ZNQBKG25LM', 'JWDL001A', 'SSWQD02LM', 'SSWQD03LM',
'XDD11LM', 'XDD12LM', 'XDD13LM', 'ZNLDP12LM', 'ZNLDP13LM', 'ZNXDD01LM', 'WS-USC01',
].includes(meta.mapped.model)) {
await entity.read('manuSpecificLumi', [0x0201]);
} else if (['ZNCZ02LM', 'QBCZ11LM', 'ZNCZ11LM', 'ZNCZ12LM'].includes(meta.mapped.model)) {
await entity.read('manuSpecificLumi', [0xFFF0]);
} else if (['ZNQBKG38LM', 'ZNQBKG39LM', 'ZNQBKG40LM', 'ZNQBKG41LM'].includes(meta.mapped.model)) {
await entity.read('manuSpecificLumi', [0x0517]);
} else {
throw new Error('Not supported');
}
},
},
lumi_led_disabled_night: {
key: ['led_disabled_night'],
convertSet: async (entity, key, value, meta) => {
if (Array.isArray(meta.mapped)) throw new Error(`Not supported for groups`);
if (['ZNCZ04LM', 'ZNCZ12LM', 'ZNCZ15LM', 'QBCZ14LM', 'QBCZ15LM', 'QBKG19LM', 'QBKG18LM', 'QBKG20LM', 'QBKG25LM', 'QBKG26LM',
'QBKG28LM', 'QBKG29LM', 'QBKG30LM', 'QBKG31LM', 'QBKG32LM', 'QBKG34LM', 'DLKZMK11LM', 'SSM-U01', 'WS-EUK01', 'WS-EUK02',
'WS-EUK03', 'WS-EUK04', 'SP-EUC01', 'ZNQBKG24LM', 'ZNQBKG25LM',
'ZNQBKG38LM', 'ZNQBKG39LM', 'ZNQBKG40LM', 'ZNQBKG41LM'].includes(meta.mapped.model)) {
await entity.write('manuSpecificLumi', {0x0203: {value: value ? 1 : 0, type: 0x10}}, manufacturerOptions.lumi);
} else if (['ZNCZ11LM'].includes(meta.mapped.model)) {
const payload = value ?
[0xaa, 0x80, 0x05, 0xd1, 0x47, 0x00, 0x03, 0x10, 0x00] :
[0xaa, 0x80, 0x05, 0xd1, 0x47, 0x01, 0x03, 0x10, 0x01];
await entity.write('genBasic', {0xFFF0: {value: payload, type: 0x41}}, manufacturerOptions.lumi);
} else {
throw new Error('Not supported');
}
return {state: {led_disabled_night: value}};
},
convertGet: async (entity, key, meta) => {
if (Array.isArray(meta.mapped)) throw new Error(`Not supported for groups`);
if (['ZNCZ04LM', 'ZNCZ12LM', 'ZNCZ15LM', 'QBCZ15LM', 'QBCZ14LM', 'QBKG19LM', 'QBKG18LM', 'QBKG20LM', 'QBKG25LM', 'QBKG26LM',
'QBKG28LM', 'QBKG29LM', 'QBKG30LM', 'QBKG31LM', 'QBKG32LM', 'QBKG34LM', 'DLKZMK11LM', 'SSM-U01', 'WS-EUK01', 'WS-EUK02',
'WS-EUK03', 'WS-EUK04', 'SP-EUC01', 'ZNQBKG24LM', 'ZNQBKG25LM',
'ZNQBKG38LM', 'ZNQBKG39LM', 'ZNQBKG40LM', 'ZNQBKG41LM'].includes(meta.mapped.model)) {
await entity.read('manuSpecificLumi', [0x0203], manufacturerOptions.lumi);
} else {
throw new Error('Not supported');
}
},
},
};
const definitions = [
{
zigbeeModel: ['lumi.switch.acn048'],
model: 'ZNQBKG38LM',
vendor: 'Aqara',
description: '(External) Smart wall switch Z1 (single rocker)',
fromZigbee: [fz.on_off, fzLocal.lumi_multistate_action, lumi.fromZigbee.lumi_specific, fz.lumi_power],
toZigbee: [tz.on_off, tz.lumi_switch_operation_mode_opple, tzLocal.lumi_switch_power_outage_memory,
tzLocal.lumi_led_disabled_night, tzLocal.lumi_switch_lock_relay_opple],
meta: { multiEndpoint: true },
endpoint: (device) => {
return { 'button': 1 };
},
exposes: [
e.power(), e.voltage(), e.device_temperature(),
e.switch().withEndpoint('button'),
e.enum('power_outage_memory', ea.ALL, ['on', 'electric_appliances_on', 'electric_appliances_off', 'inverted'])
.withDescription('Power Outage Memory').withEndpoint('button'),
e.led_disabled_night(),
e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled'])
.withDescription('Decoupled mode').withEndpoint('button'),
e.binary('lock_relay', ea.ALL, true, false)
.withDescription('Lock relay mode').withEndpoint('button'),
e.action(['single']),
],
onEvent: preventReset,
configure: async (device, coordinatorEndpoint, logger) => {
await device.getEndpoint(1).write('manuSpecificLumi', { 'mode': 1 }, { manufacturerCode: manufacturerCode, disableResponse: true });
},
extend: [lumiZigbeeOTA()],
},
{
zigbeeModel: ['lumi.switch.acn049'],
model: 'ZNQBKG39LM',
vendor: 'Aqara',
description: '(External) Aqara smart wall switch Z1 (double rocker)',
fromZigbee: [fz.on_off, fzLocal.lumi_multistate_action, lumi.fromZigbee.lumi_specific, fz.lumi_power],
toZigbee: [tz.on_off, tz.lumi_switch_operation_mode_opple, tzLocal.lumi_switch_power_outage_memory,
tzLocal.lumi_led_disabled_night, tzLocal.lumi_switch_lock_relay_opple],
meta: { multiEndpoint: true },
endpoint: (device) => {
return { 'top': 1, 'bottom': 2 };
},
exposes: [
e.power(), e.voltage(), e.device_temperature(),
e.switch().withEndpoint('top'), e.switch().withEndpoint('bottom'),
e.enum('power_outage_memory', ea.ALL, ['on', 'electric_appliances_on', 'electric_appliances_off', 'inverted'])
.withDescription('Power Outage Memory'),
e.led_disabled_night(),
e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled'])
.withDescription('Decoupled mode for top button').withEndpoint('top'),
e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled'])
.withDescription('Decoupled mode for bottom button').withEndpoint('bottom'),
e.binary('lock_relay', ea.ALL, true, false)
.withDescription('Lock relay mode for top button').withEndpoint('top'),
e.binary('lock_relay', ea.ALL, true, false)
.withDescription('Lock relay mode for bottom button').withEndpoint('bottom'),
e.action(['single_top', 'single_bottom']),
],
onEvent: preventReset,
configure: async (device, coordinatorEndpoint, logger) => {
await device.getEndpoint(1).write('manuSpecificLumi', { 'mode': 1 }, { manufacturerCode: 0x115f, disableResponse: true });
},
extend: [lumiZigbeeOTA()],
},
{
zigbeeModel: ['lumi.switch.acn054'],
model: 'ZNQBKG40LM',
vendor: 'Aqara',
description: '(External) Smart wall switch Z1 (triple rocker)',
fromZigbee: [fz.on_off, fzLocal.lumi_multistate_action, lumi.fromZigbee.lumi_specific, fz.lumi_power],
toZigbee: [tz.on_off, tz.lumi_switch_operation_mode_opple, tzLocal.lumi_switch_power_outage_memory,
tzLocal.lumi_led_disabled_night, tzLocal.lumi_switch_lock_relay_opple],
meta: { multiEndpoint: true },
endpoint: (device) => {
return { 'top': 1, 'middle': 2, 'bottom': 3 };
},
exposes: [
e.power(), e.voltage(), e.device_temperature(),
e.switch().withEndpoint('top'), e.switch().withEndpoint('middle'), e.switch().withEndpoint('bottom'),
e.enum('power_outage_memory', ea.ALL, ['on', 'electric_appliances_on', 'electric_appliances_off', 'inverted'])
.withDescription('Power Outage Memory'),
e.led_disabled_night(),
e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled'])
.withDescription('Decoupled mode for top button').withEndpoint('top'),
e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled'])
.withDescription('Decoupled mode for middle button').withEndpoint('middle'),
e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled'])
.withDescription('Decoupled mode for bottom button').withEndpoint('bottom'),
e.binary('lock_relay', ea.ALL, true, false)
.withDescription('Lock relay mode for top button').withEndpoint('top'),
e.binary('lock_relay', ea.ALL, true, false)
.withDescription('Lock relay mode for middle button').withEndpoint('middle'),
e.binary('lock_relay', ea.ALL, true, false)
.withDescription('Lock relay mode for bottom button').withEndpoint('bottom'),
e.action(['single_top', 'single_middle', 'single_bottom']),
],
onEvent: preventReset,
configure: async (device, coordinatorEndpoint, logger) => {
await device.getEndpoint(1).write('manuSpecificLumi', { 'mode': 1 }, { manufacturerCode: manufacturerCode, disableResponse: true });
},
extend: [lumiZigbeeOTA()],
},
{
zigbeeModel: ['lumi.switch.acn055'],
model: 'ZNQBKG41LM',
vendor: 'Aqara',
description: '(External) Smart wall switch Z1 (quadruple rocker)',
fromZigbee: [fz.on_off, fzLocal.lumi_multistate_action, lumi.fromZigbee.lumi_specific, fz.lumi_power],
toZigbee: [tz.on_off, tz.lumi_switch_operation_mode_opple, tzLocal.lumi_switch_power_outage_memory,
tzLocal.lumi_led_disabled_night, tzLocal.lumi_switch_lock_relay_opple, tzLocal.lumi_switch_click_mode],
meta: { multiEndpoint: true },
endpoint: (device) => {
return { 'top': 1, 'middle': 2, 'bottom': 3, 'wireless': 4 };
},
exposes: [
e.power(), e.voltage(), e.device_temperature(),
e.switch().withEndpoint('top'), e.switch().withEndpoint('middle'), e.switch().withEndpoint('bottom'),
e.enum('power_outage_memory', ea.ALL, ['on', 'electric_appliances_on', 'electric_appliances_off', 'inverted'])
.withDescription('Power Outage Memory'),
e.led_disabled_night(),
e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled'])
.withDescription('Decoupled mode for top button').withEndpoint('top'),
e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled'])
.withDescription('Decoupled mode for middle button').withEndpoint('middle'),
e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled'])
.withDescription('Decoupled mode for bottom button').withEndpoint('bottom'),
e.binary('lock_relay', ea.ALL, true, false)
.withDescription('Lock relay mode for top button').withEndpoint('top'),
e.binary('lock_relay', ea.ALL, true, false)
.withDescription('Lock relay mode for middle button').withEndpoint('middle'),
e.binary('lock_relay', ea.ALL, true, false)
.withDescription('Lock relay mode for bottom button').withEndpoint('bottom'),
e.enum('click_mode', ea.ALL, ['fast', 'multi'])
.withDescription('Click mode(Wireless button only), fast: only supports single click which will be send immediately after clicking.' +
'multi: supports more events like double and hold'),
e.action(['single_top', 'single_middle', 'single_bottom',
'hold_wireless', 'single_wireless', 'double_wireless', 'release_wireless']),
],
onEvent: preventReset,
configure: async (device, coordinatorEndpoint, logger) => {
await device.getEndpoint(1).write('manuSpecificLumi', { 'mode': 1 }, { manufacturerCode: manufacturerCode, disableResponse: true });
},
extend: [lumiZigbeeOTA()],
},
];
module.exports = definitions;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment