From aaad9a887bd9f0ab987af3ecc7c4d5710085d25b Mon Sep 17 00:00:00 2001 From: Sven Jochems Date: Mon, 13 Jan 2025 20:37:43 +0100 Subject: [PATCH 1/7] Add action_reporting parameter to niko switches --- src/devices/niko.ts | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/devices/niko.ts b/src/devices/niko.ts index e52f26d4f69a4..1ff2a5ea06eda 100644 --- a/src/devices/niko.ts +++ b/src/devices/niko.ts @@ -28,6 +28,10 @@ const local = { convert: (model, msg, publish, options, meta) => { const state: KeyValue = {}; + if (msg.data.switchActionReporting !== undefined) { + const actionReportingMap: KeyValue = {0x00: false, 0x1F: true}; + state['action_reporting'] = utils.getFromLookup(msg.data.switchActionReporting, actionReportingMap); + } if (msg.data.switchAction !== undefined) { // NOTE: a single press = two separate values reported, 16 followed by 64 // a hold/release cycle = three separate values, 16, 32, and 48 @@ -122,6 +126,27 @@ const local = { await utils.enforceEndpoint(entity, key, meta).read('manuSpecificNiko1', ['switchOperationMode']); }, } satisfies Tz.Converter, + switch_action_reporting: { + key: ['action_reporting'], + convertSet: async (entity, key, value, meta) => { + const actionReportingMap: KeyValue = {false: 0x00, true: 0x1F}; + // @ts-expect-error ignore + if (actionReportingMap[value] === undefined) { + throw new Error(`action_reporting was called with an invalid value (${value})`); + } else { + await entity.write( + 'manuSpecificNiko2', + // @ts-expect-error ignore + {switchActionReporting: actionReportingMap[value]}, + ); + await entity.read('manuSpecificNiko2', ['switchActionReporting']); + return {state: {action_reporting: value}}; + } + }, + convertGet: async (entity, key, meta) => { + await entity.read('manuSpecificNiko2', ['switchActionReporting']); + }, + } satisfies Tz.Converter, switch_led_enable: { key: ['led_enable'], convertSet: async (entity, key, value, meta) => { @@ -276,17 +301,19 @@ const definitions: DefinitionWithExtend[] = [ vendor: 'Niko', description: 'Single connectable switch', fromZigbee: [fz.on_off, local.fz.switch_operation_mode, local.fz.switch_action, local.fz.switch_status_led], - toZigbee: [tz.on_off, local.tz.switch_operation_mode, local.tz.switch_led_enable, local.tz.switch_led_state], + toZigbee: [tz.on_off, local.tz.switch_operation_mode, local.tz.switch_action_reporting, local.tz.switch_led_enable, local.tz.switch_led_state], configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(1); await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff']); await reporting.onOff(endpoint); await endpoint.read('manuSpecificNiko1', ['switchOperationMode', 'outletLedState', 'outletLedColor']); + await endpoint.read('manuSpecificNiko2', ['switchActionReporting']); }, exposes: [ e.switch(), e.action(['single', 'hold', 'release', 'single_ext', 'hold_ext', 'release_ext']), e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled']), + e.binary('action_reporting', ea.ALL, true, false), e.binary('led_enable', ea.ALL, true, false).withDescription('Enable LED'), e.binary('led_state', ea.ALL, 'ON', 'OFF').withDescription('LED State'), ], @@ -297,7 +324,7 @@ const definitions: DefinitionWithExtend[] = [ vendor: 'Niko', description: 'Double connectable switch', fromZigbee: [fz.on_off, local.fz.switch_operation_mode, local.fz.switch_action, local.fz.switch_status_led], - toZigbee: [tz.on_off, local.tz.switch_operation_mode, local.tz.switch_led_enable, local.tz.switch_led_state], + toZigbee: [tz.on_off, local.tz.switch_operation_mode, local.tz.switch_action_reporting, local.tz.switch_led_enable, local.tz.switch_led_state], endpoint: (device) => { return {l1: 1, l2: 2}; }, @@ -311,6 +338,8 @@ const definitions: DefinitionWithExtend[] = [ await reporting.onOff(ep2); await ep1.read('manuSpecificNiko1', ['switchOperationMode', 'outletLedState', 'outletLedColor']); await ep2.read('manuSpecificNiko1', ['switchOperationMode', 'outletLedState', 'outletLedColor']); + await ep1.read('manuSpecificNiko2', ['switchActionReporting']); + await ep2.read('manuSpecificNiko2', ['switchActionReporting']); }, exposes: [ e.switch().withEndpoint('l1'), @@ -330,6 +359,7 @@ const definitions: DefinitionWithExtend[] = [ 'release_right_ext', ]), e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled']), + e.binary('action_reporting', ea.ALL, true, false), e.binary('led_enable', ea.ALL, true, false).withEndpoint('l1').withDescription('Enable LED'), e.binary('led_enable', ea.ALL, true, false).withEndpoint('l2').withDescription('Enable LED'), e.binary('led_state', ea.ALL, 'ON', 'OFF').withEndpoint('l1').withDescription('LED State'), From 0a85cb546df137b13918f11f06abe46ad94d57e9 Mon Sep 17 00:00:00 2001 From: Sven Jochems Date: Sat, 18 Jan 2025 16:38:46 +0100 Subject: [PATCH 2/7] pretty --- src/devices/niko.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/devices/niko.ts b/src/devices/niko.ts index 1ff2a5ea06eda..1533c325594cf 100644 --- a/src/devices/niko.ts +++ b/src/devices/niko.ts @@ -29,12 +29,15 @@ const local = { const state: KeyValue = {}; if (msg.data.switchActionReporting !== undefined) { - const actionReportingMap: KeyValue = {0x00: false, 0x1F: true}; + const actionReportingMap: KeyValue = {0x00: false, 0x1f: true}; state['action_reporting'] = utils.getFromLookup(msg.data.switchActionReporting, actionReportingMap); } if (msg.data.switchAction !== undefined) { // NOTE: a single press = two separate values reported, 16 followed by 64 // a hold/release cycle = three separate values, 16, 32, and 48 + // NOTE: these values should be interpreted bitwise + // when pushing multiple buttons at the same time, multiple bits can be set simultaneously and should generate multiple events + // currently, these values are not mapped and thus ignored const actionMap: KeyValue = model.model == '552-721X1' ? { @@ -129,7 +132,6 @@ const local = { switch_action_reporting: { key: ['action_reporting'], convertSet: async (entity, key, value, meta) => { - const actionReportingMap: KeyValue = {false: 0x00, true: 0x1F}; // @ts-expect-error ignore if (actionReportingMap[value] === undefined) { throw new Error(`action_reporting was called with an invalid value (${value})`); @@ -301,12 +303,20 @@ const definitions: DefinitionWithExtend[] = [ vendor: 'Niko', description: 'Single connectable switch', fromZigbee: [fz.on_off, local.fz.switch_operation_mode, local.fz.switch_action, local.fz.switch_status_led], - toZigbee: [tz.on_off, local.tz.switch_operation_mode, local.tz.switch_action_reporting, local.tz.switch_led_enable, local.tz.switch_led_state], + toZigbee: [ + tz.on_off, + local.tz.switch_operation_mode, + local.tz.switch_action_reporting, + local.tz.switch_led_enable, + local.tz.switch_led_state, + ], configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(1); await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff']); await reporting.onOff(endpoint); await endpoint.read('manuSpecificNiko1', ['switchOperationMode', 'outletLedState', 'outletLedColor']); + // Enable action reporting by default + await endpoint.write('manuSpecificNiko2', {switchActionReporting: true}); await endpoint.read('manuSpecificNiko2', ['switchActionReporting']); }, exposes: [ @@ -324,7 +334,13 @@ const definitions: DefinitionWithExtend[] = [ vendor: 'Niko', description: 'Double connectable switch', fromZigbee: [fz.on_off, local.fz.switch_operation_mode, local.fz.switch_action, local.fz.switch_status_led], - toZigbee: [tz.on_off, local.tz.switch_operation_mode, local.tz.switch_action_reporting, local.tz.switch_led_enable, local.tz.switch_led_state], + toZigbee: [ + tz.on_off, + local.tz.switch_operation_mode, + local.tz.switch_action_reporting, + local.tz.switch_led_enable, + local.tz.switch_led_state, + ], endpoint: (device) => { return {l1: 1, l2: 2}; }, @@ -338,8 +354,9 @@ const definitions: DefinitionWithExtend[] = [ await reporting.onOff(ep2); await ep1.read('manuSpecificNiko1', ['switchOperationMode', 'outletLedState', 'outletLedColor']); await ep2.read('manuSpecificNiko1', ['switchOperationMode', 'outletLedState', 'outletLedColor']); + // Enable action reporting by default + await ep1.write('manuSpecificNiko2', {switchActionReporting: true}); await ep1.read('manuSpecificNiko2', ['switchActionReporting']); - await ep2.read('manuSpecificNiko2', ['switchActionReporting']); }, exposes: [ e.switch().withEndpoint('l1'), From 565debf592e5178f42f73aa0452243bc3385463c Mon Sep 17 00:00:00 2001 From: Sven Jochems Date: Sun, 19 Jan 2025 21:55:49 +0100 Subject: [PATCH 3/7] Move custom niko clusters --- src/devices/niko.ts | 95 +++++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 29 deletions(-) diff --git a/src/devices/niko.ts b/src/devices/niko.ts index 1533c325594cf..c728675d2eb04 100644 --- a/src/devices/niko.ts +++ b/src/devices/niko.ts @@ -1,6 +1,9 @@ +import {Zcl} from 'zigbee-herdsman'; + import fz from '../converters/fromZigbee'; import tz from '../converters/toZigbee'; import * as exposes from '../lib/exposes'; +import {deviceAddCustomCluster} from '../lib/modernExtend'; import * as reporting from '../lib/reporting'; import {DefinitionWithExtend, Fz, KeyValue, Tz} from '../lib/types'; import * as utils from '../lib/utils'; @@ -9,9 +12,39 @@ const e = exposes.presets; const ea = exposes.access; const local = { + modernExtend: { + addCustomClusterManuSpecificNikoConfig: () => + deviceAddCustomCluster('manuSpecificNikoConfig', { + ID: 0xfc00, + manufacturerCode: Zcl.ManufacturerCode.NIKO_NV, + attributes: { + /* WARNING: 0x0000 has different datatypes! + * enum8 (switch) vs. bitmap8 (outlet) + * unknown usage/function on outlet + */ + switchOperationMode: {ID: 0x0000, type: Zcl.DataType.ENUM8}, + outletLedColor: {ID: 0x0100, type: Zcl.DataType.UINT24}, + outletChildLock: {ID: 0x0101, type: Zcl.DataType.UINT8}, + outletLedState: {ID: 0x0104, type: Zcl.DataType.UINT8}, + }, + commands: {}, + commandsResponse: {}, + }), + addCustomClusterManuSpecificNikoState: () => + deviceAddCustomCluster('manuSpecificNikoState', { + ID: 0xfc01, + manufacturerCode: Zcl.ManufacturerCode.NIKO_NV, + attributes: { + switchActionReporting: {ID: 0x0001, type: Zcl.DataType.BITMAP8}, + switchAction: {ID: 0x0002, type: Zcl.DataType.UINT8}, + }, + commands: {}, + commandsResponse: {}, + }), + }, fz: { switch_operation_mode: { - cluster: 'manuSpecificNiko1', + cluster: 'manuSpecificNikoConfig', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const state: KeyValue = {}; @@ -23,7 +56,7 @@ const local = { }, } satisfies Fz.Converter, switch_action: { - cluster: 'manuSpecificNiko2', + cluster: 'manuSpecificNikoState', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const state: KeyValue = {}; @@ -75,7 +108,7 @@ const local = { }, } satisfies Fz.Converter, switch_status_led: { - cluster: 'manuSpecificNiko1', + cluster: 'manuSpecificNikoConfig', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const state: KeyValue = {}; @@ -89,7 +122,7 @@ const local = { }, } satisfies Fz.Converter, outlet: { - cluster: 'manuSpecificNiko1', + cluster: 'manuSpecificNikoConfig', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const state: KeyValue = {}; @@ -116,7 +149,7 @@ const local = { throw new Error(`operation_mode was called with an invalid value (${value})`); } else { await utils.enforceEndpoint(entity, key, meta).write( - 'manuSpecificNiko1', + 'manuSpecificNikoConfig', // @ts-expect-error ignore {switchOperationMode: operationModeLookup[value]}, ); @@ -126,70 +159,71 @@ const local = { }, convertGet: async (entity, key, meta) => { utils.assertEndpoint(entity); - await utils.enforceEndpoint(entity, key, meta).read('manuSpecificNiko1', ['switchOperationMode']); + await utils.enforceEndpoint(entity, key, meta).read('manuSpecificNikoConfig', ['switchOperationMode']); }, } satisfies Tz.Converter, switch_action_reporting: { key: ['action_reporting'], convertSet: async (entity, key, value, meta) => { + const actionReportingMap: KeyValue = {false: 0x00, true: 0x1f}; // @ts-expect-error ignore if (actionReportingMap[value] === undefined) { throw new Error(`action_reporting was called with an invalid value (${value})`); } else { await entity.write( - 'manuSpecificNiko2', + 'manuSpecificNikoState', // @ts-expect-error ignore {switchActionReporting: actionReportingMap[value]}, ); - await entity.read('manuSpecificNiko2', ['switchActionReporting']); + await entity.read('manuSpecificNikoState', ['switchActionReporting']); return {state: {action_reporting: value}}; } }, convertGet: async (entity, key, meta) => { - await entity.read('manuSpecificNiko2', ['switchActionReporting']); + await entity.read('manuSpecificNikoState', ['switchActionReporting']); }, } satisfies Tz.Converter, switch_led_enable: { key: ['led_enable'], convertSet: async (entity, key, value, meta) => { - await entity.write('manuSpecificNiko1', {outletLedState: value ? 1 : 0}); - await entity.read('manuSpecificNiko1', ['outletLedColor']); + await entity.write('manuSpecificNikoConfig', {outletLedState: value ? 1 : 0}); + await entity.read('manuSpecificNikoConfig', ['outletLedColor']); return {state: {led_enable: value ? true : false}}; }, convertGet: async (entity, key, meta) => { - await entity.read('manuSpecificNiko1', ['outletLedState']); + await entity.read('manuSpecificNikoConfig', ['outletLedState']); }, } satisfies Tz.Converter, switch_led_state: { key: ['led_state'], convertSet: async (entity, key, value, meta) => { utils.assertString(value, key); - await entity.write('manuSpecificNiko1', {outletLedColor: value.toLowerCase() === 'off' ? 0 : 255}); + await entity.write('manuSpecificNikoConfig', {outletLedColor: value.toLowerCase() === 'off' ? 0 : 255}); return {state: {led_state: value.toLowerCase() === 'off' ? 'OFF' : 'ON'}}; }, convertGet: async (entity, key, meta) => { - await entity.read('manuSpecificNiko1', ['outletLedColor']); + await entity.read('manuSpecificNikoConfig', ['outletLedColor']); }, } satisfies Tz.Converter, outlet_child_lock: { key: ['child_lock'], convertSet: async (entity, key, value, meta) => { utils.assertString(value, key); - await entity.write('manuSpecificNiko1', {outletChildLock: value.toLowerCase() === 'lock' ? 0 : 1}); + await entity.write('manuSpecificNikoConfig', {outletChildLock: value.toLowerCase() === 'lock' ? 0 : 1}); return {state: {child_lock: value.toLowerCase() === 'lock' ? 'LOCK' : 'UNLOCK'}}; }, convertGet: async (entity, key, meta) => { - await entity.read('manuSpecificNiko1', ['outletChildLock']); + await entity.read('manuSpecificNikoConfig', ['outletChildLock']); }, } satisfies Tz.Converter, outlet_led_enable: { key: ['led_enable'], convertSet: async (entity, key, value, meta) => { - await entity.write('manuSpecificNiko1', {outletLedState: value ? 1 : 0}); + await entity.write('manuSpecificNikoConfig', {outletLedState: value ? 1 : 0}); return {state: {led_enable: value ? true : false}}; }, convertGet: async (entity, key, meta) => { - await entity.read('manuSpecificNiko1', ['outletLedState']); + await entity.read('manuSpecificNikoConfig', ['outletLedState']); }, } satisfies Tz.Converter, }, @@ -203,6 +237,7 @@ const definitions: DefinitionWithExtend[] = [ description: 'Connected socket outlet', fromZigbee: [fz.on_off, fz.electrical_measurement, fz.metering, local.fz.outlet], toZigbee: [tz.on_off, tz.electrical_measurement_power, tz.currentsummdelivered, local.tz.outlet_child_lock, local.tz.outlet_led_enable], + extend: [local.modernExtend.addCustomClusterManuSpecificNikoConfig()], configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(1); await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff', 'haElectricalMeasurement', 'seMetering']); @@ -220,8 +255,8 @@ const definitions: DefinitionWithExtend[] = [ await reporting.readMeteringMultiplierDivisor(endpoint); await reporting.currentSummDelivered(endpoint, {min: 60, change: 1}); - await endpoint.read('manuSpecificNiko1', ['outletChildLock']); - await endpoint.read('manuSpecificNiko1', ['outletLedState']); + await endpoint.read('manuSpecificNikoConfig', ['outletChildLock']); + await endpoint.read('manuSpecificNikoConfig', ['outletLedState']); }, exposes: [ e.switch(), @@ -310,20 +345,21 @@ const definitions: DefinitionWithExtend[] = [ local.tz.switch_led_enable, local.tz.switch_led_state, ], + extend: [local.modernExtend.addCustomClusterManuSpecificNikoConfig(), local.modernExtend.addCustomClusterManuSpecificNikoState()], configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(1); await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff']); await reporting.onOff(endpoint); - await endpoint.read('manuSpecificNiko1', ['switchOperationMode', 'outletLedState', 'outletLedColor']); + await endpoint.read('manuSpecificNikoConfig', ['switchOperationMode', 'outletLedState', 'outletLedColor']); // Enable action reporting by default - await endpoint.write('manuSpecificNiko2', {switchActionReporting: true}); - await endpoint.read('manuSpecificNiko2', ['switchActionReporting']); + await endpoint.write('manuSpecificNikoState', {switchActionReporting: true}); + await endpoint.read('manuSpecificNikoState', ['switchActionReporting']); }, exposes: [ e.switch(), e.action(['single', 'hold', 'release', 'single_ext', 'hold_ext', 'release_ext']), e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled']), - e.binary('action_reporting', ea.ALL, true, false), + e.binary('action_reporting', ea.ALL, true, false).withDescription('Enable Action Reporting'), e.binary('led_enable', ea.ALL, true, false).withDescription('Enable LED'), e.binary('led_state', ea.ALL, 'ON', 'OFF').withDescription('LED State'), ], @@ -344,6 +380,7 @@ const definitions: DefinitionWithExtend[] = [ endpoint: (device) => { return {l1: 1, l2: 2}; }, + extend: [local.modernExtend.addCustomClusterManuSpecificNikoConfig(), local.modernExtend.addCustomClusterManuSpecificNikoState()], meta: {multiEndpointEnforce: {operation_mode: 1}, multiEndpoint: true}, configure: async (device, coordinatorEndpoint) => { const ep1 = device.getEndpoint(1); @@ -352,11 +389,11 @@ const definitions: DefinitionWithExtend[] = [ await reporting.bind(ep2, coordinatorEndpoint, ['genOnOff']); await reporting.onOff(ep1); await reporting.onOff(ep2); - await ep1.read('manuSpecificNiko1', ['switchOperationMode', 'outletLedState', 'outletLedColor']); - await ep2.read('manuSpecificNiko1', ['switchOperationMode', 'outletLedState', 'outletLedColor']); + await ep1.read('manuSpecificNikoConfig', ['switchOperationMode', 'outletLedState', 'outletLedColor']); + await ep2.read('manuSpecificNikoConfig', ['switchOperationMode', 'outletLedState', 'outletLedColor']); // Enable action reporting by default - await ep1.write('manuSpecificNiko2', {switchActionReporting: true}); - await ep1.read('manuSpecificNiko2', ['switchActionReporting']); + await ep1.write('manuSpecificNikoState', {switchActionReporting: true}); + await ep1.read('manuSpecificNikoState', ['switchActionReporting']); }, exposes: [ e.switch().withEndpoint('l1'), @@ -376,7 +413,7 @@ const definitions: DefinitionWithExtend[] = [ 'release_right_ext', ]), e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled']), - e.binary('action_reporting', ea.ALL, true, false), + e.binary('action_reporting', ea.ALL, true, false).withDescription('Enable Action Reporting'), e.binary('led_enable', ea.ALL, true, false).withEndpoint('l1').withDescription('Enable LED'), e.binary('led_enable', ea.ALL, true, false).withEndpoint('l2').withDescription('Enable LED'), e.binary('led_state', ea.ALL, 'ON', 'OFF').withEndpoint('l1').withDescription('LED State'), From 1da63912274d65ba9744c099a162b15fdc8d4acb Mon Sep 17 00:00:00 2001 From: Sven Jochems Date: Mon, 20 Jan 2025 20:15:58 +0100 Subject: [PATCH 4/7] Add functionality to change led color on niko switch --- src/devices/niko.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/devices/niko.ts b/src/devices/niko.ts index c728675d2eb04..dfbedde429168 100644 --- a/src/devices/niko.ts +++ b/src/devices/niko.ts @@ -116,7 +116,8 @@ const local = { state['led_enable'] = msg.data['outletLedState'] == 1; } if (msg.data.outletLedColor !== undefined) { - state['led_state'] = msg.data['outletLedColor'] == 255 ? 'ON' : 'OFF'; + const ledStateMap: KeyValue = {0x00: 'OFF', 0x0000ff: 'ON', 0x00ff00: 'Blue', 0xff0000: 'Red', 0xffffff: 'Purple'}; + state['led_state'] = utils.getFromLookup(msg.data.outletLedColor, ledStateMap); } return state; }, @@ -175,7 +176,6 @@ const local = { // @ts-expect-error ignore {switchActionReporting: actionReportingMap[value]}, ); - await entity.read('manuSpecificNikoState', ['switchActionReporting']); return {state: {action_reporting: value}}; } }, @@ -197,9 +197,18 @@ const local = { switch_led_state: { key: ['led_state'], convertSet: async (entity, key, value, meta) => { - utils.assertString(value, key); - await entity.write('manuSpecificNikoConfig', {outletLedColor: value.toLowerCase() === 'off' ? 0 : 255}); - return {state: {led_state: value.toLowerCase() === 'off' ? 'OFF' : 'ON'}}; + const ledStateMap: KeyValue = {OFF: 0x00, ON: 0x0000ff, Blue: 0x00ff00, Red: 0xff0000, Purple: 0xffffff}; + // @ts-expect-error ignore + if (ledStateMap[value] === undefined) { + throw new Error(`led_state was called with an invalid value (${value})`); + } else { + await entity.write( + 'manuSpecificNikoConfig', + // @ts-expect-error ignore + {outletLedColor: ledStateMap[value]}, + ); + return {state: {led_state: value}}; + } }, convertGet: async (entity, key, meta) => { await entity.read('manuSpecificNikoConfig', ['outletLedColor']); @@ -361,7 +370,7 @@ const definitions: DefinitionWithExtend[] = [ e.enum('operation_mode', ea.ALL, ['control_relay', 'decoupled']), e.binary('action_reporting', ea.ALL, true, false).withDescription('Enable Action Reporting'), e.binary('led_enable', ea.ALL, true, false).withDescription('Enable LED'), - e.binary('led_state', ea.ALL, 'ON', 'OFF').withDescription('LED State'), + e.enum('led_state', ea.ALL, ['ON', 'OFF', 'Blue', 'Red', 'Purple']).withDescription('LED State'), ], }, { @@ -416,8 +425,8 @@ const definitions: DefinitionWithExtend[] = [ e.binary('action_reporting', ea.ALL, true, false).withDescription('Enable Action Reporting'), e.binary('led_enable', ea.ALL, true, false).withEndpoint('l1').withDescription('Enable LED'), e.binary('led_enable', ea.ALL, true, false).withEndpoint('l2').withDescription('Enable LED'), - e.binary('led_state', ea.ALL, 'ON', 'OFF').withEndpoint('l1').withDescription('LED State'), - e.binary('led_state', ea.ALL, 'ON', 'OFF').withEndpoint('l2').withDescription('LED State'), + e.enum('led_state', ea.ALL, ['ON', 'OFF', 'Blue', 'Red', 'Purple']).withEndpoint('l1').withDescription('LED State'), + e.enum('led_state', ea.ALL, ['ON', 'OFF', 'Blue', 'Red', 'Purple']).withEndpoint('l2').withDescription('LED State'), ], }, { From 5733019a097d4c2dc46b283cb23e16ae9a2d705c Mon Sep 17 00:00:00 2001 From: Sven Jochems Date: Fri, 24 Jan 2025 20:50:21 +0100 Subject: [PATCH 5/7] Add niko switch led_sync_mode --- src/devices/niko.ts | 91 ++++++++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 30 deletions(-) diff --git a/src/devices/niko.ts b/src/devices/niko.ts index dfbedde429168..bbffce3fd4bfb 100644 --- a/src/devices/niko.ts +++ b/src/devices/niko.ts @@ -26,6 +26,8 @@ const local = { outletLedColor: {ID: 0x0100, type: Zcl.DataType.UINT24}, outletChildLock: {ID: 0x0101, type: Zcl.DataType.UINT8}, outletLedState: {ID: 0x0104, type: Zcl.DataType.UINT8}, + /* WARNING: 0x0107 is not supported on older switches */ + ledSyncMode: {ID: 0x0107, type: Zcl.DataType.BITMAP32}, }, commands: {}, commandsResponse: {}, @@ -68,43 +70,43 @@ const local = { if (msg.data.switchAction !== undefined) { // NOTE: a single press = two separate values reported, 16 followed by 64 // a hold/release cycle = three separate values, 16, 32, and 48 - // NOTE: these values should be interpreted bitwise - // when pushing multiple buttons at the same time, multiple bits can be set simultaneously and should generate multiple events - // currently, these values are not mapped and thus ignored - const actionMap: KeyValue = + + // https://github.com/Koenkk/zigbee2mqtt/issues/13737#issuecomment-1520002786 + const buttonShift: {[key: string]: number} = model.model == '552-721X1' ? { - 16: null, - 64: 'single', - 32: 'hold', - 48: 'release', - 256: null, - 1024: 'single_ext', - 512: 'hold_ext', - 768: 'release_ext', + left: 4, + left_ext: 8, + right: 12, + right_ext: 16, } : { - 16: null, - 64: 'single_left', - 32: 'hold_left', - 48: 'release_left', - 256: null, - 1024: 'single_left_ext', - 512: 'hold_left_ext', - 768: 'release_left_ext', - 4096: null, - 16384: 'single_right', - 8192: 'hold_right', - 12288: 'release_right', - 65536: null, - 262144: 'single_right_ext', - 131072: 'hold_right_ext', - 196608: 'release_right_ext', + '': 4, + ext: 8, }; + const actions: {[key: string]: number} = { + null: 1, + single: 4, + hold: 2, + release: 3, + }; - state['action'] = actionMap[msg.data.switchAction]; + for (const button in buttonShift) { + const shiftedValue = (msg.data.switchAction >> buttonShift[button]) & 0xf; + for (const action in actions) { + if (shiftedValue == actions[action]) { + const buttonPostFix = button === '' ? '' : '_' + button; + if ('null' === action) { + // publish without button name + publish({action: 'null'}); + } else { + const value = action + buttonPostFix; + publish({action: value}); + } + } + } + } } - return state; }, } satisfies Fz.Converter, switch_status_led: { @@ -119,6 +121,10 @@ const local = { const ledStateMap: KeyValue = {0x00: 'OFF', 0x0000ff: 'ON', 0x00ff00: 'Blue', 0xff0000: 'Red', 0xffffff: 'Purple'}; state['led_state'] = utils.getFromLookup(msg.data.outletLedColor, ledStateMap); } + if (msg.data.ledSyncMode !== undefined) { + const ledSyncMap: KeyValue = {0x00: 'Off', 0x11: 'On', 0x22: 'Inverted'}; + state['led_sync_mode'] = utils.getFromLookup(msg.data.ledSyncMode, ledSyncMap); + } return state; }, } satisfies Fz.Converter, @@ -214,6 +220,27 @@ const local = { await entity.read('manuSpecificNikoConfig', ['outletLedColor']); }, } satisfies Tz.Converter, + switch_led_sync_mode: { + key: ['led_sync_mode'], + convertSet: async (entity, key, value, meta) => { + // This could be set endpoint specific, where the first 4 bits are used for the left button and the last 4 bits for the right button + const ledSyncMap: KeyValue = {Off: 0x00, On: 0x11, Inverted: 0x11}; + // @ts-expect-error ignore + if (ledSyncMap[value] === undefined) { + throw new Error(`led_sync_mode was called with an invalid value (${value})`); + } else { + await entity.write( + 'manuSpecificNikoConfig', + // @ts-expect-error ignore + {ledSyncMode: ledSyncMap[value]}, + ); + return {state: {led_sync_mode: value}}; + } + }, + convertGet: async (entity, key, meta) => { + await entity.read('manuSpecificNikoConfig', ['ledSyncMode']); + }, + } satisfies Tz.Converter, outlet_child_lock: { key: ['child_lock'], convertSet: async (entity, key, value, meta) => { @@ -353,6 +380,7 @@ const definitions: DefinitionWithExtend[] = [ local.tz.switch_action_reporting, local.tz.switch_led_enable, local.tz.switch_led_state, + local.tz.switch_led_sync_mode, ], extend: [local.modernExtend.addCustomClusterManuSpecificNikoConfig(), local.modernExtend.addCustomClusterManuSpecificNikoState()], configure: async (device, coordinatorEndpoint) => { @@ -371,6 +399,7 @@ const definitions: DefinitionWithExtend[] = [ e.binary('action_reporting', ea.ALL, true, false).withDescription('Enable Action Reporting'), e.binary('led_enable', ea.ALL, true, false).withDescription('Enable LED'), e.enum('led_state', ea.ALL, ['ON', 'OFF', 'Blue', 'Red', 'Purple']).withDescription('LED State'), + e.enum('led_sync_mode', ea.ALL, ['Off', 'On', 'Inverted']).withDescription('Sync LED with relay state'), ], }, { @@ -385,6 +414,7 @@ const definitions: DefinitionWithExtend[] = [ local.tz.switch_action_reporting, local.tz.switch_led_enable, local.tz.switch_led_state, + local.tz.switch_led_sync_mode, ], endpoint: (device) => { return {l1: 1, l2: 2}; @@ -427,6 +457,7 @@ const definitions: DefinitionWithExtend[] = [ e.binary('led_enable', ea.ALL, true, false).withEndpoint('l2').withDescription('Enable LED'), e.enum('led_state', ea.ALL, ['ON', 'OFF', 'Blue', 'Red', 'Purple']).withEndpoint('l1').withDescription('LED State'), e.enum('led_state', ea.ALL, ['ON', 'OFF', 'Blue', 'Red', 'Purple']).withEndpoint('l2').withDescription('LED State'), + e.enum('led_sync_mode', ea.ALL, ['Off', 'On', 'Inverted']).withDescription('Sync LED with relay state'), ], }, { From 1d966f697c30bd2b87a9ef7bd72ea661ed466f94 Mon Sep 17 00:00:00 2001 From: Sven Jochems Date: Sun, 26 Jan 2025 20:39:18 +0100 Subject: [PATCH 6/7] Make niko led_sync_mode work per endpoint --- src/devices/niko.ts | 64 +++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/src/devices/niko.ts b/src/devices/niko.ts index bbffce3fd4bfb..4e2eaec453393 100644 --- a/src/devices/niko.ts +++ b/src/devices/niko.ts @@ -75,17 +75,16 @@ const local = { const buttonShift: {[key: string]: number} = model.model == '552-721X1' ? { + '': 4, + ext: 8, + } + : { left: 4, left_ext: 8, right: 12, right_ext: 16, - } - : { - '': 4, - ext: 8, }; const actions: {[key: string]: number} = { - null: 1, single: 4, hold: 2, release: 3, @@ -96,13 +95,8 @@ const local = { for (const action in actions) { if (shiftedValue == actions[action]) { const buttonPostFix = button === '' ? '' : '_' + button; - if ('null' === action) { - // publish without button name - publish({action: 'null'}); - } else { - const value = action + buttonPostFix; - publish({action: value}); - } + const value = action + buttonPostFix; + publish({action: value}); } } } @@ -122,8 +116,18 @@ const local = { state['led_state'] = utils.getFromLookup(msg.data.outletLedColor, ledStateMap); } if (msg.data.ledSyncMode !== undefined) { - const ledSyncMap: KeyValue = {0x00: 'Off', 0x11: 'On', 0x22: 'Inverted'}; - state['led_sync_mode'] = utils.getFromLookup(msg.data.ledSyncMode, ledSyncMap); + const ledSyncMap: {[key: number]: string} = {0: 'Off', 1: 'On', 2: 'Inverted'}; + if (model.meta.multiEndpoint) { + const endpointOffsetMap: {[key: string]: number} = {l1: 0, l2: 1}; + for (const ep in endpointOffsetMap) { + const shift = endpointOffsetMap[ep] * 4; + const mask = 0xf << shift; + const result = (msg.data.ledSyncMode & mask) >> shift; + state['led_sync_mode_' + ep] = utils.getFromLookup(result, ledSyncMap); + } + } else { + state['led_sync_mode'] = utils.getFromLookup(msg.data.ledSyncMode, ledSyncMap); + } } return state; }, @@ -224,18 +228,30 @@ const local = { key: ['led_sync_mode'], convertSet: async (entity, key, value, meta) => { // This could be set endpoint specific, where the first 4 bits are used for the left button and the last 4 bits for the right button - const ledSyncMap: KeyValue = {Off: 0x00, On: 0x11, Inverted: 0x11}; + //const ep = meta.endpoint_name === undefined ? 1 : {l1: 1, l2: 2}[meta.endpoint_name]; + const ledSyncMap: {[key: string]: number} = {Off: 0, On: 1, Inverted: 2}; // @ts-expect-error ignore if (ledSyncMap[value] === undefined) { throw new Error(`led_sync_mode was called with an invalid value (${value})`); - } else { - await entity.write( - 'manuSpecificNikoConfig', + } + const endpointOffsetMap: {[key: string]: number} = {l1: 0, l2: 1}; + let result: number = 0x00; + if (endpointOffsetMap[meta.endpoint_name] !== undefined) { + // combine states of all endpoints into single value to write to device + for (const ep in endpointOffsetMap) { // @ts-expect-error ignore - {ledSyncMode: ledSyncMap[value]}, - ); - return {state: {led_sync_mode: value}}; + const endpointState: number = ep === meta.endpoint_name ? value : meta.state['led_sync_mode_' + ep]; + // @ts-expect-error ignore + const endpointValue = ledSyncMap[endpointState] === undefined ? ledSyncMap[value] : ledSyncMap[endpointState]; + const shiftedValue = endpointValue << (endpointOffsetMap[ep] * 4); + result = result | shiftedValue; + } + } else { + // @ts-expect-error ignore + result = ledSyncMap[value]; } + await entity.write('manuSpecificNikoConfig', {ledSyncMode: result}); + return {state: {led_sync_mode: value}}; }, convertGet: async (entity, key, meta) => { await entity.read('manuSpecificNikoConfig', ['ledSyncMode']); @@ -391,6 +407,7 @@ const definitions: DefinitionWithExtend[] = [ // Enable action reporting by default await endpoint.write('manuSpecificNikoState', {switchActionReporting: true}); await endpoint.read('manuSpecificNikoState', ['switchActionReporting']); + await endpoint.read('manuSpecificNikoConfig', ['ledSyncMode']); }, exposes: [ e.switch(), @@ -433,6 +450,8 @@ const definitions: DefinitionWithExtend[] = [ // Enable action reporting by default await ep1.write('manuSpecificNikoState', {switchActionReporting: true}); await ep1.read('manuSpecificNikoState', ['switchActionReporting']); + await ep1.read('manuSpecificNikoConfig', ['ledSyncMode']); + await ep2.read('manuSpecificNikoConfig', ['ledSyncMode']); }, exposes: [ e.switch().withEndpoint('l1'), @@ -457,7 +476,8 @@ const definitions: DefinitionWithExtend[] = [ e.binary('led_enable', ea.ALL, true, false).withEndpoint('l2').withDescription('Enable LED'), e.enum('led_state', ea.ALL, ['ON', 'OFF', 'Blue', 'Red', 'Purple']).withEndpoint('l1').withDescription('LED State'), e.enum('led_state', ea.ALL, ['ON', 'OFF', 'Blue', 'Red', 'Purple']).withEndpoint('l2').withDescription('LED State'), - e.enum('led_sync_mode', ea.ALL, ['Off', 'On', 'Inverted']).withDescription('Sync LED with relay state'), + e.enum('led_sync_mode', ea.ALL, ['Off', 'On', 'Inverted']).withEndpoint('l1').withDescription('Sync LED with relay state'), + e.enum('led_sync_mode', ea.ALL, ['Off', 'On', 'Inverted']).withEndpoint('l2').withDescription('Sync LED with relay state'), ], }, { From 3062c393b60f45f814609c12db36e60bed713b4f Mon Sep 17 00:00:00 2001 From: Sven Jochems Date: Sun, 26 Jan 2025 20:55:31 +0100 Subject: [PATCH 7/7] Remove comment --- src/devices/niko.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/devices/niko.ts b/src/devices/niko.ts index 4e2eaec453393..d1ae73fac1d9d 100644 --- a/src/devices/niko.ts +++ b/src/devices/niko.ts @@ -227,8 +227,6 @@ const local = { switch_led_sync_mode: { key: ['led_sync_mode'], convertSet: async (entity, key, value, meta) => { - // This could be set endpoint specific, where the first 4 bits are used for the left button and the last 4 bits for the right button - //const ep = meta.endpoint_name === undefined ? 1 : {l1: 1, l2: 2}[meta.endpoint_name]; const ledSyncMap: {[key: string]: number} = {Off: 0, On: 1, Inverted: 2}; // @ts-expect-error ignore if (ledSyncMap[value] === undefined) {