bobaoskit - accesorios, dnssd y WebSocket


Por lo tanto, describí la estructura de un sistema de accesorios de software administrados.


El modelo simplificado incluye el proceso principal ( bobaoskit.worker ) y los scripts accesorios (que utilizan los bobaoskit.accessory bobaoskit.sdk y bobaoskit.accessory ). Desde el proceso principal se solicita un accesorio para controlar algunos campos. Desde el accesorio, a su vez, hay una solicitud a lo principal para actualizar el estado.


Tome un relevo ordinario como ejemplo.


Con un comando entrante, el relé a veces puede no cambiar su posición debido a varias razones (congelación del equipo, etc.). En consecuencia, cuánto no enviaremos equipos, el estado no cambiará. Y, en otra situación, el relé puede cambiar su estado cuando lo ordena un sistema de terceros. En este caso, su estado cambiará, el script accesorio puede reaccionar ante un evento entrante sobre el cambio de estado y enviar una solicitud al proceso principal.


Motivación


Habiendo implementado Apple HomeKit en varios objetos, comencé a buscar algo similar a Android, porque Yo solo tengo un iPad que funciona desde dispositivos iOS. El criterio principal era la capacidad de trabajar en una red local, sin servicios en la nube. Además, lo que faltaba en HomeKit era la información limitada. Por ejemplo, puedes tomar un termostato. Todo su control se reduce a la elección del modo de funcionamiento (apagado, calefacción, refrigeración y automático) y la temperatura establecida. Más simple es mejor, pero, en mi opinión, no siempre. No hay suficiente información de diagnóstico. Por ejemplo, si el aire acondicionado, convector, qué parámetros de ventilación están funcionando. Quizás el aire acondicionado no pueda funcionar debido a un error interno. Dado que esta información puede ser considerada, se decidió escribir su implementación.


Uno podría mirar opciones como ioBroker, OpenHAB, home-assistant.
Pero en node.js, solo ioBroker de los enumerados (mientras escribo un artículo, noté que redis también está involucrado en el proceso). Y para entonces, había descubierto cómo organizar la comunicación entre procesos, y fue interesante tratar con redis, que se ha escuchado últimamente.


También puede prestar atención a la siguiente especificación:


→ API de Web Thing


Dispositivo



Redis ayuda a la comunicación entre procesos y también actúa como una base de datos para accesorios.


El módulo bobaoskit.worker pasa una cola de solicitud (además de redis usando bee-queue ), ejecuta la solicitud, escribe / lee desde la base de datos.


En los scripts de usuario, el objeto bobaoskit.accessory escucha una bee-queue de bee-queue separada para este accesorio en particular, realiza las acciones prescritas y envía solicitudes a la cola del proceso principal a través del objeto bobaoskit.sdk .


Protocolo


Todas las solicitudes y mensajes publicados son cadenas en formato JSON , contienen el method y los campos de payload . Los campos son obligatorios, incluso si la payload = null .


