Added framework support for multiple devices
This commit is contained in:
parent
ea00d497c6
commit
ad6306ad6e
@ -17,20 +17,36 @@ const RECONNECT_TIMEOUT_MS = 5000;
|
|||||||
export class WSSprinklersDevice extends s.SprinklersDevice {
|
export class WSSprinklersDevice extends s.SprinklersDevice {
|
||||||
readonly api: WebSocketApiClient;
|
readonly api: WebSocketApiClient;
|
||||||
|
|
||||||
constructor(api: WebSocketApiClient) {
|
private _id: string;
|
||||||
|
|
||||||
|
constructor(api: WebSocketApiClient, id: string) {
|
||||||
super();
|
super();
|
||||||
this.api = api;
|
this.api = api;
|
||||||
|
this._id = id;
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
this.connectionState.serverToBroker = api.connectionState.serverToBroker;
|
this.connectionState.serverToBroker = api.connectionState.serverToBroker;
|
||||||
this.connectionState.clientToServer = api.connectionState.clientToServer;
|
this.connectionState.clientToServer = api.connectionState.clientToServer;
|
||||||
if (!api.connectionState.isConnected) {
|
if (!api.connectionState.isConnected) {
|
||||||
this.connectionState.brokerToDevice = null;
|
this.connectionState.brokerToDevice = null;
|
||||||
|
} else {
|
||||||
|
this.subscribe();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() {
|
get id() {
|
||||||
return "grinklers";
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe() {
|
||||||
|
if (!this.api.socket) {
|
||||||
|
throw new Error("WebSocket not connected");
|
||||||
|
}
|
||||||
|
const subscribeRequest: ws.IDeviceSubscribeRequest = {
|
||||||
|
type: "deviceSubscribeRequest",
|
||||||
|
deviceId: this.id,
|
||||||
|
};
|
||||||
|
this.api.socket.send(JSON.stringify(subscribeRequest));
|
||||||
}
|
}
|
||||||
|
|
||||||
makeRequest(request: requests.Request): Promise<requests.Response> {
|
makeRequest(request: requests.Request): Promise<requests.Response> {
|
||||||
@ -40,14 +56,15 @@ export class WSSprinklersDevice extends s.SprinklersDevice {
|
|||||||
|
|
||||||
export class WebSocketApiClient implements s.ISprinklersApi {
|
export class WebSocketApiClient implements s.ISprinklersApi {
|
||||||
readonly webSocketUrl: string;
|
readonly webSocketUrl: string;
|
||||||
device: WSSprinklersDevice;
|
|
||||||
|
devices: Map<string, WSSprinklersDevice> = new Map();
|
||||||
|
|
||||||
nextDeviceRequestId = Math.round(Math.random() * 1000000);
|
nextDeviceRequestId = Math.round(Math.random() * 1000000);
|
||||||
deviceResponseCallbacks: { [id: number]: (res: ws.IDeviceCallResponse) => void | undefined; } = {};
|
deviceResponseCallbacks: { [id: number]: (res: ws.IDeviceCallResponse) => void | undefined; } = {};
|
||||||
|
|
||||||
@observable connectionState: s.ConnectionState = new s.ConnectionState();
|
@observable connectionState: s.ConnectionState = new s.ConnectionState();
|
||||||
|
|
||||||
private socket: WebSocket | null = null;
|
socket: WebSocket | null = null;
|
||||||
private reconnectTimer: number | null = null;
|
private reconnectTimer: number | null = null;
|
||||||
|
|
||||||
get connected(): boolean {
|
get connected(): boolean {
|
||||||
@ -56,7 +73,6 @@ export class WebSocketApiClient implements s.ISprinklersApi {
|
|||||||
|
|
||||||
constructor(webSocketUrl: string) {
|
constructor(webSocketUrl: string) {
|
||||||
this.webSocketUrl = webSocketUrl;
|
this.webSocketUrl = webSocketUrl;
|
||||||
this.device = new WSSprinklersDevice(this);
|
|
||||||
this.connectionState.clientToServer = false;
|
this.connectionState.clientToServer = false;
|
||||||
this.connectionState.serverToBroker = false;
|
this.connectionState.serverToBroker = false;
|
||||||
}
|
}
|
||||||
@ -77,19 +93,21 @@ export class WebSocketApiClient implements s.ISprinklersApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getDevice(name: string): s.SprinklersDevice {
|
getDevice(id: string): s.SprinklersDevice {
|
||||||
if (name !== "grinklers") {
|
let device = this.devices.get(id);
|
||||||
throw new Error("Devices which are not grinklers are not supported yet");
|
if (!device) {
|
||||||
|
device = new WSSprinklersDevice(this, id);
|
||||||
|
this.devices.set(id, device);
|
||||||
}
|
}
|
||||||
return this.device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeDevice(name: string) {
|
removeDevice(id: string) {
|
||||||
// NOT IMPLEMENTED
|
// NOT IMPLEMENTED
|
||||||
}
|
}
|
||||||
|
|
||||||
// args must all be JSON serializable
|
// args must all be JSON serializable
|
||||||
makeDeviceCall(deviceName: string, request: requests.Request): Promise<requests.Response> {
|
makeDeviceCall(deviceId: string, request: requests.Request): Promise<requests.Response> {
|
||||||
if (this.socket == null) {
|
if (this.socket == null) {
|
||||||
const res: requests.Response = {
|
const res: requests.Response = {
|
||||||
type: request.type,
|
type: request.type,
|
||||||
@ -103,7 +121,7 @@ export class WebSocketApiClient implements s.ISprinklersApi {
|
|||||||
const id = this.nextDeviceRequestId++;
|
const id = this.nextDeviceRequestId++;
|
||||||
const data: ws.IDeviceCallRequest = {
|
const data: ws.IDeviceCallRequest = {
|
||||||
type: "deviceCallRequest",
|
type: "deviceCallRequest",
|
||||||
id, deviceName, data: requestData,
|
id, deviceId, data: requestData,
|
||||||
};
|
};
|
||||||
const promise = new Promise<requests.Response>((resolve, reject) => {
|
const promise = new Promise<requests.Response>((resolve, reject) => {
|
||||||
let timeoutHandle: number;
|
let timeoutHandle: number;
|
||||||
@ -194,10 +212,11 @@ export class WebSocketApiClient implements s.ISprinklersApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onDeviceUpdate(data: ws.IDeviceUpdate) {
|
private onDeviceUpdate(data: ws.IDeviceUpdate) {
|
||||||
if (data.name !== "grinklers") {
|
const device = this.devices.get(data.deviceId);
|
||||||
|
if (!device) {
|
||||||
return log.warn({ data }, "invalid deviceUpdate received");
|
return log.warn({ data }, "invalid deviceUpdate received");
|
||||||
}
|
}
|
||||||
update(schema.sprinklersDevice, this.device, data.data);
|
update(schema.sprinklersDevice, device, data.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDeviceCallResponse(data: ws.IDeviceCallResponse) {
|
private onDeviceCallResponse(data: ws.IDeviceCallResponse) {
|
||||||
|
@ -53,13 +53,13 @@ export class MqttApiClient implements s.ISprinklersApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getDevice(prefix: string): s.SprinklersDevice {
|
getDevice(id: string): s.SprinklersDevice {
|
||||||
if (/\//.test(prefix)) {
|
if (/\//.test(id)) {
|
||||||
throw new Error("Prefix cannot contain a /");
|
throw new Error("Device id cannot contain a /");
|
||||||
}
|
}
|
||||||
let device = this.devices.get(prefix);
|
let device = this.devices.get(id);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
this.devices.set(prefix, device = new MqttSprinklersDevice(this, prefix));
|
this.devices.set(id, device = new MqttSprinklersDevice(this, id));
|
||||||
if (this.connected) {
|
if (this.connected) {
|
||||||
device.doSubscribe();
|
device.doSubscribe();
|
||||||
}
|
}
|
||||||
@ -67,13 +67,13 @@ export class MqttApiClient implements s.ISprinklersApi {
|
|||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeDevice(prefix: string) {
|
removeDevice(id: string) {
|
||||||
const device = this.devices.get(prefix);
|
const device = this.devices.get(id);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
device.doUnsubscribe();
|
device.doUnsubscribe();
|
||||||
this.devices.delete(prefix);
|
this.devices.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMessageArrived(topic: string, payload: Buffer, packet: mqtt.Packet) {
|
private onMessageArrived(topic: string, payload: Buffer, packet: mqtt.Packet) {
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import { Response as ResponseData } from "@common/sprinklers/requests";
|
import { Response as ResponseData } from "@common/sprinklers/requests";
|
||||||
|
|
||||||
|
export interface IError {
|
||||||
|
type: "error";
|
||||||
|
message: string;
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IDeviceUpdate {
|
export interface IDeviceUpdate {
|
||||||
type: "deviceUpdate";
|
type: "deviceUpdate";
|
||||||
name: string;
|
deviceId: string;
|
||||||
data: any;
|
data: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,13 +23,20 @@ export interface IBrokerConnectionUpdate {
|
|||||||
brokerConnected: boolean;
|
brokerConnected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IServerMessage = IDeviceUpdate | IDeviceCallResponse | IBrokerConnectionUpdate;
|
export type IServerMessage = IError | IDeviceUpdate | IDeviceCallResponse | IBrokerConnectionUpdate;
|
||||||
|
|
||||||
|
export type SubscriptionType = "deviceUpdate" | "brokerConnectionUpdate";
|
||||||
|
|
||||||
|
export interface IDeviceSubscribeRequest {
|
||||||
|
type: "deviceSubscribeRequest";
|
||||||
|
deviceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IDeviceCallRequest {
|
export interface IDeviceCallRequest {
|
||||||
type: "deviceCallRequest";
|
type: "deviceCallRequest";
|
||||||
id: number;
|
id: number;
|
||||||
deviceName: string;
|
deviceId: string;
|
||||||
data: any;
|
data: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IClientMessage = IDeviceCallRequest;
|
export type IClientMessage = IDeviceSubscribeRequest | IDeviceCallRequest;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import * as bcrypt from "bcrypt";
|
||||||
import * as r from "rethinkdb";
|
import * as r from "rethinkdb";
|
||||||
import { createModelSchema, deserialize, primitive, serialize, update } from "serializr";
|
import { createModelSchema, deserialize, primitive, serialize, update } from "serializr";
|
||||||
|
|
||||||
import { Database } from "./Database";
|
import { Database } from "./Database";
|
||||||
import * as bcrypt from "bcrypt";
|
|
||||||
|
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
|
@ -1,66 +1,104 @@
|
|||||||
|
import { autorun } from "mobx";
|
||||||
|
import { serialize } from "serializr";
|
||||||
|
import * as WebSocket from "ws";
|
||||||
|
|
||||||
import log from "@common/logger";
|
import log from "@common/logger";
|
||||||
import * as requests from "@common/sprinklers/requests";
|
import * as requests from "@common/sprinklers/requests";
|
||||||
import * as schema from "@common/sprinklers/schema";
|
import * as schema from "@common/sprinklers/schema";
|
||||||
import * as ws from "@common/sprinklers/websocketData";
|
import * as ws from "@common/sprinklers/websocketData";
|
||||||
import { autorun } from "mobx";
|
|
||||||
import { serialize } from "serializr";
|
|
||||||
import * as WebSocket from "ws";
|
|
||||||
import { ServerState } from "../state";
|
import { ServerState } from "../state";
|
||||||
|
|
||||||
export class WebSocketApi {
|
export class WebSocketClient {
|
||||||
state: ServerState;
|
api: WebSocketApi;
|
||||||
|
socket: WebSocket;
|
||||||
|
|
||||||
constructor(state: ServerState) {
|
disposers: Array<() => void> = [];
|
||||||
this.state = state;
|
deviceSubscriptions: string[] = [];
|
||||||
|
|
||||||
|
/// This shall be the user id if the client has been authenticated, null otherwise
|
||||||
|
userId: string | null = null;
|
||||||
|
|
||||||
|
get state() {
|
||||||
|
return this.api.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
listen(webSocketServer: WebSocket.Server) {
|
constructor(api: WebSocketApi, socket: WebSocket) {
|
||||||
webSocketServer.on("connection", this.handleConnection);
|
this.api = api;
|
||||||
|
this.socket = socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConnection = (socket: WebSocket) => {
|
start() {
|
||||||
const disposers = [
|
this.disposers.push(autorun(() => {
|
||||||
autorun(() => {
|
const updateData: ws.IBrokerConnectionUpdate = {
|
||||||
const json = serialize(schema.sprinklersDevice, this.state.device);
|
type: "brokerConnectionUpdate",
|
||||||
log.trace({ device: json });
|
brokerConnected: this.state.mqttClient.connected,
|
||||||
const data: ws.IDeviceUpdate = { type: "deviceUpdate", name: "grinklers", data: json };
|
};
|
||||||
socket.send(JSON.stringify(data));
|
this.socket.send(JSON.stringify(updateData));
|
||||||
}, { delay: 100 }),
|
}));
|
||||||
autorun(() => {
|
this.socket.on("message", this.handleSocketMessage);
|
||||||
const data: ws.IBrokerConnectionUpdate = {
|
this.socket.on("close", this.stop);
|
||||||
type: "brokerConnectionUpdate",
|
|
||||||
brokerConnected: this.state.mqttClient.connected,
|
|
||||||
};
|
|
||||||
socket.send(JSON.stringify(data));
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
const stop = () => {
|
|
||||||
disposers.forEach((disposer) => disposer());
|
|
||||||
};
|
|
||||||
socket.on("message", (data) => this.handleSocketMessage(socket, data));
|
|
||||||
socket.on("close", () => stop());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleSocketMessage(socket: WebSocket, socketData: WebSocket.Data) {
|
stop = () => {
|
||||||
|
this.disposers.forEach((disposer) => disposer());
|
||||||
|
this.api.removeClient(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSocketMessage = (socketData: WebSocket.Data) => {
|
||||||
|
this.doHandleSocketMessage(socketData)
|
||||||
|
.catch((err) => {
|
||||||
|
this.onError({ err }, "unhandled error on handling socket message");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doHandleSocketMessage(socketData: WebSocket.Data) {
|
||||||
if (typeof socketData !== "string") {
|
if (typeof socketData !== "string") {
|
||||||
return log.error({ type: typeof socketData }, "received invalid socket data type from client");
|
return this.onError({ type: typeof socketData }, "received invalid socket data type from client");
|
||||||
}
|
}
|
||||||
let data: ws.IClientMessage;
|
let data: ws.IClientMessage;
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(socketData);
|
data = JSON.parse(socketData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return log.error({ event, err }, "received invalid websocket message from client");
|
return this.onError({ event, err }, "received invalid websocket message from client");
|
||||||
}
|
}
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
|
case "deviceSubscribeRequest":
|
||||||
|
this.deviceSubscribeRequest(data);
|
||||||
|
break;
|
||||||
case "deviceCallRequest":
|
case "deviceCallRequest":
|
||||||
this.deviceCallRequest(socket, data);
|
await this.deviceCallRequest(data);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return log.warn({ data }, "received invalid client message type");
|
return this.onError({ data }, "received invalid client message type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deviceCallRequest(socket: WebSocket, data: ws.IDeviceCallRequest): Promise<void> {
|
private onError(data: any, message: string) {
|
||||||
|
log.error(data, message);
|
||||||
|
const errorData: ws.IError = {
|
||||||
|
type: "error", message, data,
|
||||||
|
};
|
||||||
|
this.socket.send(JSON.stringify(errorData));
|
||||||
|
}
|
||||||
|
|
||||||
|
private deviceSubscribeRequest(data: ws.IDeviceSubscribeRequest) {
|
||||||
|
// TODO: somehow validate this device id?
|
||||||
|
const deviceId = data.deviceId;
|
||||||
|
if (this.deviceSubscriptions.indexOf(deviceId) !== -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.deviceSubscriptions.push(deviceId);
|
||||||
|
const device = this.state.mqttClient.getDevice(deviceId);
|
||||||
|
log.debug({ deviceId, userId: this.userId }, "websocket client subscribed to device");
|
||||||
|
this.disposers.push(autorun(() => {
|
||||||
|
const json = serialize(schema.sprinklersDevice, device);
|
||||||
|
log.trace({ device: json });
|
||||||
|
const updateData: ws.IDeviceUpdate = { type: "deviceUpdate", deviceId, data: json };
|
||||||
|
this.socket.send(JSON.stringify(updateData));
|
||||||
|
}, { delay: 100 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deviceCallRequest(data: ws.IDeviceCallRequest): Promise<void> {
|
||||||
let response: requests.Response | false;
|
let response: requests.Response | false;
|
||||||
try {
|
try {
|
||||||
response = await this.doDeviceCallRequest(data);
|
response = await this.doDeviceCallRequest(data);
|
||||||
@ -73,13 +111,13 @@ export class WebSocketApi {
|
|||||||
id: data.id,
|
id: data.id,
|
||||||
data: response,
|
data: response,
|
||||||
};
|
};
|
||||||
socket.send(JSON.stringify(resData));
|
this.socket.send(JSON.stringify(resData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doDeviceCallRequest(requestData: ws.IDeviceCallRequest): Promise<requests.Response | false> {
|
private async doDeviceCallRequest(requestData: ws.IDeviceCallRequest): Promise<requests.Response | false> {
|
||||||
const { deviceName, data } = requestData;
|
const { deviceId, data } = requestData;
|
||||||
if (deviceName !== "grinklers") {
|
if (deviceId !== "grinklers") {
|
||||||
// error handling? or just get the right device
|
// error handling? or just get the right device
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -87,3 +125,29 @@ export class WebSocketApi {
|
|||||||
return this.state.device.makeRequest(request);
|
return this.state.device.makeRequest(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class WebSocketApi {
|
||||||
|
state: ServerState;
|
||||||
|
clients: WebSocketClient[] = [];
|
||||||
|
|
||||||
|
constructor(state: ServerState) {
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(webSocketServer: WebSocket.Server) {
|
||||||
|
webSocketServer.on("connection", this.handleConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConnection = (socket: WebSocket) => {
|
||||||
|
const client = new WebSocketClient(this, socket);
|
||||||
|
client.start();
|
||||||
|
this.clients.push(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeClient(client: WebSocketClient) {
|
||||||
|
const idx = this.clients.indexOf(client);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.clients.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user