bobaoskit - acessórios, dnssd e WebSocket


Assim, descrevi a estrutura de um sistema de acessórios de software gerenciado.


O modelo simplificado inclui o processo principal ( bobaoskit.worker ) e scripts de acessórios (usando os bobaoskit.accessory e bobaoskit.accessory ). Do processo principal, há uma solicitação de um acessório para controlar alguns campos. A partir do acessório, por sua vez, há uma solicitação para a principal coisa para atualizar o status.


Tome um relé comum como exemplo.


Com um comando recebido, o relé pode às vezes não mudar de posição devido a várias razões (congelamento do equipamento etc.). Assim, quanto não enviaremos equipes, o status não mudará. E, em outra situação, o relé pode mudar de estado quando comandado por um sistema de terceiros. Nesse caso, seu status será alterado, o script acessório poderá responder a um evento recebido sobre a alteração de status e enviar uma solicitação ao processo principal.


Motivação


Tendo implementado o Apple HomeKit em vários objetos, comecei a procurar algo semelhante ao Android, porque Eu mesmo tenho apenas um iPad funcionando em dispositivos iOS. O principal critério foi a capacidade de trabalhar em uma rede local, sem serviços em nuvem. Além disso, o que faltava no HomeKit eram as informações limitadas. Por exemplo, você pode pegar um termostato. Todo o seu controle é reduzido à escolha do modo de operação (desligado, aquecimento, refrigeração e automático) e à temperatura definida. Mais simples é melhor, mas, na minha opinião, nem sempre. Não há informações de diagnóstico suficientes. Por exemplo, se o ar condicionado, o convector, quais parâmetros de ventilação estão funcionando. Talvez o ar condicionado não possa funcionar devido a um erro interno. Dado que esta informação pode ser considerada, decidiu-se escrever sua implementação.


Pode-se olhar para opções como ioBroker, OpenHAB, assistente de casa.
Mas no node.js, dos listados, apenas o ioBroker (enquanto escrevo um artigo, notou que o redis também está envolvido no processo). E, nessa época, ele havia descoberto como organizar a comunicação entre processos, e era interessante lidar com os redis, que foram ouvidos ultimamente.


Você também pode prestar atenção às seguintes especificações:


API de coisa da Web


Dispositivo



Redis ajuda na comunicação entre processos e também atua como um banco de dados para acessórios.


O módulo bobaoskit.worker acontece na fila de solicitações (na parte superior do redis usando bee-queue ), executa a solicitação, grava / lê no banco de dados.


Nos scripts do usuário, o objeto bobaoskit.accessory escuta uma bee-queue separada para este acessório específico, executa as ações prescritas, envia solicitações para a fila do processo principal através do objeto bobaoskit.sdk .


Protocolo


Todas as solicitações e mensagens publicadas são cadeias no formato JSON , contêm os campos method e payload . Os campos são obrigatórios, mesmo se payload = null .


Pedidos para bobaoskit.worker :


  • método: ping , carga: 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...]
    No campo de payload , você pode enviar o null / id acessório / massa. Se null enviado, as informações sobre todos os acessórios existentes serão retornadas.
  • método: get status value , carga útil: {id: accessoryId, status: fieldId}
    No campo de payload , você pode enviar um objeto no formato {id: accessoryId, status: fieldId} (em que o campo de status pode ser uma matriz de campos) ou a payload pode ser uma matriz de objetos desse tipo.
  • método: update status value , carga útil: {id: accessoryId, status: {field: fieldId, value: value}
    No campo de payload , você pode enviar um objeto no formato {id: accessoryId, status: {field: fieldId, value: value}} (onde o campo de status pode ser uma matriz {field: fieldId, value: value} ) ou a payload pode ser uma matriz de objetos desse tipo mais ou menos.
  • método: control accessory value , carga: {id: accessoryId, control: {field: fieldId, value: value}} .
    No campo de payload , você pode enviar um objeto no formato {id: accessoryId, control: {field: fieldId, value: value}} (onde o campo de control pode ser uma matriz {field: fieldId, value: value} ) ou a payload pode ser uma matriz de objetos desse tipo mais ou menos.

Em resposta a qualquer solicitação, se bem-sucedida, uma mensagem do formulário:


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


Em caso de falha:


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


As mensagens também são publicadas no canal config.json redis PUB/SUB (definido em config.json ) nos seguintes casos: todos os acessórios são limpos ( clear accessories ); acessório adicionado ( add accessory ); acessório removido ( remove accessory ); Status atualizado do acessório ( update status value ).


As mensagens de transmissão também contêm dois campos: method e payload .


SDK do cliente


Descrição do produto


O SDK do cliente ( bobaoskit.accessory ) permite chamar os métodos acima a partir de scripts js .


Dentro do módulo existem dois objetos construtores. O primeiro cria um objeto Sdk para acessar os métodos acima e o segundo cria um acessório - um invólucro sobre essas funções.


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

O objeto sdk suporta 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); 

O objeto BobaosKit.Accessory({..}) é um invólucro na parte superior do objeto BobaosKit.Sdk(...) .


A seguir, mostrarei como isso acontece:


 //     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 os objetos também são EventEmitter .
Sdk chama funções nos eventos ready e broadcasted event .
Accessory chama funções em eventos ready , error , control accessory value .


Exemplo


 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 escuta na porta WebSocket definida em ./config.json .


Os pedidos recebidos são cadeias JSON que devem ter os seguintes campos: request_id , method e payload .


A API é limitada aos seguintes pedidos:


  • método: ping , carga útil: null
  • método: get general info , carga: 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: ...}, ...]

Os métodos get status value , control accessory value e aceitam o campo de payload como um único objeto ou como uma matriz. Os campos de control/status dentro da payload também podem ser um único objeto ou uma matriz.


As seguintes mensagens de evento do servidor também são enviadas para todos os clientes:


  • método: clear accessories , carga útil: nulo
  • método: remove accessory , carga: id do acessório
  • método: add accessory, payload : {id: ...}
  • método: update status value, payload : {id: ...}

dnssd


O aplicativo anuncia a porta WebSocket na rede local como o serviço _bobaoskit._tcp , graças ao módulo npm dnssd .


Demo



Um artigo separado será escrito sobre como o aplicativo foi escrito com vídeo e impressões de flutter .


Posfácio


Assim, foi obtido um sistema simples para gerenciar acessórios de software.
Os acessórios podem ser contrastados com objetos do mundo real: botões, sensores, interruptores, termostatos, rádios. Como não há padronização, você pode implementar quaisquer acessórios que se encaixem no modelo de control < == > update do control < == > update .


O que poderia ser feito melhor:


  1. Um protocolo binário permitiria que menos dados fossem enviados. JSON , por outro lado, é mais rápido de desenvolver e entender. O protocolo binário também requer padronização.

Isso é tudo, terei prazer em qualquer feedback.

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


All Articles