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.sdk
and 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.worker
tries the request queue (over redis
using bee-queue
), executes the request, writes / reads from the database.
In user scripts, the object bobaoskit.accessory
listens to a separate queue bee-queue
for 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 JSON
formatted strings with fields method
and payload
. Fields are required, even if payload = null
.
Requests to bobaoskit.worker
:
- method:
ping
, payload The:null
. - method:,
get general info
payload: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 accessory
payload:accessoryId/[acc1id, acc2id, ...]
- method:,
get accessory info
payload:null/accId/[acc1id, acc2id...]
In the fieldpayload
you can sendnull
/id
accessory / massid
. If sentnull
, then in response will come information about all existing accessories. - method:,
get status value
payload:{id: accessoryId, status: fieldId}
In a field,payload
you can send a view object{id: accessoryId, status: fieldId}
(where the fieldstatus
can be an array of fields), or itpayload
can be an array of objects of this type. - method:,
update status value
payload:{id: accessoryId, status: {field: fieldId, value: value}
Youpayload
can send a view object in a{id: accessoryId, status: {field: fieldId, value: value}}
field (where the fieldstatus
can be an array{field: fieldId, value: value}
), or itpayload
can be an array of objects of this type. - method:
control accessory value
, payload The:{id: accessoryId, control: {field: fieldId, value: value}}
.
In the field,payload
you can send an object of the form{id: accessoryId, control: {field: fieldId, value: value}}
, (where the fieldcontrol
can be an array{field: fieldId, value: value}
), or itpayload
can 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/SUB
channel 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: method
and payload
.
Client SDK
Description
The client SDK ( bobaoskit.accessory
) allows you to call the above methods from js
scripts.
Inside the module are two constructor objects. The first creates an object Sdk
for 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
. Sdk
calls functions by events ready
and broadcasted event
. Accessory
It 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.worker
listens to the websocket port defined in ./config.json
.
Incoming requests - JSON
line, which must have the following fields: request_id
, method
and payload
.
API is limited to the following requests:
- method:,
ping
payload:null
- method:
get general info
, payload The: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: ...}, ...]
Methods get status value
that control accessory value
take a field payload
as a single object, or as an array. Fields control/status
inside payload
can also be a single object or an array.
The following events are sent from the server to all clients:
- method:,
clear accessories
payload: null - method:,
remove accessory
payload: 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 flutter
will 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,
JSON
faster in development and understanding. The binary protocol also requires standardization.
That's all, I will be glad to any feedback.