Приводы штор dooya

  1. Поправил для себя рекомендованный код
  2. Удалил свои апоплексические приводы штор (в данном контексте красные) из устройств.
  3. Вызвал это в моем основном скрипте createCurtain(“Штора кухня 1”, 0x01, 0x01)…для всех приводов
  4. Все устройства подхватились на лету, даже менять больше ничего не пришлось в сценариях, благодаря var curtainVdPrefix = “dooya_”
  5. Все шторы полноценно работают…
  6. 4 года этого счастья ждал, а оно было рядом
  7. Всем спасибо!
// универсальный скрипт подходит для привода 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;