From 95eb9b3b14ae62d152848457f64a13891bf8d8e1 Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 20 Jan 2025 01:27:09 +0100 Subject: [PATCH 01/20] Added support for namron edge thermostat --- src/devices/namron.ts | 530 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 530 insertions(+) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index b3ecd43f5aa99..7311747074a99 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -16,6 +16,187 @@ const e = exposes.presets; const sunricherManufacturer = {manufacturerCode: Zcl.ManufacturerCode.SHENZHEN_SUNRICHER_TECHNOLOGY_LTD}; +interface NamronPrivateAttribute { + attrId: number | string; + type: Zcl.DataType; + key: string; +} + +interface NamronPrivateTable { + [key: string]: NamronPrivateAttribute; +} + +const namron_private_hvacThermostat: NamronPrivateTable = { + window_check: {attrId: 0x8000, type: Zcl.DataType.BOOLEAN, key: 'window_open_check'}, + anti_frost_mode: {attrId: 0x8001, type: Zcl.DataType.BOOLEAN, key: 'anti_frost'}, + window_state: {attrId: 0x8002, type: Zcl.DataType.BOOLEAN, key: 'window_open'}, + work_days: {attrId: 0x8003, type: Zcl.DataType.ENUM8, key: 'work_days'}, + sensor_mode: {attrId: 0x8004, type: Zcl.DataType.ENUM8, key: 'sensor_mode'}, + active_backlight: {attrId: 0x8005, type: Zcl.DataType.UINT8, key: 'active_display_brightness'}, + fault: {attrId: 0x8006, type: Zcl.DataType.ENUM8, key: 'fault'}, + regulator: {attrId: 0x8007, type: Zcl.DataType.UINT8, key: 'regulator'}, + time_sync_flag: {attrId: 0x800a, type: Zcl.DataType.BOOLEAN, key: 'time_sync'}, + time_sync_value: {attrId: 0x800b, type: Zcl.DataType.UINT32, key: 'time_sync_value'}, + abs_min_heat_setpoint_limit_f: {attrId: 0x800c, type: Zcl.DataType.INT16, key: 'abs_min_heat_setpoint_limit_f'}, + abs_max_heat_setpoint_limit_f: {attrId: 0x800d, type: Zcl.DataType.INT16, key: 'abs_max_heat_setpoint_limit_f'}, + abs_min_cool_setpoint_limit_f: {attrId: 0x800e, type: Zcl.DataType.INT16, key: 'abs_min_cool_setpoint_limit_f'}, + abs_max_cool_setpoint_limit_f: {attrId: 0x800f, type: Zcl.DataType.INT16, key: 'abs_max_cool_setpoint_limit_f'}, + occupied_cooling_setpoint_f: {attrId: 0x8010, type: Zcl.DataType.INT16, key: 'occupied_cooling_setpoint_f'}, + occupied_heating_setpoint_f: {attrId: 0x8011, type: Zcl.DataType.INT16, key: 'occupied_heating_setpoint_f'}, + local_temperature_f: {attrId: 0x8012, type: Zcl.DataType.INT16, key: 'local_temperature_f'}, + holiday_temp_set: {attrId: 0x8013, type: Zcl.DataType.INT16, key: 'holiday_temp_set'}, + holiday_temp_set_f: {attrId: 0x801b, type: Zcl.DataType.INT16, key: 'holiday_temp_set_f'}, + regulation_mode: {attrId: 0x801c, type: Zcl.DataType.INT16, key: 'regulation_mode'}, + regulator_percentage: {attrId: 0x801d, type: Zcl.DataType.INT16, key: 'regulator_percentage'}, + summer_winter_switch: {attrId: 0x801e, type: Zcl.DataType.BOOLEAN, key: 'summer_winter_switch'}, + vacation_mode: {attrId: 0x801f, type: Zcl.DataType.BOOLEAN, key: 'vacation_mode'}, + vacation_start_date: {attrId: 0x8020, type: Zcl.DataType.UINT32, key: 'vacation_start_date'}, + vacation_end_date: {attrId: 0x8021, type: Zcl.DataType.UINT32, key: 'vacation_end_date'}, + auto_time: {attrId: 0x8022, type: Zcl.DataType.BOOLEAN, key: 'auto_time'}, + countdown_set: {attrId: 0x8023, type: Zcl.DataType.ENUM8, key: 'boost_time_set'}, + countdown_left: {attrId: 0x8024, type: Zcl.DataType.INT16, key: 'boost_time_left'}, + display_auto_off: {attrId: 0x8029, type: Zcl.DataType.ENUM8, key: 'display_auto_off'}, + system_mode: {attrId: 'systemMode', type: Zcl.DataType.ENUM8, key: 'system_mode'}, + current_operating_mode: {attrId: 'programingOperMode', type: Zcl.DataType.BITMAP8, key: 'current_operating_mode'}, +}; + +declare global { + interface Object { + fromFzToTz(): KeyValue; + } +} + +Object.prototype.fromFzToTz = function (this: KeyValue) { + return Object.fromEntries(Object.entries(this).map(([k, v]) => [v, k])); +}; + +const fzNamronBoostTable = { + 0: 'Off', + 1: '5 min', + 2: '10 min', + 3: '15 min', + 4: '20 min', + 5: '25 min', + 6: '30 min', + 7: '35 min', + 8: '40 min', + 9: '45 min', + 10: '50 min', + 11: '55 min', + 12: '1h', + 13: '1h 5 min', + 14: '1h 10 min', + 15: '1h 15 min', + 16: '1h 20 min', + 17: '1h 25 min', + 18: '1h 30 min', + 19: '1h 35 min', + 20: '1h 40 min', + 21: '1h 45 min', + 22: '1h 50 min', + 23: '1h 55 min', + 24: '2h', +}; +const tzNamronBoostTable = fzNamronBoostTable.fromFzToTz(); + +const fzNamronSystemMode = {0x00: 'off', 0x01: 'auto', 0x03: 'cool', 0x04: 'heat'}; +const tzNamronSystemMode = fzNamronSystemMode.fromFzToTz(); + +const fzNamronOnOff = {0: 'Off', 1: 'On'}; +const tzNamronOnOff = fzNamronOnOff.fromFzToTz(); + +const fzNamronOpenClose = {0: 'Closed', 1: 'Open'}; +const tzNamronOpenClose = fzNamronOpenClose.fromFzToTz(); + +const fzNamronDisplayTimeout = {0: 'Off', 1: '10s', 2: '30s', 3: '60s'}; +const tzNamronDisplayTimeout = fzNamronDisplayTimeout.fromFzToTz(); + +const fzNamronSensorMode = {0: 'Air', 1: 'Floor', 3: 'External', 6: 'Regulator'}; +const tzNamronSensorMode = fzNamronSensorMode.fromFzToTz(); + +const fzNamronOperationMode = {0: 'Manual', 1: 'Manual', 5: 'ECO'}; +const tzNamronOperationMode = fzNamronOperationMode.fromFzToTz(); + +const fzNamronFault = { + 0: 'No Fault', + 1: 'Over current Error', + 2: 'Over heat Error', + 3: 'Built-in Sensor Error', + 4: 'Air Sensor Error', + 5: 'Floor Sensor Error', +}; + +const fzNamronWorkDays = {0: 'Mon-Fri Sat-Sun', 1: 'Mon-Sat Sun', 2: 'No Time Off', 3: 'Time Off'}; +const tzNamronWorkDays = fzNamronWorkDays.fromFzToTz(); + +const findAttributeByKey = (key: string, attributes: NamronPrivateTable) => { + // Finn objektet basert på key + return Object.values(attributes).find((attr) => attr.key === key); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const assign = (data: any, attribute: NamronPrivateAttribute, target: KeyValue, defaultValue: any = null, transform = (value: any) => value) => { + // Ekstrakt `attrId` og `key` direkte fra attributtobjektet + const {attrId, key: targetKey} = attribute; + + if (data[attrId] !== undefined) { + target[targetKey] = transform(data[attrId]); + } else if (data[attrId] && defaultValue !== null && defaultValue !== undefined) { + target[targetKey] = transform(defaultValue); + } +}; + +const assignWithLookup = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any, + attribute: NamronPrivateAttribute, + target: KeyValue, + lookup = {}, + defaultValue: string | number | null = null, +) => { + // Ekstrakt `attrId` og `key` direkte fra attributtobjektet + const {attrId, key: targetKey} = attribute; + + if (data[attrId] !== undefined) { + const value = data[attrId] ?? defaultValue; + target[targetKey] = utils.getFromLookup(value, lookup); + } +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const assignDate = (data: any, attribute: NamronPrivateAttribute, target: KeyValue) => { + // Ekstrakt `attrId` og `key` direkte fra attributtobjektet + const {attrId, key: targetKey} = attribute; + const value = data[attrId]; + if (value === undefined) { + return; + } + const date = new Date(value * 86400000); + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); // Månedene er 0-indeksert + const year = date.getFullYear(); + target[targetKey] = `${year}.${month}.${day}`; +}; + +const fromDate = (value: string) => { + // Ekstrakt `attrId` og `key` direkte fra attributtobjektet + const dateParts = value.split(/[.\-/]/); + if (dateParts.length !== 3) { + throw new Error('Invalid date format'); + } + + let date: Date; + if (dateParts[0].length === 4) { + date = new Date(`${dateParts[0]}-${dateParts[1]}-${dateParts[2]}`); + } else if (dateParts[2].length === 4) { + date = new Date(`${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`); + } else { + throw new Error('Invalid date format'); + } + + return date.getTime() / 86400000 + 1; +}; + const fzLocal = { namron_panelheater: { cluster: 'hvacThermostat', @@ -63,6 +244,58 @@ const fzLocal = { return fz.thermostat.convert(model, msg, publish, options, meta); // as KeyValue; }, } satisfies Fz.Converter, + namron_edge_thermostat: { + cluster: 'hvacThermostat', + type: ['attributeReport', 'readResponse'], + convert: (model, msg, publish, options, meta) => { + const result = {}; + const data = msg.data; + + assignWithLookup(data, namron_private_hvacThermostat.work_days, result, fzNamronWorkDays, 0); + assignWithLookup(data, namron_private_hvacThermostat.sensor_mode, result, fzNamronSensorMode, 0); + assignWithLookup(data, namron_private_hvacThermostat.fault, result, fzNamronFault, 0); + + assignWithLookup(data, namron_private_hvacThermostat.window_check, result, fzNamronOnOff, 0); + assignWithLookup(data, namron_private_hvacThermostat.anti_frost_mode, result, fzNamronOnOff, 0); + assignWithLookup(data, namron_private_hvacThermostat.window_state, result, fzNamronOpenClose, 0); + assignWithLookup(data, namron_private_hvacThermostat.summer_winter_switch, result, fzNamronOnOff, 0); + assignWithLookup(data, namron_private_hvacThermostat.vacation_mode, result, fzNamronOnOff, 0); + assignWithLookup(data, namron_private_hvacThermostat.time_sync_flag, result, fzNamronOnOff, 0); + + assign(data, namron_private_hvacThermostat.active_backlight, result); + assign(data, namron_private_hvacThermostat.time_sync_value, result); + assign(data, namron_private_hvacThermostat.abs_min_heat_setpoint_limit_f, result); + assign(data, namron_private_hvacThermostat.abs_max_heat_setpoint_limit_f, result); + assign(data, namron_private_hvacThermostat.abs_min_cool_setpoint_limit_f, result); + assign(data, namron_private_hvacThermostat.abs_max_cool_setpoint_limit_f, result); + assign(data, namron_private_hvacThermostat.occupied_cooling_setpoint_f, result); + assign(data, namron_private_hvacThermostat.occupied_heating_setpoint_f, result); + assign(data, namron_private_hvacThermostat.local_temperature_f, result); + assign(data, namron_private_hvacThermostat.holiday_temp_set, result, (value: number) => { + return value / 100; + }); + assign(data, namron_private_hvacThermostat.holiday_temp_set_f, result, (value: number) => { + return value / 100; + }); + + assign(data, namron_private_hvacThermostat.regulator_percentage, result, 0, (value) => { + return value; + }); + + assignDate(data, namron_private_hvacThermostat.vacation_start_date, result); + assignDate(data, namron_private_hvacThermostat.vacation_end_date, result); + + // Auto_time (synkroniser tid med ntp?) + assignWithLookup(data, namron_private_hvacThermostat.auto_time, result, fzNamronOnOff, 0); + assignWithLookup(data, namron_private_hvacThermostat.countdown_set, result, fzNamronBoostTable, 0); + assign(data, namron_private_hvacThermostat.countdown_left, result, 0, (value) => (value > 200 ? 0 : value)); + assignWithLookup(data, namron_private_hvacThermostat.display_auto_off, result, fzNamronDisplayTimeout, 0); + assignWithLookup(data, namron_private_hvacThermostat.system_mode, result, fzNamronSystemMode, 0x00); + assignWithLookup(data, namron_private_hvacThermostat.current_operating_mode, result, fzNamronOperationMode, 0); + + return result; + }, + } satisfies Fz.Converter, }; const tzLocal = { @@ -112,6 +345,160 @@ const tzLocal = { } }, } satisfies Tz.Converter, + namron_edge_thermostat: { + key: [ + 'window_open_check', + 'anti_frost', + 'window_open', + 'work_days', + 'sensor_mode', + 'active_display_brightness', + 'fault', + 'regulator', + 'time_sync', + 'time_sync_value', + 'abs_min_heat_setpoint_limit_f', + 'abs_max_heat_setpoint_limit_f', + 'abs_min_cool_setpoint_limit_f', + 'abs_max_cool_setpoint_limit_f', + 'occupied_cooling_setpoint_f', + 'occupied_heating_setpoint_f', + 'local_temperature_f', + 'holiday_temp_set', + 'holiday_temp_set_f', + 'regulation_mode', + 'regulator_percentage', + 'summer_winter_switch', + 'vacation_mode', + 'vacation_start_date', + 'vacation_end_date', + 'auto_time', + 'boost_time_set', + 'boost_time_left', + 'display_auto_off', + 'system_mode', + 'current_operating_mode', + ], + + convertGet: async (entity, key, meta) => { + const readAttr = findAttributeByKey(key, namron_private_hvacThermostat); + if (readAttr) { + await entity.read('hvacThermostat', [readAttr.attrId]); + } else { + throw new Error(`Unhandled key toZigbee.namronEdgeThermostat.convertGet ${key}`); + } + }, + + convertSet: async (entity, key, value, meta) => { + const readAttr = findAttributeByKey(key, namron_private_hvacThermostat); + + if (!readAttr) { + throw new Error(`Unhandled key toZigbee.namronEdgeThermostat.convertSet ${key}`); + } + + if ( + [ + namron_private_hvacThermostat.window_check.key, + namron_private_hvacThermostat.anti_frost_mode.key, + namron_private_hvacThermostat.time_sync_flag.key, + namron_private_hvacThermostat.summer_winter_switch.key, + namron_private_hvacThermostat.auto_time.key, + namron_private_hvacThermostat.vacation_mode.key, + ].includes(readAttr.key) + ) { + const payload = {[readAttr.attrId]: {value: utils.getFromLookup(value, tzNamronOnOff), type: readAttr.type}}; + await entity.write('hvacThermostat', payload); + return; + } + + // Direct call + if ([Zcl.DataType.UINT8, Zcl.DataType.INT16, Zcl.DataType.UINT32].includes(readAttr.type)) { + const payload = {[readAttr.attrId]: {value: value, type: readAttr.type}}; + await entity.write('hvacThermostat', payload); + } + + if (readAttr === namron_private_hvacThermostat.countdown_set) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronBoostTable), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.window_state) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronOpenClose), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.display_auto_off) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronDisplayTimeout), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.sensor_mode) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronSensorMode), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.system_mode) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronSystemMode), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.current_operating_mode) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronOperationMode), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.holiday_temp_set) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: Number(value) * 100, + type: readAttr.type, + }, + }); + } + + if ([namron_private_hvacThermostat.vacation_start_date.key, namron_private_hvacThermostat.vacation_end_date.key].includes(readAttr.key)) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: fromDate(String(value)), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.work_days) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronWorkDays), + type: readAttr.type, + }, + }); + } + }, + } satisfies Tz.Converter, }; const definitions: DefinitionWithExtend[] = [ @@ -1518,6 +1905,149 @@ const definitions: DefinitionWithExtend[] = [ description: 'Zigbee smart plug dimmer 150W', extend: [m.light(), m.electricityMeter({cluster: 'electrical'})], }, + { + zigbeeModel: ['4512783', '4512784'], + model: 'Edge Thermostat', + vendor: 'Namron', + description: 'Namron Zigbee Edge Termostat', + fromZigbee: [fzLocal.namron_edge_thermostat, fz.namron_hvac_user_interface, fz.metering, fz.electrical_measurement], + toZigbee: [ + tz.thermostat_local_temperature, + tzLocal.namron_edge_thermostat, + tz.thermostat_occupied_heating_setpoint, + tz.thermostat_unoccupied_heating_setpoint, + tz.namron_thermostat_child_lock, + tz.thermostat_control_sequence_of_operation, + tz.thermostat_programming_operation_mode, + tz.thermostat_temperature_display_mode, + ], + onEvent: async (type, data, device, options) => { + if (type === 'stop') { + try { + const key = namron_private_hvacThermostat.time_sync_value.key; + clearInterval(globalStore.getValue(device, key)); + globalStore.clearValue(device, key); + } catch { + /* Do nothing*/ + } + } + if (!globalStore.hasValue(device, namron_private_hvacThermostat.time_sync_value.key)) { + const hours24 = 1000 * 60 * 60 * 24; + const interval = setInterval(async () => { + try { + const endpoint = device.getEndpoint(1); + // Device does not asks for the time with binding, therefore we write the time every 24 hours + const time = new Date().getTime() / 1000; + await endpoint.write('hvacThermostat', { + [namron_private_hvacThermostat.time_sync_value.attrId]: { + value: time, + type: namron_private_hvacThermostat.time_sync_value.type, + }, + }); + } catch { + /* Do nothing*/ + } + }, hours24); + globalStore.putValue(device, namron_private_hvacThermostat.time_sync_value.key, interval); + } + }, + configure: async (device, coordinatorEndpoint, _logger) => { + const endpoint = device.getEndpoint(1); + const binds = [ + 'genBasic', + 'genIdentify', + 'genOnOff', + 'hvacThermostat', + 'hvacUserInterfaceCfg', + 'msRelativeHumidity', + 'seMetering', + 'haElectricalMeasurement', + 'msOccupancySensing', + ]; + await reporting.bind(endpoint, coordinatorEndpoint, binds); + + await reporting.thermostatOccupiedHeatingSetpoint(endpoint, {min: 0, change: 50}); + await reporting.thermostatTemperature(endpoint, {min: 0, change: 50}); + await reporting.thermostatKeypadLockMode(endpoint); + + // Initial read + await endpoint.read('hvacThermostat', [0x8000, 0x8001, 0x8002, 0x801e, 0x8004, 0x8006, 0x8005, 0x8029, 0x8022, 0x8023, 0x8024]); + + // Reads holiday + await endpoint.read('hvacThermostat', [ + namron_private_hvacThermostat.holiday_temp_set.attrId, + namron_private_hvacThermostat.holiday_temp_set_f.key, + namron_private_hvacThermostat.vacation_mode.attrId, + namron_private_hvacThermostat.vacation_start_date.attrId, + namron_private_hvacThermostat.vacation_end_date.attrId, + ]); + + device.powerSource = 'Mains (single phase)'; + device.save(); + }, + extend: [m.electricityMeter({voltage: false}), m.onOff({powerOnBehavior: false})], + exposes: [ + e + .climate() + .withLocalTemperature() + .withSetpoint('occupied_heating_setpoint', 15, 35, 0.5) + .withSystemMode(['off', 'auto', 'cool', 'heat'], ea.ALL), + e.enum('temperature_display_mode', ea.ALL, ['celsius', 'fahrenheit']).withLabel('Temperature Unit').withDescription('Select Unit'), + e.enum('current_operating_mode', ea.ALL, ['Manual', 'ECO']).withDescription('Selected program for thermostat'), + e.binary('child_lock', ea.ALL, 'LOCK', 'UNLOCK').withDescription('Enables/disables physical input on the device'), + e + .numeric(namron_private_hvacThermostat.active_backlight.key, ea.ALL) + .withDescription('Desired display brightness') + .withUnit('%') + .withValueMin(1) + .withValueMax(100), + e + .enum(namron_private_hvacThermostat.display_auto_off.key, ea.ALL, Object.values(fzNamronDisplayTimeout)) + .withDescription('Turn off the display after the give time in inactivity or never'), + e.binary(namron_private_hvacThermostat.window_check.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off window check mode'), + e + .enum(namron_private_hvacThermostat.window_state.key, ea.STATE_GET, Object.values(fzNamronOpenClose)) + .withDescription('Detected state of window'), + e.binary(namron_private_hvacThermostat.anti_frost_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off anti-frost mode'), + e.binary(namron_private_hvacThermostat.summer_winter_switch.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Summar Winter switch'), + e.binary(namron_private_hvacThermostat.auto_time.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Automatic time'), + e + .enum(namron_private_hvacThermostat.countdown_set.key, ea.ALL, Object.values(fzNamronBoostTable)) + .withDescription('Starts boost with defined time'), + e.numeric(namron_private_hvacThermostat.countdown_left.key, ea.STATE_GET).withUnit('min').withDescription('Given boost time'), + e.binary(namron_private_hvacThermostat.vacation_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off vacation mode'), + e + .numeric(namron_private_hvacThermostat.holiday_temp_set.key, ea.ALL) + .withValueMin(5) + .withValueMax(35) + .withValueStep(0.5) + .withUnit('°C') + .withLabel('Vacation temperature') + .withDescription('Vacation temperature setpoint'), + e + .text(namron_private_hvacThermostat.vacation_start_date.key, ea.ALL) + .withDescription('Start date') + .withDescription("Supports dates starting with day or year with '. - /'"), + e + .text(namron_private_hvacThermostat.vacation_end_date.key, ea.ALL) + .withDescription('End date') + .withDescription("Supports dates starting with day or year with '. - /'"), + e.binary(namron_private_hvacThermostat.time_sync_flag.key, ea.ALL, 'On', 'Off'), + e.numeric(namron_private_hvacThermostat.time_sync_value.key, ea.STATE_GET), + e + .enum(namron_private_hvacThermostat.fault.key, ea.STATE_GET, Object.values(fzNamronFault)) + .withDescription('Shows current error of the device'), + e + .enum(namron_private_hvacThermostat.work_days.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) + .withDescription("Needs to be changed under 'Thermostat settings' > 'Advanced settings' > 'Schedule type'"), + e + .numeric(namron_private_hvacThermostat.regulator_percentage.key, ea.ALL) + .withUnit('%') + .withValueMin(10) + .withValueMax(100) + .withValueStep(1), + ], + }, ]; export default definitions; From 4c96fbed3a640f84a0070c23ac61658eb1d41f7e Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 20 Jan 2025 02:01:11 +0100 Subject: [PATCH 02/20] Fixed typo --- src/devices/namron.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index 7311747074a99..19fbec7d9879a 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -1976,7 +1976,7 @@ const definitions: DefinitionWithExtend[] = [ // Reads holiday await endpoint.read('hvacThermostat', [ namron_private_hvacThermostat.holiday_temp_set.attrId, - namron_private_hvacThermostat.holiday_temp_set_f.key, + namron_private_hvacThermostat.holiday_temp_set_f.attrId, namron_private_hvacThermostat.vacation_mode.attrId, namron_private_hvacThermostat.vacation_start_date.attrId, namron_private_hvacThermostat.vacation_end_date.attrId, From 106e7ffde90db011727f6551b55a5b83ed6eaebd Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 20 Jan 2025 23:09:49 +0100 Subject: [PATCH 03/20] Updated formatting --- src/devices/namron.ts | 287 +++++++++++++++++++++--------------------- 1 file changed, 145 insertions(+), 142 deletions(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index 19fbec7d9879a..f0d233ed2d1a2 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -26,38 +26,37 @@ interface NamronPrivateTable { [key: string]: NamronPrivateAttribute; } -const namron_private_hvacThermostat: NamronPrivateTable = { - window_check: {attrId: 0x8000, type: Zcl.DataType.BOOLEAN, key: 'window_open_check'}, - anti_frost_mode: {attrId: 0x8001, type: Zcl.DataType.BOOLEAN, key: 'anti_frost'}, - window_state: {attrId: 0x8002, type: Zcl.DataType.BOOLEAN, key: 'window_open'}, - work_days: {attrId: 0x8003, type: Zcl.DataType.ENUM8, key: 'work_days'}, - sensor_mode: {attrId: 0x8004, type: Zcl.DataType.ENUM8, key: 'sensor_mode'}, - active_backlight: {attrId: 0x8005, type: Zcl.DataType.UINT8, key: 'active_display_brightness'}, +const namronPrivateHvacThermostat: NamronPrivateTable = { + windowCheck: {attrId: 0x8000, type: Zcl.DataType.BOOLEAN, key: 'window_open_check'}, + antiFrostMode: {attrId: 0x8001, type: Zcl.DataType.BOOLEAN, key: 'anti_frost'}, + windowState: {attrId: 0x8002, type: Zcl.DataType.BOOLEAN, key: 'window_open'}, + workDays: {attrId: 0x8003, type: Zcl.DataType.ENUM8, key: 'work_days'}, + sensorMode: {attrId: 0x8004, type: Zcl.DataType.ENUM8, key: 'sensor_mode'}, + activeBacklight: {attrId: 0x8005, type: Zcl.DataType.UINT8, key: 'active_display_brightness'}, fault: {attrId: 0x8006, type: Zcl.DataType.ENUM8, key: 'fault'}, regulator: {attrId: 0x8007, type: Zcl.DataType.UINT8, key: 'regulator'}, - time_sync_flag: {attrId: 0x800a, type: Zcl.DataType.BOOLEAN, key: 'time_sync'}, - time_sync_value: {attrId: 0x800b, type: Zcl.DataType.UINT32, key: 'time_sync_value'}, - abs_min_heat_setpoint_limit_f: {attrId: 0x800c, type: Zcl.DataType.INT16, key: 'abs_min_heat_setpoint_limit_f'}, - abs_max_heat_setpoint_limit_f: {attrId: 0x800d, type: Zcl.DataType.INT16, key: 'abs_max_heat_setpoint_limit_f'}, - abs_min_cool_setpoint_limit_f: {attrId: 0x800e, type: Zcl.DataType.INT16, key: 'abs_min_cool_setpoint_limit_f'}, - abs_max_cool_setpoint_limit_f: {attrId: 0x800f, type: Zcl.DataType.INT16, key: 'abs_max_cool_setpoint_limit_f'}, - occupied_cooling_setpoint_f: {attrId: 0x8010, type: Zcl.DataType.INT16, key: 'occupied_cooling_setpoint_f'}, - occupied_heating_setpoint_f: {attrId: 0x8011, type: Zcl.DataType.INT16, key: 'occupied_heating_setpoint_f'}, - local_temperature_f: {attrId: 0x8012, type: Zcl.DataType.INT16, key: 'local_temperature_f'}, - holiday_temp_set: {attrId: 0x8013, type: Zcl.DataType.INT16, key: 'holiday_temp_set'}, - holiday_temp_set_f: {attrId: 0x801b, type: Zcl.DataType.INT16, key: 'holiday_temp_set_f'}, - regulation_mode: {attrId: 0x801c, type: Zcl.DataType.INT16, key: 'regulation_mode'}, - regulator_percentage: {attrId: 0x801d, type: Zcl.DataType.INT16, key: 'regulator_percentage'}, - summer_winter_switch: {attrId: 0x801e, type: Zcl.DataType.BOOLEAN, key: 'summer_winter_switch'}, - vacation_mode: {attrId: 0x801f, type: Zcl.DataType.BOOLEAN, key: 'vacation_mode'}, - vacation_start_date: {attrId: 0x8020, type: Zcl.DataType.UINT32, key: 'vacation_start_date'}, - vacation_end_date: {attrId: 0x8021, type: Zcl.DataType.UINT32, key: 'vacation_end_date'}, - auto_time: {attrId: 0x8022, type: Zcl.DataType.BOOLEAN, key: 'auto_time'}, - countdown_set: {attrId: 0x8023, type: Zcl.DataType.ENUM8, key: 'boost_time_set'}, - countdown_left: {attrId: 0x8024, type: Zcl.DataType.INT16, key: 'boost_time_left'}, - display_auto_off: {attrId: 0x8029, type: Zcl.DataType.ENUM8, key: 'display_auto_off'}, - system_mode: {attrId: 'systemMode', type: Zcl.DataType.ENUM8, key: 'system_mode'}, - current_operating_mode: {attrId: 'programingOperMode', type: Zcl.DataType.BITMAP8, key: 'current_operating_mode'}, + timeSyncFlag: {attrId: 0x800a, type: Zcl.DataType.BOOLEAN, key: 'time_sync'}, + timeSyncValue: {attrId: 0x800b, type: Zcl.DataType.UINT32, key: 'time_sync_value'}, + absMinHeatSetpointLimitF: {attrId: 0x800c, type: Zcl.DataType.INT16, key: 'abs_min_heat_setpoint_limit_f'}, + absMaxHeatSetpointLimitF: {attrId: 0x800d, type: Zcl.DataType.INT16, key: 'abs_max_heat_setpoint_limit_f'}, + absMinCoolSetpointLimitF: {attrId: 0x800e, type: Zcl.DataType.INT16, key: 'abs_min_cool_setpoint_limit_f'}, + absMaxCoolSetpointLimitF: {attrId: 0x800f, type: Zcl.DataType.INT16, key: 'abs_max_cool_setpoint_limit_f'}, + occupiedCoolingSetpointF: {attrId: 0x8010, type: Zcl.DataType.INT16, key: 'occupied_cooling_setpoint_f'}, + occupiedHeatingSetpointF: {attrId: 0x8011, type: Zcl.DataType.INT16, key: 'occupied_heating_setpoint_f'}, + localTemperatureF: {attrId: 0x8012, type: Zcl.DataType.INT16, key: 'local_temperature_f'}, + holidayTempSet: {attrId: 0x8013, type: Zcl.DataType.INT16, key: 'holiday_temp_set'}, + holidayTempSetF: {attrId: 0x801b, type: Zcl.DataType.INT16, key: 'holiday_temp_set_f'}, + regulationMode: {attrId: 0x801c, type: Zcl.DataType.INT16, key: 'regulation_mode'}, + regulatorPercentage: {attrId: 0x801d, type: Zcl.DataType.INT16, key: 'regulator_percentage'}, + summerWinterSwitch: {attrId: 0x801e, type: Zcl.DataType.BOOLEAN, key: 'summer_winter_switch'}, + vacationMode: {attrId: 0x801f, type: Zcl.DataType.BOOLEAN, key: 'vacation_mode'}, + vacationStartDate: {attrId: 0x8020, type: Zcl.DataType.UINT32, key: 'vacation_start_date'}, + vacationEndDate: {attrId: 0x8021, type: Zcl.DataType.UINT32, key: 'vacation_end_date'}, + autoTime: {attrId: 0x8022, type: Zcl.DataType.BOOLEAN, key: 'auto_time'}, + countdownSet: {attrId: 0x8023, type: Zcl.DataType.ENUM8, key: 'boost_time_set'}, + countdownLeft: {attrId: 0x8024, type: Zcl.DataType.INT16, key: 'boost_time_left'}, + displayAutoOff: {attrId: 0x8029, type: Zcl.DataType.ENUM8, key: 'display_auto_off'}, + currentOperatingMode: {attrId: 'programingOperMode', type: Zcl.DataType.BITMAP8, key: 'current_operating_mode'}, }; declare global { @@ -71,30 +70,30 @@ Object.prototype.fromFzToTz = function (this: KeyValue) { }; const fzNamronBoostTable = { - 0: 'Off', - 1: '5 min', - 2: '10 min', - 3: '15 min', - 4: '20 min', - 5: '25 min', - 6: '30 min', - 7: '35 min', - 8: '40 min', - 9: '45 min', - 10: '50 min', - 11: '55 min', + 0: 'off', + 1: '5_min', + 2: '10_min', + 3: '15_min', + 4: '20_min', + 5: '25_min', + 6: '30_min', + 7: '35_min', + 8: '40_min', + 9: '45_min', + 10: '50_min', + 11: '55_min', 12: '1h', - 13: '1h 5 min', - 14: '1h 10 min', - 15: '1h 15 min', - 16: '1h 20 min', - 17: '1h 25 min', - 18: '1h 30 min', - 19: '1h 35 min', - 20: '1h 40 min', - 21: '1h 45 min', - 22: '1h 50 min', - 23: '1h 55 min', + 13: '1h_5_min', + 14: '1h_10_min', + 15: '1h_15_min', + 16: '1h_20_min', + 17: '1h_25_min', + 18: '1h_30_min', + 19: '1h_35_min', + 20: '1h_40_min', + 21: '1h_45_min', + 22: '1h_50_min', + 23: '1h_55_min', 24: '2h', }; const tzNamronBoostTable = fzNamronBoostTable.fromFzToTz(); @@ -105,28 +104,28 @@ const tzNamronSystemMode = fzNamronSystemMode.fromFzToTz(); const fzNamronOnOff = {0: 'Off', 1: 'On'}; const tzNamronOnOff = fzNamronOnOff.fromFzToTz(); -const fzNamronOpenClose = {0: 'Closed', 1: 'Open'}; +const fzNamronOpenClose = {0: 'closed', 1: 'open'}; const tzNamronOpenClose = fzNamronOpenClose.fromFzToTz(); -const fzNamronDisplayTimeout = {0: 'Off', 1: '10s', 2: '30s', 3: '60s'}; +const fzNamronDisplayTimeout = {0: 'off', 1: '10s', 2: '30s', 3: '60s'}; const tzNamronDisplayTimeout = fzNamronDisplayTimeout.fromFzToTz(); -const fzNamronSensorMode = {0: 'Air', 1: 'Floor', 3: 'External', 6: 'Regulator'}; +const fzNamronSensorMode = {0: 'air', 1: 'floor', 3: 'external', 6: 'regulator'}; const tzNamronSensorMode = fzNamronSensorMode.fromFzToTz(); -const fzNamronOperationMode = {0: 'Manual', 1: 'Manual', 5: 'ECO'}; +const fzNamronOperationMode = {0: 'manual', 1: 'manual', 5: 'eco'}; const tzNamronOperationMode = fzNamronOperationMode.fromFzToTz(); const fzNamronFault = { - 0: 'No Fault', - 1: 'Over current Error', - 2: 'Over heat Error', - 3: 'Built-in Sensor Error', - 4: 'Air Sensor Error', - 5: 'Floor Sensor Error', + 0: 'no_fault', + 1: 'over_current_error', + 2: 'over_heat_error', + 3: 'built-in_sensor_error', + 4: 'air_sensor_error', + 5: 'floor_sensor_error', }; -const fzNamronWorkDays = {0: 'Mon-Fri Sat-Sun', 1: 'Mon-Sat Sun', 2: 'No Time Off', 3: 'Time Off'}; +const fzNamronWorkDays = {0: 'mon-fri_sat-sun', 1: 'mon-sat_sun', 2: 'no_time_off', 3: 'time_off'}; const tzNamronWorkDays = fzNamronWorkDays.fromFzToTz(); const findAttributeByKey = (key: string, attributes: NamronPrivateTable) => { @@ -251,47 +250,47 @@ const fzLocal = { const result = {}; const data = msg.data; - assignWithLookup(data, namron_private_hvacThermostat.work_days, result, fzNamronWorkDays, 0); - assignWithLookup(data, namron_private_hvacThermostat.sensor_mode, result, fzNamronSensorMode, 0); - assignWithLookup(data, namron_private_hvacThermostat.fault, result, fzNamronFault, 0); - - assignWithLookup(data, namron_private_hvacThermostat.window_check, result, fzNamronOnOff, 0); - assignWithLookup(data, namron_private_hvacThermostat.anti_frost_mode, result, fzNamronOnOff, 0); - assignWithLookup(data, namron_private_hvacThermostat.window_state, result, fzNamronOpenClose, 0); - assignWithLookup(data, namron_private_hvacThermostat.summer_winter_switch, result, fzNamronOnOff, 0); - assignWithLookup(data, namron_private_hvacThermostat.vacation_mode, result, fzNamronOnOff, 0); - assignWithLookup(data, namron_private_hvacThermostat.time_sync_flag, result, fzNamronOnOff, 0); - - assign(data, namron_private_hvacThermostat.active_backlight, result); - assign(data, namron_private_hvacThermostat.time_sync_value, result); - assign(data, namron_private_hvacThermostat.abs_min_heat_setpoint_limit_f, result); - assign(data, namron_private_hvacThermostat.abs_max_heat_setpoint_limit_f, result); - assign(data, namron_private_hvacThermostat.abs_min_cool_setpoint_limit_f, result); - assign(data, namron_private_hvacThermostat.abs_max_cool_setpoint_limit_f, result); - assign(data, namron_private_hvacThermostat.occupied_cooling_setpoint_f, result); - assign(data, namron_private_hvacThermostat.occupied_heating_setpoint_f, result); - assign(data, namron_private_hvacThermostat.local_temperature_f, result); - assign(data, namron_private_hvacThermostat.holiday_temp_set, result, (value: number) => { + assignWithLookup(data, namronPrivateHvacThermostat.work_days, result, fzNamronWorkDays, 0); + assignWithLookup(data, namronPrivateHvacThermostat.sensor_mode, result, fzNamronSensorMode, 0); + assignWithLookup(data, namronPrivateHvacThermostat.fault, result, fzNamronFault, 0); + + assignWithLookup(data, namronPrivateHvacThermostat.window_check, result, fzNamronOnOff, 0); + assignWithLookup(data, namronPrivateHvacThermostat.anti_frost_mode, result, fzNamronOnOff, 0); + assignWithLookup(data, namronPrivateHvacThermostat.window_state, result, fzNamronOpenClose, 0); + assignWithLookup(data, namronPrivateHvacThermostat.summer_winter_switch, result, fzNamronOnOff, 0); + assignWithLookup(data, namronPrivateHvacThermostat.vacation_mode, result, fzNamronOnOff, 0); + assignWithLookup(data, namronPrivateHvacThermostat.time_sync_flag, result, fzNamronOnOff, 0); + + assign(data, namronPrivateHvacThermostat.active_backlight, result); + assign(data, namronPrivateHvacThermostat.time_sync_value, result); + assign(data, namronPrivateHvacThermostat.abs_min_heat_setpoint_limit_f, result); + assign(data, namronPrivateHvacThermostat.abs_max_heat_setpoint_limit_f, result); + assign(data, namronPrivateHvacThermostat.abs_min_cool_setpoint_limit_f, result); + assign(data, namronPrivateHvacThermostat.abs_max_cool_setpoint_limit_f, result); + assign(data, namronPrivateHvacThermostat.occupied_cooling_setpoint_f, result); + assign(data, namronPrivateHvacThermostat.occupied_heating_setpoint_f, result); + assign(data, namronPrivateHvacThermostat.local_temperature_f, result); + assign(data, namronPrivateHvacThermostat.holiday_temp_set, result, (value: number) => { return value / 100; }); - assign(data, namron_private_hvacThermostat.holiday_temp_set_f, result, (value: number) => { + assign(data, namronPrivateHvacThermostat.holiday_temp_set_f, result, (value: number) => { return value / 100; }); - assign(data, namron_private_hvacThermostat.regulator_percentage, result, 0, (value) => { + assign(data, namronPrivateHvacThermostat.regulator_percentage, result, 0, (value) => { return value; }); - assignDate(data, namron_private_hvacThermostat.vacation_start_date, result); - assignDate(data, namron_private_hvacThermostat.vacation_end_date, result); + assignDate(data, namronPrivateHvacThermostat.vacation_start_date, result); + assignDate(data, namronPrivateHvacThermostat.vacation_end_date, result); // Auto_time (synkroniser tid med ntp?) - assignWithLookup(data, namron_private_hvacThermostat.auto_time, result, fzNamronOnOff, 0); - assignWithLookup(data, namron_private_hvacThermostat.countdown_set, result, fzNamronBoostTable, 0); - assign(data, namron_private_hvacThermostat.countdown_left, result, 0, (value) => (value > 200 ? 0 : value)); - assignWithLookup(data, namron_private_hvacThermostat.display_auto_off, result, fzNamronDisplayTimeout, 0); - assignWithLookup(data, namron_private_hvacThermostat.system_mode, result, fzNamronSystemMode, 0x00); - assignWithLookup(data, namron_private_hvacThermostat.current_operating_mode, result, fzNamronOperationMode, 0); + assignWithLookup(data, namronPrivateHvacThermostat.auto_time, result, fzNamronOnOff, 0); + assignWithLookup(data, namronPrivateHvacThermostat.countdown_set, result, fzNamronBoostTable, 0); + assign(data, namronPrivateHvacThermostat.countdown_left, result, 0, (value) => (value > 200 ? 0 : value)); + assignWithLookup(data, namronPrivateHvacThermostat.display_auto_off, result, fzNamronDisplayTimeout, 0); + assignWithLookup(data, namronPrivateHvacThermostat.system_mode, result, fzNamronSystemMode, 0x00); + assignWithLookup(data, namronPrivateHvacThermostat.current_operating_mode, result, fzNamronOperationMode, 0); return result; }, @@ -381,7 +380,7 @@ const tzLocal = { ], convertGet: async (entity, key, meta) => { - const readAttr = findAttributeByKey(key, namron_private_hvacThermostat); + const readAttr = findAttributeByKey(key, namronPrivateHvacThermostat); if (readAttr) { await entity.read('hvacThermostat', [readAttr.attrId]); } else { @@ -390,7 +389,7 @@ const tzLocal = { }, convertSet: async (entity, key, value, meta) => { - const readAttr = findAttributeByKey(key, namron_private_hvacThermostat); + const readAttr = findAttributeByKey(key, namronPrivateHvacThermostat); if (!readAttr) { throw new Error(`Unhandled key toZigbee.namronEdgeThermostat.convertSet ${key}`); @@ -398,12 +397,12 @@ const tzLocal = { if ( [ - namron_private_hvacThermostat.window_check.key, - namron_private_hvacThermostat.anti_frost_mode.key, - namron_private_hvacThermostat.time_sync_flag.key, - namron_private_hvacThermostat.summer_winter_switch.key, - namron_private_hvacThermostat.auto_time.key, - namron_private_hvacThermostat.vacation_mode.key, + namronPrivateHvacThermostat.window_check.key, + namronPrivateHvacThermostat.anti_frost_mode.key, + namronPrivateHvacThermostat.time_sync_flag.key, + namronPrivateHvacThermostat.summer_winter_switch.key, + namronPrivateHvacThermostat.auto_time.key, + namronPrivateHvacThermostat.vacation_mode.key, ].includes(readAttr.key) ) { const payload = {[readAttr.attrId]: {value: utils.getFromLookup(value, tzNamronOnOff), type: readAttr.type}}; @@ -417,7 +416,7 @@ const tzLocal = { await entity.write('hvacThermostat', payload); } - if (readAttr === namron_private_hvacThermostat.countdown_set) { + if (readAttr === namronPrivateHvacThermostat.countdown_set) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronBoostTable), @@ -426,7 +425,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.window_state) { + if (readAttr === namronPrivateHvacThermostat.window_state) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronOpenClose), @@ -435,7 +434,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.display_auto_off) { + if (readAttr === namronPrivateHvacThermostat.display_auto_off) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronDisplayTimeout), @@ -444,7 +443,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.sensor_mode) { + if (readAttr === namronPrivateHvacThermostat.sensor_mode) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronSensorMode), @@ -453,7 +452,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.system_mode) { + if (readAttr === namronPrivateHvacThermostat.system_mode) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronSystemMode), @@ -462,7 +461,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.current_operating_mode) { + if (readAttr === namronPrivateHvacThermostat.current_operating_mode) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronOperationMode), @@ -471,7 +470,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.holiday_temp_set) { + if (readAttr === namronPrivateHvacThermostat.holiday_temp_set) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: Number(value) * 100, @@ -480,7 +479,7 @@ const tzLocal = { }); } - if ([namron_private_hvacThermostat.vacation_start_date.key, namron_private_hvacThermostat.vacation_end_date.key].includes(readAttr.key)) { + if ([namronPrivateHvacThermostat.vacation_start_date.key, namronPrivateHvacThermostat.vacation_end_date.key].includes(readAttr.key)) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: fromDate(String(value)), @@ -489,7 +488,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.work_days) { + if (readAttr === namronPrivateHvacThermostat.work_days) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronWorkDays), @@ -1910,7 +1909,7 @@ const definitions: DefinitionWithExtend[] = [ model: 'Edge Thermostat', vendor: 'Namron', description: 'Namron Zigbee Edge Termostat', - fromZigbee: [fzLocal.namron_edge_thermostat, fz.namron_hvac_user_interface, fz.metering, fz.electrical_measurement], + fromZigbee: [fzLocal.namron_edge_thermostat, fz.thermostat, fz.namron_hvac_user_interface, fz.metering, fz.electrical_measurement], toZigbee: [ tz.thermostat_local_temperature, tzLocal.namron_edge_thermostat, @@ -1920,18 +1919,20 @@ const definitions: DefinitionWithExtend[] = [ tz.thermostat_control_sequence_of_operation, tz.thermostat_programming_operation_mode, tz.thermostat_temperature_display_mode, + tz.thermostat_running_state, + tz.thermostat_running_mode ], onEvent: async (type, data, device, options) => { if (type === 'stop') { try { - const key = namron_private_hvacThermostat.time_sync_value.key; + const key = namronPrivateHvacThermostat.time_sync_value.key; clearInterval(globalStore.getValue(device, key)); globalStore.clearValue(device, key); } catch { /* Do nothing*/ } } - if (!globalStore.hasValue(device, namron_private_hvacThermostat.time_sync_value.key)) { + if (!globalStore.hasValue(device, namronPrivateHvacThermostat.time_sync_value.key)) { const hours24 = 1000 * 60 * 60 * 24; const interval = setInterval(async () => { try { @@ -1939,16 +1940,16 @@ const definitions: DefinitionWithExtend[] = [ // Device does not asks for the time with binding, therefore we write the time every 24 hours const time = new Date().getTime() / 1000; await endpoint.write('hvacThermostat', { - [namron_private_hvacThermostat.time_sync_value.attrId]: { + [namronPrivateHvacThermostat.time_sync_value.attrId]: { value: time, - type: namron_private_hvacThermostat.time_sync_value.type, + type: namronPrivateHvacThermostat.time_sync_value.type, }, }); } catch { /* Do nothing*/ } }, hours24); - globalStore.putValue(device, namron_private_hvacThermostat.time_sync_value.key, interval); + globalStore.putValue(device, namronPrivateHvacThermostat.time_sync_value.key, interval); } }, configure: async (device, coordinatorEndpoint, _logger) => { @@ -1975,11 +1976,11 @@ const definitions: DefinitionWithExtend[] = [ // Reads holiday await endpoint.read('hvacThermostat', [ - namron_private_hvacThermostat.holiday_temp_set.attrId, - namron_private_hvacThermostat.holiday_temp_set_f.attrId, - namron_private_hvacThermostat.vacation_mode.attrId, - namron_private_hvacThermostat.vacation_start_date.attrId, - namron_private_hvacThermostat.vacation_end_date.attrId, + namronPrivateHvacThermostat.holiday_temp_set.attrId, + namronPrivateHvacThermostat.holiday_temp_set_f.attrId, + namronPrivateHvacThermostat.vacation_mode.attrId, + namronPrivateHvacThermostat.vacation_start_date.attrId, + namronPrivateHvacThermostat.vacation_end_date.attrId, ]); device.powerSource = 'Mains (single phase)'; @@ -1991,33 +1992,35 @@ const definitions: DefinitionWithExtend[] = [ .climate() .withLocalTemperature() .withSetpoint('occupied_heating_setpoint', 15, 35, 0.5) - .withSystemMode(['off', 'auto', 'cool', 'heat'], ea.ALL), + .withSystemMode(['off', 'auto', 'cool', 'heat'], ea.ALL) + .withLocalTemperatureCalibration(-3, 3, 0.1) + .withRunningState(['idle', 'heat']), e.enum('temperature_display_mode', ea.ALL, ['celsius', 'fahrenheit']).withLabel('Temperature Unit').withDescription('Select Unit'), e.enum('current_operating_mode', ea.ALL, ['Manual', 'ECO']).withDescription('Selected program for thermostat'), e.binary('child_lock', ea.ALL, 'LOCK', 'UNLOCK').withDescription('Enables/disables physical input on the device'), e - .numeric(namron_private_hvacThermostat.active_backlight.key, ea.ALL) + .numeric(namronPrivateHvacThermostat.active_backlight.key, ea.ALL) .withDescription('Desired display brightness') .withUnit('%') .withValueMin(1) .withValueMax(100), e - .enum(namron_private_hvacThermostat.display_auto_off.key, ea.ALL, Object.values(fzNamronDisplayTimeout)) + .enum(namronPrivateHvacThermostat.display_auto_off.key, ea.ALL, Object.values(fzNamronDisplayTimeout)) .withDescription('Turn off the display after the give time in inactivity or never'), - e.binary(namron_private_hvacThermostat.window_check.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off window check mode'), + e.binary(namronPrivateHvacThermostat.window_check.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off window check mode'), e - .enum(namron_private_hvacThermostat.window_state.key, ea.STATE_GET, Object.values(fzNamronOpenClose)) + .enum(namronPrivateHvacThermostat.window_state.key, ea.STATE_GET, Object.values(fzNamronOpenClose)) .withDescription('Detected state of window'), - e.binary(namron_private_hvacThermostat.anti_frost_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off anti-frost mode'), - e.binary(namron_private_hvacThermostat.summer_winter_switch.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Summar Winter switch'), - e.binary(namron_private_hvacThermostat.auto_time.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Automatic time'), + e.binary(namronPrivateHvacThermostat.anti_frost_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off anti-frost mode'), + e.binary(namronPrivateHvacThermostat.summer_winter_switch.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Summar Winter switch'), + e.binary(namronPrivateHvacThermostat.auto_time.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Automatic time'), e - .enum(namron_private_hvacThermostat.countdown_set.key, ea.ALL, Object.values(fzNamronBoostTable)) + .enum(namronPrivateHvacThermostat.countdown_set.key, ea.ALL, Object.values(fzNamronBoostTable)) .withDescription('Starts boost with defined time'), - e.numeric(namron_private_hvacThermostat.countdown_left.key, ea.STATE_GET).withUnit('min').withDescription('Given boost time'), - e.binary(namron_private_hvacThermostat.vacation_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off vacation mode'), + e.numeric(namronPrivateHvacThermostat.countdown_left.key, ea.STATE_GET).withUnit('min').withDescription('Given boost time'), + e.binary(namronPrivateHvacThermostat.vacation_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off vacation mode'), e - .numeric(namron_private_hvacThermostat.holiday_temp_set.key, ea.ALL) + .numeric(namronPrivateHvacThermostat.holiday_temp_set.key, ea.ALL) .withValueMin(5) .withValueMax(35) .withValueStep(0.5) @@ -2025,23 +2028,23 @@ const definitions: DefinitionWithExtend[] = [ .withLabel('Vacation temperature') .withDescription('Vacation temperature setpoint'), e - .text(namron_private_hvacThermostat.vacation_start_date.key, ea.ALL) + .text(namronPrivateHvacThermostat.vacation_start_date.key, ea.ALL) .withDescription('Start date') .withDescription("Supports dates starting with day or year with '. - /'"), e - .text(namron_private_hvacThermostat.vacation_end_date.key, ea.ALL) + .text(namronPrivateHvacThermostat.vacation_end_date.key, ea.ALL) .withDescription('End date') .withDescription("Supports dates starting with day or year with '. - /'"), - e.binary(namron_private_hvacThermostat.time_sync_flag.key, ea.ALL, 'On', 'Off'), - e.numeric(namron_private_hvacThermostat.time_sync_value.key, ea.STATE_GET), + e.binary(namronPrivateHvacThermostat.time_sync_flag.key, ea.ALL, 'On', 'Off'), + e.numeric(namronPrivateHvacThermostat.time_sync_value.key, ea.STATE_GET), e - .enum(namron_private_hvacThermostat.fault.key, ea.STATE_GET, Object.values(fzNamronFault)) + .enum(namronPrivateHvacThermostat.fault.key, ea.STATE_GET, Object.values(fzNamronFault)) .withDescription('Shows current error of the device'), e - .enum(namron_private_hvacThermostat.work_days.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) + .enum(namronPrivateHvacThermostat.work_days.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) .withDescription("Needs to be changed under 'Thermostat settings' > 'Advanced settings' > 'Schedule type'"), e - .numeric(namron_private_hvacThermostat.regulator_percentage.key, ea.ALL) + .numeric(namronPrivateHvacThermostat.regulator_percentage.key, ea.ALL) .withUnit('%') .withValueMin(10) .withValueMax(100) From 5ba90b9c1ab8ef355da09e3634fa3c6513b33087 Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 20 Jan 2025 23:39:26 +0100 Subject: [PATCH 04/20] Updated formatting --- src/devices/namron.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index f0d233ed2d1a2..08ae416621de8 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -1920,7 +1920,7 @@ const definitions: DefinitionWithExtend[] = [ tz.thermostat_programming_operation_mode, tz.thermostat_temperature_display_mode, tz.thermostat_running_state, - tz.thermostat_running_mode + tz.thermostat_running_mode, ], onEvent: async (type, data, device, options) => { if (type === 'stop') { @@ -2043,12 +2043,7 @@ const definitions: DefinitionWithExtend[] = [ e .enum(namronPrivateHvacThermostat.work_days.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) .withDescription("Needs to be changed under 'Thermostat settings' > 'Advanced settings' > 'Schedule type'"), - e - .numeric(namronPrivateHvacThermostat.regulator_percentage.key, ea.ALL) - .withUnit('%') - .withValueMin(10) - .withValueMax(100) - .withValueStep(1), + e.numeric(namronPrivateHvacThermostat.regulator_percentage.key, ea.ALL).withUnit('%').withValueMin(10).withValueMax(100).withValueStep(1), ], }, ]; From 55eed7a7e7ba40035e2d88c5d86f9ec20dfb6b5e Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 20 Jan 2025 23:43:21 +0100 Subject: [PATCH 05/20] camelCase.. --- src/devices/namron.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index 08ae416621de8..fb57865d7a7e9 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -1999,28 +1999,28 @@ const definitions: DefinitionWithExtend[] = [ e.enum('current_operating_mode', ea.ALL, ['Manual', 'ECO']).withDescription('Selected program for thermostat'), e.binary('child_lock', ea.ALL, 'LOCK', 'UNLOCK').withDescription('Enables/disables physical input on the device'), e - .numeric(namronPrivateHvacThermostat.active_backlight.key, ea.ALL) + .numeric(namronPrivateHvacThermostat.activeBacklight.key, ea.ALL) .withDescription('Desired display brightness') .withUnit('%') .withValueMin(1) .withValueMax(100), e - .enum(namronPrivateHvacThermostat.display_auto_off.key, ea.ALL, Object.values(fzNamronDisplayTimeout)) + .enum(namronPrivateHvacThermostat.displayAutoOff.key, ea.ALL, Object.values(fzNamronDisplayTimeout)) .withDescription('Turn off the display after the give time in inactivity or never'), - e.binary(namronPrivateHvacThermostat.window_check.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off window check mode'), + e.binary(namronPrivateHvacThermostat.windowCheck.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off window check mode'), e - .enum(namronPrivateHvacThermostat.window_state.key, ea.STATE_GET, Object.values(fzNamronOpenClose)) + .enum(namronPrivateHvacThermostat.windowState.key, ea.STATE_GET, Object.values(fzNamronOpenClose)) .withDescription('Detected state of window'), - e.binary(namronPrivateHvacThermostat.anti_frost_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off anti-frost mode'), - e.binary(namronPrivateHvacThermostat.summer_winter_switch.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Summar Winter switch'), - e.binary(namronPrivateHvacThermostat.auto_time.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Automatic time'), + e.binary(namronPrivateHvacThermostat.antiFrostMode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off anti-frost mode'), + e.binary(namronPrivateHvacThermostat.summerWinterSwitch.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Summar Winter switch'), + e.binary(namronPrivateHvacThermostat.autoTime.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Automatic time'), e - .enum(namronPrivateHvacThermostat.countdown_set.key, ea.ALL, Object.values(fzNamronBoostTable)) + .enum(namronPrivateHvacThermostat.countdownSet.key, ea.ALL, Object.values(fzNamronBoostTable)) .withDescription('Starts boost with defined time'), - e.numeric(namronPrivateHvacThermostat.countdown_left.key, ea.STATE_GET).withUnit('min').withDescription('Given boost time'), - e.binary(namronPrivateHvacThermostat.vacation_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off vacation mode'), + e.numeric(namronPrivateHvacThermostat.countdownLeft.key, ea.STATE_GET).withUnit('min').withDescription('Given boost time'), + e.binary(namronPrivateHvacThermostat.vacationMode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off vacation mode'), e - .numeric(namronPrivateHvacThermostat.holiday_temp_set.key, ea.ALL) + .numeric(namronPrivateHvacThermostat.holidayTempSet.key, ea.ALL) .withValueMin(5) .withValueMax(35) .withValueStep(0.5) @@ -2028,22 +2028,22 @@ const definitions: DefinitionWithExtend[] = [ .withLabel('Vacation temperature') .withDescription('Vacation temperature setpoint'), e - .text(namronPrivateHvacThermostat.vacation_start_date.key, ea.ALL) + .text(namronPrivateHvacThermostat.vacationStartDate.key, ea.ALL) .withDescription('Start date') .withDescription("Supports dates starting with day or year with '. - /'"), e - .text(namronPrivateHvacThermostat.vacation_end_date.key, ea.ALL) + .text(namronPrivateHvacThermostat.vacationEndDate.key, ea.ALL) .withDescription('End date') .withDescription("Supports dates starting with day or year with '. - /'"), - e.binary(namronPrivateHvacThermostat.time_sync_flag.key, ea.ALL, 'On', 'Off'), - e.numeric(namronPrivateHvacThermostat.time_sync_value.key, ea.STATE_GET), + e.binary(namronPrivateHvacThermostat.timeSyncFlag.key, ea.ALL, 'On', 'Off'), + e.numeric(namronPrivateHvacThermostat.timeSyncValue.key, ea.STATE_GET), e .enum(namronPrivateHvacThermostat.fault.key, ea.STATE_GET, Object.values(fzNamronFault)) .withDescription('Shows current error of the device'), e - .enum(namronPrivateHvacThermostat.work_days.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) + .enum(namronPrivateHvacThermostat.workDays.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) .withDescription("Needs to be changed under 'Thermostat settings' > 'Advanced settings' > 'Schedule type'"), - e.numeric(namronPrivateHvacThermostat.regulator_percentage.key, ea.ALL).withUnit('%').withValueMin(10).withValueMax(100).withValueStep(1), + e.numeric(namronPrivateHvacThermostat.regulatorPercentage.key, ea.ALL).withUnit('%').withValueMin(10).withValueMax(100).withValueStep(1), ], }, ]; From 3a62298540245e9a001447a9eaef6e775026f659 Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 20 Jan 2025 23:47:22 +0100 Subject: [PATCH 06/20] camelCase... --- src/devices/namron.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index fb57865d7a7e9..67c41a6976e90 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -101,7 +101,7 @@ const tzNamronBoostTable = fzNamronBoostTable.fromFzToTz(); const fzNamronSystemMode = {0x00: 'off', 0x01: 'auto', 0x03: 'cool', 0x04: 'heat'}; const tzNamronSystemMode = fzNamronSystemMode.fromFzToTz(); -const fzNamronOnOff = {0: 'Off', 1: 'On'}; +const fzNamronOnOff = {0: 'off', 1: 'on'}; const tzNamronOnOff = fzNamronOnOff.fromFzToTz(); const fzNamronOpenClose = {0: 'closed', 1: 'open'}; From 9d12a05b3e05ea832d645f413d94db2d6a20313d Mon Sep 17 00:00:00 2001 From: bskjon Date: Tue, 21 Jan 2025 00:01:00 +0100 Subject: [PATCH 07/20] mapping --- src/devices/namron.ts | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index 67c41a6976e90..3a1180aeedf54 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -59,14 +59,9 @@ const namronPrivateHvacThermostat: NamronPrivateTable = { currentOperatingMode: {attrId: 'programingOperMode', type: Zcl.DataType.BITMAP8, key: 'current_operating_mode'}, }; -declare global { - interface Object { - fromFzToTz(): KeyValue; - } -} -Object.prototype.fromFzToTz = function (this: KeyValue) { - return Object.fromEntries(Object.entries(this).map(([k, v]) => [v, k])); +function fromFzToTz(obj: KeyValue) { + return Object.fromEntries(Object.entries(obj).map(([k, v]) => [v, k])); }; const fzNamronBoostTable = { @@ -96,25 +91,25 @@ const fzNamronBoostTable = { 23: '1h_55_min', 24: '2h', }; -const tzNamronBoostTable = fzNamronBoostTable.fromFzToTz(); +const tzNamronBoostTable = fromFzToTz(fzNamronBoostTable); const fzNamronSystemMode = {0x00: 'off', 0x01: 'auto', 0x03: 'cool', 0x04: 'heat'}; -const tzNamronSystemMode = fzNamronSystemMode.fromFzToTz(); +const tzNamronSystemMode = fromFzToTz(fzNamronSystemMode); const fzNamronOnOff = {0: 'off', 1: 'on'}; -const tzNamronOnOff = fzNamronOnOff.fromFzToTz(); +const tzNamronOnOff = fromFzToTz(fzNamronOnOff); const fzNamronOpenClose = {0: 'closed', 1: 'open'}; -const tzNamronOpenClose = fzNamronOpenClose.fromFzToTz(); +const tzNamronOpenClose = fromFzToTz(fzNamronOpenClose); const fzNamronDisplayTimeout = {0: 'off', 1: '10s', 2: '30s', 3: '60s'}; -const tzNamronDisplayTimeout = fzNamronDisplayTimeout.fromFzToTz(); +const tzNamronDisplayTimeout = fromFzToTz(fzNamronDisplayTimeout); const fzNamronSensorMode = {0: 'air', 1: 'floor', 3: 'external', 6: 'regulator'}; -const tzNamronSensorMode = fzNamronSensorMode.fromFzToTz(); +const tzNamronSensorMode = fromFzToTz(fzNamronSensorMode); const fzNamronOperationMode = {0: 'manual', 1: 'manual', 5: 'eco'}; -const tzNamronOperationMode = fzNamronOperationMode.fromFzToTz(); +const tzNamronOperationMode = fromFzToTz(fzNamronOperationMode); const fzNamronFault = { 0: 'no_fault', @@ -126,7 +121,7 @@ const fzNamronFault = { }; const fzNamronWorkDays = {0: 'mon-fri_sat-sun', 1: 'mon-sat_sun', 2: 'no_time_off', 3: 'time_off'}; -const tzNamronWorkDays = fzNamronWorkDays.fromFzToTz(); +const tzNamronWorkDays = fromFzToTz(fzNamronWorkDays); const findAttributeByKey = (key: string, attributes: NamronPrivateTable) => { // Finn objektet basert på key From ce555ca1547ff402b667e63ca6612b4b77e3bd6a Mon Sep 17 00:00:00 2001 From: bskjon Date: Tue, 21 Jan 2025 00:36:02 +0100 Subject: [PATCH 08/20] lint --- src/devices/namron.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index 3a1180aeedf54..b7cf745bf732f 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -59,10 +59,9 @@ const namronPrivateHvacThermostat: NamronPrivateTable = { currentOperatingMode: {attrId: 'programingOperMode', type: Zcl.DataType.BITMAP8, key: 'current_operating_mode'}, }; - function fromFzToTz(obj: KeyValue) { return Object.fromEntries(Object.entries(obj).map(([k, v]) => [v, k])); -}; +} const fzNamronBoostTable = { 0: 'off', @@ -1914,6 +1913,7 @@ const definitions: DefinitionWithExtend[] = [ tz.thermostat_control_sequence_of_operation, tz.thermostat_programming_operation_mode, tz.thermostat_temperature_display_mode, + tz.thermostat_local_temperature_calibration, tz.thermostat_running_state, tz.thermostat_running_mode, ], From 8be621676b84686719f3e4b8ada21e34dc8b0053 Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 20 Jan 2025 01:27:09 +0100 Subject: [PATCH 09/20] Added support for namron edge thermostat --- src/devices/namron.ts | 530 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 530 insertions(+) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index 5e50daf1b2b2c..c93c72ba4f38b 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -16,6 +16,187 @@ const e = exposes.presets; const sunricherManufacturer = {manufacturerCode: Zcl.ManufacturerCode.SHENZHEN_SUNRICHER_TECHNOLOGY_LTD}; +interface NamronPrivateAttribute { + attrId: number | string; + type: Zcl.DataType; + key: string; +} + +interface NamronPrivateTable { + [key: string]: NamronPrivateAttribute; +} + +const namron_private_hvacThermostat: NamronPrivateTable = { + window_check: {attrId: 0x8000, type: Zcl.DataType.BOOLEAN, key: 'window_open_check'}, + anti_frost_mode: {attrId: 0x8001, type: Zcl.DataType.BOOLEAN, key: 'anti_frost'}, + window_state: {attrId: 0x8002, type: Zcl.DataType.BOOLEAN, key: 'window_open'}, + work_days: {attrId: 0x8003, type: Zcl.DataType.ENUM8, key: 'work_days'}, + sensor_mode: {attrId: 0x8004, type: Zcl.DataType.ENUM8, key: 'sensor_mode'}, + active_backlight: {attrId: 0x8005, type: Zcl.DataType.UINT8, key: 'active_display_brightness'}, + fault: {attrId: 0x8006, type: Zcl.DataType.ENUM8, key: 'fault'}, + regulator: {attrId: 0x8007, type: Zcl.DataType.UINT8, key: 'regulator'}, + time_sync_flag: {attrId: 0x800a, type: Zcl.DataType.BOOLEAN, key: 'time_sync'}, + time_sync_value: {attrId: 0x800b, type: Zcl.DataType.UINT32, key: 'time_sync_value'}, + abs_min_heat_setpoint_limit_f: {attrId: 0x800c, type: Zcl.DataType.INT16, key: 'abs_min_heat_setpoint_limit_f'}, + abs_max_heat_setpoint_limit_f: {attrId: 0x800d, type: Zcl.DataType.INT16, key: 'abs_max_heat_setpoint_limit_f'}, + abs_min_cool_setpoint_limit_f: {attrId: 0x800e, type: Zcl.DataType.INT16, key: 'abs_min_cool_setpoint_limit_f'}, + abs_max_cool_setpoint_limit_f: {attrId: 0x800f, type: Zcl.DataType.INT16, key: 'abs_max_cool_setpoint_limit_f'}, + occupied_cooling_setpoint_f: {attrId: 0x8010, type: Zcl.DataType.INT16, key: 'occupied_cooling_setpoint_f'}, + occupied_heating_setpoint_f: {attrId: 0x8011, type: Zcl.DataType.INT16, key: 'occupied_heating_setpoint_f'}, + local_temperature_f: {attrId: 0x8012, type: Zcl.DataType.INT16, key: 'local_temperature_f'}, + holiday_temp_set: {attrId: 0x8013, type: Zcl.DataType.INT16, key: 'holiday_temp_set'}, + holiday_temp_set_f: {attrId: 0x801b, type: Zcl.DataType.INT16, key: 'holiday_temp_set_f'}, + regulation_mode: {attrId: 0x801c, type: Zcl.DataType.INT16, key: 'regulation_mode'}, + regulator_percentage: {attrId: 0x801d, type: Zcl.DataType.INT16, key: 'regulator_percentage'}, + summer_winter_switch: {attrId: 0x801e, type: Zcl.DataType.BOOLEAN, key: 'summer_winter_switch'}, + vacation_mode: {attrId: 0x801f, type: Zcl.DataType.BOOLEAN, key: 'vacation_mode'}, + vacation_start_date: {attrId: 0x8020, type: Zcl.DataType.UINT32, key: 'vacation_start_date'}, + vacation_end_date: {attrId: 0x8021, type: Zcl.DataType.UINT32, key: 'vacation_end_date'}, + auto_time: {attrId: 0x8022, type: Zcl.DataType.BOOLEAN, key: 'auto_time'}, + countdown_set: {attrId: 0x8023, type: Zcl.DataType.ENUM8, key: 'boost_time_set'}, + countdown_left: {attrId: 0x8024, type: Zcl.DataType.INT16, key: 'boost_time_left'}, + display_auto_off: {attrId: 0x8029, type: Zcl.DataType.ENUM8, key: 'display_auto_off'}, + system_mode: {attrId: 'systemMode', type: Zcl.DataType.ENUM8, key: 'system_mode'}, + current_operating_mode: {attrId: 'programingOperMode', type: Zcl.DataType.BITMAP8, key: 'current_operating_mode'}, +}; + +declare global { + interface Object { + fromFzToTz(): KeyValue; + } +} + +Object.prototype.fromFzToTz = function (this: KeyValue) { + return Object.fromEntries(Object.entries(this).map(([k, v]) => [v, k])); +}; + +const fzNamronBoostTable = { + 0: 'Off', + 1: '5 min', + 2: '10 min', + 3: '15 min', + 4: '20 min', + 5: '25 min', + 6: '30 min', + 7: '35 min', + 8: '40 min', + 9: '45 min', + 10: '50 min', + 11: '55 min', + 12: '1h', + 13: '1h 5 min', + 14: '1h 10 min', + 15: '1h 15 min', + 16: '1h 20 min', + 17: '1h 25 min', + 18: '1h 30 min', + 19: '1h 35 min', + 20: '1h 40 min', + 21: '1h 45 min', + 22: '1h 50 min', + 23: '1h 55 min', + 24: '2h', +}; +const tzNamronBoostTable = fzNamronBoostTable.fromFzToTz(); + +const fzNamronSystemMode = {0x00: 'off', 0x01: 'auto', 0x03: 'cool', 0x04: 'heat'}; +const tzNamronSystemMode = fzNamronSystemMode.fromFzToTz(); + +const fzNamronOnOff = {0: 'Off', 1: 'On'}; +const tzNamronOnOff = fzNamronOnOff.fromFzToTz(); + +const fzNamronOpenClose = {0: 'Closed', 1: 'Open'}; +const tzNamronOpenClose = fzNamronOpenClose.fromFzToTz(); + +const fzNamronDisplayTimeout = {0: 'Off', 1: '10s', 2: '30s', 3: '60s'}; +const tzNamronDisplayTimeout = fzNamronDisplayTimeout.fromFzToTz(); + +const fzNamronSensorMode = {0: 'Air', 1: 'Floor', 3: 'External', 6: 'Regulator'}; +const tzNamronSensorMode = fzNamronSensorMode.fromFzToTz(); + +const fzNamronOperationMode = {0: 'Manual', 1: 'Manual', 5: 'ECO'}; +const tzNamronOperationMode = fzNamronOperationMode.fromFzToTz(); + +const fzNamronFault = { + 0: 'No Fault', + 1: 'Over current Error', + 2: 'Over heat Error', + 3: 'Built-in Sensor Error', + 4: 'Air Sensor Error', + 5: 'Floor Sensor Error', +}; + +const fzNamronWorkDays = {0: 'Mon-Fri Sat-Sun', 1: 'Mon-Sat Sun', 2: 'No Time Off', 3: 'Time Off'}; +const tzNamronWorkDays = fzNamronWorkDays.fromFzToTz(); + +const findAttributeByKey = (key: string, attributes: NamronPrivateTable) => { + // Finn objektet basert på key + return Object.values(attributes).find((attr) => attr.key === key); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const assign = (data: any, attribute: NamronPrivateAttribute, target: KeyValue, defaultValue: any = null, transform = (value: any) => value) => { + // Ekstrakt `attrId` og `key` direkte fra attributtobjektet + const {attrId, key: targetKey} = attribute; + + if (data[attrId] !== undefined) { + target[targetKey] = transform(data[attrId]); + } else if (data[attrId] && defaultValue !== null && defaultValue !== undefined) { + target[targetKey] = transform(defaultValue); + } +}; + +const assignWithLookup = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any, + attribute: NamronPrivateAttribute, + target: KeyValue, + lookup = {}, + defaultValue: string | number | null = null, +) => { + // Ekstrakt `attrId` og `key` direkte fra attributtobjektet + const {attrId, key: targetKey} = attribute; + + if (data[attrId] !== undefined) { + const value = data[attrId] ?? defaultValue; + target[targetKey] = utils.getFromLookup(value, lookup); + } +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const assignDate = (data: any, attribute: NamronPrivateAttribute, target: KeyValue) => { + // Ekstrakt `attrId` og `key` direkte fra attributtobjektet + const {attrId, key: targetKey} = attribute; + const value = data[attrId]; + if (value === undefined) { + return; + } + const date = new Date(value * 86400000); + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); // Månedene er 0-indeksert + const year = date.getFullYear(); + target[targetKey] = `${year}.${month}.${day}`; +}; + +const fromDate = (value: string) => { + // Ekstrakt `attrId` og `key` direkte fra attributtobjektet + const dateParts = value.split(/[.\-/]/); + if (dateParts.length !== 3) { + throw new Error('Invalid date format'); + } + + let date: Date; + if (dateParts[0].length === 4) { + date = new Date(`${dateParts[0]}-${dateParts[1]}-${dateParts[2]}`); + } else if (dateParts[2].length === 4) { + date = new Date(`${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`); + } else { + throw new Error('Invalid date format'); + } + + return date.getTime() / 86400000 + 1; +}; + const fzLocal = { namron_panelheater: { cluster: 'hvacThermostat', @@ -63,6 +244,58 @@ const fzLocal = { return fz.thermostat.convert(model, msg, publish, options, meta); // as KeyValue; }, } satisfies Fz.Converter, + namron_edge_thermostat: { + cluster: 'hvacThermostat', + type: ['attributeReport', 'readResponse'], + convert: (model, msg, publish, options, meta) => { + const result = {}; + const data = msg.data; + + assignWithLookup(data, namron_private_hvacThermostat.work_days, result, fzNamronWorkDays, 0); + assignWithLookup(data, namron_private_hvacThermostat.sensor_mode, result, fzNamronSensorMode, 0); + assignWithLookup(data, namron_private_hvacThermostat.fault, result, fzNamronFault, 0); + + assignWithLookup(data, namron_private_hvacThermostat.window_check, result, fzNamronOnOff, 0); + assignWithLookup(data, namron_private_hvacThermostat.anti_frost_mode, result, fzNamronOnOff, 0); + assignWithLookup(data, namron_private_hvacThermostat.window_state, result, fzNamronOpenClose, 0); + assignWithLookup(data, namron_private_hvacThermostat.summer_winter_switch, result, fzNamronOnOff, 0); + assignWithLookup(data, namron_private_hvacThermostat.vacation_mode, result, fzNamronOnOff, 0); + assignWithLookup(data, namron_private_hvacThermostat.time_sync_flag, result, fzNamronOnOff, 0); + + assign(data, namron_private_hvacThermostat.active_backlight, result); + assign(data, namron_private_hvacThermostat.time_sync_value, result); + assign(data, namron_private_hvacThermostat.abs_min_heat_setpoint_limit_f, result); + assign(data, namron_private_hvacThermostat.abs_max_heat_setpoint_limit_f, result); + assign(data, namron_private_hvacThermostat.abs_min_cool_setpoint_limit_f, result); + assign(data, namron_private_hvacThermostat.abs_max_cool_setpoint_limit_f, result); + assign(data, namron_private_hvacThermostat.occupied_cooling_setpoint_f, result); + assign(data, namron_private_hvacThermostat.occupied_heating_setpoint_f, result); + assign(data, namron_private_hvacThermostat.local_temperature_f, result); + assign(data, namron_private_hvacThermostat.holiday_temp_set, result, (value: number) => { + return value / 100; + }); + assign(data, namron_private_hvacThermostat.holiday_temp_set_f, result, (value: number) => { + return value / 100; + }); + + assign(data, namron_private_hvacThermostat.regulator_percentage, result, 0, (value) => { + return value; + }); + + assignDate(data, namron_private_hvacThermostat.vacation_start_date, result); + assignDate(data, namron_private_hvacThermostat.vacation_end_date, result); + + // Auto_time (synkroniser tid med ntp?) + assignWithLookup(data, namron_private_hvacThermostat.auto_time, result, fzNamronOnOff, 0); + assignWithLookup(data, namron_private_hvacThermostat.countdown_set, result, fzNamronBoostTable, 0); + assign(data, namron_private_hvacThermostat.countdown_left, result, 0, (value) => (value > 200 ? 0 : value)); + assignWithLookup(data, namron_private_hvacThermostat.display_auto_off, result, fzNamronDisplayTimeout, 0); + assignWithLookup(data, namron_private_hvacThermostat.system_mode, result, fzNamronSystemMode, 0x00); + assignWithLookup(data, namron_private_hvacThermostat.current_operating_mode, result, fzNamronOperationMode, 0); + + return result; + }, + } satisfies Fz.Converter, }; const tzLocal = { @@ -112,6 +345,160 @@ const tzLocal = { } }, } satisfies Tz.Converter, + namron_edge_thermostat: { + key: [ + 'window_open_check', + 'anti_frost', + 'window_open', + 'work_days', + 'sensor_mode', + 'active_display_brightness', + 'fault', + 'regulator', + 'time_sync', + 'time_sync_value', + 'abs_min_heat_setpoint_limit_f', + 'abs_max_heat_setpoint_limit_f', + 'abs_min_cool_setpoint_limit_f', + 'abs_max_cool_setpoint_limit_f', + 'occupied_cooling_setpoint_f', + 'occupied_heating_setpoint_f', + 'local_temperature_f', + 'holiday_temp_set', + 'holiday_temp_set_f', + 'regulation_mode', + 'regulator_percentage', + 'summer_winter_switch', + 'vacation_mode', + 'vacation_start_date', + 'vacation_end_date', + 'auto_time', + 'boost_time_set', + 'boost_time_left', + 'display_auto_off', + 'system_mode', + 'current_operating_mode', + ], + + convertGet: async (entity, key, meta) => { + const readAttr = findAttributeByKey(key, namron_private_hvacThermostat); + if (readAttr) { + await entity.read('hvacThermostat', [readAttr.attrId]); + } else { + throw new Error(`Unhandled key toZigbee.namronEdgeThermostat.convertGet ${key}`); + } + }, + + convertSet: async (entity, key, value, meta) => { + const readAttr = findAttributeByKey(key, namron_private_hvacThermostat); + + if (!readAttr) { + throw new Error(`Unhandled key toZigbee.namronEdgeThermostat.convertSet ${key}`); + } + + if ( + [ + namron_private_hvacThermostat.window_check.key, + namron_private_hvacThermostat.anti_frost_mode.key, + namron_private_hvacThermostat.time_sync_flag.key, + namron_private_hvacThermostat.summer_winter_switch.key, + namron_private_hvacThermostat.auto_time.key, + namron_private_hvacThermostat.vacation_mode.key, + ].includes(readAttr.key) + ) { + const payload = {[readAttr.attrId]: {value: utils.getFromLookup(value, tzNamronOnOff), type: readAttr.type}}; + await entity.write('hvacThermostat', payload); + return; + } + + // Direct call + if ([Zcl.DataType.UINT8, Zcl.DataType.INT16, Zcl.DataType.UINT32].includes(readAttr.type)) { + const payload = {[readAttr.attrId]: {value: value, type: readAttr.type}}; + await entity.write('hvacThermostat', payload); + } + + if (readAttr === namron_private_hvacThermostat.countdown_set) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronBoostTable), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.window_state) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronOpenClose), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.display_auto_off) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronDisplayTimeout), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.sensor_mode) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronSensorMode), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.system_mode) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronSystemMode), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.current_operating_mode) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronOperationMode), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.holiday_temp_set) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: Number(value) * 100, + type: readAttr.type, + }, + }); + } + + if ([namron_private_hvacThermostat.vacation_start_date.key, namron_private_hvacThermostat.vacation_end_date.key].includes(readAttr.key)) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: fromDate(String(value)), + type: readAttr.type, + }, + }); + } + + if (readAttr === namron_private_hvacThermostat.work_days) { + await entity.write('hvacThermostat', { + [readAttr.attrId]: { + value: utils.getFromLookup(value, tzNamronWorkDays), + type: readAttr.type, + }, + }); + } + }, + } satisfies Tz.Converter, }; const definitions: DefinitionWithExtend[] = [ @@ -1519,6 +1906,149 @@ const definitions: DefinitionWithExtend[] = [ description: 'Zigbee smart plug dimmer 150W', extend: [m.light(), m.electricityMeter({cluster: 'electrical'})], }, + { + zigbeeModel: ['4512783', '4512784'], + model: 'Edge Thermostat', + vendor: 'Namron', + description: 'Namron Zigbee Edge Termostat', + fromZigbee: [fzLocal.namron_edge_thermostat, fz.namron_hvac_user_interface, fz.metering, fz.electrical_measurement], + toZigbee: [ + tz.thermostat_local_temperature, + tzLocal.namron_edge_thermostat, + tz.thermostat_occupied_heating_setpoint, + tz.thermostat_unoccupied_heating_setpoint, + tz.namron_thermostat_child_lock, + tz.thermostat_control_sequence_of_operation, + tz.thermostat_programming_operation_mode, + tz.thermostat_temperature_display_mode, + ], + onEvent: async (type, data, device, options) => { + if (type === 'stop') { + try { + const key = namron_private_hvacThermostat.time_sync_value.key; + clearInterval(globalStore.getValue(device, key)); + globalStore.clearValue(device, key); + } catch { + /* Do nothing*/ + } + } + if (!globalStore.hasValue(device, namron_private_hvacThermostat.time_sync_value.key)) { + const hours24 = 1000 * 60 * 60 * 24; + const interval = setInterval(async () => { + try { + const endpoint = device.getEndpoint(1); + // Device does not asks for the time with binding, therefore we write the time every 24 hours + const time = new Date().getTime() / 1000; + await endpoint.write('hvacThermostat', { + [namron_private_hvacThermostat.time_sync_value.attrId]: { + value: time, + type: namron_private_hvacThermostat.time_sync_value.type, + }, + }); + } catch { + /* Do nothing*/ + } + }, hours24); + globalStore.putValue(device, namron_private_hvacThermostat.time_sync_value.key, interval); + } + }, + configure: async (device, coordinatorEndpoint, _logger) => { + const endpoint = device.getEndpoint(1); + const binds = [ + 'genBasic', + 'genIdentify', + 'genOnOff', + 'hvacThermostat', + 'hvacUserInterfaceCfg', + 'msRelativeHumidity', + 'seMetering', + 'haElectricalMeasurement', + 'msOccupancySensing', + ]; + await reporting.bind(endpoint, coordinatorEndpoint, binds); + + await reporting.thermostatOccupiedHeatingSetpoint(endpoint, {min: 0, change: 50}); + await reporting.thermostatTemperature(endpoint, {min: 0, change: 50}); + await reporting.thermostatKeypadLockMode(endpoint); + + // Initial read + await endpoint.read('hvacThermostat', [0x8000, 0x8001, 0x8002, 0x801e, 0x8004, 0x8006, 0x8005, 0x8029, 0x8022, 0x8023, 0x8024]); + + // Reads holiday + await endpoint.read('hvacThermostat', [ + namron_private_hvacThermostat.holiday_temp_set.attrId, + namron_private_hvacThermostat.holiday_temp_set_f.key, + namron_private_hvacThermostat.vacation_mode.attrId, + namron_private_hvacThermostat.vacation_start_date.attrId, + namron_private_hvacThermostat.vacation_end_date.attrId, + ]); + + device.powerSource = 'Mains (single phase)'; + device.save(); + }, + extend: [m.electricityMeter({voltage: false}), m.onOff({powerOnBehavior: false})], + exposes: [ + e + .climate() + .withLocalTemperature() + .withSetpoint('occupied_heating_setpoint', 15, 35, 0.5) + .withSystemMode(['off', 'auto', 'cool', 'heat'], ea.ALL), + e.enum('temperature_display_mode', ea.ALL, ['celsius', 'fahrenheit']).withLabel('Temperature Unit').withDescription('Select Unit'), + e.enum('current_operating_mode', ea.ALL, ['Manual', 'ECO']).withDescription('Selected program for thermostat'), + e.binary('child_lock', ea.ALL, 'LOCK', 'UNLOCK').withDescription('Enables/disables physical input on the device'), + e + .numeric(namron_private_hvacThermostat.active_backlight.key, ea.ALL) + .withDescription('Desired display brightness') + .withUnit('%') + .withValueMin(1) + .withValueMax(100), + e + .enum(namron_private_hvacThermostat.display_auto_off.key, ea.ALL, Object.values(fzNamronDisplayTimeout)) + .withDescription('Turn off the display after the give time in inactivity or never'), + e.binary(namron_private_hvacThermostat.window_check.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off window check mode'), + e + .enum(namron_private_hvacThermostat.window_state.key, ea.STATE_GET, Object.values(fzNamronOpenClose)) + .withDescription('Detected state of window'), + e.binary(namron_private_hvacThermostat.anti_frost_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off anti-frost mode'), + e.binary(namron_private_hvacThermostat.summer_winter_switch.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Summar Winter switch'), + e.binary(namron_private_hvacThermostat.auto_time.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Automatic time'), + e + .enum(namron_private_hvacThermostat.countdown_set.key, ea.ALL, Object.values(fzNamronBoostTable)) + .withDescription('Starts boost with defined time'), + e.numeric(namron_private_hvacThermostat.countdown_left.key, ea.STATE_GET).withUnit('min').withDescription('Given boost time'), + e.binary(namron_private_hvacThermostat.vacation_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off vacation mode'), + e + .numeric(namron_private_hvacThermostat.holiday_temp_set.key, ea.ALL) + .withValueMin(5) + .withValueMax(35) + .withValueStep(0.5) + .withUnit('°C') + .withLabel('Vacation temperature') + .withDescription('Vacation temperature setpoint'), + e + .text(namron_private_hvacThermostat.vacation_start_date.key, ea.ALL) + .withDescription('Start date') + .withDescription("Supports dates starting with day or year with '. - /'"), + e + .text(namron_private_hvacThermostat.vacation_end_date.key, ea.ALL) + .withDescription('End date') + .withDescription("Supports dates starting with day or year with '. - /'"), + e.binary(namron_private_hvacThermostat.time_sync_flag.key, ea.ALL, 'On', 'Off'), + e.numeric(namron_private_hvacThermostat.time_sync_value.key, ea.STATE_GET), + e + .enum(namron_private_hvacThermostat.fault.key, ea.STATE_GET, Object.values(fzNamronFault)) + .withDescription('Shows current error of the device'), + e + .enum(namron_private_hvacThermostat.work_days.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) + .withDescription("Needs to be changed under 'Thermostat settings' > 'Advanced settings' > 'Schedule type'"), + e + .numeric(namron_private_hvacThermostat.regulator_percentage.key, ea.ALL) + .withUnit('%') + .withValueMin(10) + .withValueMax(100) + .withValueStep(1), + ], + }, ]; export default definitions; From c3b8493b5c54ca1cb4e1d9032e0a8c2b6c48bd72 Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 20 Jan 2025 02:01:11 +0100 Subject: [PATCH 10/20] Fixed typo --- src/devices/namron.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index c93c72ba4f38b..94d67cac2ee2a 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -1977,7 +1977,7 @@ const definitions: DefinitionWithExtend[] = [ // Reads holiday await endpoint.read('hvacThermostat', [ namron_private_hvacThermostat.holiday_temp_set.attrId, - namron_private_hvacThermostat.holiday_temp_set_f.key, + namron_private_hvacThermostat.holiday_temp_set_f.attrId, namron_private_hvacThermostat.vacation_mode.attrId, namron_private_hvacThermostat.vacation_start_date.attrId, namron_private_hvacThermostat.vacation_end_date.attrId, From db5401f105f9a49f5a9a4f5045c8d812df6a0b4b Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 20 Jan 2025 23:09:49 +0100 Subject: [PATCH 11/20] Updated formatting --- src/devices/namron.ts | 287 +++++++++++++++++++++--------------------- 1 file changed, 145 insertions(+), 142 deletions(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index 94d67cac2ee2a..162fb946eaf20 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -26,38 +26,37 @@ interface NamronPrivateTable { [key: string]: NamronPrivateAttribute; } -const namron_private_hvacThermostat: NamronPrivateTable = { - window_check: {attrId: 0x8000, type: Zcl.DataType.BOOLEAN, key: 'window_open_check'}, - anti_frost_mode: {attrId: 0x8001, type: Zcl.DataType.BOOLEAN, key: 'anti_frost'}, - window_state: {attrId: 0x8002, type: Zcl.DataType.BOOLEAN, key: 'window_open'}, - work_days: {attrId: 0x8003, type: Zcl.DataType.ENUM8, key: 'work_days'}, - sensor_mode: {attrId: 0x8004, type: Zcl.DataType.ENUM8, key: 'sensor_mode'}, - active_backlight: {attrId: 0x8005, type: Zcl.DataType.UINT8, key: 'active_display_brightness'}, +const namronPrivateHvacThermostat: NamronPrivateTable = { + windowCheck: {attrId: 0x8000, type: Zcl.DataType.BOOLEAN, key: 'window_open_check'}, + antiFrostMode: {attrId: 0x8001, type: Zcl.DataType.BOOLEAN, key: 'anti_frost'}, + windowState: {attrId: 0x8002, type: Zcl.DataType.BOOLEAN, key: 'window_open'}, + workDays: {attrId: 0x8003, type: Zcl.DataType.ENUM8, key: 'work_days'}, + sensorMode: {attrId: 0x8004, type: Zcl.DataType.ENUM8, key: 'sensor_mode'}, + activeBacklight: {attrId: 0x8005, type: Zcl.DataType.UINT8, key: 'active_display_brightness'}, fault: {attrId: 0x8006, type: Zcl.DataType.ENUM8, key: 'fault'}, regulator: {attrId: 0x8007, type: Zcl.DataType.UINT8, key: 'regulator'}, - time_sync_flag: {attrId: 0x800a, type: Zcl.DataType.BOOLEAN, key: 'time_sync'}, - time_sync_value: {attrId: 0x800b, type: Zcl.DataType.UINT32, key: 'time_sync_value'}, - abs_min_heat_setpoint_limit_f: {attrId: 0x800c, type: Zcl.DataType.INT16, key: 'abs_min_heat_setpoint_limit_f'}, - abs_max_heat_setpoint_limit_f: {attrId: 0x800d, type: Zcl.DataType.INT16, key: 'abs_max_heat_setpoint_limit_f'}, - abs_min_cool_setpoint_limit_f: {attrId: 0x800e, type: Zcl.DataType.INT16, key: 'abs_min_cool_setpoint_limit_f'}, - abs_max_cool_setpoint_limit_f: {attrId: 0x800f, type: Zcl.DataType.INT16, key: 'abs_max_cool_setpoint_limit_f'}, - occupied_cooling_setpoint_f: {attrId: 0x8010, type: Zcl.DataType.INT16, key: 'occupied_cooling_setpoint_f'}, - occupied_heating_setpoint_f: {attrId: 0x8011, type: Zcl.DataType.INT16, key: 'occupied_heating_setpoint_f'}, - local_temperature_f: {attrId: 0x8012, type: Zcl.DataType.INT16, key: 'local_temperature_f'}, - holiday_temp_set: {attrId: 0x8013, type: Zcl.DataType.INT16, key: 'holiday_temp_set'}, - holiday_temp_set_f: {attrId: 0x801b, type: Zcl.DataType.INT16, key: 'holiday_temp_set_f'}, - regulation_mode: {attrId: 0x801c, type: Zcl.DataType.INT16, key: 'regulation_mode'}, - regulator_percentage: {attrId: 0x801d, type: Zcl.DataType.INT16, key: 'regulator_percentage'}, - summer_winter_switch: {attrId: 0x801e, type: Zcl.DataType.BOOLEAN, key: 'summer_winter_switch'}, - vacation_mode: {attrId: 0x801f, type: Zcl.DataType.BOOLEAN, key: 'vacation_mode'}, - vacation_start_date: {attrId: 0x8020, type: Zcl.DataType.UINT32, key: 'vacation_start_date'}, - vacation_end_date: {attrId: 0x8021, type: Zcl.DataType.UINT32, key: 'vacation_end_date'}, - auto_time: {attrId: 0x8022, type: Zcl.DataType.BOOLEAN, key: 'auto_time'}, - countdown_set: {attrId: 0x8023, type: Zcl.DataType.ENUM8, key: 'boost_time_set'}, - countdown_left: {attrId: 0x8024, type: Zcl.DataType.INT16, key: 'boost_time_left'}, - display_auto_off: {attrId: 0x8029, type: Zcl.DataType.ENUM8, key: 'display_auto_off'}, - system_mode: {attrId: 'systemMode', type: Zcl.DataType.ENUM8, key: 'system_mode'}, - current_operating_mode: {attrId: 'programingOperMode', type: Zcl.DataType.BITMAP8, key: 'current_operating_mode'}, + timeSyncFlag: {attrId: 0x800a, type: Zcl.DataType.BOOLEAN, key: 'time_sync'}, + timeSyncValue: {attrId: 0x800b, type: Zcl.DataType.UINT32, key: 'time_sync_value'}, + absMinHeatSetpointLimitF: {attrId: 0x800c, type: Zcl.DataType.INT16, key: 'abs_min_heat_setpoint_limit_f'}, + absMaxHeatSetpointLimitF: {attrId: 0x800d, type: Zcl.DataType.INT16, key: 'abs_max_heat_setpoint_limit_f'}, + absMinCoolSetpointLimitF: {attrId: 0x800e, type: Zcl.DataType.INT16, key: 'abs_min_cool_setpoint_limit_f'}, + absMaxCoolSetpointLimitF: {attrId: 0x800f, type: Zcl.DataType.INT16, key: 'abs_max_cool_setpoint_limit_f'}, + occupiedCoolingSetpointF: {attrId: 0x8010, type: Zcl.DataType.INT16, key: 'occupied_cooling_setpoint_f'}, + occupiedHeatingSetpointF: {attrId: 0x8011, type: Zcl.DataType.INT16, key: 'occupied_heating_setpoint_f'}, + localTemperatureF: {attrId: 0x8012, type: Zcl.DataType.INT16, key: 'local_temperature_f'}, + holidayTempSet: {attrId: 0x8013, type: Zcl.DataType.INT16, key: 'holiday_temp_set'}, + holidayTempSetF: {attrId: 0x801b, type: Zcl.DataType.INT16, key: 'holiday_temp_set_f'}, + regulationMode: {attrId: 0x801c, type: Zcl.DataType.INT16, key: 'regulation_mode'}, + regulatorPercentage: {attrId: 0x801d, type: Zcl.DataType.INT16, key: 'regulator_percentage'}, + summerWinterSwitch: {attrId: 0x801e, type: Zcl.DataType.BOOLEAN, key: 'summer_winter_switch'}, + vacationMode: {attrId: 0x801f, type: Zcl.DataType.BOOLEAN, key: 'vacation_mode'}, + vacationStartDate: {attrId: 0x8020, type: Zcl.DataType.UINT32, key: 'vacation_start_date'}, + vacationEndDate: {attrId: 0x8021, type: Zcl.DataType.UINT32, key: 'vacation_end_date'}, + autoTime: {attrId: 0x8022, type: Zcl.DataType.BOOLEAN, key: 'auto_time'}, + countdownSet: {attrId: 0x8023, type: Zcl.DataType.ENUM8, key: 'boost_time_set'}, + countdownLeft: {attrId: 0x8024, type: Zcl.DataType.INT16, key: 'boost_time_left'}, + displayAutoOff: {attrId: 0x8029, type: Zcl.DataType.ENUM8, key: 'display_auto_off'}, + currentOperatingMode: {attrId: 'programingOperMode', type: Zcl.DataType.BITMAP8, key: 'current_operating_mode'}, }; declare global { @@ -71,30 +70,30 @@ Object.prototype.fromFzToTz = function (this: KeyValue) { }; const fzNamronBoostTable = { - 0: 'Off', - 1: '5 min', - 2: '10 min', - 3: '15 min', - 4: '20 min', - 5: '25 min', - 6: '30 min', - 7: '35 min', - 8: '40 min', - 9: '45 min', - 10: '50 min', - 11: '55 min', + 0: 'off', + 1: '5_min', + 2: '10_min', + 3: '15_min', + 4: '20_min', + 5: '25_min', + 6: '30_min', + 7: '35_min', + 8: '40_min', + 9: '45_min', + 10: '50_min', + 11: '55_min', 12: '1h', - 13: '1h 5 min', - 14: '1h 10 min', - 15: '1h 15 min', - 16: '1h 20 min', - 17: '1h 25 min', - 18: '1h 30 min', - 19: '1h 35 min', - 20: '1h 40 min', - 21: '1h 45 min', - 22: '1h 50 min', - 23: '1h 55 min', + 13: '1h_5_min', + 14: '1h_10_min', + 15: '1h_15_min', + 16: '1h_20_min', + 17: '1h_25_min', + 18: '1h_30_min', + 19: '1h_35_min', + 20: '1h_40_min', + 21: '1h_45_min', + 22: '1h_50_min', + 23: '1h_55_min', 24: '2h', }; const tzNamronBoostTable = fzNamronBoostTable.fromFzToTz(); @@ -105,28 +104,28 @@ const tzNamronSystemMode = fzNamronSystemMode.fromFzToTz(); const fzNamronOnOff = {0: 'Off', 1: 'On'}; const tzNamronOnOff = fzNamronOnOff.fromFzToTz(); -const fzNamronOpenClose = {0: 'Closed', 1: 'Open'}; +const fzNamronOpenClose = {0: 'closed', 1: 'open'}; const tzNamronOpenClose = fzNamronOpenClose.fromFzToTz(); -const fzNamronDisplayTimeout = {0: 'Off', 1: '10s', 2: '30s', 3: '60s'}; +const fzNamronDisplayTimeout = {0: 'off', 1: '10s', 2: '30s', 3: '60s'}; const tzNamronDisplayTimeout = fzNamronDisplayTimeout.fromFzToTz(); -const fzNamronSensorMode = {0: 'Air', 1: 'Floor', 3: 'External', 6: 'Regulator'}; +const fzNamronSensorMode = {0: 'air', 1: 'floor', 3: 'external', 6: 'regulator'}; const tzNamronSensorMode = fzNamronSensorMode.fromFzToTz(); -const fzNamronOperationMode = {0: 'Manual', 1: 'Manual', 5: 'ECO'}; +const fzNamronOperationMode = {0: 'manual', 1: 'manual', 5: 'eco'}; const tzNamronOperationMode = fzNamronOperationMode.fromFzToTz(); const fzNamronFault = { - 0: 'No Fault', - 1: 'Over current Error', - 2: 'Over heat Error', - 3: 'Built-in Sensor Error', - 4: 'Air Sensor Error', - 5: 'Floor Sensor Error', + 0: 'no_fault', + 1: 'over_current_error', + 2: 'over_heat_error', + 3: 'built-in_sensor_error', + 4: 'air_sensor_error', + 5: 'floor_sensor_error', }; -const fzNamronWorkDays = {0: 'Mon-Fri Sat-Sun', 1: 'Mon-Sat Sun', 2: 'No Time Off', 3: 'Time Off'}; +const fzNamronWorkDays = {0: 'mon-fri_sat-sun', 1: 'mon-sat_sun', 2: 'no_time_off', 3: 'time_off'}; const tzNamronWorkDays = fzNamronWorkDays.fromFzToTz(); const findAttributeByKey = (key: string, attributes: NamronPrivateTable) => { @@ -251,47 +250,47 @@ const fzLocal = { const result = {}; const data = msg.data; - assignWithLookup(data, namron_private_hvacThermostat.work_days, result, fzNamronWorkDays, 0); - assignWithLookup(data, namron_private_hvacThermostat.sensor_mode, result, fzNamronSensorMode, 0); - assignWithLookup(data, namron_private_hvacThermostat.fault, result, fzNamronFault, 0); - - assignWithLookup(data, namron_private_hvacThermostat.window_check, result, fzNamronOnOff, 0); - assignWithLookup(data, namron_private_hvacThermostat.anti_frost_mode, result, fzNamronOnOff, 0); - assignWithLookup(data, namron_private_hvacThermostat.window_state, result, fzNamronOpenClose, 0); - assignWithLookup(data, namron_private_hvacThermostat.summer_winter_switch, result, fzNamronOnOff, 0); - assignWithLookup(data, namron_private_hvacThermostat.vacation_mode, result, fzNamronOnOff, 0); - assignWithLookup(data, namron_private_hvacThermostat.time_sync_flag, result, fzNamronOnOff, 0); - - assign(data, namron_private_hvacThermostat.active_backlight, result); - assign(data, namron_private_hvacThermostat.time_sync_value, result); - assign(data, namron_private_hvacThermostat.abs_min_heat_setpoint_limit_f, result); - assign(data, namron_private_hvacThermostat.abs_max_heat_setpoint_limit_f, result); - assign(data, namron_private_hvacThermostat.abs_min_cool_setpoint_limit_f, result); - assign(data, namron_private_hvacThermostat.abs_max_cool_setpoint_limit_f, result); - assign(data, namron_private_hvacThermostat.occupied_cooling_setpoint_f, result); - assign(data, namron_private_hvacThermostat.occupied_heating_setpoint_f, result); - assign(data, namron_private_hvacThermostat.local_temperature_f, result); - assign(data, namron_private_hvacThermostat.holiday_temp_set, result, (value: number) => { + assignWithLookup(data, namronPrivateHvacThermostat.work_days, result, fzNamronWorkDays, 0); + assignWithLookup(data, namronPrivateHvacThermostat.sensor_mode, result, fzNamronSensorMode, 0); + assignWithLookup(data, namronPrivateHvacThermostat.fault, result, fzNamronFault, 0); + + assignWithLookup(data, namronPrivateHvacThermostat.window_check, result, fzNamronOnOff, 0); + assignWithLookup(data, namronPrivateHvacThermostat.anti_frost_mode, result, fzNamronOnOff, 0); + assignWithLookup(data, namronPrivateHvacThermostat.window_state, result, fzNamronOpenClose, 0); + assignWithLookup(data, namronPrivateHvacThermostat.summer_winter_switch, result, fzNamronOnOff, 0); + assignWithLookup(data, namronPrivateHvacThermostat.vacation_mode, result, fzNamronOnOff, 0); + assignWithLookup(data, namronPrivateHvacThermostat.time_sync_flag, result, fzNamronOnOff, 0); + + assign(data, namronPrivateHvacThermostat.active_backlight, result); + assign(data, namronPrivateHvacThermostat.time_sync_value, result); + assign(data, namronPrivateHvacThermostat.abs_min_heat_setpoint_limit_f, result); + assign(data, namronPrivateHvacThermostat.abs_max_heat_setpoint_limit_f, result); + assign(data, namronPrivateHvacThermostat.abs_min_cool_setpoint_limit_f, result); + assign(data, namronPrivateHvacThermostat.abs_max_cool_setpoint_limit_f, result); + assign(data, namronPrivateHvacThermostat.occupied_cooling_setpoint_f, result); + assign(data, namronPrivateHvacThermostat.occupied_heating_setpoint_f, result); + assign(data, namronPrivateHvacThermostat.local_temperature_f, result); + assign(data, namronPrivateHvacThermostat.holiday_temp_set, result, (value: number) => { return value / 100; }); - assign(data, namron_private_hvacThermostat.holiday_temp_set_f, result, (value: number) => { + assign(data, namronPrivateHvacThermostat.holiday_temp_set_f, result, (value: number) => { return value / 100; }); - assign(data, namron_private_hvacThermostat.regulator_percentage, result, 0, (value) => { + assign(data, namronPrivateHvacThermostat.regulator_percentage, result, 0, (value) => { return value; }); - assignDate(data, namron_private_hvacThermostat.vacation_start_date, result); - assignDate(data, namron_private_hvacThermostat.vacation_end_date, result); + assignDate(data, namronPrivateHvacThermostat.vacation_start_date, result); + assignDate(data, namronPrivateHvacThermostat.vacation_end_date, result); // Auto_time (synkroniser tid med ntp?) - assignWithLookup(data, namron_private_hvacThermostat.auto_time, result, fzNamronOnOff, 0); - assignWithLookup(data, namron_private_hvacThermostat.countdown_set, result, fzNamronBoostTable, 0); - assign(data, namron_private_hvacThermostat.countdown_left, result, 0, (value) => (value > 200 ? 0 : value)); - assignWithLookup(data, namron_private_hvacThermostat.display_auto_off, result, fzNamronDisplayTimeout, 0); - assignWithLookup(data, namron_private_hvacThermostat.system_mode, result, fzNamronSystemMode, 0x00); - assignWithLookup(data, namron_private_hvacThermostat.current_operating_mode, result, fzNamronOperationMode, 0); + assignWithLookup(data, namronPrivateHvacThermostat.auto_time, result, fzNamronOnOff, 0); + assignWithLookup(data, namronPrivateHvacThermostat.countdown_set, result, fzNamronBoostTable, 0); + assign(data, namronPrivateHvacThermostat.countdown_left, result, 0, (value) => (value > 200 ? 0 : value)); + assignWithLookup(data, namronPrivateHvacThermostat.display_auto_off, result, fzNamronDisplayTimeout, 0); + assignWithLookup(data, namronPrivateHvacThermostat.system_mode, result, fzNamronSystemMode, 0x00); + assignWithLookup(data, namronPrivateHvacThermostat.current_operating_mode, result, fzNamronOperationMode, 0); return result; }, @@ -381,7 +380,7 @@ const tzLocal = { ], convertGet: async (entity, key, meta) => { - const readAttr = findAttributeByKey(key, namron_private_hvacThermostat); + const readAttr = findAttributeByKey(key, namronPrivateHvacThermostat); if (readAttr) { await entity.read('hvacThermostat', [readAttr.attrId]); } else { @@ -390,7 +389,7 @@ const tzLocal = { }, convertSet: async (entity, key, value, meta) => { - const readAttr = findAttributeByKey(key, namron_private_hvacThermostat); + const readAttr = findAttributeByKey(key, namronPrivateHvacThermostat); if (!readAttr) { throw new Error(`Unhandled key toZigbee.namronEdgeThermostat.convertSet ${key}`); @@ -398,12 +397,12 @@ const tzLocal = { if ( [ - namron_private_hvacThermostat.window_check.key, - namron_private_hvacThermostat.anti_frost_mode.key, - namron_private_hvacThermostat.time_sync_flag.key, - namron_private_hvacThermostat.summer_winter_switch.key, - namron_private_hvacThermostat.auto_time.key, - namron_private_hvacThermostat.vacation_mode.key, + namronPrivateHvacThermostat.window_check.key, + namronPrivateHvacThermostat.anti_frost_mode.key, + namronPrivateHvacThermostat.time_sync_flag.key, + namronPrivateHvacThermostat.summer_winter_switch.key, + namronPrivateHvacThermostat.auto_time.key, + namronPrivateHvacThermostat.vacation_mode.key, ].includes(readAttr.key) ) { const payload = {[readAttr.attrId]: {value: utils.getFromLookup(value, tzNamronOnOff), type: readAttr.type}}; @@ -417,7 +416,7 @@ const tzLocal = { await entity.write('hvacThermostat', payload); } - if (readAttr === namron_private_hvacThermostat.countdown_set) { + if (readAttr === namronPrivateHvacThermostat.countdown_set) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronBoostTable), @@ -426,7 +425,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.window_state) { + if (readAttr === namronPrivateHvacThermostat.window_state) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronOpenClose), @@ -435,7 +434,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.display_auto_off) { + if (readAttr === namronPrivateHvacThermostat.display_auto_off) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronDisplayTimeout), @@ -444,7 +443,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.sensor_mode) { + if (readAttr === namronPrivateHvacThermostat.sensor_mode) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronSensorMode), @@ -453,7 +452,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.system_mode) { + if (readAttr === namronPrivateHvacThermostat.system_mode) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronSystemMode), @@ -462,7 +461,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.current_operating_mode) { + if (readAttr === namronPrivateHvacThermostat.current_operating_mode) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronOperationMode), @@ -471,7 +470,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.holiday_temp_set) { + if (readAttr === namronPrivateHvacThermostat.holiday_temp_set) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: Number(value) * 100, @@ -480,7 +479,7 @@ const tzLocal = { }); } - if ([namron_private_hvacThermostat.vacation_start_date.key, namron_private_hvacThermostat.vacation_end_date.key].includes(readAttr.key)) { + if ([namronPrivateHvacThermostat.vacation_start_date.key, namronPrivateHvacThermostat.vacation_end_date.key].includes(readAttr.key)) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: fromDate(String(value)), @@ -489,7 +488,7 @@ const tzLocal = { }); } - if (readAttr === namron_private_hvacThermostat.work_days) { + if (readAttr === namronPrivateHvacThermostat.work_days) { await entity.write('hvacThermostat', { [readAttr.attrId]: { value: utils.getFromLookup(value, tzNamronWorkDays), @@ -1911,7 +1910,7 @@ const definitions: DefinitionWithExtend[] = [ model: 'Edge Thermostat', vendor: 'Namron', description: 'Namron Zigbee Edge Termostat', - fromZigbee: [fzLocal.namron_edge_thermostat, fz.namron_hvac_user_interface, fz.metering, fz.electrical_measurement], + fromZigbee: [fzLocal.namron_edge_thermostat, fz.thermostat, fz.namron_hvac_user_interface, fz.metering, fz.electrical_measurement], toZigbee: [ tz.thermostat_local_temperature, tzLocal.namron_edge_thermostat, @@ -1921,18 +1920,20 @@ const definitions: DefinitionWithExtend[] = [ tz.thermostat_control_sequence_of_operation, tz.thermostat_programming_operation_mode, tz.thermostat_temperature_display_mode, + tz.thermostat_running_state, + tz.thermostat_running_mode ], onEvent: async (type, data, device, options) => { if (type === 'stop') { try { - const key = namron_private_hvacThermostat.time_sync_value.key; + const key = namronPrivateHvacThermostat.time_sync_value.key; clearInterval(globalStore.getValue(device, key)); globalStore.clearValue(device, key); } catch { /* Do nothing*/ } } - if (!globalStore.hasValue(device, namron_private_hvacThermostat.time_sync_value.key)) { + if (!globalStore.hasValue(device, namronPrivateHvacThermostat.time_sync_value.key)) { const hours24 = 1000 * 60 * 60 * 24; const interval = setInterval(async () => { try { @@ -1940,16 +1941,16 @@ const definitions: DefinitionWithExtend[] = [ // Device does not asks for the time with binding, therefore we write the time every 24 hours const time = new Date().getTime() / 1000; await endpoint.write('hvacThermostat', { - [namron_private_hvacThermostat.time_sync_value.attrId]: { + [namronPrivateHvacThermostat.time_sync_value.attrId]: { value: time, - type: namron_private_hvacThermostat.time_sync_value.type, + type: namronPrivateHvacThermostat.time_sync_value.type, }, }); } catch { /* Do nothing*/ } }, hours24); - globalStore.putValue(device, namron_private_hvacThermostat.time_sync_value.key, interval); + globalStore.putValue(device, namronPrivateHvacThermostat.time_sync_value.key, interval); } }, configure: async (device, coordinatorEndpoint, _logger) => { @@ -1976,11 +1977,11 @@ const definitions: DefinitionWithExtend[] = [ // Reads holiday await endpoint.read('hvacThermostat', [ - namron_private_hvacThermostat.holiday_temp_set.attrId, - namron_private_hvacThermostat.holiday_temp_set_f.attrId, - namron_private_hvacThermostat.vacation_mode.attrId, - namron_private_hvacThermostat.vacation_start_date.attrId, - namron_private_hvacThermostat.vacation_end_date.attrId, + namronPrivateHvacThermostat.holiday_temp_set.attrId, + namronPrivateHvacThermostat.holiday_temp_set_f.attrId, + namronPrivateHvacThermostat.vacation_mode.attrId, + namronPrivateHvacThermostat.vacation_start_date.attrId, + namronPrivateHvacThermostat.vacation_end_date.attrId, ]); device.powerSource = 'Mains (single phase)'; @@ -1992,33 +1993,35 @@ const definitions: DefinitionWithExtend[] = [ .climate() .withLocalTemperature() .withSetpoint('occupied_heating_setpoint', 15, 35, 0.5) - .withSystemMode(['off', 'auto', 'cool', 'heat'], ea.ALL), + .withSystemMode(['off', 'auto', 'cool', 'heat'], ea.ALL) + .withLocalTemperatureCalibration(-3, 3, 0.1) + .withRunningState(['idle', 'heat']), e.enum('temperature_display_mode', ea.ALL, ['celsius', 'fahrenheit']).withLabel('Temperature Unit').withDescription('Select Unit'), e.enum('current_operating_mode', ea.ALL, ['Manual', 'ECO']).withDescription('Selected program for thermostat'), e.binary('child_lock', ea.ALL, 'LOCK', 'UNLOCK').withDescription('Enables/disables physical input on the device'), e - .numeric(namron_private_hvacThermostat.active_backlight.key, ea.ALL) + .numeric(namronPrivateHvacThermostat.active_backlight.key, ea.ALL) .withDescription('Desired display brightness') .withUnit('%') .withValueMin(1) .withValueMax(100), e - .enum(namron_private_hvacThermostat.display_auto_off.key, ea.ALL, Object.values(fzNamronDisplayTimeout)) + .enum(namronPrivateHvacThermostat.display_auto_off.key, ea.ALL, Object.values(fzNamronDisplayTimeout)) .withDescription('Turn off the display after the give time in inactivity or never'), - e.binary(namron_private_hvacThermostat.window_check.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off window check mode'), + e.binary(namronPrivateHvacThermostat.window_check.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off window check mode'), e - .enum(namron_private_hvacThermostat.window_state.key, ea.STATE_GET, Object.values(fzNamronOpenClose)) + .enum(namronPrivateHvacThermostat.window_state.key, ea.STATE_GET, Object.values(fzNamronOpenClose)) .withDescription('Detected state of window'), - e.binary(namron_private_hvacThermostat.anti_frost_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off anti-frost mode'), - e.binary(namron_private_hvacThermostat.summer_winter_switch.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Summar Winter switch'), - e.binary(namron_private_hvacThermostat.auto_time.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Automatic time'), + e.binary(namronPrivateHvacThermostat.anti_frost_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off anti-frost mode'), + e.binary(namronPrivateHvacThermostat.summer_winter_switch.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Summar Winter switch'), + e.binary(namronPrivateHvacThermostat.auto_time.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Automatic time'), e - .enum(namron_private_hvacThermostat.countdown_set.key, ea.ALL, Object.values(fzNamronBoostTable)) + .enum(namronPrivateHvacThermostat.countdown_set.key, ea.ALL, Object.values(fzNamronBoostTable)) .withDescription('Starts boost with defined time'), - e.numeric(namron_private_hvacThermostat.countdown_left.key, ea.STATE_GET).withUnit('min').withDescription('Given boost time'), - e.binary(namron_private_hvacThermostat.vacation_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off vacation mode'), + e.numeric(namronPrivateHvacThermostat.countdown_left.key, ea.STATE_GET).withUnit('min').withDescription('Given boost time'), + e.binary(namronPrivateHvacThermostat.vacation_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off vacation mode'), e - .numeric(namron_private_hvacThermostat.holiday_temp_set.key, ea.ALL) + .numeric(namronPrivateHvacThermostat.holiday_temp_set.key, ea.ALL) .withValueMin(5) .withValueMax(35) .withValueStep(0.5) @@ -2026,23 +2029,23 @@ const definitions: DefinitionWithExtend[] = [ .withLabel('Vacation temperature') .withDescription('Vacation temperature setpoint'), e - .text(namron_private_hvacThermostat.vacation_start_date.key, ea.ALL) + .text(namronPrivateHvacThermostat.vacation_start_date.key, ea.ALL) .withDescription('Start date') .withDescription("Supports dates starting with day or year with '. - /'"), e - .text(namron_private_hvacThermostat.vacation_end_date.key, ea.ALL) + .text(namronPrivateHvacThermostat.vacation_end_date.key, ea.ALL) .withDescription('End date') .withDescription("Supports dates starting with day or year with '. - /'"), - e.binary(namron_private_hvacThermostat.time_sync_flag.key, ea.ALL, 'On', 'Off'), - e.numeric(namron_private_hvacThermostat.time_sync_value.key, ea.STATE_GET), + e.binary(namronPrivateHvacThermostat.time_sync_flag.key, ea.ALL, 'On', 'Off'), + e.numeric(namronPrivateHvacThermostat.time_sync_value.key, ea.STATE_GET), e - .enum(namron_private_hvacThermostat.fault.key, ea.STATE_GET, Object.values(fzNamronFault)) + .enum(namronPrivateHvacThermostat.fault.key, ea.STATE_GET, Object.values(fzNamronFault)) .withDescription('Shows current error of the device'), e - .enum(namron_private_hvacThermostat.work_days.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) + .enum(namronPrivateHvacThermostat.work_days.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) .withDescription("Needs to be changed under 'Thermostat settings' > 'Advanced settings' > 'Schedule type'"), e - .numeric(namron_private_hvacThermostat.regulator_percentage.key, ea.ALL) + .numeric(namronPrivateHvacThermostat.regulator_percentage.key, ea.ALL) .withUnit('%') .withValueMin(10) .withValueMax(100) From c47183076241768aabc915591d207dcbd5107482 Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 20 Jan 2025 23:39:26 +0100 Subject: [PATCH 12/20] Updated formatting --- src/devices/namron.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index 162fb946eaf20..f80bb04aa830f 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -1921,7 +1921,7 @@ const definitions: DefinitionWithExtend[] = [ tz.thermostat_programming_operation_mode, tz.thermostat_temperature_display_mode, tz.thermostat_running_state, - tz.thermostat_running_mode + tz.thermostat_running_mode, ], onEvent: async (type, data, device, options) => { if (type === 'stop') { @@ -2044,12 +2044,7 @@ const definitions: DefinitionWithExtend[] = [ e .enum(namronPrivateHvacThermostat.work_days.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) .withDescription("Needs to be changed under 'Thermostat settings' > 'Advanced settings' > 'Schedule type'"), - e - .numeric(namronPrivateHvacThermostat.regulator_percentage.key, ea.ALL) - .withUnit('%') - .withValueMin(10) - .withValueMax(100) - .withValueStep(1), + e.numeric(namronPrivateHvacThermostat.regulator_percentage.key, ea.ALL).withUnit('%').withValueMin(10).withValueMax(100).withValueStep(1), ], }, ]; From bcc908d9e0ad9606525270e2fb37d5d84426b294 Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 20 Jan 2025 23:43:21 +0100 Subject: [PATCH 13/20] camelCase.. --- src/devices/namron.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index f80bb04aa830f..e0efd689f0cc3 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -2000,28 +2000,28 @@ const definitions: DefinitionWithExtend[] = [ e.enum('current_operating_mode', ea.ALL, ['Manual', 'ECO']).withDescription('Selected program for thermostat'), e.binary('child_lock', ea.ALL, 'LOCK', 'UNLOCK').withDescription('Enables/disables physical input on the device'), e - .numeric(namronPrivateHvacThermostat.active_backlight.key, ea.ALL) + .numeric(namronPrivateHvacThermostat.activeBacklight.key, ea.ALL) .withDescription('Desired display brightness') .withUnit('%') .withValueMin(1) .withValueMax(100), e - .enum(namronPrivateHvacThermostat.display_auto_off.key, ea.ALL, Object.values(fzNamronDisplayTimeout)) + .enum(namronPrivateHvacThermostat.displayAutoOff.key, ea.ALL, Object.values(fzNamronDisplayTimeout)) .withDescription('Turn off the display after the give time in inactivity or never'), - e.binary(namronPrivateHvacThermostat.window_check.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off window check mode'), + e.binary(namronPrivateHvacThermostat.windowCheck.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off window check mode'), e - .enum(namronPrivateHvacThermostat.window_state.key, ea.STATE_GET, Object.values(fzNamronOpenClose)) + .enum(namronPrivateHvacThermostat.windowState.key, ea.STATE_GET, Object.values(fzNamronOpenClose)) .withDescription('Detected state of window'), - e.binary(namronPrivateHvacThermostat.anti_frost_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off anti-frost mode'), - e.binary(namronPrivateHvacThermostat.summer_winter_switch.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Summar Winter switch'), - e.binary(namronPrivateHvacThermostat.auto_time.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Automatic time'), + e.binary(namronPrivateHvacThermostat.antiFrostMode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off anti-frost mode'), + e.binary(namronPrivateHvacThermostat.summerWinterSwitch.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Summar Winter switch'), + e.binary(namronPrivateHvacThermostat.autoTime.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Automatic time'), e - .enum(namronPrivateHvacThermostat.countdown_set.key, ea.ALL, Object.values(fzNamronBoostTable)) + .enum(namronPrivateHvacThermostat.countdownSet.key, ea.ALL, Object.values(fzNamronBoostTable)) .withDescription('Starts boost with defined time'), - e.numeric(namronPrivateHvacThermostat.countdown_left.key, ea.STATE_GET).withUnit('min').withDescription('Given boost time'), - e.binary(namronPrivateHvacThermostat.vacation_mode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off vacation mode'), + e.numeric(namronPrivateHvacThermostat.countdownLeft.key, ea.STATE_GET).withUnit('min').withDescription('Given boost time'), + e.binary(namronPrivateHvacThermostat.vacationMode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off vacation mode'), e - .numeric(namronPrivateHvacThermostat.holiday_temp_set.key, ea.ALL) + .numeric(namronPrivateHvacThermostat.holidayTempSet.key, ea.ALL) .withValueMin(5) .withValueMax(35) .withValueStep(0.5) @@ -2029,22 +2029,22 @@ const definitions: DefinitionWithExtend[] = [ .withLabel('Vacation temperature') .withDescription('Vacation temperature setpoint'), e - .text(namronPrivateHvacThermostat.vacation_start_date.key, ea.ALL) + .text(namronPrivateHvacThermostat.vacationStartDate.key, ea.ALL) .withDescription('Start date') .withDescription("Supports dates starting with day or year with '. - /'"), e - .text(namronPrivateHvacThermostat.vacation_end_date.key, ea.ALL) + .text(namronPrivateHvacThermostat.vacationEndDate.key, ea.ALL) .withDescription('End date') .withDescription("Supports dates starting with day or year with '. - /'"), - e.binary(namronPrivateHvacThermostat.time_sync_flag.key, ea.ALL, 'On', 'Off'), - e.numeric(namronPrivateHvacThermostat.time_sync_value.key, ea.STATE_GET), + e.binary(namronPrivateHvacThermostat.timeSyncFlag.key, ea.ALL, 'On', 'Off'), + e.numeric(namronPrivateHvacThermostat.timeSyncValue.key, ea.STATE_GET), e .enum(namronPrivateHvacThermostat.fault.key, ea.STATE_GET, Object.values(fzNamronFault)) .withDescription('Shows current error of the device'), e - .enum(namronPrivateHvacThermostat.work_days.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) + .enum(namronPrivateHvacThermostat.workDays.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) .withDescription("Needs to be changed under 'Thermostat settings' > 'Advanced settings' > 'Schedule type'"), - e.numeric(namronPrivateHvacThermostat.regulator_percentage.key, ea.ALL).withUnit('%').withValueMin(10).withValueMax(100).withValueStep(1), + e.numeric(namronPrivateHvacThermostat.regulatorPercentage.key, ea.ALL).withUnit('%').withValueMin(10).withValueMax(100).withValueStep(1), ], }, ]; From 47614b428dc74dcfd4879f8b55f94927ac064ed2 Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 20 Jan 2025 23:47:22 +0100 Subject: [PATCH 14/20] camelCase... --- src/devices/namron.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index e0efd689f0cc3..825ae88251739 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -101,7 +101,7 @@ const tzNamronBoostTable = fzNamronBoostTable.fromFzToTz(); const fzNamronSystemMode = {0x00: 'off', 0x01: 'auto', 0x03: 'cool', 0x04: 'heat'}; const tzNamronSystemMode = fzNamronSystemMode.fromFzToTz(); -const fzNamronOnOff = {0: 'Off', 1: 'On'}; +const fzNamronOnOff = {0: 'off', 1: 'on'}; const tzNamronOnOff = fzNamronOnOff.fromFzToTz(); const fzNamronOpenClose = {0: 'closed', 1: 'open'}; From 8986d8dbdb68ea92df5ed94ed22bd73c974f7203 Mon Sep 17 00:00:00 2001 From: bskjon Date: Tue, 21 Jan 2025 00:01:00 +0100 Subject: [PATCH 15/20] mapping --- src/devices/namron.ts | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index 825ae88251739..7a31dcf5c996d 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -59,14 +59,9 @@ const namronPrivateHvacThermostat: NamronPrivateTable = { currentOperatingMode: {attrId: 'programingOperMode', type: Zcl.DataType.BITMAP8, key: 'current_operating_mode'}, }; -declare global { - interface Object { - fromFzToTz(): KeyValue; - } -} -Object.prototype.fromFzToTz = function (this: KeyValue) { - return Object.fromEntries(Object.entries(this).map(([k, v]) => [v, k])); +function fromFzToTz(obj: KeyValue) { + return Object.fromEntries(Object.entries(obj).map(([k, v]) => [v, k])); }; const fzNamronBoostTable = { @@ -96,25 +91,25 @@ const fzNamronBoostTable = { 23: '1h_55_min', 24: '2h', }; -const tzNamronBoostTable = fzNamronBoostTable.fromFzToTz(); +const tzNamronBoostTable = fromFzToTz(fzNamronBoostTable); const fzNamronSystemMode = {0x00: 'off', 0x01: 'auto', 0x03: 'cool', 0x04: 'heat'}; -const tzNamronSystemMode = fzNamronSystemMode.fromFzToTz(); +const tzNamronSystemMode = fromFzToTz(fzNamronSystemMode); const fzNamronOnOff = {0: 'off', 1: 'on'}; -const tzNamronOnOff = fzNamronOnOff.fromFzToTz(); +const tzNamronOnOff = fromFzToTz(fzNamronOnOff); const fzNamronOpenClose = {0: 'closed', 1: 'open'}; -const tzNamronOpenClose = fzNamronOpenClose.fromFzToTz(); +const tzNamronOpenClose = fromFzToTz(fzNamronOpenClose); const fzNamronDisplayTimeout = {0: 'off', 1: '10s', 2: '30s', 3: '60s'}; -const tzNamronDisplayTimeout = fzNamronDisplayTimeout.fromFzToTz(); +const tzNamronDisplayTimeout = fromFzToTz(fzNamronDisplayTimeout); const fzNamronSensorMode = {0: 'air', 1: 'floor', 3: 'external', 6: 'regulator'}; -const tzNamronSensorMode = fzNamronSensorMode.fromFzToTz(); +const tzNamronSensorMode = fromFzToTz(fzNamronSensorMode); const fzNamronOperationMode = {0: 'manual', 1: 'manual', 5: 'eco'}; -const tzNamronOperationMode = fzNamronOperationMode.fromFzToTz(); +const tzNamronOperationMode = fromFzToTz(fzNamronOperationMode); const fzNamronFault = { 0: 'no_fault', @@ -126,7 +121,7 @@ const fzNamronFault = { }; const fzNamronWorkDays = {0: 'mon-fri_sat-sun', 1: 'mon-sat_sun', 2: 'no_time_off', 3: 'time_off'}; -const tzNamronWorkDays = fzNamronWorkDays.fromFzToTz(); +const tzNamronWorkDays = fromFzToTz(fzNamronWorkDays); const findAttributeByKey = (key: string, attributes: NamronPrivateTable) => { // Finn objektet basert på key From c9e2e73c0ef51ce23b94771591ec16162e162131 Mon Sep 17 00:00:00 2001 From: bskjon Date: Tue, 21 Jan 2025 00:36:02 +0100 Subject: [PATCH 16/20] lint --- src/devices/namron.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index 7a31dcf5c996d..862b3e67a844a 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -59,10 +59,9 @@ const namronPrivateHvacThermostat: NamronPrivateTable = { currentOperatingMode: {attrId: 'programingOperMode', type: Zcl.DataType.BITMAP8, key: 'current_operating_mode'}, }; - function fromFzToTz(obj: KeyValue) { return Object.fromEntries(Object.entries(obj).map(([k, v]) => [v, k])); -}; +} const fzNamronBoostTable = { 0: 'off', @@ -1915,6 +1914,7 @@ const definitions: DefinitionWithExtend[] = [ tz.thermostat_control_sequence_of_operation, tz.thermostat_programming_operation_mode, tz.thermostat_temperature_display_mode, + tz.thermostat_local_temperature_calibration, tz.thermostat_running_state, tz.thermostat_running_mode, ], From 250eaceb63abbc2ad346403e88bff63b9cdd8fb0 Mon Sep 17 00:00:00 2001 From: bskjon Date: Wed, 22 Jan 2025 23:38:31 +0100 Subject: [PATCH 17/20] Using modern extend --- src/devices/namron.ts | 476 ++++-------------------------------------- src/lib/namron.ts | 431 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 473 insertions(+), 434 deletions(-) create mode 100644 src/lib/namron.ts diff --git a/src/devices/namron.ts b/src/devices/namron.ts index 862b3e67a844a..be8eceb4b88e9 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -5,6 +5,7 @@ import tz from '../converters/toZigbee'; import * as constants from '../lib/constants'; import * as exposes from '../lib/exposes'; import * as m from '../lib/modernExtend'; +import * as namron from '../lib/namron'; import * as reporting from '../lib/reporting'; import * as globalStore from '../lib/store'; import * as tuya from '../lib/tuya'; @@ -16,180 +17,6 @@ const e = exposes.presets; const sunricherManufacturer = {manufacturerCode: Zcl.ManufacturerCode.SHENZHEN_SUNRICHER_TECHNOLOGY_LTD}; -interface NamronPrivateAttribute { - attrId: number | string; - type: Zcl.DataType; - key: string; -} - -interface NamronPrivateTable { - [key: string]: NamronPrivateAttribute; -} - -const namronPrivateHvacThermostat: NamronPrivateTable = { - windowCheck: {attrId: 0x8000, type: Zcl.DataType.BOOLEAN, key: 'window_open_check'}, - antiFrostMode: {attrId: 0x8001, type: Zcl.DataType.BOOLEAN, key: 'anti_frost'}, - windowState: {attrId: 0x8002, type: Zcl.DataType.BOOLEAN, key: 'window_open'}, - workDays: {attrId: 0x8003, type: Zcl.DataType.ENUM8, key: 'work_days'}, - sensorMode: {attrId: 0x8004, type: Zcl.DataType.ENUM8, key: 'sensor_mode'}, - activeBacklight: {attrId: 0x8005, type: Zcl.DataType.UINT8, key: 'active_display_brightness'}, - fault: {attrId: 0x8006, type: Zcl.DataType.ENUM8, key: 'fault'}, - regulator: {attrId: 0x8007, type: Zcl.DataType.UINT8, key: 'regulator'}, - timeSyncFlag: {attrId: 0x800a, type: Zcl.DataType.BOOLEAN, key: 'time_sync'}, - timeSyncValue: {attrId: 0x800b, type: Zcl.DataType.UINT32, key: 'time_sync_value'}, - absMinHeatSetpointLimitF: {attrId: 0x800c, type: Zcl.DataType.INT16, key: 'abs_min_heat_setpoint_limit_f'}, - absMaxHeatSetpointLimitF: {attrId: 0x800d, type: Zcl.DataType.INT16, key: 'abs_max_heat_setpoint_limit_f'}, - absMinCoolSetpointLimitF: {attrId: 0x800e, type: Zcl.DataType.INT16, key: 'abs_min_cool_setpoint_limit_f'}, - absMaxCoolSetpointLimitF: {attrId: 0x800f, type: Zcl.DataType.INT16, key: 'abs_max_cool_setpoint_limit_f'}, - occupiedCoolingSetpointF: {attrId: 0x8010, type: Zcl.DataType.INT16, key: 'occupied_cooling_setpoint_f'}, - occupiedHeatingSetpointF: {attrId: 0x8011, type: Zcl.DataType.INT16, key: 'occupied_heating_setpoint_f'}, - localTemperatureF: {attrId: 0x8012, type: Zcl.DataType.INT16, key: 'local_temperature_f'}, - holidayTempSet: {attrId: 0x8013, type: Zcl.DataType.INT16, key: 'holiday_temp_set'}, - holidayTempSetF: {attrId: 0x801b, type: Zcl.DataType.INT16, key: 'holiday_temp_set_f'}, - regulationMode: {attrId: 0x801c, type: Zcl.DataType.INT16, key: 'regulation_mode'}, - regulatorPercentage: {attrId: 0x801d, type: Zcl.DataType.INT16, key: 'regulator_percentage'}, - summerWinterSwitch: {attrId: 0x801e, type: Zcl.DataType.BOOLEAN, key: 'summer_winter_switch'}, - vacationMode: {attrId: 0x801f, type: Zcl.DataType.BOOLEAN, key: 'vacation_mode'}, - vacationStartDate: {attrId: 0x8020, type: Zcl.DataType.UINT32, key: 'vacation_start_date'}, - vacationEndDate: {attrId: 0x8021, type: Zcl.DataType.UINT32, key: 'vacation_end_date'}, - autoTime: {attrId: 0x8022, type: Zcl.DataType.BOOLEAN, key: 'auto_time'}, - countdownSet: {attrId: 0x8023, type: Zcl.DataType.ENUM8, key: 'boost_time_set'}, - countdownLeft: {attrId: 0x8024, type: Zcl.DataType.INT16, key: 'boost_time_left'}, - displayAutoOff: {attrId: 0x8029, type: Zcl.DataType.ENUM8, key: 'display_auto_off'}, - currentOperatingMode: {attrId: 'programingOperMode', type: Zcl.DataType.BITMAP8, key: 'current_operating_mode'}, -}; - -function fromFzToTz(obj: KeyValue) { - return Object.fromEntries(Object.entries(obj).map(([k, v]) => [v, k])); -} - -const fzNamronBoostTable = { - 0: 'off', - 1: '5_min', - 2: '10_min', - 3: '15_min', - 4: '20_min', - 5: '25_min', - 6: '30_min', - 7: '35_min', - 8: '40_min', - 9: '45_min', - 10: '50_min', - 11: '55_min', - 12: '1h', - 13: '1h_5_min', - 14: '1h_10_min', - 15: '1h_15_min', - 16: '1h_20_min', - 17: '1h_25_min', - 18: '1h_30_min', - 19: '1h_35_min', - 20: '1h_40_min', - 21: '1h_45_min', - 22: '1h_50_min', - 23: '1h_55_min', - 24: '2h', -}; -const tzNamronBoostTable = fromFzToTz(fzNamronBoostTable); - -const fzNamronSystemMode = {0x00: 'off', 0x01: 'auto', 0x03: 'cool', 0x04: 'heat'}; -const tzNamronSystemMode = fromFzToTz(fzNamronSystemMode); - -const fzNamronOnOff = {0: 'off', 1: 'on'}; -const tzNamronOnOff = fromFzToTz(fzNamronOnOff); - -const fzNamronOpenClose = {0: 'closed', 1: 'open'}; -const tzNamronOpenClose = fromFzToTz(fzNamronOpenClose); - -const fzNamronDisplayTimeout = {0: 'off', 1: '10s', 2: '30s', 3: '60s'}; -const tzNamronDisplayTimeout = fromFzToTz(fzNamronDisplayTimeout); - -const fzNamronSensorMode = {0: 'air', 1: 'floor', 3: 'external', 6: 'regulator'}; -const tzNamronSensorMode = fromFzToTz(fzNamronSensorMode); - -const fzNamronOperationMode = {0: 'manual', 1: 'manual', 5: 'eco'}; -const tzNamronOperationMode = fromFzToTz(fzNamronOperationMode); - -const fzNamronFault = { - 0: 'no_fault', - 1: 'over_current_error', - 2: 'over_heat_error', - 3: 'built-in_sensor_error', - 4: 'air_sensor_error', - 5: 'floor_sensor_error', -}; - -const fzNamronWorkDays = {0: 'mon-fri_sat-sun', 1: 'mon-sat_sun', 2: 'no_time_off', 3: 'time_off'}; -const tzNamronWorkDays = fromFzToTz(fzNamronWorkDays); - -const findAttributeByKey = (key: string, attributes: NamronPrivateTable) => { - // Finn objektet basert på key - return Object.values(attributes).find((attr) => attr.key === key); -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const assign = (data: any, attribute: NamronPrivateAttribute, target: KeyValue, defaultValue: any = null, transform = (value: any) => value) => { - // Ekstrakt `attrId` og `key` direkte fra attributtobjektet - const {attrId, key: targetKey} = attribute; - - if (data[attrId] !== undefined) { - target[targetKey] = transform(data[attrId]); - } else if (data[attrId] && defaultValue !== null && defaultValue !== undefined) { - target[targetKey] = transform(defaultValue); - } -}; - -const assignWithLookup = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any, - attribute: NamronPrivateAttribute, - target: KeyValue, - lookup = {}, - defaultValue: string | number | null = null, -) => { - // Ekstrakt `attrId` og `key` direkte fra attributtobjektet - const {attrId, key: targetKey} = attribute; - - if (data[attrId] !== undefined) { - const value = data[attrId] ?? defaultValue; - target[targetKey] = utils.getFromLookup(value, lookup); - } -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const assignDate = (data: any, attribute: NamronPrivateAttribute, target: KeyValue) => { - // Ekstrakt `attrId` og `key` direkte fra attributtobjektet - const {attrId, key: targetKey} = attribute; - const value = data[attrId]; - if (value === undefined) { - return; - } - const date = new Date(value * 86400000); - const day = String(date.getDate()).padStart(2, '0'); - const month = String(date.getMonth() + 1).padStart(2, '0'); // Månedene er 0-indeksert - const year = date.getFullYear(); - target[targetKey] = `${year}.${month}.${day}`; -}; - -const fromDate = (value: string) => { - // Ekstrakt `attrId` og `key` direkte fra attributtobjektet - const dateParts = value.split(/[.\-/]/); - if (dateParts.length !== 3) { - throw new Error('Invalid date format'); - } - - let date: Date; - if (dateParts[0].length === 4) { - date = new Date(`${dateParts[0]}-${dateParts[1]}-${dateParts[2]}`); - } else if (dateParts[2].length === 4) { - date = new Date(`${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`); - } else { - throw new Error('Invalid date format'); - } - - return date.getTime() / 86400000 + 1; -}; - const fzLocal = { namron_panelheater: { cluster: 'hvacThermostat', @@ -237,58 +64,6 @@ const fzLocal = { return fz.thermostat.convert(model, msg, publish, options, meta); // as KeyValue; }, } satisfies Fz.Converter, - namron_edge_thermostat: { - cluster: 'hvacThermostat', - type: ['attributeReport', 'readResponse'], - convert: (model, msg, publish, options, meta) => { - const result = {}; - const data = msg.data; - - assignWithLookup(data, namronPrivateHvacThermostat.work_days, result, fzNamronWorkDays, 0); - assignWithLookup(data, namronPrivateHvacThermostat.sensor_mode, result, fzNamronSensorMode, 0); - assignWithLookup(data, namronPrivateHvacThermostat.fault, result, fzNamronFault, 0); - - assignWithLookup(data, namronPrivateHvacThermostat.window_check, result, fzNamronOnOff, 0); - assignWithLookup(data, namronPrivateHvacThermostat.anti_frost_mode, result, fzNamronOnOff, 0); - assignWithLookup(data, namronPrivateHvacThermostat.window_state, result, fzNamronOpenClose, 0); - assignWithLookup(data, namronPrivateHvacThermostat.summer_winter_switch, result, fzNamronOnOff, 0); - assignWithLookup(data, namronPrivateHvacThermostat.vacation_mode, result, fzNamronOnOff, 0); - assignWithLookup(data, namronPrivateHvacThermostat.time_sync_flag, result, fzNamronOnOff, 0); - - assign(data, namronPrivateHvacThermostat.active_backlight, result); - assign(data, namronPrivateHvacThermostat.time_sync_value, result); - assign(data, namronPrivateHvacThermostat.abs_min_heat_setpoint_limit_f, result); - assign(data, namronPrivateHvacThermostat.abs_max_heat_setpoint_limit_f, result); - assign(data, namronPrivateHvacThermostat.abs_min_cool_setpoint_limit_f, result); - assign(data, namronPrivateHvacThermostat.abs_max_cool_setpoint_limit_f, result); - assign(data, namronPrivateHvacThermostat.occupied_cooling_setpoint_f, result); - assign(data, namronPrivateHvacThermostat.occupied_heating_setpoint_f, result); - assign(data, namronPrivateHvacThermostat.local_temperature_f, result); - assign(data, namronPrivateHvacThermostat.holiday_temp_set, result, (value: number) => { - return value / 100; - }); - assign(data, namronPrivateHvacThermostat.holiday_temp_set_f, result, (value: number) => { - return value / 100; - }); - - assign(data, namronPrivateHvacThermostat.regulator_percentage, result, 0, (value) => { - return value; - }); - - assignDate(data, namronPrivateHvacThermostat.vacation_start_date, result); - assignDate(data, namronPrivateHvacThermostat.vacation_end_date, result); - - // Auto_time (synkroniser tid med ntp?) - assignWithLookup(data, namronPrivateHvacThermostat.auto_time, result, fzNamronOnOff, 0); - assignWithLookup(data, namronPrivateHvacThermostat.countdown_set, result, fzNamronBoostTable, 0); - assign(data, namronPrivateHvacThermostat.countdown_left, result, 0, (value) => (value > 200 ? 0 : value)); - assignWithLookup(data, namronPrivateHvacThermostat.display_auto_off, result, fzNamronDisplayTimeout, 0); - assignWithLookup(data, namronPrivateHvacThermostat.system_mode, result, fzNamronSystemMode, 0x00); - assignWithLookup(data, namronPrivateHvacThermostat.current_operating_mode, result, fzNamronOperationMode, 0); - - return result; - }, - } satisfies Fz.Converter, }; const tzLocal = { @@ -338,160 +113,6 @@ const tzLocal = { } }, } satisfies Tz.Converter, - namron_edge_thermostat: { - key: [ - 'window_open_check', - 'anti_frost', - 'window_open', - 'work_days', - 'sensor_mode', - 'active_display_brightness', - 'fault', - 'regulator', - 'time_sync', - 'time_sync_value', - 'abs_min_heat_setpoint_limit_f', - 'abs_max_heat_setpoint_limit_f', - 'abs_min_cool_setpoint_limit_f', - 'abs_max_cool_setpoint_limit_f', - 'occupied_cooling_setpoint_f', - 'occupied_heating_setpoint_f', - 'local_temperature_f', - 'holiday_temp_set', - 'holiday_temp_set_f', - 'regulation_mode', - 'regulator_percentage', - 'summer_winter_switch', - 'vacation_mode', - 'vacation_start_date', - 'vacation_end_date', - 'auto_time', - 'boost_time_set', - 'boost_time_left', - 'display_auto_off', - 'system_mode', - 'current_operating_mode', - ], - - convertGet: async (entity, key, meta) => { - const readAttr = findAttributeByKey(key, namronPrivateHvacThermostat); - if (readAttr) { - await entity.read('hvacThermostat', [readAttr.attrId]); - } else { - throw new Error(`Unhandled key toZigbee.namronEdgeThermostat.convertGet ${key}`); - } - }, - - convertSet: async (entity, key, value, meta) => { - const readAttr = findAttributeByKey(key, namronPrivateHvacThermostat); - - if (!readAttr) { - throw new Error(`Unhandled key toZigbee.namronEdgeThermostat.convertSet ${key}`); - } - - if ( - [ - namronPrivateHvacThermostat.window_check.key, - namronPrivateHvacThermostat.anti_frost_mode.key, - namronPrivateHvacThermostat.time_sync_flag.key, - namronPrivateHvacThermostat.summer_winter_switch.key, - namronPrivateHvacThermostat.auto_time.key, - namronPrivateHvacThermostat.vacation_mode.key, - ].includes(readAttr.key) - ) { - const payload = {[readAttr.attrId]: {value: utils.getFromLookup(value, tzNamronOnOff), type: readAttr.type}}; - await entity.write('hvacThermostat', payload); - return; - } - - // Direct call - if ([Zcl.DataType.UINT8, Zcl.DataType.INT16, Zcl.DataType.UINT32].includes(readAttr.type)) { - const payload = {[readAttr.attrId]: {value: value, type: readAttr.type}}; - await entity.write('hvacThermostat', payload); - } - - if (readAttr === namronPrivateHvacThermostat.countdown_set) { - await entity.write('hvacThermostat', { - [readAttr.attrId]: { - value: utils.getFromLookup(value, tzNamronBoostTable), - type: readAttr.type, - }, - }); - } - - if (readAttr === namronPrivateHvacThermostat.window_state) { - await entity.write('hvacThermostat', { - [readAttr.attrId]: { - value: utils.getFromLookup(value, tzNamronOpenClose), - type: readAttr.type, - }, - }); - } - - if (readAttr === namronPrivateHvacThermostat.display_auto_off) { - await entity.write('hvacThermostat', { - [readAttr.attrId]: { - value: utils.getFromLookup(value, tzNamronDisplayTimeout), - type: readAttr.type, - }, - }); - } - - if (readAttr === namronPrivateHvacThermostat.sensor_mode) { - await entity.write('hvacThermostat', { - [readAttr.attrId]: { - value: utils.getFromLookup(value, tzNamronSensorMode), - type: readAttr.type, - }, - }); - } - - if (readAttr === namronPrivateHvacThermostat.system_mode) { - await entity.write('hvacThermostat', { - [readAttr.attrId]: { - value: utils.getFromLookup(value, tzNamronSystemMode), - type: readAttr.type, - }, - }); - } - - if (readAttr === namronPrivateHvacThermostat.current_operating_mode) { - await entity.write('hvacThermostat', { - [readAttr.attrId]: { - value: utils.getFromLookup(value, tzNamronOperationMode), - type: readAttr.type, - }, - }); - } - - if (readAttr === namronPrivateHvacThermostat.holiday_temp_set) { - await entity.write('hvacThermostat', { - [readAttr.attrId]: { - value: Number(value) * 100, - type: readAttr.type, - }, - }); - } - - if ([namronPrivateHvacThermostat.vacation_start_date.key, namronPrivateHvacThermostat.vacation_end_date.key].includes(readAttr.key)) { - await entity.write('hvacThermostat', { - [readAttr.attrId]: { - value: fromDate(String(value)), - type: readAttr.type, - }, - }); - } - - if (readAttr === namronPrivateHvacThermostat.work_days) { - await entity.write('hvacThermostat', { - [readAttr.attrId]: { - value: utils.getFromLookup(value, tzNamronWorkDays), - type: readAttr.type, - }, - }); - } - }, - } satisfies Tz.Converter, }; const definitions: DefinitionWithExtend[] = [ @@ -1904,10 +1525,16 @@ const definitions: DefinitionWithExtend[] = [ model: 'Edge Thermostat', vendor: 'Namron', description: 'Namron Zigbee Edge Termostat', - fromZigbee: [fzLocal.namron_edge_thermostat, fz.thermostat, fz.namron_hvac_user_interface, fz.metering, fz.electrical_measurement], + fromZigbee: [ + fz.thermostat, + namron.fromZigbee.namron_edge_thermostat_holiday_temp, + namron.fromZigbee.namron_edge_thermostat_vacation_date, + fz.namron_hvac_user_interface, + fz.metering, + fz.electrical_measurement, + ], toZigbee: [ tz.thermostat_local_temperature, - tzLocal.namron_edge_thermostat, tz.thermostat_occupied_heating_setpoint, tz.thermostat_unoccupied_heating_setpoint, tz.namron_thermostat_child_lock, @@ -1917,18 +1544,20 @@ const definitions: DefinitionWithExtend[] = [ tz.thermostat_local_temperature_calibration, tz.thermostat_running_state, tz.thermostat_running_mode, + namron.toZigbee.namron_edge_thermostat_holiday_temp, + namron.toZigbee.namron_edge_thermostat_vacation_date, ], onEvent: async (type, data, device, options) => { if (type === 'stop') { try { - const key = namronPrivateHvacThermostat.time_sync_value.key; + const key = 'time_sync_value'; clearInterval(globalStore.getValue(device, key)); globalStore.clearValue(device, key); } catch { /* Do nothing*/ } } - if (!globalStore.hasValue(device, namronPrivateHvacThermostat.time_sync_value.key)) { + if (!globalStore.hasValue(device, 'time_sync_value')) { const hours24 = 1000 * 60 * 60 * 24; const interval = setInterval(async () => { try { @@ -1936,16 +1565,16 @@ const definitions: DefinitionWithExtend[] = [ // Device does not asks for the time with binding, therefore we write the time every 24 hours const time = new Date().getTime() / 1000; await endpoint.write('hvacThermostat', { - [namronPrivateHvacThermostat.time_sync_value.attrId]: { + [0x800b]: { value: time, - type: namronPrivateHvacThermostat.time_sync_value.type, + type: Zcl.DataType.UINT32, }, }); } catch { /* Do nothing*/ } }, hours24); - globalStore.putValue(device, namronPrivateHvacThermostat.time_sync_value.key, interval); + globalStore.putValue(device, 'time_sync_value', interval); } }, configure: async (device, coordinatorEndpoint, _logger) => { @@ -1970,19 +1599,31 @@ const definitions: DefinitionWithExtend[] = [ // Initial read await endpoint.read('hvacThermostat', [0x8000, 0x8001, 0x8002, 0x801e, 0x8004, 0x8006, 0x8005, 0x8029, 0x8022, 0x8023, 0x8024]); - // Reads holiday - await endpoint.read('hvacThermostat', [ - namronPrivateHvacThermostat.holiday_temp_set.attrId, - namronPrivateHvacThermostat.holiday_temp_set_f.attrId, - namronPrivateHvacThermostat.vacation_mode.attrId, - namronPrivateHvacThermostat.vacation_start_date.attrId, - namronPrivateHvacThermostat.vacation_end_date.attrId, - ]); - device.powerSource = 'Mains (single phase)'; device.save(); }, - extend: [m.electricityMeter({voltage: false}), m.onOff({powerOnBehavior: false})], + extend: [ + m.electricityMeter({voltage: false}), + m.onOff({powerOnBehavior: false}), + namron.edgeThermostat.windowOpenDetection(), + namron.edgeThermostat.antiFrost(), + namron.edgeThermostat.summerWinterSwitch(), + namron.edgeThermostat.vacationMode(), + namron.edgeThermostat.timeSync(), + namron.edgeThermostat.autoTime(), + namron.edgeThermostat.displayActiveBacklight(), + namron.edgeThermostat.displayAutoOff(), + namron.edgeThermostat.regulatorPercentage(), + namron.edgeThermostat.regulationMode(), + namron.edgeThermostat.sensorMode(), + namron.edgeThermostat.boostTime(), + namron.edgeThermostat.readOnly.boostTimeRemaining(), + namron.edgeThermostat.systemMode(), + namron.edgeThermostat.deviceTime(), + namron.edgeThermostat.readOnly.windowState(), + namron.edgeThermostat.readOnly.deviceFault(), + namron.edgeThermostat.readOnly.workDays(), + ], exposes: [ e .climate() @@ -1992,31 +1633,10 @@ const definitions: DefinitionWithExtend[] = [ .withLocalTemperatureCalibration(-3, 3, 0.1) .withRunningState(['idle', 'heat']), e.enum('temperature_display_mode', ea.ALL, ['celsius', 'fahrenheit']).withLabel('Temperature Unit').withDescription('Select Unit'), - e.enum('current_operating_mode', ea.ALL, ['Manual', 'ECO']).withDescription('Selected program for thermostat'), + e.enum('operating_mode', ea.ALL, ['Manual', 'ECO']).withDescription('Selected program for thermostat'), e.binary('child_lock', ea.ALL, 'LOCK', 'UNLOCK').withDescription('Enables/disables physical input on the device'), e - .numeric(namronPrivateHvacThermostat.activeBacklight.key, ea.ALL) - .withDescription('Desired display brightness') - .withUnit('%') - .withValueMin(1) - .withValueMax(100), - e - .enum(namronPrivateHvacThermostat.displayAutoOff.key, ea.ALL, Object.values(fzNamronDisplayTimeout)) - .withDescription('Turn off the display after the give time in inactivity or never'), - e.binary(namronPrivateHvacThermostat.windowCheck.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off window check mode'), - e - .enum(namronPrivateHvacThermostat.windowState.key, ea.STATE_GET, Object.values(fzNamronOpenClose)) - .withDescription('Detected state of window'), - e.binary(namronPrivateHvacThermostat.antiFrostMode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off anti-frost mode'), - e.binary(namronPrivateHvacThermostat.summerWinterSwitch.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Summar Winter switch'), - e.binary(namronPrivateHvacThermostat.autoTime.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off Automatic time'), - e - .enum(namronPrivateHvacThermostat.countdownSet.key, ea.ALL, Object.values(fzNamronBoostTable)) - .withDescription('Starts boost with defined time'), - e.numeric(namronPrivateHvacThermostat.countdownLeft.key, ea.STATE_GET).withUnit('min').withDescription('Given boost time'), - e.binary(namronPrivateHvacThermostat.vacationMode.key, ea.ALL, 'On', 'Off').withDescription('Turn on/off vacation mode'), - e - .numeric(namronPrivateHvacThermostat.holidayTempSet.key, ea.ALL) + .numeric('holiday_temp_set', ea.ALL) .withValueMin(5) .withValueMax(35) .withValueStep(0.5) @@ -2024,22 +1644,10 @@ const definitions: DefinitionWithExtend[] = [ .withLabel('Vacation temperature') .withDescription('Vacation temperature setpoint'), e - .text(namronPrivateHvacThermostat.vacationStartDate.key, ea.ALL) + .text('vacation_start_date', ea.ALL) .withDescription('Start date') .withDescription("Supports dates starting with day or year with '. - /'"), - e - .text(namronPrivateHvacThermostat.vacationEndDate.key, ea.ALL) - .withDescription('End date') - .withDescription("Supports dates starting with day or year with '. - /'"), - e.binary(namronPrivateHvacThermostat.timeSyncFlag.key, ea.ALL, 'On', 'Off'), - e.numeric(namronPrivateHvacThermostat.timeSyncValue.key, ea.STATE_GET), - e - .enum(namronPrivateHvacThermostat.fault.key, ea.STATE_GET, Object.values(fzNamronFault)) - .withDescription('Shows current error of the device'), - e - .enum(namronPrivateHvacThermostat.workDays.key, ea.STATE_GET, Object.values(fzNamronWorkDays)) - .withDescription("Needs to be changed under 'Thermostat settings' > 'Advanced settings' > 'Schedule type'"), - e.numeric(namronPrivateHvacThermostat.regulatorPercentage.key, ea.ALL).withUnit('%').withValueMin(10).withValueMax(100).withValueStep(1), + e.text('vacation_end_date', ea.ALL).withDescription('End date').withDescription("Supports dates starting with day or year with '. - /'"), ], }, ]; diff --git a/src/lib/namron.ts b/src/lib/namron.ts new file mode 100644 index 0000000000000..bb6fb4b4e60c9 --- /dev/null +++ b/src/lib/namron.ts @@ -0,0 +1,431 @@ +import {Zcl} from 'zigbee-herdsman'; + +import * as utils from '../lib/utils'; +import * as modernExtend from './modernExtend'; +import {Fz, KeyValue, Tz} from './types'; + +function toDate(value: number): string { + if (value === undefined) { + return; + } + const date = new Date(value * 86400000); + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); // Månedene er 0-indeksert + const year = date.getFullYear(); + return `${year}.${month}.${day}`; +} + +function fromDate(value: string): number { + // Ekstrakt `attrId` og `key` direkte fra attributtobjektet + const dateParts = value.split(/[.\-/]/); + if (dateParts.length !== 3) { + throw new Error('Invalid date format'); + } + + let date: Date; + if (dateParts[0].length === 4) { + date = new Date(`${dateParts[0]}-${dateParts[1]}-${dateParts[2]}`); + } else if (dateParts[2].length === 4) { + date = new Date(`${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`); + } else { + throw new Error('Invalid date format'); + } + + return date.getTime() / 86400000 + 1; +} + +export const fromZigbee = { + namron_edge_thermostat_vacation_date: { + cluster: 'hvacThermostat', + type: ['attributeReport', 'readResponse'], + convert: (model, msg, publish, options, meta) => { + const result: KeyValue = {}; + if (msg.data[0x8020] !== undefined) { + result.vacation_start_date = toDate(msg.data[0x8020]); + } + if (msg.data[0x8021] !== undefined) { + result.vacation_end_date = toDate(msg.data[0x8021]); + } + return result; + }, + } satisfies Fz.Converter, + namron_edge_thermostat_holiday_temp: { + cluster: 'hvacThermostat', + type: ['attributeReport', 'readResponse'], + convert: (model, msg, publish, options, meta) => { + const result: KeyValue = {}; + if (msg.data.programingOperMode !== undefined) { + result.operating_mode = utils.getFromLookup(msg.data['programingOperMode'], {0: 'manual', 1: 'manual', 5: 'eco'}); + } + if (msg.data[0x8013] !== undefined) { + result.holiday_temp_set = parseInt(msg.data[0x8013]) / 100; + } + if (msg.data[0x801b] !== undefined) { + result.holiday_temp_set_f = parseInt(msg.data[0x801b]) / 100; + } + return result; + }, + } satisfies Fz.Converter, +}; + +export const toZigbee = { + namron_edge_thermostat_vacation_date: { + key: ['vacation_start_date', 'vacation_end_date'], + convertGet: async (entity, key, meta) => { + switch (key) { + case 'vacation_start_date': + await entity.read('hvacThermostat', [0x8020]); + break; + case 'vacation_end_date': + await entity.read('hvacThermostat', [0x8021]); + break; + } + }, + convertSet: async (entity, key, value, meta) => { + switch (key) { + case 'vacation_start_date': + await entity.write('hvacThermostat', {0x8020: fromDate(String(value))}); + break; + case 'vacation_end_date': + await entity.write('hvacThermostat', {0x8021: fromDate(String(value))}); + break; + } + }, + } satisfies Tz.Converter, + namron_edge_thermostat_holiday_temp: { + key: ['operating_mode', 'holiday_temp_set', 'holiday_temp_set_f'], + convertSet: async (entity, key, value, meta) => { + let lookupValue = 0; + switch (key) { + case 'operating_mode': + if (value != 1) { + lookupValue = Number(value); + } + await entity.write('hvacThermostat', {value: utils.getFromLookup(lookupValue, {manual: 0, eco: 5}), type: Zcl.DataType.BITMAP8}); + break; + case 'holiday_temp_set': + case 'holiday_temp_set_f': + await entity.write('hvacThermostat', {value: Number(value) * 100, type: Zcl.DataType.INT16}); + break; + } + }, + convertGet: async (entity, key, meta) => { + switch (key) { + case 'operating_mode': + await entity.read('hvacThermostat', ['programingOperMode']); + break; + case 'holiday_temp_set': + await entity.read('hvacThermostat', [0x8013]); + break; + case 'holiday_temp_set_f': + await entity.read('hvacThermostat', [0x801b]); + break; + } + }, + } satisfies Tz.Converter, +}; + +export const edgeThermostat = { + windowOpenDetection: (args?: Partial) => + modernExtend.binary({ + name: 'window_open_check', + valueOn: ['ON', 1], + valueOff: ['OFF', 0], + cluster: 'hvacThermostat', + attribute: {ID: 0x8000, type: Zcl.DataType.BOOLEAN}, + description: 'Enables or disables the window open detection', + access: 'ALL', + ...args, + }), + antiFrost: (args?: Partial) => + modernExtend.binary({ + name: 'anti_frost', + valueOn: ['ON', 1], + valueOff: ['OFF', 0], + cluster: 'hvacThermostat', + attribute: {ID: 0x8001, type: Zcl.DataType.BOOLEAN}, + description: 'Enables or disables the anti-frost mode', + access: 'ALL', + ...args, + }), + summerWinterSwitch: (args?: Partial) => + modernExtend.binary({ + name: 'summer_winter_switch', + valueOn: ['ON', 1], + valueOff: ['OFF', 0], + cluster: 'hvacThermostat', + attribute: {ID: 0x801e, type: Zcl.DataType.BOOLEAN}, + description: 'Summer/winter switch', + access: 'ALL', + ...args, + }), + vacationMode: (args?: Partial) => + modernExtend.binary({ + name: 'vacation_mode', + valueOn: ['ON', 1], + valueOff: ['OFF', 0], + cluster: 'hvacThermostat', + attribute: {ID: 0x801f, type: Zcl.DataType.BOOLEAN}, + description: 'Vacation mode', + access: 'ALL', + ...args, + }), + timeSync: (args?: Partial) => + modernExtend.binary({ + name: 'time_sync', + valueOn: ['ON', 1], + valueOff: ['OFF', 0], + cluster: 'hvacThermostat', + attribute: {ID: 0x800a, type: Zcl.DataType.BOOLEAN}, + description: 'Time sync', + access: 'ALL', + ...args, + }), + autoTime: (args?: Partial) => + modernExtend.binary({ + name: 'auto_time', + valueOn: ['ON', 1], + valueOff: ['OFF', 0], + cluster: 'hvacThermostat', + attribute: {ID: 0x8022, type: Zcl.DataType.BOOLEAN}, + description: 'Auto time', + access: 'ALL', + ...args, + }), + + displayActiveBacklight: (args?: Partial) => + modernExtend.numeric({ + name: 'display_active_backlight', + cluster: 'hvacThermostat', + attribute: {ID: 0x8005, type: Zcl.DataType.UINT8}, + description: 'Display active backlight', + valueMin: 1, + valueMax: 100, + valueStep: 1, + unit: '%', + access: 'ALL', + ...args, + }), + regulatorPercentage: (args?: Partial) => + modernExtend.numeric({ + name: 'regulator_percentage', + cluster: 'hvacThermostat', + attribute: {ID: 0x801d, type: Zcl.DataType.UINT8}, + description: 'Regulator percentage', + unit: '%', + valueMax: 100, + valueMin: 0, + valueStep: 1, + access: 'ALL', + ...args, + }), + regulationMode: (args?: Partial) => + modernExtend.enumLookup({ + name: 'regulation_mode', + cluster: 'hvacThermostat', + attribute: {ID: 0x801c, type: Zcl.DataType.ENUM8}, + description: 'Regulation mode', + lookup: {0: 'off', 1: 'heat', 2: 'cool'}, + access: 'ALL', + ...args, + }), + displayAutoOff: (args?: Partial) => + modernExtend.enumLookup({ + name: 'display_auto_off', + cluster: 'hvacThermostat', + attribute: {ID: 0x8029, type: Zcl.DataType.ENUM8}, + description: 'Display auto off', + lookup: {0: 'always_on', 1: 'auto_off_after_10s', 2: 'auto_off_after_30s', 3: 'auto_off_after_60s'}, + access: 'ALL', + ...args, + }), + sensorMode: (args?: Partial) => + modernExtend.enumLookup({ + name: 'sensor_mode', + cluster: 'hvacThermostat', + attribute: {ID: 0x8004, type: Zcl.DataType.ENUM8}, + description: 'Sensor mode', + lookup: {0: 'air', 1: 'floor', 3: 'external', 6: 'regulator'}, + access: 'ALL', + ...args, + }), + boostTime: (args?: Partial) => + modernExtend.enumLookup({ + name: 'boost_time_set', + cluster: 'hvacThermostat', + attribute: {ID: 0x8023, type: Zcl.DataType.ENUM8}, + description: 'Boost time', + lookup: { + 0: 'off', + 1: '5_min', + 2: '10_min', + 3: '15_min', + 4: '20_min', + 5: '25_min', + 6: '30_min', + 7: '35_min', + 8: '40_min', + 9: '45_min', + 10: '50_min', + 11: '55_min', + 12: '1h', + 13: '1h_5_min', + 14: '1h_10_min', + 15: '1h_15_min', + 16: '1h_20_min', + 17: '1h_25_min', + 18: '1h_30_min', + 19: '1h_35_min', + 20: '1h_40_min', + 21: '1h_45_min', + 22: '1h_50_min', + 23: '1h_55_min', + 24: '2h', + }, + access: 'ALL', + ...args, + }), + systemMode: (args?: Partial) => + modernExtend.enumLookup({ + name: 'system_mode', + cluster: 'hvacThermostat', + attribute: {ID: 0x8008, type: Zcl.DataType.ENUM8}, + description: 'System mode', + lookup: {0x00: 'off', 0x01: 'auto', 0x03: 'cool', 0x04: 'heat'}, + access: 'ALL', + ...args, + }), + + deviceTime: (args?: Partial) => + modernExtend.numeric({ + name: 'time_sync_value', + cluster: 'hvacThermostat', + attribute: {ID: 0x800b, type: Zcl.DataType.UINT32}, + description: 'Device time', + valueMin: 0, + valueMax: 4294967295, + access: 'ALL', + ...args, + }), + absMinHeatSetpointLimitF: (args?: Partial) => + modernExtend.numeric({ + name: 'abs_min_heat_setpoint_limit_f', + cluster: 'hvacThermostat', + attribute: {ID: 0x800c, type: Zcl.DataType.INT16}, + description: 'Absolute min heat setpoint limit', + unit: '°F', + access: 'ALL', + ...args, + }), + absMaxHeatSetpointLimitF: (args?: Partial) => + modernExtend.numeric({ + name: 'abs_max_heat_setpoint_limit_f', + cluster: 'hvacThermostat', + attribute: {ID: 0x800d, type: Zcl.DataType.INT16}, + description: 'Absolute max heat setpoint limit', + unit: '°F', + access: 'ALL', + ...args, + }), + absMinCoolSetpointLimitF: (args?: Partial) => + modernExtend.numeric({ + name: 'abs_min_cool_setpoint_limit_f', + cluster: 'hvacThermostat', + attribute: {ID: 0x800e, type: Zcl.DataType.INT16}, + description: 'Absolute min cool setpoint limit', + unit: '°F', + access: 'ALL', + ...args, + }), + absMaxCoolSetpointLimitF: (args?: Partial) => + modernExtend.numeric({ + name: 'abs_max_cool_setpoint_limit_f', + cluster: 'hvacThermostat', + attribute: {ID: 0x800f, type: Zcl.DataType.INT16}, + description: 'Absolute max cool setpoint limit', + unit: '°F', + access: 'ALL', + ...args, + }), + occupiedCoolingSetpointF: (args?: Partial) => + modernExtend.numeric({ + name: 'occupied_cooling_setpoint_f', + cluster: 'hvacThermostat', + attribute: {ID: 0x8010, type: Zcl.DataType.INT16}, + description: 'Occupied cooling setpoint', + unit: '°F', + access: 'ALL', + ...args, + }), + occupiedHeatingSetpointF: (args?: Partial) => + modernExtend.numeric({ + name: 'occupied_heating_setpoint_f', + cluster: 'hvacThermostat', + attribute: {ID: 0x8011, type: Zcl.DataType.INT16}, + description: 'Occupied heating setpoint', + unit: '°F', + access: 'ALL', + ...args, + }), + localTemperatureF: (args?: Partial) => + modernExtend.numeric({ + name: 'local_temperature_f', + cluster: 'hvacThermostat', + attribute: {ID: 0x8012, type: Zcl.DataType.INT16}, + description: 'Local temperature', + unit: '°F', + access: 'ALL', + ...args, + }), + + readOnly: { + windowState: (args?: Partial) => + modernExtend.binary({ + name: 'window_state', + valueOn: ['OPEN', 1], + valueOff: ['CLOSED', 0], + cluster: 'hvacThermostat', + attribute: {ID: 0x8002, type: Zcl.DataType.BOOLEAN}, + description: 'Window state', + access: 'STATE_GET', + ...args, + }), + deviceFault: (args?: Partial) => + modernExtend.enumLookup({ + name: 'fault', + cluster: 'hvacThermostat', + attribute: {ID: 0x8006, type: Zcl.DataType.ENUM8}, + description: 'Device fault', + lookup: { + 0: 'no_fault', + 1: 'over_current_error', + 2: 'over_heat_error', + 3: 'built-in_sensor_error', + 4: 'air_sensor_error', + 5: 'floor_sensor_error', + }, + access: 'STATE_GET', + ...args, + }), + workDays: (args?: Partial) => + modernExtend.enumLookup({ + name: 'work_days', + cluster: 'hvacThermostat', + attribute: {ID: 0x8003, type: Zcl.DataType.ENUM8}, + description: 'Work days', + lookup: {0: 'monday-friday_saturday-sunday', 1: 'monday-saturday_sunday', 2: 'no_time_off', 3: 'time_off'}, + access: 'STATE_GET', + ...args, + }), + boostTimeRemaining: (args?: Partial) => + modernExtend.numeric({ + name: 'boost_time_remaining', + cluster: 'hvacThermostat', + attribute: {ID: 0x8024, type: Zcl.DataType.UINT8}, + description: 'Boost time remaining', + unit: 'min', + access: 'STATE_GET', + ...args, + }), + }, +}; From 1cd478ee1a1528f9a8ff25344f7ce833241284b3 Mon Sep 17 00:00:00 2001 From: bskjon Date: Wed, 22 Jan 2025 23:52:31 +0100 Subject: [PATCH 18/20] Shared model name --- src/devices/namron.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index be8eceb4b88e9..dc51a020c431a 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -1522,7 +1522,7 @@ const definitions: DefinitionWithExtend[] = [ }, { zigbeeModel: ['4512783', '4512784'], - model: 'Edge Thermostat', + model: '4512783/4512784', vendor: 'Namron', description: 'Namron Zigbee Edge Termostat', fromZigbee: [ From b02f1d0e71fb526d88b996d92d84f628e4424e2d Mon Sep 17 00:00:00 2001 From: bskjon Date: Thu, 23 Jan 2025 15:48:13 +0100 Subject: [PATCH 19/20] Added humidity --- src/devices/namron.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index dc51a020c431a..7ab94635a417d 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -1623,6 +1623,7 @@ const definitions: DefinitionWithExtend[] = [ namron.edgeThermostat.readOnly.windowState(), namron.edgeThermostat.readOnly.deviceFault(), namron.edgeThermostat.readOnly.workDays(), + m.humidity(), ], exposes: [ e From a60ef9ee4b5c00e69abf56beb3b1f4302648a98f Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Thu, 23 Jan 2025 20:02:29 +0100 Subject: [PATCH 20/20] u --- src/devices/namron.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devices/namron.ts b/src/devices/namron.ts index 7ab94635a417d..fb702d603a9ca 100644 --- a/src/devices/namron.ts +++ b/src/devices/namron.ts @@ -1524,7 +1524,7 @@ const definitions: DefinitionWithExtend[] = [ zigbeeModel: ['4512783', '4512784'], model: '4512783/4512784', vendor: 'Namron', - description: 'Namron Zigbee Edge Termostat', + description: 'Zigbee edge thermostat', fromZigbee: [ fz.thermostat, namron.fromZigbee.namron_edge_thermostat_holiday_temp,