Solicitudes a bobaoskit.worker :


  • Método: ping , carga útil: null .
  • Método: get general info , carga útil: null
  • Método: clear accessories , carga útil: null ,
  • método: add accessory ,
    carga útil:

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

  • Método: remove accessory , carga útil: accessoryId/[acc1id, acc2id, ...]
  • Método: get accessory info , carga útil: null/accId/[acc1id, acc2id...]
    En el campo de payload , puede enviar el null / id accesorio / id masivo. Si null envía un null , se devolverá la información sobre todos los accesorios existentes.
  • método: get status value , carga útil: {id: accessoryId, status: fieldId}
    En el campo de payload , puede enviar un objeto de la forma {id: accessoryId, status: fieldId} , (donde el campo de status puede ser una matriz de campos), o la payload puede ser una matriz de objetos de este tipo.
  • método: update status value , carga útil: {id: accessoryId, status: {field: fieldId, value: value}
    En el campo de payload , puede enviar un objeto de la forma {id: accessoryId, status: {field: fieldId, value: value}} , (donde el campo de status puede ser una matriz {field: fieldId, value: value} ), o la payload puede ser una matriz de objetos de tal tipo de
  • método: control accessory value , carga útil: {id: accessoryId, control: {field: fieldId, value: value}} .
    En el campo de payload , puede enviar un objeto de la forma {id: accessoryId, control: {field: fieldId, value: value}} , (donde el campo de control puede ser una matriz {field: fieldId, value: value} ), o la payload puede ser una matriz de objetos de tal tipo de

En respuesta a cualquier solicitud, si tiene éxito, un mensaje del formulario:


{ method: "success", payload: <...> }


En caso de falla:


{ method: "error", payload: "Error description" }


Los mensajes también se publican en el redis PUB/SUB (definido en config.json ) en los siguientes casos: se borran todos los accesorios ( clear accessories ); accesorio agregado ( add accessory ); accesorio quitado ( remove accessory ); Estado actualizado del accesorio ( update status value ).


Los mensajes de difusión también contienen dos campos: method y payload .


SDK de cliente


Descripción


El SDK del cliente ( bobaoskit.accessory ) le permite llamar a los métodos anteriores desde scripts js .


Dentro del módulo hay dos objetos constructores. El primero crea un objeto Sdk para acceder a los métodos anteriores, y el segundo crea un accesorio: un envoltorio sobre estas funciones.


 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" }); 

El objeto sdk admite métodos Promise :


 sdk.ping(); sdk.getGeneralInfo(); sdk.clearAccessories(); sdk.addAccessory(payload); sdk.removeAccessory(payload); sdk.getAccessoryInfo(payload); sdk.getStatusValue(payload); sdk.updateStatusValue(payload); sdk.controlAccessoryValue(payload); 

El objeto BobaosKit.Accessory({..}) es un contenedor encima del objeto BobaosKit.Sdk(...) .


A continuación, mostraré cómo esto cambia:


 //     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 }); }; 

Ambos objetos también son EventEmitter .
Sdk llama a funciones en los eventos ready y broadcasted event .
Accessory llama a funciones en eventos ready , error , control accessory value .


Ejemplo


 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); 

API de WebSocket


bobaoskit.worker escucha en el puerto WebSocket definido en ./config.json .


Las solicitudes entrantes son cadenas JSON que deben tener los siguientes campos: request_id , method y payload .


La API está limitada a las siguientes solicitudes:


  • método: ping , carga útil: null
  • método: get general info , carga útil: null ,
  • Método: get accessory info , carga útil: null/accId/[acc1Id, ...]
  • método: get status value , carga útil: {id: accId, status: field1/[field1, ...]}/[{id: ...}...]
  • método: control accessory value , carga útil: {id: accId, control: {field: field1, value: value}/[{field: .. value: ..}]}/[{id: ...}, ...]

Los métodos de get status value control accessory value aceptan el campo de payload como un solo objeto o como una matriz. Los campos de control/status dentro de la payload también pueden ser un solo objeto o una matriz.


Los siguientes mensajes de eventos del servidor también se envían a todos los clientes:


  • Método: clear accessories , carga útil: nulo
  • Método: remove accessory , carga útil: ID de accesorio
  • Método: add accessory, payload : {id: ...}
  • método: update status value, payload : {id: ...}

dnssd


La aplicación anuncia el puerto WebSocket en la red local como el servicio _bobaoskit._tcp , gracias al módulo npm dnssd .


Demo



Se escribirá un artículo separado sobre cómo se escribió la aplicación con video e impresiones de flutter .


Epílogo


Por lo tanto, se obtuvo un sistema simple para administrar accesorios de software.
Los accesorios se pueden contrastar con objetos del mundo real: botones, sensores, interruptores, termostatos, radios. Como no hay estandarización, puede implementar cualquier accesorio que se ajuste al modelo de control < == > update .


Qué se podría hacer mejor:


  1. Un protocolo binario permitiría enviar menos datos. JSON , por otro lado, es más rápido de desarrollar y comprender. El protocolo binario también requiere estandarización.

Eso es todo, me alegraré de cualquier comentario.

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


All Articles