Better connection state stuff
This commit is contained in:
		
							parent
							
								
									472df851f4
								
							
						
					
					
						commit
						63689e14ff
					
				@ -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 (
 | 
			
		||||
        <div className={classes}>
 | 
			
		||||
            <Icon name={connected ? "linkify" : "unlinkify"}/> 
 | 
			
		||||
            <Icon name={iconName}/> 
 | 
			
		||||
            {connectionText}
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
@ -64,7 +73,9 @@ class DeviceView extends React.Component<DeviceViewProps> {
 | 
			
		||||
                    <Item.Meta>
 | 
			
		||||
                        Raspberry Pi Grinklers Device
 | 
			
		||||
                    </Item.Meta>
 | 
			
		||||
                    <SectionRunnerView sectionRunner={sectionRunner} sections={sections}/>
 | 
			
		||||
                    {connectionState.isAvailable &&
 | 
			
		||||
                    <SectionRunnerView sectionRunner={sectionRunner} sections={sections}/>}
 | 
			
		||||
                    {connectionState.isAvailable &&
 | 
			
		||||
                    <Grid>
 | 
			
		||||
                        <Grid.Column mobile="16" tablet="16" computer="8">
 | 
			
		||||
                            <SectionTable sections={sections}/>
 | 
			
		||||
@ -73,7 +84,10 @@ class DeviceView extends React.Component<DeviceViewProps> {
 | 
			
		||||
                            <RunSectionForm device={this.device} uiStore={uiStore}/>
 | 
			
		||||
                        </Grid.Column>
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                    }
 | 
			
		||||
                    {connectionState.isAvailable &&
 | 
			
		||||
                    <ProgramTable programs={programs} sections={sections}/>
 | 
			
		||||
                    }
 | 
			
		||||
                </Item.Content>
 | 
			
		||||
            </Item>
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@ -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<requests.Response> {
 | 
			
		||||
        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) {
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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) {
 | 
			
		||||
 | 
			
		||||
@ -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";
 | 
			
		||||
 | 
			
		||||
@ -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<void> {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user