You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
166 lines
5.5 KiB
166 lines
5.5 KiB
import { update } from "serializr"; |
|
|
|
import logger from "@common/logger"; |
|
import * as s from "@common/sprinklers/index"; |
|
import * as requests from "@common/sprinklers/requests"; |
|
import * as schema from "@common/sprinklers/schema/index"; |
|
import { seralizeRequest } from "@common/sprinklers/schema/requests"; |
|
import * as ws from "@common/sprinklers/websocketData"; |
|
import { action, autorun, observable } from "mobx"; |
|
|
|
const log = logger.child({ source: "websocket" }); |
|
|
|
export class WSSprinklersDevice extends s.SprinklersDevice { |
|
readonly api: WebSocketApiClient; |
|
|
|
constructor(api: WebSocketApiClient) { |
|
super(); |
|
this.api = api; |
|
autorun(() => { |
|
this.connectionState.serverToBroker = api.connectionState.serverToBroker; |
|
this.connectionState.clientToServer = api.connectionState.clientToServer; |
|
if (!api.connectionState.isConnected) { |
|
this.connectionState.brokerToDevice = null; |
|
} |
|
}); |
|
} |
|
|
|
get id() { |
|
return "grinklers"; |
|
} |
|
|
|
makeRequest(request: requests.Request): Promise<requests.Response> { |
|
return this.api.makeDeviceCall(this.id, request); |
|
} |
|
} |
|
|
|
export class WebSocketApiClient implements s.ISprinklersApi { |
|
readonly webSocketUrl: string; |
|
socket!: WebSocket; |
|
device: WSSprinklersDevice; |
|
|
|
nextDeviceRequestId = Math.round(Math.random() * 1000000); |
|
deviceResponseCallbacks: { [id: number]: (res: ws.IDeviceCallResponse) => void | undefined; } = {}; |
|
|
|
@observable connectionState: s.ConnectionState = new s.ConnectionState(); |
|
|
|
get connected(): boolean { |
|
return this.connectionState.isConnected; |
|
} |
|
|
|
constructor(webSocketUrl: string) { |
|
this.webSocketUrl = webSocketUrl; |
|
this.device = new WSSprinklersDevice(this); |
|
this.connectionState.clientToServer = false; |
|
this.connectionState.serverToBroker = false; |
|
} |
|
|
|
start() { |
|
log.debug({ url: this.webSocketUrl }, "connecting to websocket"); |
|
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); |
|
} |
|
|
|
getDevice(name: string): s.SprinklersDevice { |
|
if (name !== "grinklers") { |
|
throw new Error("Devices which are not grinklers are not supported yet"); |
|
} |
|
return this.device; |
|
} |
|
|
|
removeDevice(name: string) { |
|
// NOT IMPLEMENTED |
|
} |
|
|
|
// args must all be JSON serializable |
|
makeDeviceCall(deviceName: string, request: requests.Request): Promise<requests.Response> { |
|
const requestData = seralizeRequest(request); |
|
const id = this.nextDeviceRequestId++; |
|
const data: ws.IDeviceCallRequest = { |
|
type: "deviceCallRequest", |
|
id, deviceName, data: requestData, |
|
}; |
|
const promise = new Promise<requests.Response>((resolve, reject) => { |
|
this.deviceResponseCallbacks[id] = (resData) => { |
|
if (resData.data.result === "success") { |
|
resolve(resData.data); |
|
} else { |
|
reject(resData.data); |
|
} |
|
delete this.deviceResponseCallbacks[id]; |
|
}; |
|
}); |
|
this.socket.send(JSON.stringify(data)); |
|
return promise; |
|
} |
|
|
|
private onOpen() { |
|
log.info("established websocket connection"); |
|
this.connectionState.clientToServer = true; |
|
} |
|
|
|
/* tslint:disable-next-line:member-ordering */ |
|
private onDisconnect = action(() => { |
|
this.connectionState.serverToBroker = null; |
|
this.connectionState.clientToServer = false; |
|
}); |
|
|
|
private onClose(event: CloseEvent) { |
|
log.info({ reason: event.reason, wasClean: event.wasClean }, |
|
"disconnected from websocket"); |
|
this.onDisconnect(); |
|
} |
|
|
|
private onError(event: Event) { |
|
log.error({ event }, "websocket error"); |
|
action(() => { |
|
this.connectionState.serverToBroker = null; |
|
this.connectionState.clientToServer = false; |
|
}); |
|
this.onDisconnect(); |
|
} |
|
|
|
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"); |
|
} |
|
log.trace({ data }, "websocket message"); |
|
switch (data.type) { |
|
case "deviceUpdate": |
|
this.onDeviceUpdate(data); |
|
break; |
|
case "deviceCallResponse": |
|
this.onDeviceCallResponse(data); |
|
break; |
|
case "brokerConnectionUpdate": |
|
this.onBrokerConnectionUpdate(data); |
|
break; |
|
default: |
|
log.warn({ data }, "unsupported event type received"); |
|
} |
|
} |
|
|
|
private onDeviceUpdate(data: ws.IDeviceUpdate) { |
|
if (data.name !== "grinklers") { |
|
return log.warn({ data }, "invalid deviceUpdate received"); |
|
} |
|
update(schema.sprinklersDevice, this.device, data.data); |
|
} |
|
|
|
private onDeviceCallResponse(data: ws.IDeviceCallResponse) { |
|
const cb = this.deviceResponseCallbacks[data.id]; |
|
if (typeof cb === "function") { |
|
cb(data); |
|
} |
|
} |
|
|
|
private onBrokerConnectionUpdate(data: ws.IBrokerConnectionUpdate) { |
|
this.connectionState.serverToBroker = data.brokerConnected; |
|
} |
|
}
|
|
|