Bon je suis pas loin d’y arriver. J’ai trouvé comment faire:
J’ai donc crée un fichier sinope.js, que j’ai mis dans le repertoire hommeassistant/zigbee2mqtt, et dans lequel j’ai copier coller le code ci dessous. En gros j’ai pris le contenu du fichier sinope.ts qui est dans le lien precedent, j’ai supprimé tous les devices, excepté le TH1300ZB, et j’ai gardé les definitions du debut, car je ne suis pas sur si je peux les supprimer ou non.
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,
};
const definitions: DefinitionWithExtend[] = [
{
zigbeeModel: ['OTH3600-GA-ZB'],
model: 'OTH3600-GA-ZB',
vendor: 'Sinopé',
description: 'Zigbee smart floor heating thermostat',
extend: [electricityMeter()],
fromZigbee: [fzLocal.thermostat, fzLocal.sinope, fz.hvac_user_interface, fz.ignore_temperature_report],
toZigbee: [
tz.thermostat_local_temperature,
tz.thermostat_occupied_heating_setpoint,
tz.thermostat_unoccupied_heating_setpoint,
tz.thermostat_temperature_display_mode,
tz.thermostat_keypad_lockout,
tz.thermostat_system_mode,
tzLocal.backlight_autodim,
tzLocal.thermostat_time,
tzLocal.time_format,
tzLocal.enable_outdoor_temperature,
tzLocal.second_display_mode,
tzLocal.thermostat_outdoor_temperature,
tzLocal.outdoor_temperature_timeout,
tzLocal.thermostat_occupancy,
tzLocal.floor_control_mode,
tzLocal.ambiant_max_heat_setpoint,
tzLocal.floor_min_heat_setpoint,
tzLocal.floor_max_heat_setpoint,
tzLocal.temperature_sensor,
],
exposes: [
e
.climate()
.withSetpoint('occupied_heating_setpoint', 5, 36, 0.5)
.withSetpoint('unoccupied_heating_setpoint', 5, 36, 0.5)
.withLocalTemperature()
.withSystemMode(['off', 'heat'], ea.ALL, 'Mode of the thermostat')
.withPiHeatingDemand()
.withRunningState(['idle', 'heat'], ea.STATE),
e.enum('thermostat_occupancy', ea.ALL, ['unoccupied', 'occupied']).withDescription('Occupancy state of the thermostat'),
e
.enum('second_display_mode', ea.ALL, ['auto', 'setpoint', 'outdoor temp'])
.withDescription(
'Displays the outdoor temperature and then returns to the set point in "auto" mode, or clears ' +
'in "outdoor temp" mode when expired.',
),
e
.numeric('thermostat_outdoor_temperature', ea.ALL)
.withUnit('°C')
.withValueMin(-99.5)
.withValueMax(99.5)
.withValueStep(0.5)
.withDescription('Outdoor temperature for the secondary display'),
e
.numeric('outdoor_temperature_timeout', ea.ALL)
.withUnit('s')
.withValueMin(30)
.withValueMax(64800)
.withPreset('15 min', 900, '15 minutes')
.withPreset('30 min', 1800, '30 minutes')
.withPreset('1 hour', 3600, '1 hour')
.withDescription('Time in seconds after which the outdoor temperature is considered to have expired'),
e
.binary('enable_outdoor_temperature', ea.ALL, 'ON', 'OFF')
.withDescription('DEPRECATED: Use second_display_mode or control via outdoor_temperature_timeout'),
e
.enum('temperature_display_mode', ea.ALL, ['celsius', 'fahrenheit'])
.withDescription('The temperature format displayed on the thermostat screen'),
e.enum('time_format', ea.ALL, ['24h', '12h']).withDescription('The time format featured on the thermostat display'),
e.enum('backlight_auto_dim', ea.ALL, ['on_demand', 'sensing']).withDescription('Control backlight dimming behavior'),
e.enum('keypad_lockout', ea.ALL, ['unlock', 'lock1']).withDescription('Enables or disables the device’s buttons'),
],
configure: async (device, coordinatorEndpoint) => {
const endpoint = device.getEndpoint(1);
const binds = [
'genBasic',
'genIdentify',
'genGroups',
'hvacThermostat',
'hvacUserInterfaceCfg',
'msTemperatureMeasurement',
'manuSpecificSinope',
];
await reporting.bind(endpoint, coordinatorEndpoint, binds);
await reporting.thermostatTemperature(endpoint);
await reporting.thermostatPIHeatingDemand(endpoint);
await reporting.thermostatOccupiedHeatingSetpoint(endpoint);
try {
await reporting.thermostatKeypadLockMode(endpoint);
} catch {
// Not all support this: https://github.com/Koenkk/zigbee2mqtt/issues/3760
}
await endpoint.configureReporting('manuSpecificSinope', [
{attribute: 'GFCiStatus', minimumReportInterval: 1, maximumReportInterval: constants.repInterval.HOUR, reportableChange: 1},
]);
await endpoint.configureReporting('manuSpecificSinope', [
{attribute: 'floorLimitStatus', minimumReportInterval: 1, maximumReportInterval: constants.repInterval.HOUR, reportableChange: 1},
]);
await reporting.temperature(endpoint, {min: 1, max: 0xffff}); // disable reporting
},
},
];
export default definitions;
module.exports = definitions;
enuiste dans le fichier de configuration de zigbee2mqtt j’ai ajouté les deux lignes suivantes:
external-converters:
- sinope.js
J’ai redemarré, et ca ne marche pas J’ai probblement fait une erreur quelque part.
Mais pour me compliquer la tache, a chaque fois que je redemarre zigbee2mqtt, ma clé Conbee II perd les pedales (ou la VM, un des deux). Je n’ai pas d’autres choix que d’arreter mon NAS, et de tout redemarrer, ce qui prend un temps fou. J’ai hate de recevoir mon nouveau controlleur Sonoff…