diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..55712c1
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib"
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 3ccf9d1..246d755 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -1,27 +1,28 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
- "version": "0.1.0",
- "command": "npm",
- "isShellCommand": true,
- "showOutput": "always",
- "suppressTaskName": true,
+ "version": "2.0.0",
"tasks": [
{
- "taskName": "install",
- "args": ["install"]
- },
- {
- "taskName": "update",
- "args": ["update"]
- },
- {
- "taskName": "start",
- "args": ["run", "start"]
- },
- {
- "taskName": "test",
- "args": ["run", "test"]
+ "type": "npm",
+ "script": "start",
+ "problemMatcher": {
+ "owner": "webpack",
+ "severity": "error",
+ "fileLocation": "relative",
+ "pattern": [
+ {
+ "regexp": "ERROR in (.*)",
+ "file": 1
+ },
+ {
+ "regexp": "\\((\\d+),(\\d+)\\):(.*)",
+ "line": 1,
+ "column": 2,
+ "message": 3
+ }
+ ]
+ }
}
]
}
\ No newline at end of file
diff --git a/app/script/components/DeviceView.tsx b/app/script/components/DeviceView.tsx
index 8c6470c..2671709 100644
--- a/app/script/components/DeviceView.tsx
+++ b/app/script/components/DeviceView.tsx
@@ -1,19 +1,19 @@
import * as classNames from "classnames";
-import {observer} from "mobx-react";
+import { observer } from "mobx-react";
import * as React from "react";
-import {Header, Item} from "semantic-ui-react";
-import {ProgramTable, RunSectionForm, SectionTable, SectionRunnerView} from ".";
+import { Header, Item } from "semantic-ui-react";
+import { ProgramTable, RunSectionForm, SectionRunnerView, SectionTable } from ".";
-import {SprinklersDevice} from "../sprinklers";
+import { SprinklersDevice } from "../sprinklers";
import FontAwesome = require("react-fontawesome");
-const ConnectionState = ({connected}: { connected: boolean }) =>
+const ConnectionState = ({ connected }: { connected: boolean }) =>
-
+
{connected ? "Connected" : "Disconnected"}
;
@@ -21,22 +21,22 @@ const ConnectionState = ({connected}: { connected: boolean }) =>
@observer
export default class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, {}> {
render() {
- const {id, connected, sections, programs, sectionRunner} = this.props.device;
+ const { id, connected, sections, programs, sectionRunner } = this.props.device;
return (
-
- ("app/images/raspberry_pi.png")}/>
+ ("app/images/raspberry_pi.png")} />
-
-
-
-
+
+
+
+
);
diff --git a/app/script/components/DurationInput.tsx b/app/script/components/DurationInput.tsx
index 198d140..c1da564 100644
--- a/app/script/components/DurationInput.tsx
+++ b/app/script/components/DurationInput.tsx
@@ -1,11 +1,11 @@
import * as React from "react";
-import {Input} from "semantic-ui-react";
-import {Duration} from "../sprinklers";
+import { Input, InputOnChangeData } from "semantic-ui-react";
+import { Duration } from "../sprinklers";
export default class DurationInput extends React.Component<{
duration: Duration,
- onDurationChange?: (newDuration: Duration) => void;
-}, {}> {
+ onDurationChange: (newDuration: Duration) => void;
+}> {
render() {
const duration = this.props.duration;
// const editing = this.props.onDurationChange != null;
@@ -13,25 +13,25 @@ export default class DurationInput extends React.Component<{
+ value={duration.minutes} onChange={this.onMinutesChange}
+ label="M" labelPosition="right" />
+ value={duration.seconds} onChange={this.onSecondsChange} max="60"
+ label="S" labelPosition="right" />
;
}
- private onMinutesChange = (e, {value}) => {
- if (value.length === 0 || isNaN(value)) {
+ private onMinutesChange = (e: React.SyntheticEvent, { value }: InputOnChangeData) => {
+ if (value.length === 0 || isNaN(Number(value))) {
return;
}
const newMinutes = parseInt(value, 10);
this.props.onDurationChange(this.props.duration.withMinutes(newMinutes));
}
- private onSecondsChange = (e, {value}) => {
- if (value.length === 0 || isNaN(value)) {
+ private onSecondsChange = (e: React.SyntheticEvent, { value }: InputOnChangeData) => {
+ if (value.length === 0 || isNaN(Number(value))) {
return;
}
const newSeconds = parseInt(value, 10);
diff --git a/app/script/components/ProgramTable.tsx b/app/script/components/ProgramTable.tsx
index 531d50c..2f867d9 100644
--- a/app/script/components/ProgramTable.tsx
+++ b/app/script/components/ProgramTable.tsx
@@ -14,7 +14,7 @@ export class ScheduleView extends React.PureComponent<{ schedule: Schedule }, {}
@observer
export default class ProgramTable extends React.PureComponent<{ programs: Program[] }, {}> {
- private static renderRow(program: Program, i: number): JSX.Element[] {
+ private static renderRows(program: Program, i: number): JSX.Element[] | null {
if (!program) {
return null;
}
@@ -57,7 +57,7 @@ export default class ProgramTable extends React.PureComponent<{ programs: Progra
{
- Array.prototype.concat.apply([], this.props.programs.map(ProgramTable.renderRow))
+ Array.prototype.concat.apply([], this.props.programs.map(ProgramTable.renderRows))
}
diff --git a/app/script/components/RunSectionForm.tsx b/app/script/components/RunSectionForm.tsx
index 6ff15c9..b4a6d82 100644
--- a/app/script/components/RunSectionForm.tsx
+++ b/app/script/components/RunSectionForm.tsx
@@ -1,7 +1,6 @@
import {computed} from "mobx";
import {observer} from "mobx-react";
import * as React from "react";
-import {SyntheticEvent} from "react";
import {DropdownItemProps, DropdownProps, Form, Header, Segment} from "semantic-ui-react";
import {DurationInput} from ".";
import {Duration, Section} from "../sprinklers";
@@ -37,7 +36,7 @@ export default class RunSectionForm extends React.Component<{
;
}
- private onSectionChange = (e: SyntheticEvent, v: DropdownProps) => {
+ private onSectionChange = (e: React.SyntheticEvent, v: DropdownProps) => {
this.setState({section: v.value as number});
}
@@ -45,10 +44,13 @@ export default class RunSectionForm extends React.Component<{
this.setState({duration: newDuration});
}
- private run = (e: SyntheticEvent) => {
+ private run = (e: React.SyntheticEvent) => {
e.preventDefault();
+ if (typeof this.state.section !== "number") {
+ return;
+ }
const section: Section = this.props.sections[this.state.section];
- console.log(`should run section ${section} for ${this.state.duration}`);
+ console.log(`running section ${section} for ${this.state.duration}`);
section.run(this.state.duration)
.then((a) => console.log("ran section", a))
.catch((err) => console.error("error running section", err));
diff --git a/app/script/index.tsx b/app/script/index.tsx
index 0202a3c..e8607c5 100644
--- a/app/script/index.tsx
+++ b/app/script/index.tsx
@@ -1,10 +1,10 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
-import {AppContainer} from "react-hot-loader";
+import { AppContainer } from "react-hot-loader";
import App from "./components/App";
-import {MqttApiClient} from "./mqtt";
-import {Message, UiStore} from "./ui";
+import { MqttApiClient } from "./mqtt";
+import { Message, UiStore } from "./ui";
const client = new MqttApiClient();
client.start();
@@ -15,14 +15,14 @@ uiStore.addMessage(new Message("asdf", "boo!", Message.Type.Error));
const rootElem = document.getElementById("app");
ReactDOM.render(
-
+
, rootElem);
if (module.hot) {
module.hot.accept("./components/App", () => {
const NextApp = require("./components/App").default as typeof App;
ReactDOM.render(
-
+
, rootElem);
});
}
diff --git a/app/script/mqtt.ts b/app/script/mqtt.ts
index ecda84f..8dbc11a 100644
--- a/app/script/mqtt.ts
+++ b/app/script/mqtt.ts
@@ -1,26 +1,26 @@
-import {EventEmitter} from "events";
+import { EventEmitter } from "events";
import "paho-mqtt";
import {
Duration,
- ISectionRun,
ISprinklersApi,
- ITimeOfDay,
Program,
+ ProgramItem,
Schedule,
Section,
+ SectionRun,
SectionRunner,
SprinklersDevice,
+ TimeOfDay,
} from "./sprinklers";
-import {checkedIndexOf} from "./utils";
+import { checkedIndexOf } from "./utils";
import MQTT = Paho.MQTT;
-export class MqttApiClient extends EventEmitter implements ISprinklersApi {
+export class MqttApiClient implements ISprinklersApi {
client: MQTT.Client;
connected: boolean;
devices: { [prefix: string]: MqttSprinklersDevice } = {};
constructor() {
- super();
this.client = new MQTT.Client(location.hostname, 1884, MqttApiClient.newClientId());
this.client.onMessageArrived = (m) => this.onMessageArrived(m);
this.client.onConnectionLost = (e) => this.onConnectionLost(e);
@@ -80,6 +80,10 @@ export class MqttApiClient extends EventEmitter implements ISprinklersApi {
private processMessage(m: MQTT.Message) {
// console.log("message arrived: ", m);
+ if (m.destinationName == null) {
+ console.warn(`revieved invalid message: ${m}`);
+ return;
+ }
const topicIdx = m.destinationName.indexOf("/"); // find the first /
const prefix = m.destinationName.substr(0, topicIdx); // assume prefix does not contain a /
const topic = m.destinationName.substr(topicIdx + 1);
@@ -130,7 +134,7 @@ class MqttSprinklersDevice extends SprinklersDevice {
doSubscribe() {
const c = this.apiClient.client;
this.subscriptions
- .forEach((filter) => c.subscribe(filter, {qos: 1}));
+ .forEach((filter) => c.subscribe(filter, { qos: 1 }));
}
doUnsubscribe() {
@@ -186,7 +190,7 @@ class MqttSprinklersDevice extends SprinklersDevice {
}
matches = topic.match(/^section_runner$/);
if (matches != null) {
- (this.sectionRunner as MqttSectionRunner).onMessage(null, payload);
+ (this.sectionRunner as MqttSectionRunner).onMessage(payload);
return;
}
matches = topic.match(/^responses\/(\d+)$/);
@@ -219,7 +223,15 @@ class MqttSprinklersDevice extends SprinklersDevice {
}
cancelSectionRunById(id: number) {
- return this.makeRequest(`section_runner/cancel_id`, {id});
+ return this.makeRequest(`section_runner/cancel_id`, { id });
+ }
+
+ pauseSectionRunner() {
+ return this.makeRequest(`section_runner/pause`);
+ }
+
+ unpauseSectionRunner() {
+ return this.makeRequest(`section_runner/unpause`);
}
//noinspection JSMethodCanBeStatic
@@ -227,7 +239,7 @@ class MqttSprinklersDevice extends SprinklersDevice {
return Math.floor(Math.random() * 1000000000);
}
- private makeRequest(topic: string, payload: object | string): Promise {
+ private makeRequest(topic: string, payload: object | string = {}): Promise {
return new Promise((resolve, reject) => {
const payloadStr = (typeof payload === "string") ?
payload : JSON.stringify(payload);
@@ -254,7 +266,7 @@ interface IResponseData {
[key: string]: any;
}
-type ResponseCallback = (IResponseData) => void;
+type ResponseCallback = (data: IResponseData) => void;
interface ISectionJSON {
name: string;
@@ -276,8 +288,19 @@ class MqttSection extends Section {
}
}
+interface ITimeOfDayJSON {
+ hour: number;
+ minute: number;
+ second: number;
+ millisecond: number;
+}
+
+function timeOfDayFromJSON(json: ITimeOfDayJSON): TimeOfDay {
+ return new TimeOfDay(json.hour, json.minute, json.second, json.millisecond);
+}
+
interface IScheduleJSON {
- times: ITimeOfDay[];
+ times: ITimeOfDayJSON[];
weekdays: number[];
from?: string;
to?: string;
@@ -285,7 +308,7 @@ interface IScheduleJSON {
function scheduleFromJSON(json: IScheduleJSON): Schedule {
const sched = new Schedule();
- sched.times = json.times;
+ sched.times = json.times.map(timeOfDayFromJSON);
sched.weekdays = json.weekdays;
sched.from = json.from == null ? null : new Date(json.from);
sched.to = json.to == null ? null : new Date(json.to);
@@ -322,11 +345,10 @@ class MqttProgram extends Program {
this.enabled = json.enabled;
}
if (json.sequence != null) {
- // tslint:disable:object-literal-sort-keys
- this.sequence = json.sequence.map((item) => ({
- section: item.section,
- duration: Duration.fromSeconds(item.duration),
- }));
+ this.sequence = json.sequence.map((item) => (new ProgramItem(
+ item.section,
+ Duration.fromSeconds(item.duration),
+ )));
}
if (json.sched != null) {
this.schedule = scheduleFromJSON(json.sched);
@@ -334,23 +356,42 @@ class MqttProgram extends Program {
}
}
+export interface ISectionRunJSON {
+ id: number;
+ section: number;
+ duration: number;
+ startTime?: number;
+ pauseTime?: number;
+}
+
+function sectionRunFromJSON(json: ISectionRunJSON) {
+ const run = new SectionRun();
+ run.id = json.id;
+ run.section = json.section;
+ run.duration = Duration.fromSeconds(json.duration);
+ run.startTime = json.startTime == null ? null : new Date(json.startTime);
+ run.pauseTime = json.pauseTime == null ? null : new Date(json.pauseTime);
+ return run;
+}
+
interface ISectionRunnerJSON {
- queue: ISectionRun[];
- current?: ISectionRun;
+ queue: ISectionRunJSON[];
+ current: ISectionRunJSON | null;
+ paused: boolean;
}
class MqttSectionRunner extends SectionRunner {
- onMessage(topic: string, payload: string) {
+ onMessage(payload: string) {
const json = JSON.parse(payload) as ISectionRunnerJSON;
this.updateFromJSON(json);
}
updateFromJSON(json: ISectionRunnerJSON) {
- if (!json.queue) { // null means empty queue
+ if (!json.queue || !json.queue.length) { // null means empty queue
this.queue.clear();
} else {
- this.queue.replace(json.queue);
+ this.queue.replace(json.queue.map(sectionRunFromJSON));
}
- this.current = json.current;
+ this.current = json.current == null ? null : sectionRunFromJSON(json.current);
}
}
diff --git a/app/script/sprinklers.ts b/app/script/sprinklers.ts
index 403d733..d7900fc 100644
--- a/app/script/sprinklers.ts
+++ b/app/script/sprinklers.ts
@@ -1,4 +1,4 @@
-import {IObservableArray, observable} from "mobx";
+import { IObservableArray, observable } from "mobx";
export abstract class Section {
device: SprinklersDevice;
@@ -22,11 +22,22 @@ export abstract class Section {
}
}
-export interface ITimeOfDay {
+export class TimeOfDay {
hour: number;
minute: number;
second: number;
millisecond: number;
+
+ constructor(hour: number, minute: number = 0, second: number = 0, millisecond: number = 0) {
+ this.hour = hour;
+ this.minute = minute;
+ this.second = second;
+ this.millisecond = millisecond;
+ }
+
+ static fromDate(date: Date): TimeOfDay {
+ return new TimeOfDay(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
+ }
}
export enum Weekday {
@@ -34,17 +45,17 @@ export enum Weekday {
}
export class Schedule {
- times: ITimeOfDay[] = [];
+ times: TimeOfDay[] = [];
weekdays: Weekday[] = [];
- from?: Date = null;
- to?: Date = null;
+ from: Date | null = null;
+ to: Date | null = null;
}
export class Duration {
minutes: number = 0;
seconds: number = 0;
- constructor(minutes: number, seconds: number) {
+ constructor(minutes: number = 0, seconds: number = 0) {
this.minutes = minutes;
this.seconds = seconds;
}
@@ -82,11 +93,16 @@ export class Duration {
}
}
-export interface IProgramItem {
+export class ProgramItem {
// the section number
section: number;
// duration of the run
duration: Duration;
+
+ constructor(section: number, duration: Duration) {
+ this.section = section;
+ this.duration = duration;
+ }
}
export class Program {
@@ -101,7 +117,7 @@ export class Program {
schedule: Schedule = new Schedule();
@observable
- sequence: IProgramItem[] = [];
+ sequence: ProgramItem[] = [];
@observable
running: boolean = false;
@@ -120,21 +136,38 @@ export class Program {
}
}
-export interface ISectionRun {
+export class SectionRun {
id: number;
section: number;
- duration: number;
- startTime?: Date;
+ duration: Duration;
+ startTime: Date | null;
+ pauseTime: Date | null;
+
+ constructor(id: number = 0, section: number = 0, duration: Duration = new Duration()) {
+ this.id = id;
+ this.section = section;
+ this.duration = duration;
+ this.startTime = null;
+ this.pauseTime = null;
+ }
+
+ toString() {
+ return `SectionRun{id=${this.id}, section=${this.section}, duration=${this.duration},` +
+ ` startTime=${this.startTime}, pauseTime=${this.pauseTime}}`;
+ }
}
export class SectionRunner {
device: SprinklersDevice;
@observable
- queue: IObservableArray = observable([]);
+ queue: IObservableArray = observable([]);
@observable
- current: ISectionRun = null;
+ current: SectionRun | null = null;
+
+ @observable
+ paused: boolean = false;
constructor(device: SprinklersDevice) {
this.device = device;
@@ -145,7 +178,7 @@ export class SectionRunner {
}
toString(): string {
- return `SectionRunner{queue="${this.queue}", current="${this.current}"}`;
+ return `SectionRunner{queue="${this.queue}", current="${this.current}", paused=${this.paused}}`;
}
}
@@ -154,10 +187,10 @@ export abstract class SprinklersDevice {
connected: boolean = false;
@observable
- sections: IObservableArray = [] as IObservableArray;
+ sections: IObservableArray = observable.array();
@observable
- programs: IObservableArray = [] as IObservableArray;
+ programs: IObservableArray = observable.array();
@observable
sectionRunner: SectionRunner;
@@ -169,12 +202,16 @@ export abstract class SprinklersDevice {
abstract runProgram(program: number | Program): Promise<{}>;
abstract cancelSectionRunById(id: number): Promise<{}>;
+
+ abstract pauseSectionRunner(): Promise<{}>;
+
+ abstract unpauseSectionRunner(): Promise<{}>;
}
export interface ISprinklersApi {
- start();
+ start(): void;
getDevice(id: string): SprinklersDevice;
- removeDevice(id: string);
+ removeDevice(id: string): void;
}
diff --git a/package.json b/package.json
index d698612..cd833bb 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"clean": "rm -rf ./dist ./build",
"build": "webpack --config ./webpack/prod.config.js",
"start": "webpack-dev-server --config ./webpack/dev.config.js",
- "lint": "tslint \"app/script/**/*\" --force"
+ "lint": "tslint --project . --force"
},
"repository": {
"type": "git",
@@ -29,6 +29,7 @@
"@types/react": "^16",
"@types/react-dom": "^15.5.0",
"@types/react-fontawesome": "^1.5.0",
+ "@types/react-hot-loader": "^3.0.4",
"@types/react-transition-group": "^1.1.1",
"classnames": "^2.2.5",
"core-js": "^2.4.1",
diff --git a/tsconfig.json b/tsconfig.json
index 88d4781..caae317 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,8 @@
"experimentalDecorators": true,
"target": "es5",
"lib": ["es6", "dom"],
- "typeRoots": ["node_modules/@types"]
+ "typeRoots": ["node_modules/@types"],
+ "strict": true
},
"files": [
"./node_modules/@types/webpack-env/index.d.ts",
diff --git a/tslint.json b/tslint.json
index c05a219..1c0575b 100644
--- a/tslint.json
+++ b/tslint.json
@@ -26,6 +26,13 @@
{
"order": "fields-first"
}
+ ],
+ "object-literal-sort-keys": [
+ false
+ ],
+ "no-submodule-imports": false,
+ "no-unused-variable": [
+ true
]
},
"rulesDirectory": []
diff --git a/yarn.lock b/yarn.lock
index 34ff7c9..0333983 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -34,6 +34,12 @@
dependencies:
"@types/react" "*"
+"@types/react-hot-loader@^3.0.4":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/react-hot-loader/-/react-hot-loader-3.0.4.tgz#7fc081509830c64218d8a99a865e2fb4a94572ad"
+ dependencies:
+ "@types/react" "*"
+
"@types/react-transition-group@^1.1.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-1.1.2.tgz#a349e70788a6dc723a5f439217011ef926c27b4f"