zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
563 lines • 24.4 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.presetsZosung = exports.tzZosung = exports.fzZosung = exports.zosungExtend = void 0;
const zigbee_herdsman_1 = require("zigbee-herdsman");
const m = __importStar(require("../lib/modernExtend"));
const exposes = __importStar(require("./exposes"));
const logger_1 = require("./logger");
const globalStore = __importStar(require("./store"));
const NS = "zhc:zosung";
const ea = exposes.access;
const e = exposes.presets;
exports.zosungExtend = {
addZosungIRTransmitCluster: () => m.deviceAddCustomCluster("zosungIRTransmit", {
name: "zosungIRTransmit",
ID: 0xed00,
attributes: {},
commands: {
zosungSendIRCode00: {
name: "zosungSendIRCode00",
ID: 0x00,
parameters: [
{ name: "seq", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "length", type: zigbee_herdsman_1.Zcl.DataType.UINT32, max: 0xffffffff },
{ name: "unk1", type: zigbee_herdsman_1.Zcl.DataType.UINT32, max: 0xffffffff },
{ name: "unk2", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "unk3", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
{ name: "cmd", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
{ name: "unk4", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
],
},
zosungSendIRCode01: {
name: "zosungSendIRCode01",
ID: 0x01,
parameters: [
{ name: "zero", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
{ name: "seq", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "length", type: zigbee_herdsman_1.Zcl.DataType.UINT32, max: 0xffffffff },
{ name: "unk1", type: zigbee_herdsman_1.Zcl.DataType.UINT32, max: 0xffffffff },
{ name: "unk2", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "unk3", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
{ name: "cmd", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
{ name: "unk4", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
],
},
zosungSendIRCode02: {
name: "zosungSendIRCode02",
ID: 0x02,
parameters: [
{ name: "seq", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "position", type: zigbee_herdsman_1.Zcl.DataType.UINT32, max: 0xffffffff },
{ name: "maxlen", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
],
},
zosungSendIRCode03: {
name: "zosungSendIRCode03",
ID: 0x03,
parameters: [
{ name: "zero", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
{ name: "seq", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "position", type: zigbee_herdsman_1.Zcl.DataType.UINT32, max: 0xffffffff },
{ name: "msgpart", type: zigbee_herdsman_1.Zcl.DataType.OCTET_STR },
{ name: "msgpartcrc", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
],
},
zosungSendIRCode04: {
name: "zosungSendIRCode04",
ID: 0x04,
parameters: [
{ name: "zero0", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
{ name: "seq", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "zero1", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
],
},
zosungSendIRCode05: {
name: "zosungSendIRCode05",
ID: 0x05,
parameters: [
{ name: "seq", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "zero", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
],
},
},
commandsResponse: {
zosungSendIRCode03Resp: {
name: "zosungSendIRCode03Resp",
ID: 0x03,
parameters: [
{ name: "zero", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
{ name: "seq", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "position", type: zigbee_herdsman_1.Zcl.DataType.UINT32, max: 0xffffffff },
{ name: "msgpart", type: zigbee_herdsman_1.Zcl.DataType.OCTET_STR },
{ name: "msgpartcrc", type: zigbee_herdsman_1.Zcl.DataType.UINT8, max: 0xff },
],
},
zosungSendIRCode05Resp: {
name: "zosungSendIRCode05Resp",
ID: 0x05,
parameters: [
{ name: "seq", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
{ name: "zero", type: zigbee_herdsman_1.Zcl.DataType.UINT16, max: 0xffff },
],
},
},
}),
addZosungIRControlCluster: () => m.deviceAddCustomCluster("zosungIRControl", {
name: "zosungIRControl",
ID: 0xe004,
attributes: {},
commands: {
zosungControlIRCommand00: {
name: "zosungControlIRCommand00",
ID: 0x00,
parameters: [
// JSON string with a command.
{ name: "data", type: zigbee_herdsman_1.Zcl.BuffaloZclDataType.BUFFER },
],
},
},
commandsResponse: {},
}),
};
function nextSeq(entity) {
return (globalStore.getValue(entity, "seq", -1) + 1) % 0x10000;
}
function messagesGet(entity, seq) {
const info = globalStore.getValue(entity, "irMessageInfo");
const expected = info?.seq || 0;
if (expected !== seq) {
throw new Error(`Unexpected sequence value (expected: ${expected} current: ${seq}).`);
}
if (info) {
return info.data;
}
logger_1.logger.debug("Ignoring, no message send yet", NS);
}
function messagesSet(entity, seq, data) {
globalStore.putValue(entity, "irMessageInfo", { seq: seq, data: data });
}
function messagesClear(entity, seq) {
const info = globalStore.getValue(entity, "irMessageInfo");
const expected = info?.seq || 0;
if (expected !== seq) {
throw new Error(`Unexpected sequence value (expected: ${expected} current: ${seq}).`);
}
globalStore.clearValue(entity, "irMessageInfo");
}
function calcArrayCrc(values) {
return Array.from(values).reduce((a, b) => a + b, 0) % 0x100;
}
function calcStringCrc(str) {
return (str
.split("")
.map((x) => x.charCodeAt(0))
.reduce((a, b) => a + b, 0) % 0x100);
}
function encodeTuyaTimings(timings) {
const rawBytes = new Uint8Array(timings.length * 2);
for (let i = 0; i < timings.length; i++) {
const v = Math.abs(Math.trunc(Number(timings[i])));
if (!Number.isFinite(v) || v > 0xffff) {
throw new Error(`Invalid timing at index ${i}: ${timings[i]}`);
}
rawBytes[i * 2] = v & 0xff;
rawBytes[i * 2 + 1] = (v >> 8) & 0xff;
}
const compressed = [];
let pos = 0;
while (pos < rawBytes.length) {
const chunkLen = Math.min(32, rawBytes.length - pos);
compressed.push((chunkLen - 1) & 0x1f);
for (let i = 0; i < chunkLen; i++) {
compressed.push(rawBytes[pos + i]);
}
pos += chunkLen;
}
return Buffer.from(Uint8Array.from(compressed)).toString("base64");
}
function buildIrMsgFromTimings(obj) {
const timings = obj.timings ?? [];
const modulation = obj.modulation ?? 38000;
const repeatCount = obj.repeat_count ?? 0;
if (!Array.isArray(timings) || timings.length === 0) {
throw new Error("timings must be a non-empty array");
}
if (typeof modulation !== "number" || modulation <= 30000) {
throw new Error("Invalid modulation frequency");
}
const encoded = encodeTuyaTimings(timings);
return JSON.stringify({
key_num: 1,
delay: 300,
key1: {
num: 1 + repeatCount,
freq: modulation,
type: 1,
key_code: encoded,
},
});
}
function normalizeIrMsg(value) {
if (value === undefined || value === null || value === "") {
throw new Error("IR code payload is empty");
}
// Object input.
if (typeof value === "object" && "timings" in value) {
if (Array.isArray(value.timings)) {
return buildIrMsgFromTimings(value);
}
// Already full Zosung-style object.
const zozung_value = value;
if (zozung_value.key_num !== undefined && zozung_value.key1 !== undefined) {
return JSON.stringify(value);
}
// Direct key_code object.
if (zozung_value.key_code !== undefined) {
return JSON.stringify({
key_num: 1,
delay: zozung_value.delay ?? 300,
key1: {
num: zozung_value.num ?? 1,
freq: zozung_value.freq ?? 38000,
type: zozung_value.type ?? 1,
key_code: zozung_value.key_code,
},
});
}
return JSON.stringify(value);
}
if (typeof value !== "string") {
value = String(value);
}
const trimmed = value.trim();
if (!trimmed) {
throw new Error("IR code payload is empty");
}
// JSON string input.
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
try {
const parsed = JSON.parse(trimmed);
if (parsed && typeof parsed === "object") {
if (Array.isArray(parsed.timings)) {
return buildIrMsgFromTimings(parsed);
}
if (parsed.key_num !== undefined && parsed.key1 !== undefined) {
return JSON.stringify(parsed);
}
if (parsed.key_code !== undefined) {
return JSON.stringify({
key_num: 1,
delay: parsed.delay ?? 300,
key1: {
num: parsed.num ?? 1,
freq: parsed.freq ?? 38000,
type: parsed.type ?? 1,
key_code: parsed.key_code,
},
});
}
}
}
catch (err) {
logger_1.logger.debug(`JSON parse failed, treating as captured key_code: ${err}`, NS);
}
}
// Fallback:
// Captured base64 strings like H4IKjQP7... are key_code only,
// not full Zosung messages. Wrap them.
return JSON.stringify({
key_num: 1,
delay: 300,
key1: {
num: 1,
freq: 38000,
type: 1,
key_code: trimmed,
},
});
}
function decodeTuyaTimings(compressed) {
const decompressed = [];
let pos = 0;
try {
while (pos < compressed.length) {
const header = compressed[pos++];
const blockType = header >> 5;
if (blockType === 0) {
// Literal block
const length = (header & 0x1f) + 1;
for (let i = 0; i < length; i++) {
if (pos >= compressed.length)
throw new Error("Truncated literal");
decompressed.push(compressed[pos++]);
}
}
else {
// Reference block
let length = blockType + 2;
if (length === 9) {
// extended length: accumulate 255 bytes per 0xFF then a remainder
while (pos < compressed.length && compressed[pos] === 0xff) {
length += 255;
pos++;
}
if (pos >= compressed.length)
throw new Error("Truncated extended length");
length += compressed[pos++];
}
if (pos >= compressed.length)
throw new Error("Truncated distance");
const distance = (((header & 0x1f) << 8) | compressed[pos++]) >>> 0;
const offset = distance + 1;
for (let i = 0; i < length; i++) {
const idx = decompressed.length - offset;
if (idx < 0)
throw new Error("Invalid reference offset");
decompressed.push(decompressed[idx]);
}
}
}
}
catch {
// truncated or malformed stream: stop decoding and continue with what we have
}
// Interpret decompressed bytes as little-endian 16-bit unsigned values
const numTimings = Math.floor(decompressed.length / 2);
if (numTimings === 0)
return [];
const timings = new Array(numTimings);
for (let i = 0; i < numTimings; i++) {
const lo = decompressed[i * 2];
const hi = decompressed[i * 2 + 1];
const val = (hi << 8) | lo;
// alternate sign: even index positive, odd index negative
timings[i] = i % 2 === 0 ? val : -val;
}
return timings;
}
exports.fzZosung = {
zosung_send_ir_code_01: {
cluster: "zosungIRTransmit",
type: ["commandZosungSendIRCode01"],
convert: (model, msg, publish, options, meta) => {
logger_1.logger.debug(`"IR-Message-Code01" received (msg:${JSON.stringify(msg.data)})`, NS);
const seq = msg.data.seq;
const irMsg = messagesGet(msg.endpoint, seq);
if (irMsg) {
logger_1.logger.debug(`IRCode to send: ${JSON.stringify(irMsg)} (seq:${seq})`, NS);
}
},
},
zosung_send_ir_code_02: {
cluster: "zosungIRTransmit",
type: ["commandZosungSendIRCode02"],
convert: async (model, msg, publish, options, meta) => {
logger_1.logger.debug(`"IR-Message-Code02" received (msg:${JSON.stringify(msg.data)})`, NS);
const seq = msg.data.seq;
const position = msg.data.position;
const irMsg = messagesGet(msg.endpoint, seq);
if (irMsg) {
const part = irMsg.substring(position, position + 0x32);
const sum = calcStringCrc(part);
await msg.endpoint.command("zosungIRTransmit", "zosungSendIRCode03", {
zero: 0,
seq: seq,
position: position,
msgpart: Buffer.from(part),
msgpartcrc: sum,
}, { disableDefaultResponse: true });
logger_1.logger.debug(`Sent IRCode part: ${part} (sum: ${sum}, seq:${seq})`, NS);
}
},
},
zosung_send_ir_code_04: {
cluster: "zosungIRTransmit",
type: ["commandZosungSendIRCode04"],
convert: async (model, msg, publish, options, meta) => {
logger_1.logger.debug(`"IR-Message-Code04" received (msg:${JSON.stringify(msg.data)})`, NS);
const seq = msg.data.seq;
await msg.endpoint.command("zosungIRTransmit", "zosungSendIRCode05", {
seq: seq,
zero: 0,
}, { disableDefaultResponse: true });
messagesClear(msg.endpoint, seq);
logger_1.logger.debug(`IRCode has been successfully sent. (seq:${seq})`, NS);
},
},
zosung_send_ir_code_00: {
cluster: "zosungIRTransmit",
type: ["commandZosungSendIRCode00"],
convert: async (model, msg, publish, options, meta) => {
logger_1.logger.debug(`"IR-Message-Code00" received (msg:${JSON.stringify(msg.data)})`, NS);
const seq = msg.data.seq;
const length = msg.data.length;
messagesSet(msg.endpoint, seq, { position: 0, buf: Buffer.alloc(length) });
await msg.endpoint.command("zosungIRTransmit", "zosungSendIRCode01", {
zero: 0,
seq: seq,
length: length,
unk1: msg.data.unk1,
unk2: msg.data.unk2,
unk3: msg.data.unk3,
cmd: msg.data.cmd,
unk4: msg.data.unk4,
}, { disableDefaultResponse: true });
logger_1.logger.debug(`"IR-Message-Code00" response sent.`, NS);
await msg.endpoint.command("zosungIRTransmit", "zosungSendIRCode02", {
seq: msg.data.seq,
position: 0,
maxlen: 0x38,
}, { disableDefaultResponse: true });
logger_1.logger.debug(`"IR-Message-Code00" transfer started.`, NS);
},
},
zosung_send_ir_code_03: {
cluster: "zosungIRTransmit",
type: ["commandZosungSendIRCode03Resp"],
convert: async (model, msg, publish, options, meta) => {
logger_1.logger.debug(`"IR-Message-Code03" received (msg:${JSON.stringify(msg.data)})`, NS);
const seq = msg.data.seq;
const rcv = messagesGet(msg.endpoint, seq);
if (rcv) {
if (rcv.position === msg.data.position) {
const rcvMsgPart = msg.data.msgpart;
const sum = calcArrayCrc(rcvMsgPart);
const expectedPartCrc = msg.data.msgpartcrc;
if (sum === expectedPartCrc) {
const position = rcvMsgPart.copy(rcv.buf, rcv.position);
rcv.position += position;
if (rcv.position < rcv.buf.length) {
await msg.endpoint.command("zosungIRTransmit", "zosungSendIRCode02", {
seq: seq,
position: rcv.position,
maxlen: 0x38,
}, { disableDefaultResponse: true });
}
else {
await msg.endpoint.command("zosungIRTransmit", "zosungSendIRCode04", {
zero0: 0,
seq: seq,
zero1: 0,
}, { disableDefaultResponse: true });
}
logger_1.logger.debug(`${rcvMsgPart.length} bytes received.`, NS);
}
else {
logger_1.logger.error(`Invalid msg part CRC: ${sum} expecting: ${expectedPartCrc}.`, NS);
}
}
else {
logger_1.logger.error(`Unexpected IR code position: ${JSON.stringify(msg.data)}, expecting: ${rcv.position}.`, NS);
}
}
},
},
zosung_send_ir_code_05: {
cluster: "zosungIRTransmit",
type: ["commandZosungSendIRCode05Resp"],
convert: async (model, msg, publish, options, meta) => {
logger_1.logger.debug(`"IR-Message-Code05" received (msg:${JSON.stringify(msg.data)})`, NS);
const seq = msg.data.seq;
const rcv = messagesGet(msg.endpoint, seq);
if (rcv) {
const learnedIRCode = rcv.buf.toString("base64");
const learnedTimings = decodeTuyaTimings(rcv.buf);
logger_1.logger.debug(`Received: ${learnedIRCode}`, NS);
messagesClear(msg.endpoint, seq);
await msg.endpoint.command("zosungIRControl", "zosungControlIRCommand00", {
data: Buffer.from(JSON.stringify({ study: 1 })),
}, { disableDefaultResponse: true });
// Clear learned IR timings for HA
clearTimeout(globalStore.getValue(msg.endpoint, "timer"));
globalStore.putValue(msg.endpoint, "timer", setTimeout(() => publish({ learned_ir_timings: "" }), 500).unref());
return {
learned_ir_code: learnedIRCode,
learned_ir_timings: {
modulation: 38000,
timings: learnedTimings,
timestamp: Date.now(),
},
};
}
},
},
};
exports.tzZosung = {
zosung_ir_code_to_send: {
key: ["ir_code_to_send", "ir_emitter"],
convertSet: async (entity, key, value, meta) => {
const irMsg = normalizeIrMsg(value);
logger_1.logger.debug(`Sending IR code: ${irMsg}`, NS);
const seq = nextSeq(entity);
messagesSet(entity, seq, irMsg);
await entity.command("zosungIRTransmit", "zosungSendIRCode00", {
seq: seq,
length: irMsg.length,
unk1: 0x00000000,
unk2: 0xe004,
unk3: 0x01,
cmd: 0x02,
unk4: 0x0000,
}, { disableDefaultResponse: true });
logger_1.logger.debug("Sending IR code initiated.", NS);
},
},
zosung_learn_ir_code: {
key: ["learn_ir_code"],
convertSet: async (entity, key, value, meta) => {
logger_1.logger.debug("Starting IR Code Learning...", NS);
await entity.command("zosungIRControl", "zosungControlIRCommand00", {
data: Buffer.from(JSON.stringify({ study: 0 })),
}, { disableDefaultResponse: true });
logger_1.logger.debug("IR Code Learning started.", NS);
},
},
};
exports.presetsZosung = {
learn_ir_code: () => e.binary("learn_ir_code", ea.SET, "ON", "OFF").withDescription("Turn on to learn new IR code").withHomeAssistant({ type: "button" }),
learned_ir_code: () => e.text("learned_ir_code", ea.STATE).withDescription("The IR code learned by device"),
learned_ir_timings: () => e.text("learned_ir_timings", ea.STATE).withDescription("The IR timings learned by device").withHomeAssistant({
type: "infrared",
schema: "receiver",
valueTemplate: "{{ value_json.learned_ir_timings | tojson }}",
}),
ir_code_to_send: () => e.text("ir_code_to_send", ea.SET).withDescription("The IR code or timings to send by device"),
ir_emitter: () => e
.text("ir_emitter", ea.SET)
.withDescription("The IR code or timings to send by device")
.withHomeAssistant({ type: "infrared", schema: "emitter", valueTemplate: null }),
};
//# sourceMappingURL=zosung.js.map