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