sprinklers3/app/sprinklers/websocket.ts

249 lines
8.0 KiB
TypeScript
Raw Normal View History

2017-10-09 08:09:08 -06:00
import { update } from "serializr";
2018-06-29 18:16:06 -06:00
import { action, autorun, observable, when } from "mobx";
2017-10-09 08:09:08 -06:00
import logger from "@common/logger";
2018-06-25 17:37:36 -06:00
import { ErrorCode } from "@common/sprinklers/ErrorCode";
2018-06-16 23:54:03 -06:00
import * as s from "@common/sprinklers/index";
import * as requests from "@common/sprinklers/requests";
2018-06-16 23:54:03 -06:00
import * as schema from "@common/sprinklers/schema/index";
import { seralizeRequest } from "@common/sprinklers/schema/requests";
2017-10-09 08:09:08 -06:00
import * as ws from "@common/sprinklers/websocketData";
const log = logger.child({ source: "websocket" });
2018-06-25 17:37:36 -06:00
const TIMEOUT_MS = 5000;
2018-06-27 00:59:58 -06:00
const RECONNECT_TIMEOUT_MS = 5000;
2018-06-25 17:37:36 -06:00
2018-06-16 23:54:03 -06:00
export class WSSprinklersDevice extends s.SprinklersDevice {
readonly api: WebSocketApiClient;
2017-10-09 08:09:08 -06:00
private _id: string;
constructor(api: WebSocketApiClient, id: string) {
2017-10-09 08:09:08 -06:00
super();
this.api = api;
this._id = id;
2018-06-29 18:16:06 -06:00
when(() => api.connectionState.isConnected, () => {
this.subscribe();
2018-06-16 23:54:03 -06:00
});
2017-10-09 08:09:08 -06:00
}
get id() {
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));
2017-10-09 08:09:08 -06:00
}
2018-06-29 18:16:06 -06:00
onSubscribeResponse(data: ws.IDeviceSubscribeResponse) {
this.connectionState.serverToBroker = true;
this.connectionState.clientToServer = true;
if (data.result === "success") {
this.connectionState.hasPermission = true;
this.connectionState.brokerToDevice = false;
} else if (data.result === "noPermission") {
this.connectionState.hasPermission = false;
}
}
makeRequest(request: requests.Request): Promise<requests.Response> {
return this.api.makeDeviceCall(this.id, request);
2017-10-09 08:09:08 -06:00
}
}
2018-06-16 23:54:03 -06:00
export class WebSocketApiClient implements s.ISprinklersApi {
2017-10-09 08:09:08 -06:00
readonly webSocketUrl: string;
devices: Map<string, WSSprinklersDevice> = new Map();
2017-10-09 08:09:08 -06:00
nextDeviceRequestId = Math.round(Math.random() * 1000000);
deviceResponseCallbacks: { [id: number]: (res: ws.IDeviceCallResponse) => void | undefined; } = {};
2018-06-16 23:54:03 -06:00
@observable connectionState: s.ConnectionState = new s.ConnectionState();
socket: WebSocket | null = null;
2018-06-27 00:59:58 -06:00
private reconnectTimer: number | null = null;
2018-06-16 23:54:03 -06:00
get connected(): boolean {
return this.connectionState.isConnected;
}
2017-10-09 08:09:08 -06:00
constructor(webSocketUrl: string) {
this.webSocketUrl = webSocketUrl;
2018-06-16 23:54:03 -06:00
this.connectionState.clientToServer = false;
this.connectionState.serverToBroker = false;
2017-10-09 08:09:08 -06:00
}
start() {
log.debug({ url: this.webSocketUrl }, "connecting to websocket");
2018-06-27 00:59:58 -06:00
this._connect();
}
stop() {
if (this.reconnectTimer != null) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.socket != null) {
this.socket.close();
this.socket = null;
}
2017-10-09 08:09:08 -06:00
}
getDevice(id: string): s.SprinklersDevice {
let device = this.devices.get(id);
if (!device) {
device = new WSSprinklersDevice(this, id);
this.devices.set(id, device);
2017-10-09 08:09:08 -06:00
}
return device;
2017-10-09 08:09:08 -06:00
}
removeDevice(id: string) {
2017-10-09 08:09:08 -06:00
// NOT IMPLEMENTED
}
// args must all be JSON serializable
makeDeviceCall(deviceId: string, request: requests.Request): Promise<requests.Response> {
2018-06-27 00:59:58 -06:00
if (this.socket == null) {
const res: requests.Response = {
type: request.type,
result: "error",
code: ErrorCode.ServerDisconnected,
message: "the server is not connected",
};
throw res;
}
const requestData = seralizeRequest(request);
2017-10-09 08:09:08 -06:00
const id = this.nextDeviceRequestId++;
const data: ws.IDeviceCallRequest = {
type: "deviceCallRequest",
id, deviceId, data: requestData,
2017-10-09 08:09:08 -06:00
};
const promise = new Promise<requests.Response>((resolve, reject) => {
2018-06-25 17:37:36 -06:00
let timeoutHandle: number;
2017-10-09 08:09:08 -06:00
this.deviceResponseCallbacks[id] = (resData) => {
2018-06-25 17:37:36 -06:00
clearTimeout(timeoutHandle);
delete this.deviceResponseCallbacks[id];
if (resData.data.result === "success") {
2017-10-09 08:09:08 -06:00
resolve(resData.data);
} else {
reject(resData.data);
}
};
2018-06-27 00:59:58 -06:00
timeoutHandle = window.setTimeout(() => {
2018-06-25 17:37:36 -06:00
delete this.deviceResponseCallbacks[id];
2018-06-27 00:59:58 -06:00
const res: requests.Response = {
type: request.type,
2018-06-25 17:37:36 -06:00
result: "error",
code: ErrorCode.Timeout,
message: "the request timed out",
};
reject(res);
}, TIMEOUT_MS);
2017-10-09 08:09:08 -06:00
});
this.socket.send(JSON.stringify(data));
return promise;
}
2018-06-27 00:59:58 -06:00
private _reconnect = () => {
this._connect();
}
private _connect() {
this.socket = new WebSocket(this.webSocketUrl);
this.socket.onopen = this.onOpen.bind(this);
this.socket.onclose = this.onClose.bind(this);
this.socket.onerror = this.onError.bind(this);
this.socket.onmessage = this.onMessage.bind(this);
}
2017-10-09 08:09:08 -06:00
private onOpen() {
log.info("established websocket connection");
2018-06-16 23:54:03 -06:00
this.connectionState.clientToServer = true;
2017-10-09 08:09:08 -06:00
}
2018-06-17 01:04:30 -06:00
/* tslint:disable-next-line:member-ordering */
private onDisconnect = action(() => {
this.connectionState.serverToBroker = null;
this.connectionState.clientToServer = false;
});
2017-10-09 08:09:08 -06:00
private onClose(event: CloseEvent) {
log.info({ reason: event.reason, wasClean: event.wasClean },
"disconnected from websocket");
2018-06-17 01:04:30 -06:00
this.onDisconnect();
2018-06-27 00:59:58 -06:00
this.reconnectTimer = window.setTimeout(this._reconnect, RECONNECT_TIMEOUT_MS);
2017-10-09 08:09:08 -06:00
}
private onError(event: Event) {
2018-06-17 01:04:30 -06:00
log.error({ event }, "websocket error");
action(() => {
this.connectionState.serverToBroker = null;
this.connectionState.clientToServer = false;
});
this.onDisconnect();
2017-10-09 08:09:08 -06:00
}
private onMessage(event: MessageEvent) {
let data: ws.IServerMessage;
try {
data = JSON.parse(event.data);
} catch (err) {
return log.error({ event, err }, "received invalid websocket message");
}
2018-06-16 23:54:03 -06:00
log.trace({ data }, "websocket message");
2017-10-09 08:09:08 -06:00
switch (data.type) {
2018-06-29 18:16:06 -06:00
case "deviceSubscribeResponse":
this.onDeviceSubscribeResponse(data);
break;
2017-10-09 08:09:08 -06:00
case "deviceUpdate":
this.onDeviceUpdate(data);
break;
case "deviceCallResponse":
this.onDeviceCallResponse(data);
break;
2018-06-16 23:54:03 -06:00
case "brokerConnectionUpdate":
this.onBrokerConnectionUpdate(data);
break;
2017-10-09 08:09:08 -06:00
default:
log.warn({ data }, "unsupported event type received");
}
}
2018-06-29 18:16:06 -06:00
private onDeviceSubscribeResponse(data: ws.IDeviceSubscribeResponse) {
const device = this.devices.get(data.deviceId);
if (!device) {
return log.warn({ data }, "invalid deviceSubscribeResponse received");
}
device.onSubscribeResponse(data);
}
2017-10-09 08:09:08 -06:00
private onDeviceUpdate(data: ws.IDeviceUpdate) {
const device = this.devices.get(data.deviceId);
if (!device) {
2017-10-09 08:09:08 -06:00
return log.warn({ data }, "invalid deviceUpdate received");
}
update(schema.sprinklersDevice, device, data.data);
2017-10-09 08:09:08 -06:00
}
private onDeviceCallResponse(data: ws.IDeviceCallResponse) {
const cb = this.deviceResponseCallbacks[data.id];
if (typeof cb === "function") {
cb(data);
}
}
2018-06-16 23:54:03 -06:00
private onBrokerConnectionUpdate(data: ws.IBrokerConnectionUpdate) {
this.connectionState.serverToBroker = data.brokerConnected;
}
2017-10-09 08:09:08 -06:00
}