bobaoskit - accessories, dnssd and websocket

Thus, I described the structure of the system of controlled software accessories.
The simplified model includes the main process ( bobaoskit.worker) and accessory scripts (using objects bobaoskit.sdkand bobaoskit.accessory). From the main process comes a request to the accessory to control some fields. From the accessory, in turn, there is a request to the principal on the status update.
As an example, take the usual relay.
When an incoming command is received, the relay may sometimes not change its position for various reasons (the equipment is stuck, etc.). Accordingly, how many we will not send commands, the status will not change. And, in another situation, the relay can change its state with a command from a third-party system. In this case, its status will change, the accessory script may react to an incoming event of a status change and send a request to the main process.
Motivation
Having introduced Apple HomeKit to several objects, I began to look for something similar to Android, because I only have a working iPad from iOS devices. The main criterion was the ability to work in a local network, without cloud services. Also, what was missing in HomeKit is the limited information. For example, you can take a thermostat. All his management is reduced to the choice of operating mode (off, heating, cooling and auto) and a given temperature. Simpler is better, but in my opinion, not always. Not enough diagnostic information. For example, whether the air conditioner, convector, what ventilation parameters. The air conditioner may not work due to an internal error. Considering that this information can be considered, it was decided to write its own implementation.
It was possible to look at options such as ioBroker, OpenHAB, home-assistant.
But on node.js from the listed only ioBroker (while I am writing an article, I noticed that redis also participates in the process). And by that moment I discovered how to organize interprocess communication and it was interesting to deal with redis, which has been heard recently.
You can also pay attention to the following specification:
Device

Redis helps interprocess communication, and also acts as a database for accessories.
The module bobaoskit.workertries the request queue (over redisusing bee-queue), executes the request, writes / reads from the database.
In user scripts, the object bobaoskit.accessorylistens to a separate queue bee-queuefor this particular accessory, performs the prescribed actions, sends requests to the main process queue through the object bobaoskit.sdk.
Protocol
All requests and published messages are JSONformatted strings with fields methodand payload. Fields are required, even if payload = null.
Requests to bobaoskit.worker:
- method:
ping, payload The:null. - method:,
get general infopayload:null - method:
clear accessories, payload The:null, - method:
add accessory,
payload The:
{
id: "accessoryId",
type: "switch/sensor/etc",
name: "Accessory Display Name",
control: [<array of control fields>],
status: [<array of status fields>]
}- method:,
remove accessorypayload:accessoryId/[acc1id, acc2id, ...] - method:,
get accessory infopayload:null/accId/[acc1id, acc2id...]
In the fieldpayloadyou can sendnull/idaccessory / massid. If sentnull, then in response will come information about all existing accessories. - method:,
get status valuepayload:{id: accessoryId, status: fieldId}
In a field,payloadyou can send a view object{id: accessoryId, status: fieldId}(where the fieldstatuscan be an array of fields), or itpayloadcan be an array of objects of this type. - method:,
update status valuepayload:{id: accessoryId, status: {field: fieldId, value: value}
Youpayloadcan send a view object in a{id: accessoryId, status: {field: fieldId, value: value}}field (where the fieldstatuscan be an array{field: fieldId, value: value}), or itpayloadcan be an array of objects of this type. - method:
control accessory value, payload The:{id: accessoryId, control: {field: fieldId, value: value}}.
In the field,payloadyou can send an object of the form{id: accessoryId, control: {field: fieldId, value: value}}, (where the fieldcontrolcan be an array{field: fieldId, value: value}), or itpayloadcan be an array of objects of this type.
In response to any request in case of success, a message of the following type is received:
{ method: "success", payload: <...> }
In case of failure:
{ method: "error", payload: "Error description" }
Messages to the redis PUB/SUBchannel are also published (defined in config.json) in the following cases: all accessories are cleaned ( clear accessories); accessory added ( add accessory); accessory deleted ( remove accessory); Accessory updated status ( update status value).
Broadcast messages also contain two fields: methodand payload.
Client SDK
Description
The client SDK ( bobaoskit.accessory) allows you to call the above methods from jsscripts.
Inside the module are two constructor objects. The first creates an object Sdkfor access to the above methods, and the second creates an accessory - a wrapper on top of these functions.
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"
});The sdk object supports Promise-methods:
sdk.ping();
sdk.getGeneralInfo();
sdk.clearAccessories();
sdk.addAccessory(payload);
sdk.removeAccessory(payload);
sdk.getAccessoryInfo(payload);
sdk.getStatusValue(payload);
sdk.updateStatusValue(payload);
sdk.controlAccessoryValue(payload);The object BobaosKit.Accessory({..})is wrapped on top of the object BobaosKit.Sdk(...).
Further I will show how it turns around:
// из исходного кода модуля
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 });
};Both objects are the same EventEmitter. Sdkcalls functions by events readyand broadcasted event. AccessoryIt calls functions on events ready, error, control accessory value.
Example
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.workerlistens to the websocket port defined in ./config.json.
Incoming requests - JSONline, which must have the following fields: request_id, methodand payload.
API is limited to the following requests:
- method:,
pingpayload:null - method:
get general info, payload The:null, - method:,
get accessory infopayload:null/accId/[acc1Id, ...] - method:,
get status valuepayload:{id: accId, status: field1/[field1, ...]}/[{id: ...}...] - method:,
control accessory valuepayload:{id: accId, control: {field: field1, value: value}/[{field: .. value: ..}]}/[{id: ...}, ...]
Methods get status valuethat control accessory valuetake a field payloadas a single object, or as an array. Fields control/statusinside payloadcan also be a single object or an array.
The following events are sent from the server to all clients:
- method:,
clear accessoriespayload: null - method:,
remove accessorypayload: accessory id - method::
add accessory, payload{id: ...} - method::
update status value, payload{id: ...}
dnssd
The application advertises the WebSocket port on the local network as a service _bobaoskit._tcp, thanks to the npm module dnssd.
Demo
There flutterwill be a separate article about how the app is written with video and impressions of it.
Afterword
Thus, it turned out a simple system for managing software accessories.
Accessories can be opposed to objects from the real world: buttons, sensors, switches, thermostats, radio. Since there is no standardization, you can implement any accessories, fitting into the model control < == > update.
What could be done better:
- A binary protocol would allow sending less data. On the other hand,
JSONfaster in development and understanding. The binary protocol also requires standardization.
That's all, I will be glad to any feedback.