
Таким образом я описал строение системы управляемых программных аксессуаров.
Упрощенная модель включает в себя главный процесс(bobaoskit.worker) и скрипты аксессуаров(использующие объекты bobaoskit.sdk и bobaoskit.accessory). От главного процесса идет запрос к аксессуару для контроля некоторых полей. От аксессуара, в свою очередь, идет запрос к главному на обновление статуса.
В качестве примера возьмем обычное реле.
При входящей команде реле может иногда не изменить свое положение в силу различных причин(зависло оборудование, и прочее). Соответственно, сколько мы не будет отправять команд, статус меняться не будет. И, в другой ситауции, реле может поменять свое состояние при команде от сторонней системы. Его статус в таком случае изменится, скрипт аксессуара может среагировать на входящее событие о смене статуса и отправить запрос главному процессу.
Мотивация
Внедрив на несколько объектов Apple HomeKit, я начал искать похожее на Android, т.к. сам из iOS устройств имею только рабочий iPad. Основным критерием была возможность работать в локальной сети, без облачных сервисов. Так же, что недоставало в HomeKit — ограниченность информации. Для примера можно взять термостат. Все его управление сводится к выбору режима работы(выкл, нагрев, охлаждение и авто) и заданной температуре. Проще — лучше, но, по моему мнению, не всегда. Не хватает диагностической информации. Например, работает ли кондиционер, конвектор, какие параметры вентиляции. Возможно, кондиционер работать не может из-за внутренней ошибки. Учитывая то, что эту информацию можно считать, решено было написать свою реализацию.
Можно было посмотреть варианты, такие как ioBroker, OpenHAB, home-assistant.
Но на node.js из перечисленных только ioBroker(пока пишу статью, обратил внимание, что redis тоже участвует в процессе). И к тому моменту обнаружил каким образом можно организовать межпроцессное взаимодействие и интересно было разобраться с redis, который на слуху в последнее время.
Так же можно обратить внимание на следующую спецификацию:
Устройство

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.
Что можно было сделать лучше:
- Бинарный протокол позволил бы отправлять меньше данных. С другой стороны,
JSONбыстрее в разработке и понимании. Так же бинарный протокол требует стандартизации.
На этом все, буду рад любой обратной связи.
