bobaoskit — аксессуары, dnssd и WebSocket


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


    Упрощенная модель включает в себя главный процесс(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 быстрее в разработке и понимании. Так же бинарный протокол требует стандартизации.

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

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое