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 the 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.
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:
Redis
helps interprocess communication, and also acts as a database for accessories.
The bobaoskit.worker
module happens a request queue (on top of redis
using bee-queue
), executes a request, writes / reads from a database.
In user scripts, the bobaoskit.accessory
object listens to a separate bee-queue
for this particular accessory, performs the prescribed actions, sends requests to the main process queue via the bobaoskit.sdk
object.
All requests and published messages are strings in JSON
format, contain the field method
and payload
. Fields are required, even if payload = null
.
bobaoskit.worker
:ping
, payload: null
.get general info
, payload: null
clear accessories
, payload: null
,add accessory
, { id: "accessoryId", type: "switch/sensor/etc", name: "Accessory Display Name", control: [<array of control fields>], status: [<array of status fields>] }
remove accessory
, payload: accessoryId/[acc1id, acc2id, ...]
get accessory info
, payload: null/accId/[acc1id, acc2id...]
payload
field, you can send an accessory null
/ id
id
. If sent null
, then in response will come information about all existing accessories.get status value
, payload: {id: accessoryId, status: fieldId}
payload
field, you can send an object of the form {id: accessoryId, status: fieldId}
, (where the status
field can be an array of fields), or payload
can be an array of objects of this type.update status value
, payload: {id: accessoryId, status: {field: fieldId, value: value}
payload
field, you can send an object of the form {id: accessoryId, status: {field: fieldId, value: value}}
, (where the status
field can be an array of {field: fieldId, value: value}
), or payload
can be an array of objects such kind of.control accessory value
, payload: {id: accessoryId, control: {field: fieldId, value: value}}
.payload
field, you can send an object of the form {id: accessoryId, control: {field: fieldId, value: value}}
, (where the control
field can be an array of {field: fieldId, value: value}
), or payload
can be an array of objects such kind of.In response to any request, if successful, 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 (defined in config.json
) are also published in the following cases: all accessories ( clear accessories
) are cleared; add accessory
; accessory removed ( remove accessory
); Accessory updated status ( update status value
).
Broadcast messages also contain two fields: method
and payload
.
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 Sdk
object to access 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 BobaosKit.Accessory({..})
object is a wrapper over the BobaosKit.Sdk(...)
object.
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 also EventEmitter
.Sdk
calls functions for ready
and broadcasted event
.Accessory
calls functions on ready
, error
, control accessory value
events.
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);
bobaoskit.worker
listens on the WebSocket port defined in ./config.json
.
Incoming requests are JSON
strings, which must have the following fields: request_id
, method
and payload
.
API is limited to the following requests:
ping
, payload: null
get general info
, payload: null
,get accessory info
, payload: null/accId/[acc1Id, ...]
get status value
, payload: {id: accId, status: field1/[field1, ...]}/[{id: ...}...]
control accessory value
, payload: {id: accId, control: {field: field1, value: value}/[{field: .. value: ..}]}/[{id: ...}, ...]
The get status value
, control accessory value
get status value
methods take the payload
field as a single object, or as an array. The control/status
fields inside the payload
can also be one object or an array.
The following events are sent from the server to all clients:
clear accessories
, payload: nullremove accessory
, payload: accessory idadd accessory, payload
: {id: ...}update status value, payload
: {id: ...}The application advertises the WebSocket port on the local network as a _bobaoskit._tcp
service, thanks to the npm dnssd
module.
There will be a separate article about how the application with the video is written and about the flutter
impressions.
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 by fitting into the model control < == > update
.
What could be done better:
JSON
faster in development and understanding. The binary protocol also requires standardization.That's all, I will be glad to any feedback.
Source: https://habr.com/ru/post/437846/