Привод штор AT6501

Оставляю скрипт управления приводом AT6501, если пригодится кому-нибудь.
Добавлять экземпляры приводов нужно в самом конце по примеру:

// createBlind(NameBlind, portBlind, speedPortBlind, parityBlind, stopBitBlind, blindID, blindCh_L, blindCh_H)
createBlind("Штора кухня", "/dev/ttyRS485-2", 9600, "N", 1, 0, 0x01, 0x02);

Внимание: Идентификатор привода (BlindID) и Имя(NameBlind) должны быть уникальны.

Скрипт правила:

// хранит флаги типа операции установки слайдера позиции
var positionSetAuto = {};

// Путь к RPC
var pathRPC = "/rpc/v1/wb-mqtt-serial/port/Load/";

function crc16Modbus(buffer) {
    var crc = 0xFFFF;
    for (var pos = 0; pos < buffer.length; pos++) {
        crc ^= buffer[pos];
        for (var i = 0; i < 8; i++) {
            if ((crc & 0x0001) !== 0) {
                crc >>= 1;
                crc ^= 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    // Return as a 16-bit integer (LSB first, Modbus style)
    return [crc & 0xFF, (crc >> 8) & 0xFF];
}

function getByteAt(hexStr, pos) {
    // Each byte is 2 hex characters
    var start = pos * 2;
    if (start + 2 > hexStr.length) {
        throw new Error("Position out of range");
    }
    return parseInt(hexStr.substr(start, 2), 16);
}

function bytesToHexString(arr) {
    var result = '';
    for (var i = 0; i < arr.length; i++) {
        var hex = arr[i].toString(16);
        if (hex.length < 2) {
            hex = '0' + hex;
        }
        result += hex;
    }
    return result;
}

function hexStringToBytes(hex) {
    var bytes = [];
    for (var i = 0; i < hex.length; i += 2) {
        bytes.push(parseInt(hex.substr(i, 2), 16));
    }
    return bytes;
}

function checkCrc16Modbus(hexStr) {
    var bytes = hexStringToBytes(hexStr);
    if (bytes.length < 3) {
        return false; // Not enough data
    }
    // Split data and CRC
    var dataBytes = bytes.slice(0, -2);
    var crcGiven = [bytes[bytes.length - 2], bytes[bytes.length - 1]];
    // Calculate CRC
    var crcCalc = crc16Modbus(dataBytes);
    // Compare
    return (crcGiven[0] === crcCalc[0] && crcGiven[1] === crcCalc[1]);
}

function createBlind(NameBlind, portBlind, speedPortBlind, parityBlind, stopBitBlind, blindID, blindCh_L, blindCh_H) {
    // Создаем виртуальное устройство
    makeNewVirtualControl(NameBlind, "Position", { type: "range", value: 0, min: 0, max: 100, readonly: false });
    makeNewVirtualControl(NameBlind, "Up", { type: "pushbutton", readonly: false });
    makeNewVirtualControl(NameBlind, "Stop", { type: "pushbutton", readonly: false });
    makeNewVirtualControl(NameBlind, "Down", { type: "pushbutton", readonly: false });

    // Up
    defineRule(NameBlind + "_rule_Up", {
        whenChanged: NameBlind + "/Up",
        then: function () {
            log.info(NameBlind + "_Up");
            var req = blindRequestMsg(blindID, blindCh_L, blindCh_H, 0x03, 0x01, null);
            log.info("Up=", req);
            requestRPC(portBlind, speedPortBlind, parityBlind, stopBitBlind, NameBlind, 1, "HEX", req, 8);
        }
    });

    // Stop
    defineRule(NameBlind + "_rule_Stop", {
        whenChanged: NameBlind + "/Stop",
        then: function () {
            var req = blindRequestMsg(blindID, blindCh_L, blindCh_H, 0x03, 0x03, null);
            log.info("req_Stop=", req);
            requestRPC(portBlind, speedPortBlind, parityBlind, stopBitBlind, NameBlind, 1, "HEX", req, 8);
        }
    });

    // Down
    defineRule(NameBlind + "_rule_Down", {
        whenChanged: NameBlind + "/Down",
        then: function () {
            var req = blindRequestMsg(blindID, blindCh_L, blindCh_H, 0x03, 0x02, null);
            requestRPC(portBlind, speedPortBlind, parityBlind, stopBitBlind, NameBlind, 1, "HEX", req, 8);
        }
    });

    // Position slider
    defineRule(NameBlind + "_rule_position", {
        whenChanged: NameBlind + "/Position",
        then: function (value) {
            var is_auto_set = positionSetAuto[blindID];
            if (is_auto_set === 0) {
                var val = (dev[NameBlind] && dev[NameBlind]["Position"] !== undefined) ? dev[NameBlind]["Position"] : 0;
                var req = blindRequestMsg(blindID, blindCh_L, blindCh_H, 0x03, 0x04, val);
                requestRPC(portBlind, speedPortBlind, parityBlind, stopBitBlind, NameBlind, 1, "HEX", req, 8);
            } else {
                positionSetAuto[blindID] = 0;
            }
        }
    });

    // Таймер опроса позиции (500мс)
    startTicker(NameBlind + "_timer", 500);
    defineRule(NameBlind + "_rule", {
        when: function () { return timers[NameBlind + "_timer"] && timers[NameBlind + "_timer"].firing; },
        then: function () {
            var req = blindRequestMsg(blindID, blindCh_L, blindCh_H, 0x01, 0x02, 0x01);
            log.info("req_timer " + NameBlind + "=", req);
            requestRPC(portBlind, speedPortBlind, parityBlind, stopBitBlind, NameBlind, 1, "HEX", req, 9);
        }
    });

    // MQTT reply tracking
    trackMqtt(pathRPC + NameBlind + "/reply", function (message) {
        log.info("from " + NameBlind + " name: {}, value: {}".format(message.topic, message.value));
        var replyObj;
        try {
            replyObj = JSON.parse(message.value);
        } catch (e) {
            log.error("JSON parse error: " + e);
            return;
        }
        if (replyObj && replyObj.error === null) {
            var hexStr = replyObj.result.response;
            log.info("from " + NameBlind + " replyObj.result.response: {}".format(hexStr));
            if (checkCrc16Modbus(hexStr)) {
                log.info("reply CRC is correct!");
                // если получена текущая позиция
                if (getByteAt(hexStr, 3) === 0x01 && getByteAt(hexStr, 4) === 0x02) {
                    var byte6 = getByteAt(hexStr, 6);
                    if (dev[NameBlind] && byte6 !== dev[NameBlind]["Position"]) {
                        // устанавливаем флаг авто-обновления слайдера
                        positionSetAuto[blindID] = 1;
                        dev[NameBlind]["Position"] = byte6;
                    }
                }
            } else {
                log.info("reply CRC ERROR!");
            }
        }
    });

    return 0;
}

function requestRPC(modbusPort, modbusSpeed, modbusParity, reqStopbit, clientID, requestID, messageType, message, responseSize) {
    // Формируем JSON запрос:
    var strJson = JSON.stringify({
        params: {
            response_size: responseSize,
            format: messageType,
            path: modbusPort,
            baud_rate: modbusSpeed,
            parity: modbusParity,
            data_bits: 8,
            stop_bits: reqStopbit,
            msg: message
        },
        id: requestID
    });
    publish(pathRPC + clientID, strJson, 2, false);
}

function blindRequestMsg(blindID, blindCh_L, blindCh_H, commandBlind, dataCode, dataData) {
    // Сформируем набор байт команды.
    var data = [0x55, blindCh_L, blindCh_H, commandBlind, dataCode];
    if (dataData !== null && dataData !== undefined) {
        if (Object.prototype.toString.call(dataData) === '[object Array]') {
            data = data.concat(dataData);
        } else {
            data.push(dataData);
        }
    }
    var crc = crc16Modbus(data);
    data = data.concat(crc);
    var bytesStr = bytesToHexString(data);
    return bytesStr;
}

function makeNewVirtualControl(vdName, nameControl, typeControl) {
    if (getDevice(vdName) === undefined) {
        log.info("Define new");
        defineVirtualDevice(vdName, {
            title: vdName,
            cells: {},
        });
    }
    // Проверим есть ли уже контрол и если нет - создадим.
    var devObj = getDevice(vdName);
    if (devObj && !devObj.isControlExists(nameControl)) {
        log.debug("Контрола " + nameControl + " нет, создаем.");
        devObj.addControl(nameControl, typeControl);
    }
}

// Создаем виртуальное устройство, с контролами для шторы
// ID делаем уникальным
// createBlind(NameBlind, portBlind, speedPortBlind, parityBlind, stopBitBlind, blindID, blindCh_L, blindCh_H)
//                имя               порт        bod parity stopbit  ID address
createBlind("Штора кухня", "/dev/ttyRS485-2", 9600, "N", 1, 0, 0x01, 0x02);

1 лайк