Таким образом я описал строение системы управляемых программных аксессуаров.


Упрощенная модель включает в себя главный процесс(bobaoskit.worker) и скрипты аксессуаров(использующие объекты bobaoskit.sdk и bobaoskit.accessory). От главного процесса идет запрос к аксессуару для контроля некоторых полей. От аксессуара, в свою очередь, идет запрос к главному на обновление статуса.


В качестве примера возьмем обычное реле.


При входящей команде реле может иногда не изменить свое положение в силу различных причин(зависло оборудование, и прочее). Соответственно, сколько мы не будет отправять команд, статус меняться не будет. И, в другой ситауции, реле может поменять свое состояние при команде от сторонней системы. Его статус в таком случае изменится, скрипт аксессуара может среагировать на входящее событие о смене статуса и отправить запрос главному процессу.


Мотивация


Внедрив на несколько объектов Apple HomeKit, я начал искать похожее на Android, т.к. сам из iOS устройств имею только рабочий iPad. Основным критерием была возможность работать в локальной сети, без облачных сервисов. Так же, что недоставало в HomeKit — ограниченность информации. Для примера можно взять термостат. Все его управление сводится к выбору режима работы(выкл, нагрев, охлаждение и авто) и заданной температуре. Проще — лучше, но, по моему мнению, не всегда. Не хватает диагностической информации. Например, работает ли кондиционер, конвектор, какие параметры вентиляции. Возможно, кондиционер работать не может из-за внутренней ошибки. Учитывая то, что эту информацию можно считать, решено было написать свою реализацию.


Можно было посмотреть варианты, такие как ioBroker, OpenHAB, home-assistant.
Но на node.js из перечисленных только ioBroker(пока пишу статью, обратил внимание, что redis тоже участвует в процессе). И к тому моменту обнаружил каким образом можно организовать межпроцессное взаимодействие и интересно было разобраться с redis, который на слуху в последнее время.


Так же можно обратить внимание на следующую спецификацию:


> Web Thing API


Устройство



Redis помогает межпроцессному взаимодействию, а так же выступает в качестве базы данных для аксессуаров.


Модуль bobaoskit.worker случает очередь запросов(поверх redis с использованием bee-queue), исполняет запрос, записывает/читает из базы данных.


В пользовательских скриптах объект bobaoskit.accessory слушает отдельную очередь bee-queue для данного конкретного аксессуара, выполняет прописанные действия, отправляет запросы в очередь главного процесса посредством объекта bobaoskit.sdk.


Протокол


Все запросы и опубликованные сообщения — строки в JSON формате, содержат поля method и payload. Поля обязательны, даже если payload = null.


Запросы к bobaoskit.worker:


  • method: ping, payload: null.
  • method: get general info, payload: null
  • method: clear accessories, payload: null,
  • method: add accessory,
    payload:

