- Поправил для себя рекомендованный код
- Удалил свои апоплексические приводы штор (в данном контексте красные) из устройств.
- Вызвал это в моем основном скрипте createCurtain(“Штора кухня 1”, 0x01, 0x01)…для всех приводов
- Все устройства подхватились на лету, даже менять больше ничего не пришлось в сценариях, благодаря var curtainVdPrefix = “dooya_”
- Все шторы полноценно работают…
- 4 года этого счастья ждал, а оно было рядом
- Всем спасибо!
// универсальный скрипт подходит для привода AT6501 и Dooya DT82
// Хранит флаги автоматической установки слайдера позиции
var positionSetAuto = {};
// Путь к RPC
var pathRPC = "/rpc/v1/wb-mqtt-serial/port/Load/";
var portCurtain = "/dev/ttyMOD1";
var speedPortCurtain = 9600;
var parityCurtain = "N";
var stopBitCurtain = 1;
var pollIntervalMs = 1500;
var curtainVdPrefix = "dooya_";
var writeResponseSize = 8;
// Список приводов: CurtainCh хранится как 0xHHLL, в протокол отправляется L,H.
var curtainDrivers = [
{ title: "Штора кухня 1", CurtainCh: 0x0101, readResponseSize: 8 },
{ title: "Штора кухня 2", CurtainCh: 0x0102, readResponseSize: 9 },
{ title: "Штора спальня 1", CurtainCh: 0x0201, readResponseSize: 9 },
{ title: "Штора спальня 2", CurtainCh: 0x0202, readResponseSize: 8 },
{ title: "Штора спальня 3", CurtainCh: 0x0203, readResponseSize: 9 }
];
function crc16Modbus(buffer) {
var crc = 0xFFFF;
for (var pos = 0; pos < buffer.length; pos++) {
crc ^= buffer[pos];
for (var i = 0; i < 8; i++) {
crc = (crc & 0x0001) !== 0 ? (crc >> 1) ^ 0xA001 : crc >> 1;
}
}
return [crc & 0xFF, (crc >> 8) & 0xFF];
}
// Переводит массив байтов в HEX-строку для RPC format="HEX".
function bytesToHexString(arr) {
return arr.map(function (byte) {
return ("0" + (byte & 0xFF).toString(16)).slice(-2);
}).join("");
}
// Разбирает HEX-строку ответа в массив байтов.
function hexStringToBytes(hex) {
var bytes = [];
for (var i = 0; i < hex.length; i += 2) {
bytes.push(parseInt(hex.substr(i, 2), 16));
}
return bytes;
}
// Проверяет два последних байта ответа как CRC16 Modbus.
function checkCrc16Modbus(hexStr) {
var bytes = hexStringToBytes(hexStr);
if (bytes.length < 3) return false;
var crcCalc = crc16Modbus(bytes.slice(0, -2));
return bytes[bytes.length - 2] === crcCalc[0] && bytes[bytes.length - 1] === crcCalc[1];
}
function requestRPC(clientID, requestID, messageType, message, responseSize) {
// Формируем JSON запрос:
var strJson = JSON.stringify({
params: {
response_size: responseSize,
format: messageType,
path: portCurtain,
baud_rate: speedPortCurtain,
parity: parityCurtain,
data_bits: 8,
stop_bits: stopBitCurtain,
msg: message
},
id: requestID
});
publish(pathRPC + clientID, strJson, 2, false);
}
// Собирает команду привода и добавляет CRC.
function CurtainRequestMsg(CurtainCh, commandCurtain, dataCode, dataData) {
// Сформируем набор байт команды.
var data = [0x55, CurtainCh & 0xFF, (CurtainCh >> 8) & 0xFF, commandCurtain, dataCode];
if (dataData !== null && dataData !== undefined) {
data = data.concat(Array.isArray(dataData) ? dataData : [dataData]);
}
return bytesToHexString(data.concat(crc16Modbus(data)));
}
function makeNewVirtualControl(vdName, vdTitle, nameControl, typeControl) {
if (getDevice(vdName) === undefined) {
log.info("Define new device " + vdName);
defineVirtualDevice(vdName, { title: vdTitle, cells: {} });
}
// Проверим есть ли уже контрол и если нет - создадим.
var devObj = getDevice(vdName);
if (devObj && !devObj.isControlExists(nameControl)) {
log.debug("Контрола " + nameControl + " нет, создаем.");
devObj.addControl(nameControl, typeControl);
}
}
function createCurtain(driver) {
var name = driver.title;
var ch = driver.CurtainCh;
var resSize = driver.readResponseSize || 9;
// Адрес VD и ключ runtime-флагов: 0xHHLL.
var addr = "0x" + ("0000" + (ch & 0xFFFF).toString(16).toUpperCase()).slice(-4);
var vd = curtainVdPrefix + addr;
var cmds = [
{ id: "Position", t: "Позиция", cfg: { type: "range", value: 0, min: 0, max: 100, readonly: false }, cmd: 0x03, code: 0x04 },
{ id: "Open", t: "Открыть", cfg: { type: "pushbutton", readonly: false }, cmd: 0x03, code: 0x01 },
{ id: "Stop", t: "Стоп", cfg: { type: "pushbutton", readonly: false }, cmd: 0x03, code: 0x03 },
{ id: "Close", t: "Закрыть", cfg: { type: "pushbutton", readonly: false }, cmd: 0x03, code: 0x02 }
];
cmds.forEach(function (c) {
c.cfg.title = c.t;
makeNewVirtualControl(vd, name, c.id, c.cfg);
defineRule(vd + "_rule_" + c.id, {
whenChanged: vd + "/" + c.id,
then: function () {
var data = null;
var req;
if (c.id === "Position") {
// Авто-обновление слайдера из ответа привода не должно отправлять команду обратно.
if (positionSetAuto[addr]) {
positionSetAuto[addr] = 0;
return;
}
data = (dev[vd] && dev[vd]["Position"] !== undefined) ? dev[vd]["Position"] : 0;
}
req = CurtainRequestMsg(ch, c.cmd, c.code, data);
log.info(name + " " + c.t + "=", req);
requestRPC(vd, 1, "HEX", req, writeResponseSize);
}
});
});
// Таймер опроса позиции.
var timerName = vd + "_timer";
if (typeof killTicker === "function") killTicker(timerName);
startTicker(timerName, pollIntervalMs);
defineRule(vd + "_rule", {
when: function () { return timers[timerName] && timers[timerName].firing; },
then: function () {
requestRPC(vd, 1, "HEX", CurtainRequestMsg(ch, 0x01, 0x02, 0x01), resSize);
}
});
// MQTT reply tracking
trackMqtt(pathRPC + vd + "/reply", function (message) {
var obj;
try {
obj = JSON.parse(message.value);
} catch (e) {
log.error("JSON parse error: " + e);
return;
}
if (!obj || obj.error !== null) return;
var hex = obj.result.response;
if (!checkCrc16Modbus(hex)) {
log.info("reply CRC ERROR!");
return;
}
var b = hexStringToBytes(hex);
// Ответ текущей позиции бывает в двух форматах: 8 байт и 9 байт.
var pos = (b.length === 8 && b[3] === 0x01 && b[4] === 0x01) ? b[5] :
(b.length === 9 && b[3] === 0x01 && b[4] === 0x02) ? b[6] : null;
if (pos !== null && dev[vd] && pos !== dev[vd]["Position"]) {
positionSetAuto[addr] = 1;
dev[vd]["Position"] = pos;
}
});
return 0;
}
function createAllCurtain() {
curtainDrivers.forEach(createCurtain);
}
exports.createCurtain = createCurtain;
exports.createAllCurtain = createAllCurtain;