Part 1:
import {Zcl} from 'zigbee-herdsman';
import fz from '../converters/fromZigbee';
import tz from '../converters/toZigbee';
import * as constants from '../lib/constants';
import * as exposes from '../lib/exposes';
import {electricityMeter, light, onOff} from '../lib/modernExtend';
import * as reporting from '../lib/reporting';
import {DefinitionWithExtend, Fz, KeyValue, KeyValueAny, Tz} from '../lib/types';
import * as utils from '../lib/utils';
import {precisionRound} from '../lib/utils';
const e = exposes.presets;
const ea = exposes.access;
const manuSinope = {manufacturerCode: Zcl.ManufacturerCode.SINOPE_TECHNOLOGIES};
const fzLocal = {
ias_water_leak_alarm: {
// RM3500ZB specific
cluster: 'ssIasZone',
type: ['commandStatusChangeNotification', 'attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const zoneStatus = msg.data.zoneStatus;
return {
water_leak: (zoneStatus & 1) > 0,
tamper: (zoneStatus & (1 << 2)) > 0,
};
},
} satisfies Fz.Converter,
thermostat: {
cluster: 'hvacThermostat',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
// @ts-expect-error ignore
delete msg['running_state'];
const result: KeyValue = {};
const occupancyLookup = {0: 'unoccupied', 1: 'occupied'};
const cycleOutputLookup = {15: '15_sec', 300: '5_min', 600: '10_min', 900: '15_min', 1200: '20_min', 1800: '30_min', 65535: 'off'};
if (msg.data['1024'] !== undefined) {
result.thermostat_occupancy = utils.getFromLookup(msg.data['1024'], occupancyLookup);
}
if (msg.data.SinopeOccupancy !== undefined) {
result.thermostat_occupancy = utils.getFromLookup(msg.data['SinopeOccupancy'], occupancyLookup);
}
if (msg.data['1025'] !== undefined) {
result.main_cycle_output = utils.getFromLookup(msg.data['1025'], cycleOutputLookup);
}
if (msg.data.SinopeMainCycleOutput !== undefined) {
result.main_cycle_output = utils.getFromLookup(msg.data['SinopeMainCycleOutput'], cycleOutputLookup);
}
if (msg.data['1026'] !== undefined) {
const lookup = {0: 'on_demand', 1: 'sensing'};
result.backlight_auto_dim = utils.getFromLookup(msg.data['1026'], lookup);
}
if (msg.data.SinopeBacklight !== undefined) {
const lookup = {0: 'on_demand', 1: 'sensing'};
result.backlight_auto_dim = utils.getFromLookup(msg.data['SinopeBacklight'], lookup);
}
if (msg.data['1028'] !== undefined) {
result.aux_cycle_output = utils.getFromLookup(msg.data['1028'], cycleOutputLookup);
}
if (msg.data.localTemp !== undefined) {
result.local_temperature = precisionRound(msg.data['localTemp'], 2) / 100;
}
if (msg.data.localTemperatureCalibration !== undefined) {
result.local_temperature_calibration = precisionRound(msg.data['localTemperatureCalibration'], 2) / 10;
}
if (msg.data.outdoorTemp !== undefined) {
result.outdoor_temperature = precisionRound(msg.data['outdoorTemp'], 2) / 100;
}
if (msg.data.occupiedHeatingSetpoint !== undefined) {
result.occupied_heating_setpoint = precisionRound(msg.data['occupiedHeatingSetpoint'], 2) / 100;
}
if (msg.data.unoccupiedHeatingSetpoint !== undefined) {
result.unoccupied_heating_setpoint = precisionRound(msg.data['unoccupiedHeatingSetpoint'], 2) / 100;
}
if (msg.data.occupiedCoolingSetpoint !== undefined) {
result.occupied_cooling_setpoint = precisionRound(msg.data['occupiedCoolingSetpoint'], 2) / 100;
}
if (msg.data.unoccupiedCoolingSetpoint !== undefined) {
result.unoccupied_cooling_setpoint = precisionRound(msg.data['unoccupiedCoolingSetpoint'], 2) / 100;
}
if (msg.data.ctrlSeqeOfOper !== undefined) {
result.control_sequence_of_operation = constants.thermostatControlSequenceOfOperations[msg.data['ctrlSeqeOfOper']];
}
if (msg.data.systemMode !== undefined) {
result.system_mode = constants.thermostatSystemModes[msg.data['systemMode']];
}
if (msg.data.pIHeatingDemand !== undefined) {
result.pi_heating_demand = precisionRound(msg.data['pIHeatingDemand'], 0);
}
if (msg.data.minHeatSetpointLimit !== undefined) {
result.min_heat_setpoint_limit = precisionRound(msg.data['minHeatSetpointLimit'], 2) / 100;
}
if (msg.data.maxHeatSetpointLimit !== undefined) {
result.max_heat_setpoint_limit = precisionRound(msg.data['maxHeatSetpointLimit'], 2) / 100;
}
if (msg.data.absMinHeatSetpointLimit !== undefined) {
result.abs_min_heat_setpoint_limit = precisionRound(msg.data['absMinHeatSetpointLimit'], 2) / 100;
}
if (msg.data.absMaxHeatSetpointLimit !== undefined) {
result.abs_max_heat_setpoint_limit = precisionRound(msg.data['absMaxHeatSetpointLimit'], 2) / 100;
}
if (msg.data.pIHeatingDemand !== undefined) {
result.running_state = msg.data['pIHeatingDemand'] >= 10 ? 'heat' : 'idle';
}
return result;
},
} satisfies Fz.Converter,
tank_level: {
cluster: 'genAnalogInput',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const result: KeyValue = {};
if (msg.data.presentValue !== undefined) {
let x = msg.data['presentValue'];
if (x == -1) {
result.tank_level = 0;
} else {
const xMin = 110;
const xMax = 406;
const delta = 46;
if (delta <= x && x <= 70) {
x = delta;
}
if (0 <= x && x <= delta) {
x = x + 360;
}
const y = (x - xMin) / (xMax - xMin);
const lowerLimit = 10;
const upperLimit = 80;
const valueRange = upperLimit - lowerLimit;
const pct = y * valueRange + lowerLimit;
result.tank_level = utils.precisionRound(pct, 2);
}
}
return result;
},
} satisfies Fz.Converter,
sinope: {
cluster: 'manuSpecificSinope',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const result: KeyValue = {};
if (msg.data.GFCiStatus !== undefined) {
const lookup = {0: 'off', 1: 'on'};
result.gfci_status = utils.getFromLookup(msg.data['GFCiStatus'], lookup);
}
if (msg.data.floorLimitStatus !== undefined) {
const lookup = {0: 'off', 1: 'on'};
result.floor_limit_status = utils.getFromLookup(msg.data['floorLimitStatus'], lookup);
}
if (msg.data.secondScreenBehavior !== undefined) {
const lookup = {0: 'auto', 1: 'setpoint', 2: 'outdoor temp'};
result.second_display_mode = utils.getFromLookup(msg.data['secondScreenBehavior'], lookup);
}
if (msg.data.outdoorTempToDisplayTimeout !== undefined) {
result.outdoor_temperature_timeout = msg.data['outdoorTempToDisplayTimeout'];
// DEPRECATED: Use Second Display Mode or control via set outdoorTempToDisplayTimeout
result.enable_outdoor_temperature = msg.data['outdoorTempToDisplayTimeout'] === 12 ? 'OFF' : 'ON';
}
if (msg.data.outdoorTempToDisplay !== undefined) {
result.thermostat_outdoor_temperature = precisionRound(msg.data['outdoorTempToDisplay'], 2) / 100;
}
if (msg.data.currentTimeToDisplay !== undefined) {
result.current_time_to_display = msg.data['currentTimeToDisplay'];
}
if (msg.data.floorControlMode !== undefined) {
const lookup = {1: 'ambiant', 2: 'floor'};
result.floor_control_mode = utils.getFromLookup(msg.data['floorControlMode'], lookup);
}
if (msg.data.ambiantMaxHeatSetpointLimit !== undefined) {
result.ambiant_max_heat_setpoint = msg.data['ambiantMaxHeatSetpointLimit'] / 100.0;
if (result.ambiant_max_heat_setpoint === -327.68) {
result.ambiant_max_heat_setpoint = 'off';
}
}
if (msg.data.floorMinHeatSetpointLimit !== undefined) {
result.floor_min_heat_setpoint = msg.data['floorMinHeatSetpointLimit'] / 100.0;
if (result.floor_min_heat_setpoint === -327.68) {
result.floor_min_heat_setpoint = 'off';
}
}
if (msg.data.floorMaxHeatSetpointLimit !== undefined) {
result.floor_max_heat_setpoint = msg.data['floorMaxHeatSetpointLimit'] / 100.0;
if (result.floor_max_heat_setpoint === -327.68) {
result.floor_max_heat_setpoint = 'off';
}
}
if (msg.data.temperatureSensor !== undefined) {
const lookup = {0: '10k', 1: '12k'};
result.floor_temperature_sensor = utils.getFromLookup(msg.data['temperatureSensor'], lookup);
}
if (msg.data.timeFormatToDisplay !== undefined) {
const lookup = {0: '24h', 1: '12h'};
result.time_format = utils.getFromLookup(msg.data['timeFormatToDisplay'], lookup);
}
if (msg.data.connectedLoad !== undefined) {
result.connected_load = msg.data['connectedLoad'];
}
if (msg.data.auxConnectedLoad !== undefined) {
result.aux_connected_load = msg.data['auxConnectedLoad'];
if (result.aux_connected_load == 65535) {
result.aux_connected_load = 'disabled';
}
}
if (msg.data.pumpProtection !== undefined) {
result.pump_protection = msg.data['pumpProtection'] == 1 ? 'ON' : 'OFF';
}
if (msg.data.dimmerTimmer !== undefined) {
result.timer_seconds = msg.data['dimmerTimmer'];
}
if (msg.data.ledIntensityOn !== undefined) {
result.led_intensity_on = msg.data['ledIntensityOn'];
}
if (msg.data.ledIntensityOff !== undefined) {
result.led_intensity_off = msg.data['ledIntensityOff'];
}
if (msg.data.minimumBrightness !== undefined) {
result.minimum_brightness = msg.data['minimumBrightness'];
}
if (msg.data.actionReport !== undefined) {
const lookup = {
1: 'up_clickdown',
2: 'up_single',
3: 'up_hold',
4: 'up_double',
17: 'down_clickdown',
18: 'down_single',
19: 'down_hold',
20: 'down_double',
};
result.action = utils.getFromLookup(msg.data['actionReport'], lookup);
}
if (msg.data.keypadLockout !== undefined) {
const lookup = {0: 'unlock', 1: 'lock'};
result.keypad_lockout = utils.getFromLookup(msg.data['keypadLockout'], lookup);
}
if (msg.data.drConfigWaterTempMin !== undefined) {
result.low_water_temp_protection = msg.data['drConfigWaterTempMin'];
}
return result;
},
} satisfies Fz.Converter,
};
const tzLocal = {
thermostat_occupancy: {
key: ['thermostat_occupancy'],
convertSet: async (entity, key, value, meta) => {
const sinopeOccupancy = {0: 'unoccupied', 1: 'occupied'};
const SinopeOccupancy = utils.getKey(sinopeOccupancy, value, value, Number);
await entity.write('hvacThermostat', {SinopeOccupancy}, manuSinope);
return {state: {thermostat_occupancy: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacThermostat', ['SinopeOccupancy'], manuSinope);
},
} satisfies Tz.Converter,
backlight_autodim: {
key: ['backlight_auto_dim'],
convertSet: async (entity, key, value, meta) => {
const sinopeBacklightParam = {0: 'on_demand', 1: 'sensing'};
const SinopeBacklight = utils.getKey(sinopeBacklightParam, value, value, Number);
await entity.write('hvacThermostat', {SinopeBacklight}, manuSinope);
return {state: {backlight_auto_dim: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacThermostat', ['SinopeBacklight'], manuSinope);
},
} satisfies Tz.Converter,
main_cycle_output: {
key: ['main_cycle_output'],
convertSet: async (entity, key, value, meta) => {
const lookup = {'15_sec': 15, '5_min': 300, '10_min': 600, '15_min': 900, '20_min': 1200, '30_min': 1800};
await entity.write('hvacThermostat', {SinopeMainCycleOutput: utils.getFromLookup(value, lookup)}, manuSinope);
return {state: {main_cycle_output: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacThermostat', ['SinopeMainCycleOutput'], manuSinope);
},
} satisfies Tz.Converter,
aux_cycle_output: {
// TH1400ZB specific
key: ['aux_cycle_output'],
convertSet: async (entity, key, value, meta) => {
const lookup = {off: 65535, '15_sec': 15, '5_min': 300, '10_min': 600, '15_min': 900, '20_min': 1200, '30_min': 1800};
await entity.write('hvacThermostat', {SinopeAuxCycleOutput: utils.getFromLookup(value, lookup)});
return {state: {aux_cycle_output: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacThermostat', ['SinopeAuxCycleOutput']);
},
} satisfies Tz.Converter,
enable_outdoor_temperature: {
// DEPRECATED: Use Second Display Mode or control via the timeout
key: ['enable_outdoor_temperature'],
convertSet: async (entity, key, value, meta) => {
utils.assertString(value);
if (value.toLowerCase() == 'on') {
await entity.write('manuSpecificSinope', {outdoorTempToDisplayTimeout: 10800}, manuSinope);
} else if (value.toLowerCase() == 'off') {
// set timer to 12 sec in order to disable outdoor temperature
await entity.write('manuSpecificSinope', {outdoorTempToDisplayTimeout: 12}, manuSinope);
}
return {state: {enable_outdoor_temperature: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['outdoorTempToDisplayTimeout'], manuSinope);
},
} satisfies Tz.Converter,
second_display_mode: {
key: ['second_display_mode'],
convertSet: async (entity, key, value, meta) => {
const lookup = {auto: 0, setpoint: 1, 'outdoor temp': 2};
await entity.write('manuSpecificSinope', {secondScreenBehavior: utils.getFromLookup(value, lookup)});
return {state: {second_display_mode: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['secondScreenBehavior']);
},
} satisfies Tz.Converter,
thermostat_outdoor_temperature: {
key: ['thermostat_outdoor_temperature'],
convertSet: async (entity, key, value, meta) => {
const number = utils.toNumber(value);
if (number >= -99.5 && number <= 99.5) {
await entity.write('manuSpecificSinope', {outdoorTempToDisplay: number * 100}, manuSinope);
}
return {state: {thermostat_outdoor_temperature: number}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['outdoorTempToDisplay'], manuSinope);
},
} satisfies Tz.Converter,
outdoor_temperature_timeout: {
key: ['outdoor_temperature_timeout'],
convertSet: async (entity, key, value, meta) => {
const number = utils.toNumber(value);
if (number >= 30 && number <= 64800) {
await entity.write('manuSpecificSinope', {outdoorTempToDisplayTimeout: number});
return {state: {outdoor_temperature_timeout: number}};
}
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['outdoorTempToDisplayTimeout']);
},
} satisfies Tz.Converter,
thermostat_time: {
key: ['thermostat_time'],
convertSet: async (entity, key, value, meta) => {
if (value === '') {
const thermostatDate = new Date();
const thermostatTimeSec = thermostatDate.getTime() / 1000;
const thermostatTimezoneOffsetSec = thermostatDate.getTimezoneOffset() * 60;
const currentTimeToDisplay = Math.round(thermostatTimeSec - thermostatTimezoneOffsetSec - 946684800);
await entity.write('manuSpecificSinope', {currentTimeToDisplay}, manuSinope);
} else if (value !== '') {
await entity.write('manuSpecificSinope', {currentTimeToDisplay: value}, manuSinope);
}
},
} satisfies Tz.Converter,
floor_control_mode: {
// TH1300ZB and TH1400ZB specific
key: ['floor_control_mode'],
convertSet: async (entity, key, value, meta) => {
if (typeof value !== 'string') {
return;
}
const lookup = {ambiant: 1, floor: 2};
value = value.toLowerCase();
// @ts-expect-error ignore
if (lookup[value] !== undefined) {
await entity.write('manuSpecificSinope', {floorControlMode: utils.getFromLookup(value, lookup)});
}
return {state: {floor_control_mode: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['floorControlMode']);
},
} satisfies Tz.Converter,
ambiant_max_heat_setpoint: {
// TH1300ZB and TH1400ZB specific
key: ['ambiant_max_heat_setpoint'],
convertSet: async (entity, key, value, meta) => {
// @ts-expect-error ignore
if ((value >= 5 && value <= 36) || value == 'off') {
// @ts-expect-error ignore
await entity.write('manuSpecificSinope', {ambiantMaxHeatSetpointLimit: value == 'off' ? -32768 : value * 100});
return {state: {ambiant_max_heat_setpoint: value}};
}
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['ambiantMaxHeatSetpointLimit']);
},
} satisfies Tz.Converter,
floor_min_heat_setpoint: {
// TH1300ZB and TH1400ZB specific
key: ['floor_min_heat_setpoint'],
convertSet: async (entity, key, value, meta) => {
// @ts-expect-error ignore
if ((value >= 5 && value <= 34) || value == 'off') {
// @ts-expect-error ignore
await entity.write('manuSpecificSinope', {floorMinHeatSetpointLimit: value == 'off' ? -32768 : value * 100});
return {state: {floor_min_heat_setpoint: value}};
}
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['floorMinHeatSetpointLimit']);
},
} satisfies Tz.Converter,
floor_max_heat_setpoint: {
// TH1300ZB and TH1400ZB specific
key: ['floor_max_heat_setpoint'],
convertSet: async (entity, key, value, meta) => {
// @ts-expect-error ignore
if ((value >= 7 && value <= 36) || value == 'off') {
// @ts-expect-error ignore
await entity.write('manuSpecificSinope', {floorMaxHeatSetpointLimit: value == 'off' ? -32768 : value * 100});
return {state: {floor_max_heat_setpoint: value}};
}
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['floorMaxHeatSetpointLimit']);
},
} satisfies Tz.Converter,
temperature_sensor: {
// TH1300ZB and TH1400ZB specific
key: ['floor_temperature_sensor'],
convertSet: async (entity, key, value, meta) => {
if (typeof value !== 'string') {
return;
}
const lookup = {'10k': 0, '12k': 1};
value = value.toLowerCase();
// @ts-expect-error ignore
if (lookup[value] !== undefined) {
await entity.write('manuSpecificSinope', {temperatureSensor: utils.getFromLookup(value, lookup)});
}
return {state: {floor_temperature_sensor: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['temperatureSensor']);
},
} satisfies Tz.Converter,
time_format: {
key: ['time_format'],
convertSet: async (entity, key, value, meta) => {
await entity.write('manuSpecificSinope', {timeFormatToDisplay: utils.getFromLookup(value, {'24h': 0, '12h': 1})}, manuSinope);
return {state: {time_format: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['timeFormatToDisplay'], manuSinope);
},
} satisfies Tz.Converter,
connected_load: {
// TH1400ZB and SW2500ZB
key: ['connected_load'],
convertSet: async (entity, key, value, meta) => {
await entity.write('manuSpecificSinope', {connectedLoad: value});
return {state: {connected_load: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['connectedLoad']);
},
} satisfies Tz.Converter,
aux_connected_load: {
// TH1400ZB specific
key: ['aux_connected_load'],
convertSet: async (entity, key, value, meta) => {
await entity.write('manuSpecificSinope', {auxConnectedLoad: value});
return {state: {aux_connected_load: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['auxConnectedLoad']);
},
} satisfies Tz.Converter,
pump_protection: {
// TH1400ZB specific
key: ['pump_protection'],
convertSet: async (entity, key, value, meta) => {
utils.assertString(value);
if (value.toLowerCase() == 'on') {
await entity.write('manuSpecificSinope', {pumpProtection: 1});
} else if (value.toLowerCase() == 'off') {
await entity.write('manuSpecificSinope', {pumpProtection: 255});
}
return {state: {pump_protection: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['pumpProtection']);
},
} satisfies Tz.Converter,
led_intensity_on: {
// DM25x0ZB and SW2500ZB
key: ['led_intensity_on'],
convertSet: async (entity, key, value, meta) => {
const number = utils.toNumber(value);
if (number >= 0 && number <= 100) {
await entity.write('manuSpecificSinope', {ledIntensityOn: number});
}
return {state: {led_intensity_on: number}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['ledIntensityOn']);
},
} satisfies Tz.Converter,
led_intensity_off: {
// DM25x0ZB and SW2500ZB
key: ['led_intensity_off'],
convertSet: async (entity, key, value, meta) => {
const number = utils.toNumber(value);
if (number >= 0 && number <= 100) {
await entity.write('manuSpecificSinope', {ledIntensityOff: number});
}
return {state: {led_intensity_off: number}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['ledIntensityOff']);
},
} satisfies Tz.Converter,
led_color_on: {
// DM25x0ZB and SW2500ZB
key: ['led_color_on'],
convertSet: async (entity, key, value: KeyValueAny, meta) => {
const r = value.r >= 0 && value.r <= 255 ? value.r : 0;
const g = value.g >= 0 && value.g <= 255 ? value.g : 0;
const b = value.b >= 0 && value.b <= 255 ? value.b : 0;
const valueHex = r + g * 256 + b * 256 ** 2;
await entity.write('manuSpecificSinope', {ledColorOn: valueHex});
},
} satisfies Tz.Converter,
led_color_off: {
// DM25x0ZB and SW2500ZB
key: ['led_color_off'],
convertSet: async (entity, key, value: KeyValueAny, meta) => {
const r = value.r >= 0 && value.r <= 255 ? value.r : 0;
const g = value.g >= 0 && value.g <= 255 ? value.g : 0;
const b = value.b >= 0 && value.b <= 255 ? value.b : 0;
const valueHex = r + g * 256 + b * 256 ** 2;
await entity.write('manuSpecificSinope', {ledColorOff: valueHex});
},
} satisfies Tz.Converter,
minimum_brightness: {
// DM25x0ZB
key: ['minimum_brightness'],
convertSet: async (entity, key, value, meta) => {
const number = utils.toNumber(value);
if (number >= 0 && number <= 3000) {
await entity.write('manuSpecificSinope', {minimumBrightness: number});
}
return {state: {minimumBrightness: number}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['minimumBrightness']);
},
} satisfies Tz.Converter,
timer_seconds: {
// DM25x0ZB and SW2500ZB
key: ['timer_seconds'],
convertSet: async (entity, key, value, meta) => {
const number = utils.toNumber(value);
if (number >= 0 && number <= 65535) {
await entity.write('manuSpecificSinope', {dimmerTimmer: number});
}
return {state: {timer_seconds: number}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['dimmerTimmer']);
},
} satisfies Tz.Converter,
keypad_lockout: {
// SW2500ZB
key: ['keypad_lockout'],
convertSet: async (entity, key, value, meta) => {
const lookup = {unlock: 0, lock: 1};
await entity.write('manuSpecificSinope', {keypadLockout: utils.getFromLookup(value, lookup)});
return {state: {keypad_lockout: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['keypadLockout']);
},
} satisfies Tz.Converter,
low_water_temp_protection: {
// RM3500ZB specific
key: ['low_water_temp_protection'],
convertSet: async (entity, key, value, meta) => {
await entity.write('manuSpecificSinope', {drConfigWaterTempMin: value});
return {state: {low_water_temp_protection: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificSinope', ['drConfigWaterTempMin']);
},
} satisfies Tz.Converter,
};