bobaoskit - accessoires, dnssd et WebSocket


Ainsi, j'ai décrit la structure d'un système d'accessoires logiciels gérés.


Le modèle simplifié comprend le processus principal ( bobaoskit.worker ) et les scripts accessoires (en utilisant les bobaoskit.accessory bobaoskit.sdk et bobaoskit.accessory ). Depuis le processus principal, il y a une demande d'accessoire pour contrôler certains champs. De l'accessoire, à son tour, il y a une demande à l'essentiel pour mettre à jour le statut.


Prenons l'exemple d'un relais ordinaire.


Avec une commande entrante, le relais peut parfois ne pas changer de position pour diverses raisons (gel de l'équipement, etc.). En conséquence, combien nous n'enverrons pas d'équipes, le statut ne changera pas. Et, dans une autre situation, le relais peut changer son état lorsqu'il est commandé par un système tiers. Dans ce cas, son état va changer, le script accessoire peut répondre à un événement entrant sur le changement d'état et envoyer une requête au processus principal.


La motivation


Ayant implémenté Apple HomeKit sur plusieurs objets, j'ai commencé à chercher quelque chose de similaire à Android, car Je n'ai moi-même qu'un iPad fonctionnel à partir d'appareils iOS. Le critère principal était la capacité à travailler sur un réseau local, sans services cloud. De plus, ce qui manquait dans HomeKit, c'était les informations limitées. Par exemple, vous pouvez prendre un thermostat. Toute sa commande est réduite au choix du mode de fonctionnement (arrêt, chauffage, refroidissement et auto) et de la température réglée. Plus simple, c'est mieux, mais, à mon avis, pas toujours. Pas assez d'informations de diagnostic. Par exemple, si le climatiseur, le convecteur, quels paramètres de ventilation fonctionnent. Peut-être que le climatiseur ne peut pas fonctionner en raison d'une erreur interne. Étant donné que ces informations peuvent être prises en compte, il a été décidé d'en rédiger la mise en œuvre.


On pourrait regarder des options telles que ioBroker, OpenHAB, home-assistant.
Mais sur node.js, parmi ceux listés, seul ioBroker (pendant que j'écris un article, a remarqué que redis est également impliqué dans le processus). Et à ce moment-là, il avait découvert comment organiser la communication interprocessus, et il était intéressant de traiter de redis, qui a été entendu récemment.


Vous pouvez également prêter attention aux spécifications suivantes:


API Web Thing


Périphérique



Redis facilite la communication interprocessus et sert également de base de données pour les accessoires.


Le module bobaoskit.worker passe la file d'attente des demandes (en plus de redis utilisant la bee-queue ), exécute la demande, écrit / lit à partir de la base de données.


Dans les scripts utilisateur, l'objet bobaoskit.accessory écoute une bee-queue d' bee-queue d' bee-queue distincte pour cet accessoire particulier, exécute les actions prescrites, envoie des requêtes à la file d'attente de processus principale via l'objet bobaoskit.sdk .


Protocole


Toutes les demandes et tous les messages publiés sont des chaînes au format JSON , contiennent les champs de method et de payload . Les champs sont obligatoires, même si payload = null .


Demandes à bobaoskit.worker :


  • méthode: ping , charge utile: null .
  • méthode: get general info , charge utile: null
  • méthode: clear accessories , charge utile: null ,
  • méthode: add accessory ,
    charge utile:

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

  • méthode: remove accessory , charge utile: accessoryId/[acc1id, acc2id, ...]
  • méthode: get accessory info , charge utile: null/accId/[acc1id, acc2id...]
    Dans le champ de payload , vous pouvez envoyer le null / id accessoire / id masse. Si null envoyé, les informations sur tous les accessoires existants seront retournées.
  • méthode: get status value , charge utile: {id: accessoryId, status: fieldId}
    Dans le champ de payload , vous pouvez envoyer un objet de la forme {id: accessoryId, status: fieldId} , (où le champ d' status peut être un tableau de champs), ou la payload peut être un tableau d'objets de ce type.
  • méthode: update status value , charge utile: {id: accessoryId, status: {field: fieldId, value: value}
    Dans le champ de payload , vous pouvez envoyer un objet de la forme {id: accessoryId, status: {field: fieldId, value: value}} , (où le champ d' status peut être un tableau {field: fieldId, value: value} ), ou la payload peut être un tableau d'objets de ce type en quelque sorte.
  • méthode: control accessory value , charge utile: {id: accessoryId, control: {field: fieldId, value: value}} .
    Dans le champ de payload , vous pouvez envoyer un objet de la forme {id: accessoryId, control: {field: fieldId, value: value}} , (où le champ de control peut être un tableau {field: fieldId, value: value} ), ou la payload peut être un tableau d'objets de ce type en quelque sorte.

En réponse à toute demande, en cas de succès, un message du formulaire:


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


En cas d'échec:


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


Les messages sont également publiés dans le redis PUB/SUB (défini dans config.json ) dans les cas suivants: tous les accessoires sont effacés ( clear accessories ); accessoire ajouté ( add accessory ); accessoire retiré ( remove accessory ); État mis à jour de l'accessoire ( update status value )


Les messages diffusés contiennent également deux champs: method et payload .


SDK client


La description


Le SDK client ( bobaoskit.accessory ) vous permet d'appeler les méthodes ci-dessus à partir de scripts js .


A l'intérieur du module se trouvent deux objets constructeurs. Le premier crée un objet Sdk pour accéder aux méthodes ci-dessus, et le second crée un accessoire - un wrapper au-dessus de ces fonctions.


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

L'objet sdk prend en charge les méthodes 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); 

L'objet BobaosKit.Accessory({..}) est un wrapper au-dessus de l'objet BobaosKit.Sdk(...) .


Ensuite, je vais montrer comment cela se retourne:


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

Les deux objets sont également EventEmitter .
Sdk appelle des fonctions sur les événements ready et broadcasted event .
Accessory appelle les fonctions sur les événements ready , les error , la control accessory value


Exemple


 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 WebSocket


bobaoskit.worker écoute sur le port WebSocket défini dans ./config.json .


Les demandes entrantes sont JSON chaînes JSON qui doivent avoir les champs suivants: request_id , method et payload .


L'API est limitée aux demandes suivantes:


  • méthode: ping , charge utile: null
  • méthode: get general info , charge utile: null ,
  • méthode: get accessory info , charge utile: null/accId/[acc1Id, ...]
  • méthode: get status value , charge utile: {id: accId, status: field1/[field1, ...]}/[{id: ...}...]
  • méthode: control accessory value , charge utile: {id: accId, control: {field: field1, value: value}/[{field: .. value: ..}]}/[{id: ...}, ...]

Les méthodes d' get status value , de control accessory value acceptent le champ de payload comme un objet unique ou comme un tableau. Les champs de control/status intérieur de la payload peuvent également être un objet unique ou un tableau.


Les messages d'événement suivants du serveur sont également envoyés à tous les clients:


  • méthode: clear accessories , charge utile: null
  • méthode: remove accessory , charge utile: identifiant de l'accessoire
  • méthode: add accessory, payload : {id: ...}
  • méthode: update status value, payload : {id: ...}

dnssd


L'application annonce le port WebSocket sur le réseau local en tant _bobaoskit._tcp service _bobaoskit._tcp , grâce au module npm dnssd .


Démo



Un article séparé sera écrit sur la façon dont l'application a été écrite avec une vidéo et des impressions de flutter .


Postface


Ainsi, un système simple de gestion des accessoires logiciels a été obtenu.
Les accessoires peuvent être contrastés avec des objets du monde réel: boutons, capteurs, interrupteurs, thermostats, radios. Comme il n'y a pas de standardisation, vous pouvez implémenter tous les accessoires qui correspondent au modèle de control < == > update .


Quoi de mieux:


  1. Un protocole binaire permettrait d'envoyer moins de données. JSON , en revanche, est plus rapide à développer et à comprendre. Le protocole binaire nécessite également une standardisation.

C'est tout, je serai heureux de tout commentaire.

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


All Articles