{
  id: "accessoryId",
  type: "switch/sensor/etc",
  name: "Accessory Display Name",
  control: [<array of control fields>],
  status: [<array of status fields>]
}

  • method: remove accessory, payload: accessoryId/[acc1id, acc2id, ...]
  • method: get accessory info, payload: null/accId/[acc1id, acc2id...]
    В поле payload можно отправить null/id аксессуара/массов id. Если отправлен null, тогда в ответ придет информация о всех существующих аксессуарах.
  • method: get status value, payload: {id: accessoryId, status: fieldId}
    В поле payload можно отправить объект вида {id: accessoryId, status: fieldId}, (где поле status может быть массивом полей), либо payload может быть массивом объектов такого вида.
  • method: update status value, payload: {id: accessoryId, status: {field: fieldId, value: value}
    В поле payload можно отправить объект вида {id: accessoryId, status: {field: fieldId, value: value}}, (где поле status может быть массивом {field: fieldId, value: value}), либо payload может быть массивом объектов такого вида.
  • method: control accessory value, payload: {id: accessoryId, control: {field: fieldId, value: value}}.
    В поле payload можно отправить объект вида {id: accessoryId, control: {field: fieldId, value: value}}, (где поле control может быть массивом {field: fieldId, value: value}), либо payload может быть массивом объектов такого вида.

В качестве ответа на любой запрос в случае успеха приходит сообщение вида:


{ method: "success", payload: <...> }


В случае неудачи:


{ method: "error", payload: "Error description" }


Также публикуются сообщения в redis PUB/SUB канал(определенный в config.json) в следующих случаях: очищены все аксессуары(clear accessories); аксессуар добавлен(add accessory); аксессуар удален(remove accessory); аксесуар обновил статус(update status value).


Широковещательные сообщения также содержат два поля: method и payload.


Клиентский SDK


Описание


Клиентский SDK(bobaoskit.accessory) позволяет вызывать вышеперечисленные методы из js скриптов.


Внутри модуля два объекта конструктора. Первый создает объект Sdk для доступа к вышеперечисленным методам, а второй создает аксессуар — обертку поверх этих функций.


const BobaosKit = require("bobaoskit.accessory");

// Создаем объект sdk. 
// Не обязательно,
// но если планируется много аксессуаров,
// то лучше использовать общий sdk, 
const sdk = BobaosKit.Sdk({
  redis: redisClient // optional
  job_channel: "bobaoskit_job", // optional. default: bobaoskit_job
  broadcast_channel: "bobaoskit_bcast" // optional. default: bobaoskit_bcast
});

// Создаем аксессуар
const dummySwitchAcc = BobaosKit.Accessory({
    id: "dummySwitch", // required
    name: "Dummy Switch", // required
    type: "switch", // required
    control: ["state"], // requried. Поля, которыми можем управлять.
    status: ["state"], // required. Поля со значениями.
    sdk: sdk, // optional. 
    // Если не определен, новый объект sdk будет создан
    // со следующими опциональными параметрами
    redis: undefined,
    job_channel: "bobaoskit_job",
    broadcast_channel: "bobaoskit_bcast"
  });

Объект sdk поддерживает Promise-методы:


sdk.ping();
sdk.getGeneralInfo();
sdk.clearAccessories();
sdk.addAccessory(payload);
sdk.removeAccessory(payload);
sdk.getAccessoryInfo(payload);
sdk.getStatusValue(payload);
sdk.updateStatusValue(payload);
sdk.controlAccessoryValue(payload);

Объект BobaosKit.Accessory({..}) является оберткой поверх объекта BobaosKit.Sdk(...).


Далее покажу каким образом это оборачивается:


// из исходного кода модуля
self.getAccessoryInfo = _ => {
  return _sdk.getAccessoryInfo(id);
};
self.getStatusValue = payload => {
  return _sdk.getStatusValue({ id: id, status: payload });
};
self.updateStatusValue = payload => {
  return _sdk.updateStatusValue({ id: id, status: payload });
};

Оба объекта являются так же EventEmitter.
Sdk вызывает функции по событиям ready и broadcasted event.
Accessory вызывает функции по событиям ready, error, control accessory value.


Пример


const BobaosKit = require("bobaoskit.accessory");
const Bobaos = require("bobaos.sub");

// init bobaos with default params
const bobaos = Bobaos();

// init sdk with default params
const accessorySdk = BobaosKit.Sdk();

const SwitchAccessory = params => {
  let { id, name, controlDatapoint, stateDatapoint } = params;

  // init accessory
  const swAcc = BobaosKit.Accessory({
    id: id,
    name: name,
    type: "switch",
    control: ["state"],
    status: ["state"],
    sdk: accessorySdk
  });

  // по входящему запросу на переключение поля state
  // отправляем запрос в шину KNX посредством bobaos
  swAcc.on("control accessory value", async (payload, cb) => {
    const processOneAccessoryValue = async payload => {
      let { field, value } = payload;
      if (field === "state") {
        await bobaos.setValue({ id: controlDatapoint, value: value });
      }
    };

    if (Array.isArray(payload)) {
      await Promise.all(payload.map(processOneAccessoryValue));
      return;
    }

    await processOneAccessoryValue(payload);
  });

  const processOneBaosValue = async payload => {
    let { id, value } = payload;
    if (id === stateDatapoint) {
      await swAcc.updateStatusValue({ field: "state", value: value });
    }
  };

  // при входящем значении с шины KNX 
  // обновляем поле state аксессуара
  bobaos.on("datapoint value", payload => {
    if (Array.isArray(payload)) {
      return payload.forEach(processOneBaosValue);
    }

    return processOneBaosValue(payload);
  });

  return swAcc;
};

const switches = [
  { id: "sw651", name: "Санузел", controlDatapoint: 651, stateDatapoint: 652 },
  { id: "sw653", name: "Щитовая 1", controlDatapoint: 653, stateDatapoint: 653 },
  { id: "sw655", name: "Щитовая 2", controlDatapoint: 655, stateDatapoint: 656 },
  { id: "sw657", name: "Комната 1", controlDatapoint: 657, stateDatapoint: 658 },
  { id: "sw659", name: "Кинотеатр", controlDatapoint: 659, stateDatapoint: 660 }
];

switches.forEach(SwitchAccessory);

WebSocket API


bobaoskit.worker слушает WebSocket порт, определенный в ./config.json.


Входящие запросы — JSON строки, которые должны иметь следующие поля: request_id, method и payload.


API ограничен следующими запросами:


  • method: ping, payload: null
  • method: get general info, payload: null,
  • method: get accessory info, payload: null/accId/[acc1Id, ...]
  • method: get status value, payload: {id: accId, status: field1/[field1, ...]}/[{id: ...}...]
  • method: control accessory value, payload: {id: accId, control: {field: field1, value: value}/[{field: .. value: ..}]}/[{id: ...}, ...]

Методы get status value, control accessory value принимают поле payload как один объект, либо как массив. Поля control/status внутри payload так же могут быть как одним объектом, так и массивом.


Так же рассылаются следующие сообщения о событиях от сервера всем клиентам:


  • method: clear accessories, payload: null
  • method: remove accessory, payload: accessory id
  • method: add accessory, payload: {id: ...}
  • method: update status value, payload: {id: ...}

dnssd


Приложение рекламирует WebSocket порт в локальной сети как сервис _bobaoskit._tcp, благодаря npm модулю dnssd.


Демо



О том как написано приложение с видео и о впечатлениях от flutter будет отдельная статья.


Послесловие


Таким образом, получилась простая система для управления программными аксессуарами.
Аксессуары можно противопоставить объектам из реального мира: кнопки, датчики, переключатели, термостаты, радио. Поскольку нет стандартизации, можно реализовывать любые аксессуары, укладываясь в модель control < == > update.


Что можно было сделать лучше:


  1. Бинарный протокол позволил бы отправлять меньше данных. С другой стороны, JSON быстрее в разработке и понимании. Так же бинарный протокол требует стандартизации.

На этом все, буду рад любой обратной связи.

Комментарии (0)