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