Alex Mikhalev
7 years ago
14 changed files with 279 additions and 78 deletions
@ -1,46 +0,0 @@ |
|||||||
import { update } from "serializr"; |
|
||||||
|
|
||||||
import * as s from "@common/sprinklers"; |
|
||||||
import * as schema from "@common/sprinklers/json"; |
|
||||||
|
|
||||||
export class WebSprinklersDevice extends s.SprinklersDevice { |
|
||||||
get id() { |
|
||||||
return "grinklers"; |
|
||||||
} |
|
||||||
async runSection(section: number | s.Section, duration: s.Duration): Promise<{}> { |
|
||||||
return {}; |
|
||||||
} |
|
||||||
async runProgram(program: number | s.Program): Promise<{}> { |
|
||||||
return {}; |
|
||||||
} |
|
||||||
async cancelSectionRunById(id: number): Promise<{}> { |
|
||||||
return {}; |
|
||||||
} |
|
||||||
async pauseSectionRunner(): Promise<{}> { |
|
||||||
return {}; |
|
||||||
} |
|
||||||
async unpauseSectionRunner(): Promise<{}> { |
|
||||||
return {}; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export class WebApiClient implements s.ISprinklersApi { |
|
||||||
start() { |
|
||||||
// NOT IMPLEMENTED
|
|
||||||
} |
|
||||||
|
|
||||||
getDevice(name: string): s.SprinklersDevice { |
|
||||||
const device = new WebSprinklersDevice(); |
|
||||||
fetch("/api/grinklers") |
|
||||||
.then((res) => res.json()) |
|
||||||
.then((json) => { |
|
||||||
update(schema.sprinklersDeviceSchema, device, json); |
|
||||||
}) |
|
||||||
.catch((e) => alert(e)); |
|
||||||
return device; |
|
||||||
} |
|
||||||
|
|
||||||
removeDevice(name: string) { |
|
||||||
// NOT IMPLEMENTED
|
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,146 @@ |
|||||||
|
import { update } from "serializr"; |
||||||
|
|
||||||
|
import logger from "@common/logger"; |
||||||
|
import * as s from "@common/sprinklers"; |
||||||
|
import * as schema from "@common/sprinklers/json"; |
||||||
|
import * as ws from "@common/sprinklers/websocketData"; |
||||||
|
import { checkedIndexOf } from "@common/utils"; |
||||||
|
|
||||||
|
const log = logger.child({ source: "websocket" }); |
||||||
|
|
||||||
|
export class WebSprinklersDevice extends s.SprinklersDevice { |
||||||
|
readonly api: WebApiClient; |
||||||
|
|
||||||
|
constructor(api: WebApiClient) { |
||||||
|
super(); |
||||||
|
this.api = api; |
||||||
|
} |
||||||
|
|
||||||
|
get id() { |
||||||
|
return "grinklers"; |
||||||
|
} |
||||||
|
|
||||||
|
runSection(section: number | s.Section, duration: s.Duration): Promise<{}> { |
||||||
|
const secNum = checkedIndexOf(section, this.sections, "Section"); |
||||||
|
const dur = duration.toSeconds(); |
||||||
|
return this.makeCall("runSection", secNum, dur); |
||||||
|
} |
||||||
|
async runProgram(program: number | s.Program): Promise<{}> { |
||||||
|
return {}; |
||||||
|
} |
||||||
|
async cancelSectionRunById(id: number): Promise<{}> { |
||||||
|
return {}; |
||||||
|
} |
||||||
|
async pauseSectionRunner(): Promise<{}> { |
||||||
|
return {}; |
||||||
|
} |
||||||
|
async unpauseSectionRunner(): Promise<{}> { |
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
private makeCall(method: string, ...args: any[]) { |
||||||
|
return this.api.makeDeviceCall(this.id, method, ...args); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class WebApiClient implements s.ISprinklersApi { |
||||||
|
readonly webSocketUrl: string; |
||||||
|
socket: WebSocket; |
||||||
|
device: WebSprinklersDevice; |
||||||
|
|
||||||
|
nextDeviceRequestId = Math.round(Math.random() * 1000000); |
||||||
|
deviceResponseCallbacks: { [id: number]: (res: ws.IDeviceCallResponse) => void | undefined; } = {}; |
||||||
|
|
||||||
|
constructor(webSocketUrl: string) { |
||||||
|
this.webSocketUrl = webSocketUrl; |
||||||
|
this.device = new WebSprinklersDevice(this); |
||||||
|
} |
||||||
|
|
||||||
|
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, method: string, ...args: any[]): Promise<any> { |
||||||
|
const id = this.nextDeviceRequestId++; |
||||||
|
const data: ws.IDeviceCallRequest = { |
||||||
|
type: "deviceCallRequest", |
||||||
|
id, deviceName, method, args, |
||||||
|
}; |
||||||
|
const promise = new Promise((resolve, reject) => { |
||||||
|
this.deviceResponseCallbacks[id] = (resData) => { |
||||||
|
if (resData.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"); |
||||||
|
} |
||||||
|
|
||||||
|
private onClose(event: CloseEvent) { |
||||||
|
log.info({ reason: event.reason, wasClean: event.wasClean }, |
||||||
|
"disconnected from websocket"); |
||||||
|
} |
||||||
|
|
||||||
|
private onError(event: Event) { |
||||||
|
log.error(event, "websocket error"); |
||||||
|
} |
||||||
|
|
||||||
|
private onMessage(event: MessageEvent) { |
||||||
|
log.trace({ event }, "websocket message"); |
||||||
|
let data: ws.IServerMessage; |
||||||
|
try { |
||||||
|
data = JSON.parse(event.data); |
||||||
|
} catch (err) { |
||||||
|
return log.error({ event, err }, "received invalid websocket message"); |
||||||
|
} |
||||||
|
switch (data.type) { |
||||||
|
case "deviceUpdate": |
||||||
|
this.onDeviceUpdate(data); |
||||||
|
break; |
||||||
|
case "deviceCallResponse": |
||||||
|
this.onDeviceCallResponse(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.sprinklersDeviceSchema, this.device, data.data); |
||||||
|
} |
||||||
|
|
||||||
|
private onDeviceCallResponse(data: ws.IDeviceCallResponse) { |
||||||
|
const cb = this.deviceResponseCallbacks[data.id]; |
||||||
|
if (typeof cb === "function") { |
||||||
|
cb(data); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
export interface IDeviceUpdate { |
||||||
|
type: "deviceUpdate"; |
||||||
|
name: string; |
||||||
|
data: any; |
||||||
|
} |
||||||
|
|
||||||
|
export interface IDeviceCallResponse { |
||||||
|
type: "deviceCallResponse"; |
||||||
|
id: number; |
||||||
|
result: "success" | "error"; |
||||||
|
data: any; |
||||||
|
} |
||||||
|
|
||||||
|
export type IServerMessage = IDeviceUpdate | IDeviceCallResponse; |
||||||
|
|
||||||
|
export interface IDeviceCallRequest { |
||||||
|
type: "deviceCallRequest"; |
||||||
|
id: number; |
||||||
|
deviceName: string; |
||||||
|
method: string; |
||||||
|
args: any[]; |
||||||
|
} |
||||||
|
|
||||||
|
export type IClientMessage = IDeviceCallRequest; |
@ -1,5 +1,5 @@ |
|||||||
import log, { setLogger } from "@common/logger"; |
import log from "@common/logger"; |
||||||
setLogger(log.child({ |
Object.assign(log, { |
||||||
name: "sprinklers3/server", |
name: "sprinklers3/server", |
||||||
level: "debug", |
level: "debug", |
||||||
})); |
}); |
||||||
|
Loading…
Reference in new issue