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 {