Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Legrand 067776(A): Support calibration + Venetian mode #6333

Merged
merged 7 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 24 additions & 49 deletions src/devices/legrand.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,15 @@
import {Definition, Fz, OnEvent, Tz} from '../lib/types';
import {Definition} from '../lib/types';
import * as exposes from '../lib/exposes';
import fz from '../converters/fromZigbee';
import * as legacy from '../lib/legacy';
import tz from '../converters/toZigbee';
import * as reporting from '../lib/reporting';
import extend from '../lib/extend';
import * as utils from '../lib/utils';
import * as ota from '../lib/ota';
import {tzLegrand, fzLegrand, readInitialBatteryState, _067776} from '../lib/legrand';
const e = exposes.presets;
const ea = exposes.access;

const readInitialBatteryState: OnEvent = async (type, data, device, options) => {
if (['deviceAnnounce'].includes(type)) {
const endpoint = device.getEndpoint(1);
const options = {manufacturerCode: 0x1021, disableDefaultResponse: true};
await endpoint.read('genPowerCfg', ['batteryVoltage'], options);
}
};

const tzLocal = {
auto_mode: {
key: ['auto_mode'],
convertSet: async (entity, key, value, meta) => {
const mode = utils.getFromLookup(value, {'off': 0x00, 'auto': 0x02, 'on_override': 0x03});
const payload = {data: Buffer.from([mode])};
await entity.command('manuSpecificLegrandDevices3', 'command0', payload);
return {state: {'auto_mode': value}};
},
} as Tz.Converter,
};

const fzlocal = {
legrand_600087l: {
cluster: 'greenPower',
type: ['commandNotification'],
convert: (model, msg, publish, options, meta) => {
const commandID = msg.data.commandID;
const lookup: {[s: number]: string} = {0x34: 'stop', 0x35: 'up', 0x36: 'down'};
if (commandID === 224) return;
if (!lookup.hasOwnProperty(commandID)) {
meta.logger.error(`GreenPower_3 error: missing command '${commandID}'`);
} else {
return {action: lookup[commandID]};
}
},
} as Fz.Converter,
};

