UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

1,051 lines (1,050 loc) 132 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.definitions = void 0; const zigbee_herdsman_1 = require("zigbee-herdsman"); const fz = __importStar(require("../converters/fromZigbee")); const tz = __importStar(require("../converters/toZigbee")); const constants = __importStar(require("../lib/constants")); const exposes = __importStar(require("../lib/exposes")); const m = __importStar(require("../lib/modernExtend")); const namron = __importStar(require("../lib/namron")); const reporting = __importStar(require("../lib/reporting")); const store = __importStar(require("../lib/store")); const tuya = __importStar(require("../lib/tuya")); const utils = __importStar(require("../lib/utils")); const ea = exposes.access; const e = exposes.presets; const sunricherManufacturer = { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHENZHEN_SUNRICHER_TECHNOLOGY_LTD }; const fzLocal = { namron_panelheater: { cluster: "hvacThermostat", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; const data = msg.data; const isPro = model.model === "4512776/4512777"; if (data.operateDisplayBrightness !== undefined) { // OperateDisplayBrightness if (isPro) { result.display_brightness = data.operateDisplayBrightness; } else { result.display_brightnesss = data.operateDisplayBrightness; } } if (data.displayAutoOff !== undefined) { // DisplayAutoOffActivation if (isPro) { result.display_auto_off = data.displayAutoOff === 1; } else { const lookup = { 0: "deactivated", 1: "activated" }; result.display_auto_off = utils.getFromLookup(data.displayAutoOff, lookup); } } if (data.powerUpStatus !== undefined) { // PowerUpStatus (non-PRO only) const lookup = { 0: "manual", 1: "last_state" }; result.power_up_status = utils.getFromLookup(data.powerUpStatus, lookup); } if (data.windowOpenCheck2 !== undefined) { // WindowOpenCheck if (isPro) { // PRO: 0=enable, 1=disable result.window_open_detection = data.windowOpenCheck2 === 0; } else { // Non-PRO: According to real life testing 0: disable, 1: enable result.window_detection = data.windowOpenCheck2 === 1; } } if (data.hysterersis !== undefined) { // Hysteresis const value = utils.precisionRound(data.hysterersis, 2) / 10; if (isPro) { result.hysteresis = value; } else { result.hysterersis = value; } } if (data.windowOpen !== undefined) { // WindowOpen, 0: Window is not opened, 1: Window is opened result.window_open = data.windowOpen === 1; } // PRO-specific attributes if (data.controlMethod !== undefined) { // System control method: 0=PID, 1=Hysteresis result.control_method = data.controlMethod === 0 ? "pid" : "hysteresis"; } if (data.adaptiveFunction !== undefined) { // Adaptive function AS: 0=Enable, 1=Disable result.adaptive_function = data.adaptiveFunction === 0; } if (data.pidKp !== undefined) { result.pid_kp = data.pidKp / 1000.0; } if (data.pidKd !== undefined) { result.pid_kd = data.pidKd / 1000.0; } if (data.pidKi !== undefined) { result.pid_ki = data.pidKi / 1000.0; } return result; }, }, namron_thermostat2: { cluster: "hvacThermostat", type: ["attributeReport", "readResponse"], options: [exposes.options.local_temperature_based_on_sensor()], convert: (model, msg, publish, options, meta) => { const runningModeStateMap = { 0: 0, 3: 2, 4: 5 }; // override mode "idle" - not a supported running mode if (msg.data.runningMode === 0x10) msg.data.runningMode = 0; // map running *mode* to *state*, as that's what used // in homeAssistant climate ui card (red background) if (msg.data.runningMode !== undefined) msg.data.runningState = runningModeStateMap[msg.data.runningMode]; return fz.thermostat.convert(model, msg, publish, options, meta); // as KeyValue; }, }, namronSimplifyRemote: { cluster: "namronPrivateE004", type: ["raw"], convert(model, msg, publish, _options, meta) { const bytes = parseNamronBytes(msg); if (bytes.length === 0) return; const btn = bytes.at(-2); const raw = bytes.at(-1); if (btn == null || raw == null) return; const kind = NAMRON_SIMPLIFY_ACTIONS[raw]; const base = `button_${simplify_col(btn)}_${simplify_sub(btn)}_`; // Firmware sometimes sends empty action after hold: synthesize release if (!kind) { const lastHold = store.getValue(meta.device, HOLD_KEY_SIMPLIFY); if (lastHold?.endsWith("_hold")) { publish({ action: lastHold.replace("_hold", "_release") }); store.putValue(meta.device, HOLD_KEY_SIMPLIFY, null); } return; } if (kind === "hold") { store.putValue(meta.device, HOLD_KEY_SIMPLIFY, `${base}hold`); publish({ action: `${base}hold` }); return; } if (kind === "release") { publish({ action: `${base}press` }); publish({ action: `${base}release` }); return; } publish({ action: `${base}press` }); }, }, }; // Namron Simplify 3-button remote (4512793 / 4512794) // ----------------------------------------------------------- const NAMRON_SIMPLIFY_ACTIONS = { 0: "press", 1: "release", 2: "hold", }; const HOLD_KEY_SIMPLIFY = "namron_simplify_lastHold"; const simplify_col = (n) => Math.floor((n - 1) / 2) + 1; const simplify_sub = (n) => (n % 2 === 1 ? "up" : "down"); // Helper to safely parse bytes from msg without any/unknown function parseNamronBytes(msg) { const m = msg; if (m.type === "raw" && m.data && typeof m.data === "object" && "data" in m.data && Array.isArray(m.data.data)) { return m.data.data; } if (Array.isArray(m.data)) { return m.data; } if (m.data && typeof m.data === "object") { const obj = m.data; const keys = Object.keys(obj) .filter((k) => !Number.isNaN(Number(k))) .sort((a, b) => Number(a) - Number(b)); return keys.map((k) => obj[k]); } return []; } // END SimplifyBryter const tzLocal = { namron_panelheater: { key: ["display_brightnesss", "display_auto_off", "power_up_status", "window_detection", "hysterersis", "window_open"], convertSet: async (entity, key, value, meta) => { if (key === "display_brightnesss") { const payload = { 4096: { value: value, type: zigbee_herdsman_1.Zcl.DataType.ENUM8 } }; await entity.write("hvacThermostat", payload, sunricherManufacturer); } else if (key === "display_auto_off") { const lookup = { deactivated: 0, activated: 1 }; const payload = { 4097: { value: utils.getFromLookup(value, lookup), type: zigbee_herdsman_1.Zcl.DataType.ENUM8 } }; await entity.write("hvacThermostat", payload, sunricherManufacturer); } else if (key === "power_up_status") { const lookup = { manual: 0, last_state: 1 }; const payload = { 4100: { value: utils.getFromLookup(value, lookup), type: zigbee_herdsman_1.Zcl.DataType.ENUM8 } }; await entity.write("hvacThermostat", payload, sunricherManufacturer); } else if (key === "window_detection") { const payload = { 4105: { value: value ? 1 : 0, type: zigbee_herdsman_1.Zcl.DataType.ENUM8 } }; await entity.write("hvacThermostat", payload, sunricherManufacturer); } else if (key === "hysterersis") { const payload = { 4106: { value: utils.toNumber(value, "hysterersis") * 10, type: zigbee_herdsman_1.Zcl.DataType.UINT8 } }; await entity.write("hvacThermostat", payload, sunricherManufacturer); } }, convertGet: async (entity, key, meta) => { switch (key) { case "display_brightnesss": await entity.read("hvacThermostat", ["operateDisplayBrightness"], sunricherManufacturer); break; case "display_auto_off": await entity.read("hvacThermostat", ["displayAutoOff"], sunricherManufacturer); break; case "power_up_status": await entity.read("hvacThermostat", ["powerUpStatus"], sunricherManufacturer); break; case "window_detection": await entity.read("hvacThermostat", ["windowOpenCheck2"], sunricherManufacturer); break; case "hysterersis": await entity.read("hvacThermostat", ["hysterersis"], sunricherManufacturer); break; case "window_open": await entity.read("hvacThermostat", ["windowOpen"], sunricherManufacturer); break; default: // Unknown key throw new Error(`Unhandled key toZigbee.namron_panelheater.convertGet ${key}`); } }, }, namron_panelheater_pro_hysteresis: { key: ["hysteresis"], convertSet: async (entity, key, value, meta) => { let num = utils.toNumber(value, "hysteresis"); if (num < 0.5) num = 0.5; if (num > 5.0) num = 5.0; const raw = Math.round(num * 10); await entity.write("hvacThermostat", { 4106: { value: raw, type: zigbee_herdsman_1.Zcl.DataType.UINT8 } }, sunricherManufacturer); return { state: { hysteresis: num } }; }, convertGet: async (entity, key, meta) => { await entity.read("hvacThermostat", ["hysterersis"], sunricherManufacturer); }, }, namron_panelheater_pro_window_open_detection: { key: ["window_open_detection"], convertSet: async (entity, key, value, meta) => { const enable = value === true || String(value).toUpperCase() === "ON"; // 0=enable, 1=disable const raw = enable ? 0 : 1; await entity.write("hvacThermostat", { 4105: { value: raw, type: zigbee_herdsman_1.Zcl.DataType.ENUM8 } }, sunricherManufacturer); return { state: { window_open_detection: enable } }; }, convertGet: async (entity, key, meta) => { await entity.read("hvacThermostat", ["windowOpenCheck2", "windowOpen"], sunricherManufacturer); }, }, namron_panelheater_pro_display_auto_off: { key: ["display_auto_off"], convertSet: async (entity, key, value, meta) => { const enable = value === true || String(value).toUpperCase() === "ON"; const raw = enable ? 1 : 0; await entity.write("hvacThermostat", { 4097: { value: raw, type: zigbee_herdsman_1.Zcl.DataType.ENUM8 } }, sunricherManufacturer); return { state: { display_auto_off: enable } }; }, convertGet: async (entity, key, meta) => { await entity.read("hvacThermostat", ["displayAutoOff"], sunricherManufacturer); }, }, namron_panelheater_pro_control_method: { key: ["control_method"], convertSet: async (entity, key, value, meta) => { const mode = String(value).toLowerCase(); let raw; if (mode === "pid" || mode === "0") raw = 0; else if (mode === "hysteresis" || mode === "1") raw = 1; else return; await entity.write("hvacThermostat", { 8201: { value: raw, type: zigbee_herdsman_1.Zcl.DataType.ENUM8 } }, sunricherManufacturer); return { state: { control_method: raw === 0 ? "pid" : "hysteresis" } }; }, convertGet: async (entity, key, meta) => { await entity.read("hvacThermostat", ["controlMethod"], sunricherManufacturer); }, }, namron_panelheater_pro_adaptive_function: { key: ["adaptive_function"], convertSet: async (entity, key, value, meta) => { const enable = value === true || String(value).toUpperCase() === "ON"; // 0=Enable, 1=Disable const raw = enable ? 0 : 1; await entity.write("hvacThermostat", { 4108: { value: raw, type: zigbee_herdsman_1.Zcl.DataType.ENUM8 } }, sunricherManufacturer); return { state: { adaptive_function: enable } }; }, convertGet: async (entity, key, meta) => { await entity.read("hvacThermostat", ["adaptiveFunction"], sunricherManufacturer); }, }, namron_panelheater_pro_pid_kp: { key: ["pid_kp"], convertSet: async (entity, key, value, meta) => { let num = utils.toNumber(value, "pid_kp"); num = Math.min(Math.max(num, 0), 1); await entity.write("hvacThermostat", { 8198: { value: Math.round(num * 1000), type: zigbee_herdsman_1.Zcl.DataType.UINT16 } }, sunricherManufacturer); return { state: { pid_kp: num } }; }, convertGet: async (entity, key, meta) => { await entity.read("hvacThermostat", ["pidKp"], sunricherManufacturer); }, }, namron_panelheater_pro_pid_ki: { key: ["pid_ki"], convertSet: async (entity, key, value, meta) => { let num = utils.toNumber(value, "pid_ki"); num = Math.min(Math.max(num, 0), 1); await entity.write("hvacThermostat", { 8200: { value: Math.round(num * 1000), type: zigbee_herdsman_1.Zcl.DataType.UINT16 } }, sunricherManufacturer); return { state: { pid_ki: num } }; }, convertGet: async (entity, key, meta) => { await entity.read("hvacThermostat", ["pidKi"], sunricherManufacturer); }, }, namron_panelheater_pro_pid_kd: { key: ["pid_kd"], convertSet: async (entity, key, value, meta) => { let num = utils.toNumber(value, "pid_kd"); num = Math.min(Math.max(num, 0), 1); await entity.write("hvacThermostat", { 8199: { value: Math.round(num * 1000), type: zigbee_herdsman_1.Zcl.DataType.UINT16 } }, sunricherManufacturer); return { state: { pid_kd: num } }; }, convertGet: async (entity, key, meta) => { await entity.read("hvacThermostat", ["pidKd"], sunricherManufacturer); }, }, namron_panelheater_pro_state: { key: ["state"], convertSet: async (entity, key, value, meta) => { const v = String(value).toUpperCase(); const isOn = v === "ON"; const systemMode = isOn ? 0x04 : 0x00; // 0x04=heat, 0x00=off await entity.write("hvacThermostat", { systemMode }, { disableDefaultResponse: true }); return { state: { state: isOn ? "ON" : "OFF" } }; }, convertGet: async (entity, key, meta) => { await entity.read("hvacThermostat", ["systemMode"]); }, }, namron_panelheater_pro_frost_mode: { key: ["frost_mode"], convertSet: async (entity, key, value, meta) => { const enable = value === true || String(value).toUpperCase() === "ON"; if (enable) { // Save current state before enabling frost mode if (meta.state) { if (meta.state._prev_system_mode === undefined && meta.state.system_mode !== undefined) { meta.state._prev_system_mode = meta.state.system_mode; } if (meta.state._prev_occupied_heating_setpoint === undefined && meta.state.occupied_heating_setpoint !== undefined) { meta.state._prev_occupied_heating_setpoint = meta.state.occupied_heating_setpoint; } } // Set to heat mode with 7°C setpoint (700 = 7.00°C) await entity.write("hvacThermostat", { systemMode: 0x04, occupiedHeatingSetpoint: 700 }, { disableDefaultResponse: true }); } else { // Restore previous state let systemMode = 0x04; // Default to heat let occupiedHeatingSetpoint = 2100; // Default to 21°C if (meta.state) { if (meta.state._prev_system_mode !== undefined) { const sm = meta.state._prev_system_mode; if (typeof sm === "number") { systemMode = sm; } else { const smStr = String(sm); if (smStr === "off") systemMode = 0x00; else if (smStr === "auto") systemMode = 0x01; else if (smStr === "heat") systemMode = 0x04; } } if (meta.state._prev_occupied_heating_setpoint !== undefined) { let sp = meta.state._prev_occupied_heating_setpoint; // Convert to centidegrees if needed if (typeof sp === "number" && sp < 100) { sp = Math.round(sp * 100); } occupiedHeatingSetpoint = sp; } delete meta.state._prev_system_mode; delete meta.state._prev_occupied_heating_setpoint; } await entity.write("hvacThermostat", { systemMode, occupiedHeatingSetpoint }, { disableDefaultResponse: true }); } return { state: { frost_mode: enable } }; }, }, }; // Simplify Dimmer (4512791) — local toZigbee converters (repo-check-safe) const sdClamp = (v, min, max) => Math.min(Math.max(v, min), max); const sdSecToZclTime = (s) => Math.max(0, Math.round(Number(s || 0) * 10)); // ZCL = 1/10s // Percent <-> level (1..254) const sdPctToLevel = (pct) => sdClamp(Math.round((Number(pct) / 100) * 254), 1, 254); const sdLevelToPct = (lvl) => sdClamp(Math.round((Number(lvl) / 254) * 100), 1, 100); const _tzLocalSimplifyDimmer4512791 = { // Software clamp only -> stored in store, no device attribute to read => no convertGet min_brightness: { key: ["min_brightness"], convertSet: (entity, key, value, meta) => { const pct = Number(value); if (!Number.isFinite(pct) || pct < 1 || pct > 50) throw new Error("min_brightness must be 1..50 (%)"); const lvl = sdClamp(sdPctToLevel(pct), 1, 127); const maxLvl = store.getValue(meta.device, "max_brightness_level"); if (typeof maxLvl === "number" && lvl > maxLvl) { throw new Error(`min_brightness (${pct}%) cannot exceed max_brightness (${sdLevelToPct(maxLvl)}%)`); } store.putValue(meta.device, "min_brightness_level", lvl); return { state: { min_brightness: sdLevelToPct(lvl) } }; }, }, // Software clamp only -> stored in store, no device attribute to read => no convertGet max_brightness: { key: ["max_brightness"], convertSet: (entity, key, value, meta) => { const pct = Number(value); if (!Number.isFinite(pct) || pct < 50 || pct > 100) throw new Error("max_brightness must be 50..100 (%)"); const lvl = sdClamp(sdPctToLevel(pct), 127, 254); const minLvl = store.getValue(meta.device, "min_brightness_level"); if (typeof minLvl === "number" && lvl < minLvl) { throw new Error(`max_brightness (${pct}%) cannot be below min_brightness (${sdLevelToPct(minLvl)}%)`); } store.putValue(meta.device, "max_brightness_level", lvl); return { state: { max_brightness: sdLevelToPct(lvl) } }; }, }, // Software default transition only -> stored in store, we do NOT write unsupported attributes => no convertGet dimming_speed: { key: ["dimming_speed"], convertSet: (entity, key, value, meta) => { const s = Number(value); if (!Number.isFinite(s) || s < 1 || s > 10) throw new Error("dimming_speed must be 1..10 seconds"); store.putValue(meta.device, "dimming_speed", s); return { state: { dimming_speed: s } }; }, }, // Brightness set with clamp + required optionsMask/optionsOverride to avoid "optionsMask is missing" brightness_clamped: { key: ["brightness", "brightness_percent", "transition"], convertSet: async (entity, key, value, meta) => { // meta.message typing varies; keep it safe without any const msg = meta.message ?? {}; let level = key === "brightness" ? Number(value) : sdPctToLevel(Number(value)); if (!Number.isFinite(level)) return; const minLvl = store.getValue(meta.device, "min_brightness_level"); const maxLvl = store.getValue(meta.device, "max_brightness_level"); const minClamp = typeof minLvl === "number" ? minLvl : 1; const maxClamp = typeof maxLvl === "number" ? maxLvl : 254; level = Math.round(sdClamp(level, minClamp, maxClamp)); const storedSpeed = store.getValue(meta.device, "dimming_speed"); const transitionSec = msg["transition"] != null ? Number(msg["transition"]) : typeof storedSpeed === "number" ? storedSpeed : 0; const transtime = sdSecToZclTime(transitionSec); await entity.command("genLevelCtrl", "moveToLevelWithOnOff", { level, transtime, optionsMask: 0, optionsOverride: 0 }, { disableDefaultResponse: true }); return { state: { state: "ON", brightness: level } }; }, }, }; // ----------------------------------------------------------------------------- // End Simplify Dimmer (4512791) // ----------------------------------------------------------------------------- // ─── Namron Zigbee Edge Thermostat (4566702/4566703/4512783/4512784) ────────── const ZIGBEE_EPOCH_OFFSET = 946684800; // seconds between 1970-01-01 and 2000-01-01 function smartDateDecode(value) { if (!value) return null; try { if (value > 100000) { const s = String(value).padStart(6, "0"); return `20${s.slice(0, 2)}-${s.slice(2, 4)}-${s.slice(4, 6)}`; } return new Date(946684800000 + value * 86400000).toISOString().slice(0, 10); } catch (_) { return null; } } function dateToYymmdd(value) { const match = String(value).match(/^20(\d{2})-(\d{2})-(\d{2})$/); if (!match) throw new Error(`Invalid date: ${value}. Use YYYY-MM-DD format, e.g. 2026-06-05.`); return Number(match[1] + match[2] + match[3]); } function deriveEdgeThermostatMode(frost, vacationMode, sensorMode, progOpMode, boostTimeSet) { if (frost === "ON") return "frost"; if (vacationMode === "ON") return "holiday"; if (sensorMode === "percent") return "regulator"; if (boostTimeSet > 0) return "boost"; if (progOpMode === "schedule") return "schedule"; if (progOpMode === "eco") return "eco"; return "manual"; } const edgeSensorModeLookup = { "0": "air", "1": "floor", "2": "both", "3": "air2", "4": "both2", "5": "floor_percent", "6": "percent" }; const edgeOnOffLookup = { OFF: 0, ON: 1 }; const edgeOnOffReverseLookup = { "0": "OFF", "1": "ON" }; const edgeScreenOnTimeLookup = { "0": "always_on", "1": "10s", "2": "60s", "3": "30s" }; const edgeScreenOnTimeValueLookup = { always_on: 0, "10s": 1, "60s": 2, "30s": 3 }; // biome-ignore lint/suspicious/noExplicitAny: endpoint type is complex generic async function safeReadEdge(endpoint, cluster, attrs) { try { await endpoint.read(cluster, attrs); } catch (_) { } } // biome-ignore lint/suspicious/noExplicitAny: entity type is complex generic async function writeEdgeHvac(entity, attr, value, type) { await entity.write("hvacThermostat", { [attr]: { value, type } }); } const fzEdge = { basic: { cluster: "genBasic", type: ["attributeReport", "readResponse"], convert: (model, msg) => { const result = {}; if (msg.data["swBuildId"] !== undefined) result["firmware_version"] = msg.data["swBuildId"]; if (msg.data["dateCode"] !== undefined) result["firmware_date"] = msg.data["dateCode"]; return result; }, }, namron_private: { cluster: "hvacThermostat", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; for (const [key, value] of Object.entries(msg.data)) { switch (Number(key)) { case 0x8000: result["window_open_check"] = edgeOnOffReverseLookup[String(value)] ?? String(value); break; case 0x8001: result["frost"] = edgeOnOffReverseLookup[String(value)] ?? String(value); break; case 0x8002: result["window_state"] = value ? "open" : "closed"; break; case 0x8004: result["sensor_mode"] = edgeSensorModeLookup[String(value)] ?? String(value); break; case 0x8005: result["panel_brightness"] = value; break; case 0x8007: result["regulator_cycle"] = value; break; case 0x8013: result["holiday_temp_set"] = value / 100; break; case 0x801d: result["regulator_percentage"] = value; break; case 0x801f: result["vacation_mode"] = edgeOnOffReverseLookup[String(value)] ?? String(value); break; case 0x8020: result["vacation_start"] = smartDateDecode(value); break; case 0x8021: result["vacation_end"] = smartDateDecode(value); break; case 0x800a: result["time_sync_flag"] = edgeOnOffReverseLookup[String(value)] ?? String(value); if (value === 1) { const ts = Math.round(Date.now() / 1000) - ZIGBEE_EPOCH_OFFSET; msg.endpoint .write("hvacThermostat", { 32779: { value: ts, type: 0x23 } }) .then(() => msg.endpoint.write("hvacThermostat", { 32778: { value: 0, type: 0x10 } })) .catch(() => { }); } break; case 0x800b: try { result["time_sync_value"] = `${new Date((value + ZIGBEE_EPOCH_OFFSET) * 1000).toISOString().replace("T", " ").slice(0, 19)} UTC`; } catch (_) { result["time_sync_value"] = String(value); } break; case 0x8022: result["auto_time"] = edgeOnOffReverseLookup[String(value)] ?? String(value); break; case 0x8023: result["boost_time_set"] = value; break; case 0x8024: result["boost_time_remaining"] = value; break; case 0x8025: result["max_heat_temp"] = value / 10; break; case 0x8029: result["screen_on_time"] = edgeScreenOnTimeLookup[String(value)] ?? String(value); break; } } const merged = Object.assign({}, meta?.state ?? {}, result); result["thermostat_mode"] = deriveEdgeThermostatMode(merged["frost"], merged["vacation_mode"], merged["sensor_mode"], merged["programming_operation_mode"], merged["boost_time_set"] ?? 0); return result; }, }, }; const tzEdge = { thermostat_mode: { key: ["thermostat_mode", "thermostat_mode_extra"], convertSet: async (entity, key, value, meta) => { const state = {}; const wasRegulator = meta.state?.["sensor_mode"] === "percent"; switch (value) { case "manual": case "schedule": case "eco": await writeEdgeHvac(entity, 0x8001, 0, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); await writeEdgeHvac(entity, 0x801f, 0, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); await tz.thermostat_programming_operation_mode.convertSet(entity, "programming_operation_mode", value === "manual" ? "setpoint" : value, meta); if (wasRegulator) { await writeEdgeHvac(entity, 0x8004, 1, zigbee_herdsman_1.Zcl.DataType.ENUM8); state["sensor_mode"] = "floor"; } state["frost"] = "OFF"; state["vacation_mode"] = "OFF"; state["programming_operation_mode"] = value === "manual" ? "setpoint" : value; state["boost_time_set"] = 0; break; case "regulator": await writeEdgeHvac(entity, 0x8001, 0, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); await writeEdgeHvac(entity, 0x801f, 0, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); await writeEdgeHvac(entity, 0x8004, 6, zigbee_herdsman_1.Zcl.DataType.ENUM8); state["frost"] = "OFF"; state["vacation_mode"] = "OFF"; state["sensor_mode"] = "percent"; state["boost_time_set"] = 0; break; case "frost": await writeEdgeHvac(entity, 0x801f, 0, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); await writeEdgeHvac(entity, 0x8001, 1, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); state["vacation_mode"] = "OFF"; state["frost"] = "ON"; state["boost_time_set"] = 0; break; case "holiday": await writeEdgeHvac(entity, 0x8001, 0, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); await writeEdgeHvac(entity, 0x801f, 1, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); state["frost"] = "OFF"; state["vacation_mode"] = "ON"; state["boost_time_set"] = 0; break; case "boost": { await writeEdgeHvac(entity, 0x8001, 0, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); await writeEdgeHvac(entity, 0x801f, 0, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); const hours = meta.state?.["boost_time_set"] > 0 ? meta.state["boost_time_set"] : 1; await writeEdgeHvac(entity, 0x8023, hours, zigbee_herdsman_1.Zcl.DataType.ENUM8); state["frost"] = "OFF"; state["vacation_mode"] = "OFF"; state["boost_time_set"] = hours; break; } default: throw new Error(`Invalid thermostat_mode: ${value}`); } state["thermostat_mode"] = value; state["thermostat_mode_extra"] = value; return { state }; }, convertGet: async (entity) => { await entity.read("hvacThermostat", [0x8001, 0x8004, 0x801f, 0x8023]); await entity.read("hvacThermostat", ["programingOperMode"]); }, }, frost: { key: ["frost"], convertSet: async (entity, key, value) => { if (value === "ON") { await entity.write("hvacThermostat", { 32799: { value: 0, type: 0x10 } }); await entity.write("hvacThermostat", { 32769: { value: 1, type: 0x10 } }); } else { await entity.write("hvacThermostat", { 32769: { value: 0, type: 0x10 } }); } return { state: { frost: value } }; }, }, keypad_lockout: { key: ["keypad_lockout"], convertSet: async (entity, key, value, meta) => { const mapped = value === "lock" ? "lock1" : "unlock"; await tz.thermostat_keypad_lockout.convertSet(entity, key, mapped, meta); return { state: { keypad_lockout: value } }; }, convertGet: async (entity, key, meta) => tz.thermostat_keypad_lockout.convertGet(entity, key, meta), }, regulator_percentage: { key: ["regulator_percentage"], convertSet: async (entity, key, value) => { const num = Number(value); if (Number.isNaN(num) || num < 0 || num > 100) throw new Error(`Invalid regulator_percentage: ${value}`); await writeEdgeHvac(entity, 0x801d, Math.round(num), zigbee_herdsman_1.Zcl.DataType.INT16); return { state: { regulator_percentage: num } }; }, convertGet: async (entity) => { await entity.read("hvacThermostat", [0x801d]); }, }, regulator_cycle: { key: ["regulator_cycle"], convertSet: async (entity, key, value) => { const num = Math.round(Number(value)); if (Number.isNaN(num) || num < 1 || num > 30) throw new Error(`Invalid regulator_cycle: ${value}`); await writeEdgeHvac(entity, 0x8007, num, zigbee_herdsman_1.Zcl.DataType.UINT8); return { state: { regulator_cycle: num } }; }, convertGet: async (entity) => { await entity.read("hvacThermostat", [0x8007]); }, }, max_heat_temp: { key: ["max_heat_temp"], convertSet: async (entity, key, value) => { const num = Number(value); if (Number.isNaN(num) || num < 15 || num > 35) throw new Error(`Invalid max_heat_temp: ${value}`); await writeEdgeHvac(entity, 0x8025, Math.round(num * 10), zigbee_herdsman_1.Zcl.DataType.INT16); return { state: { max_heat_temp: num } }; }, convertGet: async (entity) => { await entity.read("hvacThermostat", [0x8025]); }, }, vacation_start: { key: ["vacation_start"], convertSet: async (entity, key, value) => { const raw = dateToYymmdd(value); await writeEdgeHvac(entity, 0x8020, raw, zigbee_herdsman_1.Zcl.DataType.UINT32); return { state: { vacation_start: value } }; }, convertGet: async (entity) => { await entity.read("hvacThermostat", [0x8020]); }, }, vacation_end: { key: ["vacation_end"], convertSet: async (entity, key, value) => { const raw = dateToYymmdd(value); await writeEdgeHvac(entity, 0x8021, raw, zigbee_herdsman_1.Zcl.DataType.UINT32); return { state: { vacation_end: value } }; }, convertGet: async (entity) => { await entity.read("hvacThermostat", [0x8021]); }, }, holiday_temp_set: { key: ["holiday_temp_set"], convertSet: async (entity, key, value) => { const num = Number(value); if (Number.isNaN(num) || num < 5 || num > 35) throw new Error(`Invalid holiday_temp_set: ${value}`); await writeEdgeHvac(entity, 0x8013, Math.round(num * 100), zigbee_herdsman_1.Zcl.DataType.INT16); return { state: { holiday_temp_set: num } }; }, convertGet: async (entity) => { await entity.read("hvacThermostat", [0x8013]); }, }, boost_time_set: { key: ["boost_time_set"], convertSet: async (entity, key, value) => { const num = Math.round(Number(value)); if (Number.isNaN(num) || num < 0 || num > 24) throw new Error(`Invalid boost_time_set: ${value}`); await writeEdgeHvac(entity, 0x8023, num, zigbee_herdsman_1.Zcl.DataType.ENUM8); return { state: { boost_time_set: num } }; }, convertGet: async (entity) => { await entity.read("hvacThermostat", [0x8023, 0x8024]); }, }, window_open_check: { key: ["window_open_check"], convertSet: async (entity, key, value) => { const raw = edgeOnOffLookup[value]; if (raw === undefined) throw new Error(`Invalid window_open_check: ${value}`); await writeEdgeHvac(entity, 0x8000, raw, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); return { state: { window_open_check: value } }; }, convertGet: async (entity) => { await entity.read("hvacThermostat", [0x8000]); }, }, screen_on_time: { key: ["screen_on_time"], convertSet: async (entity, key, value) => { const raw = edgeScreenOnTimeValueLookup[value]; if (raw === undefined) throw new Error(`Invalid screen_on_time: ${value}`); await writeEdgeHvac(entity, 0x8029, raw, zigbee_herdsman_1.Zcl.DataType.ENUM8); return { state: { screen_on_time: value } }; }, convertGet: async (entity) => { await entity.read("hvacThermostat", [0x8029]); }, }, panel_brightness: { key: ["panel_brightness"], convertSet: async (entity, key, value) => { const num = Math.round(Number(value)); if (Number.isNaN(num) || num < 0 || num > 100) throw new Error(`Invalid panel_brightness: ${value}`); await writeEdgeHvac(entity, 0x8005, num, zigbee_herdsman_1.Zcl.DataType.UINT8); return { state: { panel_brightness: num } }; }, convertGet: async (entity) => { await entity.read("hvacThermostat", [0x8005]); }, }, auto_time: { key: ["auto_time"], convertSet: async (entity, key, value) => { const raw = edgeOnOffLookup[value]; if (raw === undefined) throw new Error(`Invalid auto_time: ${value}`); await writeEdgeHvac(entity, 0x8022, raw, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); return { state: { auto_time: value } }; }, convertGet: async (entity) => { await entity.read("hvacThermostat", [0x8022]); }, }, sync_time: { key: ["sync_time"], convertSet: async (entity) => { const ts = Math.round(Date.now() / 1000) - ZIGBEE_EPOCH_OFFSET; await writeEdgeHvac(entity, 0x800b, ts, zigbee_herdsman_1.Zcl.DataType.UINT32); await writeEdgeHvac(entity, 0x800a, 0, zigbee_herdsman_1.Zcl.DataType.BOOLEAN); await entity.read("hvacThermostat", [0x800a, 0x800b]); return { state: { sync_time: "sync" } }; }, }, }; // ─── Namron Zigbee Edge Thermostat END ─────────────────────────────────────── exports.definitions = [ { zigbeeModel: ["4566702", "4566703", "4512783", "4512784"], model: "4566702", vendor: "Namron", description: "Zigbee Edge Thermostat", ota: true, extend: [m.humidity()], fromZigbee: [fzEdge.basic, fz.thermostat, fzEdge.namron_private, fz.hvac_user_interface, fz.metering, fz.electrical_measurement], toZigbee: [ tz.thermostat_occupied_heating_setpoint, tz.thermostat_system_mode, tz.thermostat_local_temperature_calibration, tz.thermostat_programming_operation_mode, tz.thermostat_temperature_display_mode, tzEdge.thermostat_mode, tzEdge.frost, tzEdge.keypad_lockout, tzEdge.regulator_percentage, tzEdge.regulator_cycle, tzEdge.max_heat_temp, tzEdge.vacation_start, tzEdge.vacation_end, tzEdge.holiday_temp_set, tzEdge.boost_time_set, tzEdge.window_open_check, tzEdge.screen_on_time, tzEdge.panel_brightness, tzEdge.auto_time, tzEdge.sync_time, ], configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(1); await reporting.bind(endpoint, coordinatorEndpoint, [ "genTime", "genOta", "hvacThermostat", "hvacUserInterfaceCfg", "msRelativeHumidity", "seMetering", "haElectricalMeasurement", ]); await reporting.thermostatTemperature(endpoint, { min: 10, max: 300, change: 10 }); await reporting.thermostatOccupiedHeatingSetpoint(endpoint, { min: 10, max: 300, change: 50 }); try { await endpoint.configureReporting("haElectricalMeasurement", [ { attribute: "rmsCurrent", minimumReportInterval: 10, maximumReportInterval: 300, reportableChange: 1 }, { attribute: "activePower", minimumReportInterval: 10, maximumReportInterval: 300, reportableChange: 100 }, ]); } catch (_) { } await safeReadEdge(endpoint, "genBasic", ["swBuildId", "dateCode"]); await safeReadEdge(endpoint, "hvacThermostat", [ "localTemp", "occupiedHeatingSetpoint", "systemMode", "runningMode", "runningState", "localTemperatureCalibration", "pIHeatingDemand", "programingOperMode", "tempDisplayMode", ]); await safeReadEdge(endpoint, "hvacUserInterfaceCfg", ["keypadLockout"]); await safeReadEdge(endpoint, "seMetering", ["currentSummDelivered", "divisor", "multiplier"]); await safeReadEdge(endpoint, "haElectricalMeasurement", [ "activePower", "rmsCurrent", "acPowerMultiplier", "acPowerDivisor", "acCurrentMultiplier", "acCurrentDivisor", ]); await safeReadEdge(endpoint, "hvacThermostat", [ 0x8000, 0x8001, 0x8002, 0x8004, 0x8005, 0x8007, 0x8013, 0x801d, 0x801f, 0x8020, 0x8021, 0x800a, 0x800b, 0x8022, 0x8023, 0x8024, 0x8025, 0x8029, ]); // Sync time at configure const ts = Math.round(Date.now() / 1000) - ZIGBEE_EPOCH_OFFSET; await endpoint.write("hvacThermostat", { 32779: { value: ts, type: zigbee_herdsman_1.Zcl.DataType.UINT32 } }); await endpoint.write("hvacThermostat", { 32778: { value: 0, type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN } }); // Write defaults: screen_on_time = 30s, temperature_display_mode = celsius await endpoint.write("hvacThermostat", { [0x8029]: { value: 3, type: zigbee_herdsman_1.Zcl.DataType.ENUM8 } }); await endpoint.write("hvacUserInterfaceCfg", { [0x0000]: { value: 0, type: zigbee_herdsman_1.Zcl.DataType.ENUM8 } }); device.powerSource = "Mains (single phase)"; device.save(); }, // Periodic time sync regardless of heating status. // Syncs time at most once per hour so vacation mode always has correct clock. onEvent: async (event) => { if (event.type === "stop") return; const now = Date.now(); const device = event.data.device; const lastSync = device.meta["lastTimeSync"] ?? 0; if (now - lastSync > 60 * 60 * 1000) { try { const endpoint = device.getEndpoint(1); const ts = Math.round(now / 1000) - ZIGBEE_EPOCH_OFFSET; await endpoint.write("hvacThermostat", { 32779: { value: ts, type: zigbee_herdsman_1.Zcl.DataType.UINT32 } }); await endpoint.write("hvacThermostat", { 32778: { value: 0, type: zigbee_herdsman_1.Zcl.DataType.BOOLEAN } }); device.meta["lastTimeSync"] = now; device.save(); } catch (_) { /* ignore */ } } }, exposes: [ e .climate() .withLocalTemperature() .withSetpoint("occupied_heating_setpoint", 5, 35, 0.5) .withSystemMode(["off", "heat"]) .withRunningState(["idle", "heat"]) .withLocalTemperatureCalibration(-5, 5, 0.5), e.numeric("max_heat_temp", ea.ALL).withUnit("°C").withValueMin(15).withValueMax(35).withValueStep(0.5).withLabel("Max heat temperature"), e.enum("thermostat_mode", ea.ALL, ["manual", "schedule", "regulator"]).withLabel("Thermostat mode"), e.enum("thermostat_mode_extra", ea.ALL, ["eco", "frost", "holiday"]).withLabel("Special mode"), e.binary("frost", ea.STATE_SET, "ON", "OFF").withLabel("Frost Mode"), e.enum("temperature_display_mode", ea.STATE_SET, ["celsius", "fahrenheit"]).withLabel("Temperature unit"), e .numeric("regulator_percentage", ea.ALL) .withUnit("%") .withValueMin(0) .withValueMax(100) .withValueStep(5) .withLabel("Regulator set point"), e .numeric("regulator_cycle", ea.ALL) .withUnit("min") .withValueMin(1) .withValueMax(30) .withValueStep(1) .withLabel("Regulator cycle duration"), e.binary("vacation_mode", ea.STATE, "ON", "OFF").withLabel("Vacation active"), e.text("vacation_start", ea.ALL).withLabel("Vacation start (YYYY-MM-DD)"), e.text("vacation_end", ea.ALL).withLabel("Vacation end (YYYY-MM-DD)"), e.numeric("holiday_temp_set", ea.ALL).withUnit("°C").withValueMin(5).withValueMax(35).withValueStep(0.5).withLabel("Holiday temperature"), e .numeric("boost_time_set", ea.ALL) .withUnit("h") .withValueMin(0) .withValueMax(24) .withValueStep(1) .withLabel("Boost time set")