
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: nullclear 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: nullget 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/