Browse Source

Work on lots of cool stuff

update-deps
Alex Mikhalev 8 years ago
parent
commit
66a7cceb36
  1. 30
      app/script/App.tsx
  2. 7
      app/script/index.tsx
  3. 93
      app/script/mqtt.ts
  4. 21
      app/script/sprinklers.ts
  5. 30
      app/script/ui.ts
  6. 12
      app/script/utils.ts
  7. 11
      package-lock.json
  8. 2
      package.json

30
app/script/App.tsx

@ -4,13 +4,14 @@ import {computed} from "mobx"; @@ -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<{ @@ -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> @@ -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 <div>
{this.props.uiStore.messages.map(this.renderMessage)}
</div>;
}
private renderMessage = (message: UiMessage, index: number) => {
const {header, content, type} = message;
return <Message header={header} content={content} success={type === UiMessage.Type.Success}
info={type === UiMessage.Type.Info} warning={type === UiMessage.Type.Warning}
error={type === UiMessage.Type.Error} onDismiss={() => 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 <Item.Group divided>
<MessagesView uiStore={this.props.uiStore} />
<DeviceView device={this.props.device}/>
<DevTools />
</Item.Group>;

7
app/script/index.tsx

@ -4,22 +4,25 @@ import { AppContainer } from "react-hot-loader"; @@ -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(<AppContainer>
<App device={device} />
<App device={device} uiStore={uiStore} />
</AppContainer>, rootElem);
if (module.hot) {
module.hot.accept("./App", () => {
const NextApp = require<any>("./App").default;
ReactDOM.render(<AppContainer>
<NextApp device={device} />
<App device={device} uiStore={uiStore} />
</AppContainer>, rootElem);
});
}

93
app/script/mqtt.ts

@ -1,10 +1,12 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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<IResponseData> {
return new Promise<IResponseData>((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 { @@ -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 { @@ -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");

21
app/script/sprinklers.ts

@ -14,7 +14,7 @@ export abstract class Section { @@ -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 { @@ -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 { @@ -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 { @@ -115,7 +130,9 @@ export abstract class SprinklersDevice {
@observable
public programs: IObservableArray<Program> = [] as IObservableArray<Program>;
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;
}

30
app/script/ui.ts

@ -0,0 +1,30 @@ @@ -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);
}
}

12
app/script/utils.ts

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
export function checkedIndexOf<T>(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;
}

11
package-lock.json generated

@ -3,6 +3,11 @@ @@ -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 @@ @@ -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",

2
package.json

@ -22,12 +22,14 @@ @@ -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",

Loading…
Cancel
Save