zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
1,051 lines (1,050 loc) • 132 kB
JavaScript
"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")