const definitions: Definition[] = [
{
zigbeeModel: [' Pocket remote\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000',
Expand Down Expand Up @@ -100,7 +63,7 @@ const definitions: Definition[] = [
extend: extend.switch(),
ota: ota.zigbeeOTA,
fromZigbee: [fz.identify, fz.on_off, fz.electrical_measurement, fz.legrand_cluster_fc01, fz.ignore_basic_report, fz.ignore_genOta],
toZigbee: [tz.legrand_deviceMode, tz.on_off, tz.legrand_identify, tz.electrical_measurement_power, tzLocal.auto_mode],
toZigbee: [tz.legrand_deviceMode, tz.on_off, tz.legrand_identify, tz.electrical_measurement_power, tzLegrand.auto_mode],
exposes: [
e.switch().withState('state', true, 'On/off (works only if device is in "switch" mode)'),
e.power().withAccess(ea.STATE_GET),
Expand Down Expand Up @@ -170,20 +133,26 @@ const definitions: Definition[] = [
vendor: 'Legrand',
description: 'Netatmo wired shutter switch',
ota: ota.zigbeeOTA,
fromZigbee: [fz.ignore_basic_report, fz.cover_position_tilt, fz.legrand_binary_input_moving, fz.identify, fz.legrand_led_in_dark],
toZigbee: [tz.cover_state, tz.cover_position_tilt, tz.legrand_identify, tz.legrand_settingEnableLedInDark],
fromZigbee: [fz.ignore_basic_report, fz.cover_position_tilt, fz.legrand_binary_input_moving, fz.identify,
fz.legrand_led_in_dark, fzLegrand.calibration_mode(false)],
toZigbee: [tz.cover_state, tz.cover_position_tilt, tz.legrand_identify, tz.legrand_settingEnableLedInDark, tzLegrand.calibration_mode(false)],
exposes: [
e.cover_position(),
_067776.getCover(),
e.action(['moving', 'identify']),
e.enum('identify', ea.SET, ['blink'])
.withDescription('Blinks the built-in LED to make it easier to identify the device'),
e.binary('led_in_dark', ea.ALL, 'ON', 'OFF')
.withDescription('Enables the built-in LED allowing to see the switch in the dark'),
_067776.getCalibrationModes(false),
],
configure: async (device, coordinatorEndpoint, logger) => {
const endpoint = device.getEndpoint(1);
await reporting.bind(endpoint, coordinatorEndpoint, ['genBinaryInput', 'closuresWindowCovering', 'genIdentify']);
await reporting.currentPositionLiftPercentage(endpoint);
let p = reporting.payload('currentPositionLiftPercentage', 1, 120, 1);
await endpoint.configureReporting('closuresWindowCovering', p, {manufacturerCode: 4129});

p = reporting.payload('currentPositionTiltPercentage', 1, 120, 1);
await endpoint.configureReporting('closuresWindowCovering', p, {manufacturerCode: 4129});
},
},
{
Expand Down Expand Up @@ -220,20 +189,26 @@ const definitions: Definition[] = [
vendor: 'Legrand',
description: 'Netatmo wired shutter switch with level control (NLLV)',
ota: ota.zigbeeOTA,
fromZigbee: [fz.ignore_basic_report, fz.cover_position_tilt, fz.legrand_binary_input_moving, fz.identify, fz.legrand_led_in_dark],
toZigbee: [tz.cover_state, tz.cover_position_tilt, tz.legrand_identify, tz.legrand_settingEnableLedInDark],
fromZigbee: [fz.ignore_basic_report, fz.cover_position_tilt, fz.legrand_binary_input_moving, fz.identify,
fz.legrand_led_in_dark, fzLegrand.calibration_mode(true)],
toZigbee: [tz.cover_state, tz.cover_position_tilt, tz.legrand_identify, tz.legrand_settingEnableLedInDark, tzLegrand.calibration_mode(true)],
exposes: [
e.cover_position(),
_067776.getCover(),
e.action(['moving', 'identify']),
e.enum('identify', ea.SET, ['blink'])
.withDescription('Blinks the built-in LED to make it easier to identify the device'),
e.binary('led_in_dark', ea.ALL, 'ON', 'OFF')
.withDescription('Enables the built-in LED allowing to see the switch in the dark'),
_067776.getCalibrationModes(true),
],
configure: async (device, coordinatorEndpoint, logger) => {
const endpoint = device.getEndpoint(1);
await reporting.bind(endpoint, coordinatorEndpoint, ['genBinaryInput', 'closuresWindowCovering', 'genIdentify']);
await reporting.currentPositionLiftPercentage(endpoint);
let p = reporting.payload('currentPositionLiftPercentage', 1, 120, 1);
await endpoint.configureReporting('closuresWindowCovering', p, {manufacturerCode: 4129});

p = reporting.payload('currentPositionTiltPercentage', 1, 120, 1);
await endpoint.configureReporting('closuresWindowCovering', p, {manufacturerCode: 4129});
},
},
{
Expand Down Expand Up @@ -538,7 +513,7 @@ const definitions: Definition[] = [
model: '600087L',
vendor: 'Legrand',
description: 'Wireless and batteryless blind control switch',
fromZigbee: [fzlocal.legrand_600087l],
fromZigbee: [fzLegrand.legrand_600087l],
toZigbee: [],
exposes: [e.action(['stop', 'up', 'down'])],
},
Expand Down
112 changes: 112 additions & 0 deletions src/lib/legrand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {Fz, Tz, OnEvent, KeyValueString} from '../lib/types';
import * as exposes from './exposes';
import * as utils from '../lib/utils';
const e = exposes.presets;
const ea = exposes.access;

const legrandOptions = {manufacturerCode: 4129, disableDefaultResponse: true};

const shutterCalibrationModes: {[k: number]: {description: string, onlyNLLV: boolean}} = {
0: {description: 'classic_nllv', onlyNLLV: true},
1: {description: 'specific_nllv', onlyNLLV: true},
2: {description: 'up_down_stop', onlyNLLV: false},
3: {description: 'temporal', onlyNLLV: false},
4: {description: 'venetian_bso', onlyNLLV: false},
};

const getApplicableCalibrationModes = (isNLLVSwitch: boolean): KeyValueString => {
return Object.fromEntries(Object.entries(shutterCalibrationModes)
.filter((e) => isNLLVSwitch ? true : e[1].onlyNLLV === false)
.map((e) => [e[0], e[1].description]));
};

export const readInitialBatteryState: OnEvent = async (type, data, device, options) => {
if (['deviceAnnounce'].includes(type)) {
const endpoint = device.getEndpoint(1);
const options = {manufacturerCode: 0x1021, disableDefaultResponse: true};
await endpoint.read('genPowerCfg', ['batteryVoltage'], options);
}
};

export const tzLegrand = {
auto_mode: {
key: ['auto_mode'],
convertSet: async (entity, key, value, meta) => {
const mode = utils.getFromLookup(value, {'off': 0x00, 'auto': 0x02, 'on_override': 0x03});
const payload = {data: Buffer.from([mode])};
await entity.command('manuSpecificLegrandDevices3', 'command0', payload);
return {state: {'auto_mode': value}};
},
} as Tz.Converter,
calibration_mode: (isNLLVSwitch: boolean) => {
return {
key: ['calibration_mode'],
convertSet: async (entity, key, value, meta) => {
const applicableModes = getApplicableCalibrationModes(isNLLVSwitch);
utils.validateValue(value, Object.values(applicableModes));
const idx = utils.getKey(applicableModes, value);
await entity.write('closuresWindowCovering', {'calibrationMode': idx}, legrandOptions);
},
convertGet: async (entity, key, meta) => {
await entity.read('closuresWindowCovering', ['calibrationMode'], legrandOptions);
},
} as Tz.Converter;
},
};

export const fzLegrand = {
legrand_600087l: {
cluster: 'greenPower',
type: ['commandNotification'],
convert: (model, msg, publish, options, meta) => {
const commandID = msg.data.commandID;
const lookup: {[s: number]: string} = {0x34: 'stop', 0x35: 'up', 0x36: 'down'};
if (commandID === 224) return;
if (!lookup.hasOwnProperty(commandID)) {
meta.logger.error(`GreenPower_3 error: missing command '${commandID}'`);
} else {
return {action: lookup[commandID]};
}
},
} as Fz.Converter,
calibration_mode: (isNLLVSwitch: boolean) => {
return {
cluster: 'closuresWindowCovering',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const attr = 'calibrationMode';
if (msg.data.hasOwnProperty(attr)) {
const applicableModes = getApplicableCalibrationModes(isNLLVSwitch);
const idx = msg.data[attr];
utils.validateValue(String(idx), Object.keys(applicableModes));
const calMode = applicableModes[idx];
return {calibration_mode: calMode};
}
},
} as Fz.Converter;
},
};

export const _067776 = {
getCover: () => {
const c = e.cover_position();
if (c.hasOwnProperty('features')) {
c.features.push(new exposes.Numeric('tilt', ea.ALL)
.withValueMin(0).withValueMax(100)
.withValueStep(25)
.withPreset('Closed', 0, 'Vertical')
.withPreset('25 %', 25, '25%')
.withPreset('50 %', 50, '50%')
.withPreset('75 %', 75, '75%')
.withPreset('Open', 100, 'Horizontal')
.withUnit('%')
.withDescription('Tilt percentage of that cover'));
}
return c;
},
getCalibrationModes: (isNLLVSwitch: boolean) => {
const modes = getApplicableCalibrationModes(isNLLVSwitch);
return e.enum('calibration_mode', ea.ALL, Object.values(modes))
.withDescription('Defines the calibration mode of the switch. (Caution: Changing modes requires a recalibration of the shutter switch!)');
},
};