Skip to content

Commit

Permalink
fix: Aqara: refactor rotary knobs to modern extend (#7392)
Browse files Browse the repository at this point in the history
* Add lumiKnobRotation modern extend

* Convert ZNXNKG01LM to modern extend

* Add lumiCommandMode modern extend

* Use lumiCommandMode in ZNXNKG01LM

* Add lumiBattery modern extend

* Convert ZNXNKG02LM to modern extend

* Remove unused lumi_knob_rotation converter
  • Loading branch information
mrskycriper committed Apr 13, 2024
1 parent be49ef4 commit fedbf7a
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 60 deletions.
60 changes: 19 additions & 41 deletions src/devices/lumi.ts
Expand Up @@ -19,6 +19,7 @@ const {
lumiOverloadProtection, lumiLedIndicator, lumiButtonLock, lumiMotorSpeed,
lumiOnOff, lumiLedDisabledNight, lumiFlipIndicatorLight, lumiPreventReset,
lumiClickMode, lumiSlider, lumiSetEventMode, lumiSwitchMode, lumiVibration,
lumiKnobRotation, lumiCommandMode, lumiBattery,
} = lumi.modernExtend;
import {Definition} from '../lib/types';
import {logger} from '../lib/logger';
Expand Down Expand Up @@ -2651,25 +2652,14 @@ const definitions: Definition[] = [
model: 'ZNXNKG02LM',
vendor: 'Aqara',
description: 'Smart rotary knob H1 (wireless)',
meta: {battery: {voltageToPercentage: '3V_2850_3000'}},
extend: [quirkCheckinInterval('1_HOUR'), lumiPreventReset()],
exposes: [e.battery(), e.battery_voltage(),
e.action(['single', 'double', 'hold', 'release', 'start_rotating', 'rotation', 'stop_rotating']),
e.enum('operation_mode', ea.ALL, ['event', 'command']).withDescription('Button mode'),
e.enum('action_rotation_button_state', ea.STATE, ['released', 'pressed']).withDescription('Button state during rotation'),
e.numeric('action_rotation_angle', ea.STATE).withUnit('*').withDescription('Rotation angle'),
e.numeric('action_rotation_angle_speed', ea.STATE).withUnit('*').withDescription('Rotation angle speed'),
e.numeric('action_rotation_percent', ea.STATE).withUnit('%').withDescription('Rotation percent'),
e.numeric('action_rotation_percent_speed', ea.STATE).withUnit('%').withDescription('Rotation percent speed'),
e.numeric('action_rotation_time', ea.STATE).withUnit('ms').withDescription('Rotation time'),
extend: [
quirkCheckinInterval('1_HOUR'),
lumiPreventReset(),
lumiCommandMode(),
lumiAction({actionLookup: {'hold': 0, 'single': 1, 'double': 2, 'release': 255}}),
lumiBattery({voltageToPercentage: '3V_2850_3000'}),
lumiKnobRotation(),
],
fromZigbee: [lumi.fromZigbee.lumi_action, lumi.fromZigbee.lumi_action_multistate, lumi.fromZigbee.lumi_basic,
lumi.fromZigbee.lumi_specific, lumi.fromZigbee.lumi_knob_rotation],
toZigbee: [lumi.toZigbee.lumi_operation_mode_opple],
configure: async (device, coordinatorEndpoint) => {
const endpoint1 = device.getEndpoint(1);
await endpoint1.write('manuSpecificLumi', {'mode': 1}, {manufacturerCode: manufacturerCode, disableResponse: true});
},
},
{
zigbeeModel: ['lumi.remote.acn003'],
Expand Down Expand Up @@ -3300,30 +3290,18 @@ const definitions: Definition[] = [
zigbeeModel: ['lumi.switch.rkna01'],
model: 'ZNXNKG01LM',
vendor: 'Aqara',
description: 'Aqara knob H1 (with Neutral)',
fromZigbee: [fz.on_off, lumi.fromZigbee.lumi_action, lumi.fromZigbee.lumi_action_multistate, lumi.fromZigbee.lumi_basic,
lumi.fromZigbee.lumi_specific, lumi.fromZigbee.lumi_knob_rotation],
toZigbee: [tz.on_off, lumi.toZigbee.lumi_switch_operation_mode_opple, lumi.toZigbee.lumi_switch_power_outage_memory,
lumi.toZigbee.lumi_switch_mode_switch],

meta: {multiEndpoint: true},
endpoint: (device) => {
return {'left': 1, 'center': 2, 'right': 3};
},

exposes: [
e.switch().withEndpoint('left'), e.switch().withEndpoint('center'), e.switch().withEndpoint('right'),
e.device_temperature(),
e.action(['single', 'double', 'hold', 'release', 'start_rotating', 'rotation', 'stop_rotating']),
e.power_outage_count(), e.energy(), e.voltage(), e.power(),
e.enum('action_rotation_button_state', ea.STATE, ['released', 'pressed']).withDescription('Button state during rotation'),
e.numeric('action_rotation_angle', ea.STATE).withUnit('*').withDescription('Rotation angle'),
e.numeric('action_rotation_angle_speed', ea.STATE).withUnit('*').withDescription('Rotation angle speed'),
e.numeric('action_rotation_percent', ea.STATE).withUnit('%').withDescription('Rotation percent'),
e.numeric('action_rotation_percent_speed', ea.STATE).withUnit('%').withDescription('Rotation percent speed'),
e.numeric('action_rotation_time', ea.STATE).withUnit('ms').withDescription('Rotation time'),
description: 'Smart rotary knob H1 (with neutral)',
extend: [
lumiPreventReset(),
deviceEndpoints({endpoints: {'left': 1, 'center': 2, 'right': 3}}),
lumiOnOff({powerOutageMemory: 'binary', endpointNames: ['left', 'center', 'right']}),
lumiCommandMode(),
lumiAction({actionLookup: {'hold': 0, 'single': 1, 'double': 2, 'release': 255}}),
lumiKnobRotation(),
lumiElectricityMeter(),
lumiPower(),
lumiZigbeeOTA(),
],
extend: [lumiZigbeeOTA(), lumiPreventReset()],
},
];

Expand Down
157 changes: 138 additions & 19 deletions src/lib/lumi.ts
Expand Up @@ -839,6 +839,33 @@ export const numericAttributes2Payload = async (msg: Fz.Message, meta: Fz.Meta,
return payload;
};

const numericAttributes2Lookup = async (dataObject: KeyValue) => {
let result: KeyValue = {};
for (const [key, value] of Object.entries(dataObject)) {
switch (key) {
case '247':
{
// @ts-expect-error
const dataObject247 = buffer2DataObject(meta, model, value);
const result247 = await numericAttributes2Lookup(dataObject247);
result = {...result, ...result247};
}
break;
case '65281':
{
// @ts-expect-error
const result65281 = await numericAttributes2Lookup(value);
result = {...result, ...result65281};
}
break;
default:
result[key] = value;
}
}

return result;
};

type LumiPresenceRegionZone = {x: number, y: number}

const lumiPresenceConstants = {
Expand Down Expand Up @@ -1766,6 +1793,117 @@ export const lumiModernExtend = {
},
];

return {exposes, fromZigbee, isModernExtend: true};
},
lumiKnobRotation: (): ModernExtend => {
const exposes: Expose[] = [
e.action(['start_rotating', 'rotation', 'stop_rotating']),
e.enum('action_rotation_button_state', ea.STATE, ['released', 'pressed'])
.withDescription('Button state during rotation').withCategory('diagnostic'),
e.numeric('action_rotation_angle', ea.STATE)
.withUnit('*').withDescription('Rotation angle').withCategory('diagnostic'),
e.numeric('action_rotation_angle_speed', ea.STATE)
.withUnit('*').withDescription('Rotation angle speed').withCategory('diagnostic'),
e.numeric('action_rotation_percent', ea.STATE).withUnit('%')
.withDescription('Rotation percent').withCategory('diagnostic'),
e.numeric('action_rotation_percent_speed', ea.STATE)
.withUnit('%').withDescription('Rotation percent speed').withCategory('diagnostic'),
e.numeric('action_rotation_time', ea.STATE)
.withUnit('ms').withDescription('Rotation time').withCategory('diagnostic'),
];

const fromZigbee: Fz.Converter[] = [{
cluster: 'manuSpecificLumi',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
if (msg.data.hasOwnProperty(570)) {
const act: KeyValueNumberString = {1: 'start_rotating', 2: 'rotation', 3: 'stop_rotating'};
const state: KeyValueNumberString = {0: 'released', 128: 'pressed'};
return {
action: act[msg.data[570] & ~128],
action_rotation_button_state: state[msg.data[570] & 128],
action_rotation_angle: msg.data[558],
action_rotation_angle_speed: msg.data[560],
action_rotation_percent: msg.data[563],
action_rotation_percent_speed: msg.data[562],
action_rotation_time: msg.data[561],
};
}
},
}];

return {exposes, fromZigbee, isModernExtend: true};
},
lumiCommandMode: (args?: {setEventMode: boolean}): ModernExtend => {
args = {setEventMode: true, ...args};
const exposes: Expose[] = [
e.enum('operation_mode', ea.ALL, ['event', 'command'])
.withDescription('Command mode is usefull for binding. Event mode is usefull for processing.'),
];

const toZigbee: Tz.Converter[] = [{
key: ['operation_mode'],
convertSet: async (entity, key, value, meta) => {
assertString(value);
// modes:
// 0 - 'command' mode. keys send commands. useful for binding
// 1 - 'event' mode. keys send events. useful for handling
const lookup = {command: 0, event: 1};
const endpoint = meta.device.getEndpoint(1);
await endpoint.write('manuSpecificLumi', {'mode': getFromLookup(value.toLowerCase(), lookup)},
{manufacturerCode: manufacturerOptions.lumi.manufacturerCode});
return {state: {operation_mode: value.toLowerCase()}};
},
convertGet: async (entity, key, meta) => {
const endpoint = meta.device.getEndpoint(1);
await endpoint.read('manuSpecificLumi', ['mode'], {manufacturerCode: manufacturerOptions.lumi.manufacturerCode});
},
}];
const result: ModernExtend = {exposes, toZigbee, isModernExtend: true};

if (args.setEventMode) {
result.configure = lumiModernExtend.lumiSetEventMode().configure;
}

return result;
},
lumiBattery: (args?: {
cluster?: 'genBasic' | 'manuSpecificLumi',
voltageToPercentage?: string | {min: number, max: number},
percentageAtrribute?: number,
voltageAttribute?: number,
}): ModernExtend => {
args = {
cluster: 'manuSpecificLumi',
percentageAtrribute: 1,
voltageAttribute: 1,
...args,
};
const exposes: Expose[] = [e.battery(), e.battery_voltage()];

const fromZigbee: Fz.Converter[] = [
{
cluster: args.cluster,
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const payload: KeyValueAny = {};
const lookup: KeyValueAny = numericAttributes2Lookup(msg.data);
if (lookup[args.percentageAtrribute.toString()]) {
const value = lookup[args.percentageAtrribute];
assertNumber(value);
if (!args.voltageToPercentage) payload.battery = value;
}
if (lookup[args.voltageAttribute.toString()]) {
const value = lookup[args.voltageAttribute];
assertNumber(value);
payload.voltage = value;
if (args.voltageToPercentage) payload.battery = batteryVoltageToPercentage(value, args.voltageToPercentage);
}
return payload;
},
},
];

return {exposes, fromZigbee, isModernExtend: true};
},
};
Expand Down Expand Up @@ -2478,25 +2616,6 @@ export const fromZigbee = {
}
},
} satisfies Fz.Converter,
lumi_knob_rotation: {
cluster: 'manuSpecificLumi',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
if (msg.data.hasOwnProperty(570)) {
const act: KeyValueNumberString = {1: 'start_rotating', 2: 'rotation', 3: 'stop_rotating'};
const state: KeyValueNumberString = {0: 'released', 128: 'pressed'};
return {
action: act[msg.data[570] & ~128],
action_rotation_button_state: state[msg.data[570] & 128],
action_rotation_angle: msg.data[558],
action_rotation_angle_speed: msg.data[560],
action_rotation_percent: msg.data[563],
action_rotation_percent_speed: msg.data[562],
action_rotation_time: msg.data[561],
};
}
},
} satisfies Fz.Converter,
lumi_curtain_status: {
cluster: 'genMultistateOutput',
type: ['attributeReport'],
Expand Down

0 comments on commit fedbf7a

Please sign in to comment.