📜 ⬆️ ⬇️

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 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.


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:


Web Thing API


Device



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.


Protocol


All requests and published messages are strings in JSON format, contain the field method and payload . Fields are required, even if payload = null .


Requests to bobaoskit.worker :



 { id: "accessoryId", type: "switch/sensor/etc", name: "Accessory Display Name", control: [<array of control fields>], status: [<array of status fields>] } 


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 .


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 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.


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 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:



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:



dnssd


The application advertises the WebSocket port on the local network as a _bobaoskit._tcp service, thanks to the npm dnssd module.


Demo



There will be a separate article about how the application with the video is written and about the flutter impressions.


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 by fitting into the model control < == > update .


What could be done better:


  1. 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.



Source: https://habr.com/ru/post/437846/