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