diff --git a/app/components/DeviceView.tsx b/app/components/DeviceView.tsx index eb72e10..31bbdb1 100644 --- a/app/components/DeviceView.tsx +++ b/app/components/DeviceView.tsx @@ -1,7 +1,7 @@ import * as classNames from "classnames"; import { observer } from "mobx-react"; import * as React from "react"; -import { Grid, Header, Icon, Item } from "semantic-ui-react"; +import { Grid, Header, Icon, Item, SemanticICONS } from "semantic-ui-react"; import { injectState, StateBase } from "@app/state"; import { ConnectionState as ConState, SprinklersDevice } from "@common/sprinklers"; @@ -13,12 +13,21 @@ const ConnectionState = observer(({ connectionState, className }: const connected = connectionState.isConnected; const classes = classNames({ connectionState: true, - connected: connected, /* tslint:disable-line:object-literal-shorthand */ - disconnected: !connected, + connected: connected === true, + disconnected: connected === false, + unknown: connected === null, }, className); let connectionText: string; + let iconName: SemanticICONS = "unlinkify"; if (connected) { connectionText = "Connected"; + iconName = "linkify"; + } else if (connected === null) { + connectionText = "Unknown"; + iconName = "question"; + } else if (connectionState.noPermission) { + connectionText = "No permission for this device"; + iconName = "ban"; } else if (connectionState.serverToBroker) { connectionText = "Device Disconnected"; } else if (connectionState.clientToServer) { @@ -28,7 +37,7 @@ const ConnectionState = observer(({ connectionState, className }: } return (
-   +   {connectionText}
); @@ -64,7 +73,9 @@ class DeviceView extends React.Component { Raspberry Pi Grinklers Device - + {connectionState.isAvailable && + } + {connectionState.isAvailable && @@ -73,7 +84,10 @@ class DeviceView extends React.Component { + } + {connectionState.isAvailable && + } ); diff --git a/app/sprinklers/websocket.ts b/app/sprinklers/websocket.ts index 50d6c21..810625f 100644 --- a/app/sprinklers/websocket.ts +++ b/app/sprinklers/websocket.ts @@ -1,4 +1,5 @@ import { update } from "serializr"; +import { action, autorun, observable, when } from "mobx"; import logger from "@common/logger"; import { ErrorCode } from "@common/sprinklers/ErrorCode"; @@ -7,7 +8,6 @@ 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" }); @@ -23,14 +23,8 @@ export class WSSprinklersDevice extends s.SprinklersDevice { super(); this.api = api; this._id = id; - autorun(() => { - this.connectionState.serverToBroker = api.connectionState.serverToBroker; - this.connectionState.clientToServer = api.connectionState.clientToServer; - if (!api.connectionState.isConnected) { - this.connectionState.brokerToDevice = null; - } else { - this.subscribe(); - } + when(() => api.connectionState.isConnected, () => { + this.subscribe(); }); } @@ -49,6 +43,17 @@ export class WSSprinklersDevice extends s.SprinklersDevice { this.api.socket.send(JSON.stringify(subscribeRequest)); } + 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 { return this.api.makeDeviceCall(this.id, request); } @@ -197,6 +202,9 @@ export class WebSocketApiClient implements s.ISprinklersApi { } log.trace({ data }, "websocket message"); switch (data.type) { + case "deviceSubscribeResponse": + this.onDeviceSubscribeResponse(data); + break; case "deviceUpdate": this.onDeviceUpdate(data); break; @@ -211,6 +219,14 @@ export class WebSocketApiClient implements s.ISprinklersApi { } } + private onDeviceSubscribeResponse(data: ws.IDeviceSubscribeResponse) { + const device = this.devices.get(data.deviceId); + if (!device) { + return log.warn({ data }, "invalid deviceSubscribeResponse received"); + } + device.onSubscribeResponse(data); + } + private onDeviceUpdate(data: ws.IDeviceUpdate) { const device = this.devices.get(data.deviceId); if (!device) { diff --git a/common/sprinklers/ConnectionState.ts b/common/sprinklers/ConnectionState.ts index 38bf6f4..236ce3c 100644 --- a/common/sprinklers/ConnectionState.ts +++ b/common/sprinklers/ConnectionState.ts @@ -19,9 +19,22 @@ export class ConnectionState { */ @observable brokerToDevice: boolean | null = null; - @computed get isConnected(): boolean { + /** + * Represents if whoever is trying to access this device has permission to access it. + * Is null if there is no concept of access involved. + */ + @observable hasPermission: boolean | null = null; + + @computed get noPermission() { + return this.hasPermission === false; + } + + @computed get isAvailable(): boolean { + if (this.hasPermission === false) { + return false; + } if (this.brokerToDevice != null) { - return this.brokerToDevice; + return true; } if (this.serverToBroker != null) { return this.serverToBroker; @@ -31,4 +44,20 @@ export class ConnectionState { } return false; } + + @computed get isConnected(): boolean | null { + if (this.hasPermission === false) { + return false; + } + if (this.brokerToDevice != null) { + return this.brokerToDevice; + } + if (this.serverToBroker != null) { + return this.serverToBroker; + } + if (this.clientToServer != null) { + return this.clientToServer; + } + return null; + } } diff --git a/common/sprinklers/mqtt/index.ts b/common/sprinklers/mqtt/index.ts index 75d2026..d449bc3 100644 --- a/common/sprinklers/mqtt/index.ts +++ b/common/sprinklers/mqtt/index.ts @@ -1,3 +1,4 @@ +import { autorun, observable } from "mobx"; import * as mqtt from "mqtt"; import { update } from "serializr"; @@ -6,7 +7,6 @@ import * as s from "@common/sprinklers"; import * as requests from "@common/sprinklers/requests"; import * as schema from "@common/sprinklers/schema"; import { seralizeRequest } from "@common/sprinklers/schema/requests"; -import { autorun, observable } from "mobx"; const log = logger.child({ source: "mqtt" }); @@ -161,14 +161,30 @@ class MqttSprinklersDevice extends s.SprinklersDevice { return this.prefix; } - doSubscribe() { + doSubscribe(): Promise { const topics = subscriptions.map((filter) => this.prefix + filter); - this.apiClient.client.subscribe(topics, { qos: 1 }); + return new Promise((resolve, reject) => { + this.apiClient.client.subscribe(topics, { qos: 1 }, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); } - doUnsubscribe() { + doUnsubscribe(): Promise { const topics = subscriptions.map((filter) => this.prefix + filter); - this.apiClient.client.unsubscribe(topics); + return new Promise((resolve, reject) => { + this.apiClient.client.unsubscribe(topics, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); } onMessage(topic: string, payload: string) { diff --git a/common/sprinklers/websocketData.ts b/common/sprinklers/websocketData.ts index 0b67f79..3c8cbab 100644 --- a/common/sprinklers/websocketData.ts +++ b/common/sprinklers/websocketData.ts @@ -6,6 +6,12 @@ export interface IError { data: any; } +export interface IDeviceSubscribeResponse { + type: "deviceSubscribeResponse"; + deviceId: string; + result: "success" | "noPermission"; +} + export interface IDeviceUpdate { type: "deviceUpdate"; deviceId: string; @@ -23,9 +29,8 @@ export interface IBrokerConnectionUpdate { brokerConnected: boolean; } -export type IServerMessage = IError | IDeviceUpdate | IDeviceCallResponse | IBrokerConnectionUpdate; - -export type SubscriptionType = "deviceUpdate" | "brokerConnectionUpdate"; +export type IServerMessage = IError | IDeviceSubscribeResponse | IDeviceUpdate | IDeviceCallResponse | + IBrokerConnectionUpdate; export interface IDeviceSubscribeRequest { type: "deviceSubscribeRequest"; diff --git a/server/websocket/index.ts b/server/websocket/index.ts index c695eb9..f3bbd0f 100644 --- a/server/websocket/index.ts +++ b/server/websocket/index.ts @@ -82,20 +82,29 @@ export class WebSocketClient { } private deviceSubscribeRequest(data: ws.IDeviceSubscribeRequest) { - // TODO: somehow validate this device id? const deviceId = data.deviceId; - if (this.deviceSubscriptions.indexOf(deviceId) !== -1) { - return; + let result: ws.IDeviceSubscribeResponse["result"]; + if (deviceId !== "grinklers") { // TODO: somehow validate this device id? + result = "noPermission"; + } else { + 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 })); + result = "success"; } - 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 })); + const response: ws.IDeviceSubscribeResponse = { + type: "deviceSubscribeResponse", deviceId, result, + }; + this.socket.send(JSON.stringify(response)); } private async deviceCallRequest(data: ws.IDeviceCallRequest): Promise {