
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 depayload, você pode enviar onull/idacessório / massa. Senullenviado, 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 depayload, você pode enviar um objeto no formato{id: accessoryId, status: fieldId}(em que o campo destatuspode ser uma matriz de campos) ou apayloadpode ser uma matriz de objetos desse tipo.
- método: update status value, carga útil:{id: accessoryId, status: {field: fieldId, value: value}
 No campo depayload, você pode enviar um objeto no formato{id: accessoryId, status: {field: fieldId, value: value}}(onde o campo destatuspode ser uma matriz{field: fieldId, value: value}) ou apayloadpode 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 depayload, você pode enviar um objeto no formato{id: accessoryId, control: {field: fieldId, value: value}}(onde o campo decontrolpode ser uma matriz{field: fieldId, value: value}) ou apayloadpode 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:
- 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.