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",