From 66a7cceb36bb3c8def065ec657a4e34bdf14abdf Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Tue, 30 May 2017 16:45:25 -0600 Subject: [PATCH] Work on lots of cool stuff --- app/script/App.tsx | 30 +++++++++++-- app/script/index.tsx | 7 ++- app/script/mqtt.ts | 93 +++++++++++++++++++++++++++++----------- app/script/sprinklers.ts | 21 ++++++++- app/script/ui.ts | 30 +++++++++++++ app/script/utils.ts | 12 ++++++ package-lock.json | 11 +++-- package.json | 2 + 8 files changed, 172 insertions(+), 34 deletions(-) create mode 100644 app/script/ui.ts create mode 100644 app/script/utils.ts diff --git a/app/script/App.tsx b/app/script/App.tsx index c9e0dd6..3e95878 100644 --- a/app/script/App.tsx +++ b/app/script/App.tsx @@ -4,13 +4,14 @@ import {computed} from "mobx"; import DevTools from "mobx-react-devtools"; import {observer} from "mobx-react"; import {SprinklersDevice, Section, Program, Duration, Schedule} from "./sprinklers"; -import {Item, Table, Header, Segment, Form, Input, Button, DropdownItemProps, DropdownProps} from "semantic-ui-react"; +import {Item, Table, Header, Segment, Form, Input, Button, DropdownItemProps, DropdownProps, Message} from "semantic-ui-react"; import FontAwesome = require("react-fontawesome"); import * as classNames from "classnames"; import "semantic-ui-css/semantic.css"; import "font-awesome/css/font-awesome.css"; import "app/style/app.css"; +import {Message as UiMessage, UiStore} from "./ui"; /* tslint:disable:object-literal-sort-keys */ @@ -139,7 +140,9 @@ class RunSectionForm extends React.Component<{ e.preventDefault(); const section: Section = this.props.sections[this.state.section]; console.log(`should run section ${section} for ${this.state.duration}`); - section.run(this.state.duration); + section.run(this.state.duration) + .then((a) => console.log("ran section", a)) + .catch((err) => console.error("error running section", err)); } private get isValid(): boolean { @@ -253,9 +256,30 @@ class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void> } @observer -export default class App extends React.PureComponent<{ device: SprinklersDevice }, any> { +class MessagesView extends React.PureComponent<{uiStore: UiStore}, void> { + public render() { + return
+ {this.props.uiStore.messages.map(this.renderMessage)} +
; + } + + private renderMessage = (message: UiMessage, index: number) => { + const {header, content, type} = message; + return this.dismiss(index)}/>; + } + + private dismiss(index: number) { + this.props.uiStore.messages.splice(index, 1); + } +} + +@observer +export default class App extends React.PureComponent<{ device: SprinklersDevice, uiStore: UiStore }, any> { public render() { return + ; diff --git a/app/script/index.tsx b/app/script/index.tsx index f957fd9..a9c5903 100644 --- a/app/script/index.tsx +++ b/app/script/index.tsx @@ -4,22 +4,25 @@ import { AppContainer } from "react-hot-loader"; import App from "./App"; import { MqttApiClient } from "./mqtt"; +import {Message, UiStore} from "./ui"; const client = new MqttApiClient(); client.start(); const device = client.getDevice("grinklers"); +const uiStore = new UiStore(); +uiStore.addMessage(new Message("asdf", "boo!", Message.Type.Error)); const rootElem = document.getElementById("app"); ReactDOM.render( - + , rootElem); if (module.hot) { module.hot.accept("./App", () => { const NextApp = require("./App").default; ReactDOM.render( - + , rootElem); }); } diff --git a/app/script/mqtt.ts b/app/script/mqtt.ts index 9d72816..4c5778e 100644 --- a/app/script/mqtt.ts +++ b/app/script/mqtt.ts @@ -1,10 +1,12 @@ import "paho-mqtt/mqttws31"; import MQTT = Paho.MQTT; -import { EventEmitter } from "events"; +import {EventEmitter} from "events"; import { SprinklersDevice, ISprinklersApi, Section, Program, IProgramItem, Schedule, ITimeOfDay, Weekday, Duration, } from "./sprinklers"; +import {checkedIndexOf} from "./utils"; +import * as Promise from "bluebird"; export class MqttApiClient extends EventEmitter implements ISprinklersApi { private static newClientId() { @@ -94,6 +96,10 @@ class MqttSprinklersDevice extends SprinklersDevice { public readonly apiClient: MqttApiClient; public readonly prefix: string; + private responseCallbacks: { + [rid: number]: ResponseCallback; + } = {}; + constructor(apiClient: MqttApiClient, prefix: string) { super(); this.apiClient = apiClient; @@ -103,7 +109,7 @@ class MqttSprinklersDevice extends SprinklersDevice { public doSubscribe() { const c = this.apiClient.client; this.subscriptions - .forEach((filter) => c.subscribe(filter, { qos: 1 })); + .forEach((filter) => c.subscribe(filter, {qos: 1})); } public doUnsubscribe() { @@ -149,13 +155,25 @@ class MqttSprinklersDevice extends SprinklersDevice { const progNum = Number(progStr); let program = this.programs[progNum]; if (!program) { - this.programs[progNum] = program = new MqttProgram(); + this.programs[progNum] = program = new MqttProgram(this); } (program as MqttProgram).onMessage(subTopic, payload); } - } else { - console.warn(`MqttSprinklersDevice recieved invalid topic: ${topic}`); + return; } + matches = topic.match(/^responses\/(\d+)$/); + if (matches != null) { + const [_topic, respIdStr] = matches; + console.log(`response: ${respIdStr}`); + const respId = parseInt(respIdStr, 10); + const data = JSON.parse(payload) as IResponseData; + const cb = this.responseCallbacks[respId]; + if (typeof cb === "function") { + cb(data); + } + return; + } + console.warn(`MqttSprinklersDevice recieved invalid topic: ${topic}`); } get id(): string { @@ -163,20 +181,43 @@ class MqttSprinklersDevice extends SprinklersDevice { } public runSection(section: Section | number, duration: Duration) { - let sectionNum: number; - if (typeof section === "number") { - sectionNum = section; - } else { - sectionNum = this.sections.indexOf(section); - } - if (sectionNum < 0 || sectionNum > this.sections.length) { - throw new Error(`Invalid section to run: ${section}`); - } - const message = new MQTT.Message(JSON.stringify({ - duration: duration.toSeconds(), - } as IRunSectionJSON)); - message.destinationName = `${this.prefix}/sections/${sectionNum}/run`; - this.apiClient.client.send(message); + const sectionNum = checkedIndexOf(section, this.sections, "Section"); + return this.makeRequest(`sections/${sectionNum}/run`, + { + duration: duration.toSeconds(), + } as IRunSectionJSON); + } + + public runProgram(program: Program | number) { + const programNum = checkedIndexOf(program, this.programs, "Program"); + return this.makeRequest(`programs/${programNum}/run`, {}); + } + + private nextRequestId(): number { + return Math.floor(Math.random() * 1000000000); + } + + private makeRequest(topic: string, payload: object | string): Promise { + return new Promise((resolve, reject) => { + let payloadStr: string; + if (typeof payload === "string") { + payloadStr = payload; + } else { + payloadStr = JSON.stringify(payload); + } + const message = new MQTT.Message(payloadStr); + message.destinationName = this.prefix + "/" + topic; + const requestId = this.nextRequestId(); + this.responseCallbacks[requestId] = (data) => { + if (data.error != null) { + reject(data); + } else { + resolve(data); + } + }; + this.apiClient.client.send(message); + }); + } private get subscriptions() { @@ -186,10 +227,19 @@ class MqttSprinklersDevice extends SprinklersDevice { `${this.prefix}/sections/+/#`, `${this.prefix}/programs`, `${this.prefix}/programs/+/#`, + `${this.prefix}/responses/+`, ]; } } +interface IResponseData { + reqTopic: string; + error?: string; + [key: string]: any; +} + +type ResponseCallback = (IResponseData) => void; + interface ISectionJSON { name: string; pin: number; @@ -200,11 +250,6 @@ interface IRunSectionJSON { } class MqttSection extends Section { - - constructor(device: MqttSprinklersDevice) { - super(device); - } - public onMessage(topic: string, payload: string) { if (topic === "state") { this.state = (payload === "true"); diff --git a/app/script/sprinklers.ts b/app/script/sprinklers.ts index aca1ce4..5df50c4 100644 --- a/app/script/sprinklers.ts +++ b/app/script/sprinklers.ts @@ -14,7 +14,7 @@ export abstract class Section { } public run(duration: Duration) { - this.device.runSection(this, duration); + return this.device.runSection(this, duration); } public toString(): string { @@ -90,6 +90,12 @@ export interface IProgramItem { } export class Program { + public device: SprinklersDevice; + + constructor(device: SprinklersDevice) { + this.device = device; + } + @observable public name: string = ""; @observable @@ -103,6 +109,15 @@ export class Program { @observable public running: boolean = false; + + public run() { + return this.device.runProgram(this); + } + + public toString(): string { + return `Program{name="${this.name}", enabled=${this.enabled}, schedule=${this.schedule}, + sequence=${this.sequence}, running=${this.running}}`; + } } export abstract class SprinklersDevice { @@ -115,7 +130,9 @@ export abstract class SprinklersDevice { @observable public programs: IObservableArray = [] as IObservableArray; - public abstract runSection(section: number | Section, duration: Duration); + public abstract runSection(section: number | Section, duration: Duration): Promise<{}>; + + public abstract runProgram(program: number | Program): Promise<{}>; abstract get id(): string; } diff --git a/app/script/ui.ts b/app/script/ui.ts new file mode 100644 index 0000000..1530f12 --- /dev/null +++ b/app/script/ui.ts @@ -0,0 +1,30 @@ +import {observable} from "mobx"; + +export class Message { + public id: string; + public header: string = ""; + public content: string = ""; + public type: Message.Type = Message.Type.Default; + + constructor(header: string, content: string = "", type: Message.Type = Message.Type.Default) { + this.id = "" + Math.floor(Math.random() * 1000000000); + this.header = header; + this.content = content; + this.type = type; + } +} + +export namespace Message { + export enum Type { + Default, Success, Info, Warning, Error, + } +} + +export class UiStore { + @observable + public messages: Message[] = []; + + public addMessage(message: Message) { + this.messages.push(message); + } +} diff --git a/app/script/utils.ts b/app/script/utils.ts new file mode 100644 index 0000000..099a312 --- /dev/null +++ b/app/script/utils.ts @@ -0,0 +1,12 @@ +export function checkedIndexOf(o: T | number, arr: T[], type: string = "object"): number { + let idx: number; + if (typeof o === "number") { + idx = o; + } else { + idx = arr.indexOf(o); + } + if (idx < 0 || idx > arr.length) { + throw new Error(`Invalid ${type} specified: ${o}`); + } + return idx; +} diff --git a/package-lock.json b/package-lock.json index 4bc9ecc..66af332 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3,6 +3,11 @@ "version": "1.0.0", "lockfileVersion": 1, "dependencies": { + "@types/bluebird": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.4.tgz", + "integrity": "sha1-8SAWKwT9bVXhA0bX4ulu7UYrAs8=" + }, "@types/classnames": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.0.tgz", @@ -239,9 +244,9 @@ "dev": true }, "bluebird": { - "version": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", - "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=", - "dev": true + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" }, "bn.js": { "version": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", diff --git a/package.json b/package.json index d8ec1bd..89959dd 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,14 @@ }, "homepage": "https://github.com/amikhalev/sprinklers3#readme", "dependencies": { + "@types/bluebird": "^3.5.4", "@types/classnames": "^2.2.0", "@types/node": "^7.0.22", "@types/object-assign": "^4.0.30", "@types/react": "^15.0.25", "@types/react-dom": "^15.5.0", "@types/react-fontawesome": "^1.5.0", + "bluebird": "^3.5.0", "classnames": "^2.2.5", "font-awesome": "^4.7.0", "mobx": "^3.1.10",