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:


    Web Thing API


    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 field payloadyou can send null/ idaccessory / mass id. If sent null, 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 field statuscan be an array of fields), or it payloadcan be an array of objects of this type.
    • method:, update status valuepayload: {id: accessoryId, status: {field: fieldId, value: value}
      You payloadcan send a view object in a {id: accessoryId, status: {field: fieldId, value: value}}field (where the field statuscan be an array {field: fieldId, value: value}), or it payloadcan 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 field controlcan be an array {field: fieldId, value: value}), or it payloadcan 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:


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


    Also popular now: