diff --git a/app/script/App.tsx b/app/script/App.tsx
index 46cc661..c9e0dd6 100644
--- a/app/script/App.tsx
+++ b/app/script/App.tsx
@@ -1,9 +1,10 @@
import * as React from "react";
-import { computed } from "mobx";
+import {SyntheticEvent} from "react";
+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, DropdownItemProps } from "semantic-ui-react";
+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 FontAwesome = require("react-fontawesome");
import * as classNames from "classnames";
@@ -19,7 +20,7 @@ class SectionTable extends React.PureComponent<{ sections: Section[] }, void> {
if (!section) {
return null;
}
- const { name, state } = section;
+ const {name, state} = section;
return (
{"" + (index + 1)}
@@ -29,7 +30,7 @@ class SectionTable extends React.PureComponent<{ sections: Section[] }, void> {
"section--state-true": state,
"section--state-false": !state,
})}>{state ?
- ( Irrigating)
+ ( Irrigating)
: "Not irrigating"}
@@ -38,22 +39,22 @@ class SectionTable extends React.PureComponent<{ sections: Section[] }, void> {
public render() {
return (
-
-
- Sections
-
-
- #
- Name
- State
-
-
-
- {
- this.props.sections.map(SectionTable.renderRow)
- }
-
-
+
+
+ Sections
+
+
+ #
+ Name
+ State
+
+
+
+ {
+ this.props.sections.map(SectionTable.renderRow)
+ }
+
+
);
}
}
@@ -64,60 +65,87 @@ class DurationInput extends React.Component<{
}, void> {
public render() {
const duration = this.props.duration;
- const editing = this.props.onDurationChange != null;
+ // const editing = this.props.onDurationChange != null;
return ;
}
- private onMinutesChange = (e, { value }) => {
- this.props.onDurationChange(new Duration(Number(value), this.props.duration.seconds));
+ private onMinutesChange = (e, {value}) => {
+ if (value.length === 0 || isNaN(value)) {
+ return;
+ }
+ const newMinutes = parseInt(value, 10);
+ this.props.onDurationChange(this.props.duration.withMinutes(newMinutes));
}
- private onSecondsChange = (e, { value }) => {
- let newSeconds = Number(value);
- let newMinutes = this.props.duration.minutes;
- if (newSeconds >= 60) {
- newMinutes++;
- newSeconds = 0;
- }
- if (newSeconds < 0) {
- newMinutes = Math.max(0, newMinutes - 1);
- newSeconds = 59;
+ private onSecondsChange = (e, {value}) => {
+ if (value.length === 0 || isNaN(value)) {
+ return;
}
- this.props.onDurationChange(new Duration(newMinutes, newSeconds));
+ const newSeconds = parseInt(value, 10);
+ this.props.onDurationChange(this.props.duration.withSeconds(newSeconds));
}
}
@observer
-class RunSectionForm extends React.Component<{ sections: Section[] }, { duration: Duration }> {
- public componentWillMount() {
- this.setState({
+class RunSectionForm extends React.Component<{
+ sections: Section[],
+}, {
+ duration: Duration,
+ section: number | "",
+}> {
+ constructor() {
+ super();
+ this.state = {
duration: new Duration(1, 1),
- });
+ section: "",
+ };
}
public render() {
+ const {section, duration} = this.state;
return
-
- this.setState({ duration: newDuration })} />
+
+
+ {/*Label must be to align it properly*/}
+ Run
;
}
+ private onSectionChange = (e: SyntheticEvent, v: DropdownProps) => {
+ this.setState({section: v.value as number});
+ }
+
+ private onDurationChange = (newDuration: Duration) => {
+ this.setState({duration: newDuration});
+ }
+
+ private run = (e: SyntheticEvent) => {
+ 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);
+ }
+
+ private get isValid(): boolean {
+ return typeof this.state.section === "number";
+ }
+
@computed
private get sectionOptions(): DropdownItemProps[] {
return this.props.sections.map((s, i) => ({
@@ -138,11 +166,11 @@ class ScheduleView extends React.PureComponent<{ schedule: Schedule }, void> {
@observer
class ProgramTable extends React.PureComponent<{ programs: Program[] }, void> {
- private static renderRow(program: Program, i: number) {
+ private static renderRow(program: Program, i: number): JSX.Element[] {
if (!program) {
return null;
}
- const { name, running, enabled, schedule, sequence } = program;
+ const {name, running, enabled, schedule, sequence} = program;
return [
{"" + (i + 1)}
@@ -155,10 +183,10 @@ class ProgramTable extends React.PureComponent<{ programs: Program[] }, void> {
{sequence.map((item) =>
- (- Section {item.section + 1 + ""} for
- {item.duration.minutes}M {item.duration.seconds}S
))}
-
-
+ (Section {item.section + 1 + ""} for
+ {item.duration.minutes}M {item.duration.seconds}S))}
+
+
,
@@ -189,13 +217,13 @@ class ProgramTable extends React.PureComponent<{ programs: Program[] }, void> {
}
}
-const ConnectionState = ({ connected }: { connected: boolean }) =>
+const ConnectionState = ({connected}: { connected: boolean }) =>
-
+
{connected ? "Connected" : "Disconnected"}
;
@@ -203,21 +231,21 @@ const ConnectionState = ({ connected }: { connected: boolean }) =>
@observer
class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void> {
public render() {
- const { id, connected, sections, programs } = this.props.device;
+ const {id, connected, sections, programs} = this.props.device;
return (
-
- ("app/images/raspberry_pi.png")} />
+ ("app/images/raspberry_pi.png")}/>
-
-
-
+
+
+
);
@@ -228,7 +256,7 @@ class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void>
export default class App extends React.PureComponent<{ device: SprinklersDevice }, any> {
public render() {
return
-
+
;
}
diff --git a/app/script/mqtt.ts b/app/script/mqtt.ts
index df3a31a..9d72816 100644
--- a/app/script/mqtt.ts
+++ b/app/script/mqtt.ts
@@ -2,7 +2,6 @@ import "paho-mqtt/mqttws31";
import MQTT = Paho.MQTT;
import { EventEmitter } from "events";
-import * as objectAssign from "object-assign";
import {
SprinklersDevice, ISprinklersApi, Section, Program, IProgramItem, Schedule, ITimeOfDay, Weekday, Duration,
} from "./sprinklers";
@@ -80,7 +79,7 @@ export class MqttApiClient extends EventEmitter implements ISprinklersApi {
const topic = m.destinationName.substr(topicIdx + 1);
const device = this.devices[prefix];
if (!device) {
- console.warn(`recieved message for unknown device. prefix: ${prefix}`);
+ console.warn(`received message for unknown device. prefix: ${prefix}`);
return;
}
device.onMessage(topic, m.payloadString);
@@ -134,7 +133,7 @@ class MqttSprinklersDevice extends SprinklersDevice {
const secNum = Number(secStr);
let section = this.sections[secNum];
if (!section) {
- this.sections[secNum] = section = new MqttSection();
+ this.sections[secNum] = section = new MqttSection(this);
}
(section as MqttSection).onMessage(subTopic, payload);
}
@@ -163,6 +162,23 @@ class MqttSprinklersDevice extends SprinklersDevice {
return this.prefix;
}
+ 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);
+ }
+
private get subscriptions() {
return [
`${this.prefix}/connected`,
@@ -179,7 +195,16 @@ interface ISectionJSON {
pin: number;
}
+interface IRunSectionJSON {
+ duration: number;
+}
+
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 d80fed0..aca1ce4 100644
--- a/app/script/sprinklers.ts
+++ b/app/script/sprinklers.ts
@@ -1,11 +1,25 @@
import { observable, IObservableArray } from "mobx";
-export class Section {
+export abstract class Section {
+ public device: SprinklersDevice;
+
@observable
public name: string = "";
@observable
public state: boolean = false;
+
+ constructor(device: SprinklersDevice) {
+ this.device = device;
+ }
+
+ public run(duration: Duration) {
+ this.device.runSection(this, duration);
+ }
+
+ public toString(): string {
+ return `Section{name="${this.name}", state=${this.state}}`;
+ }
}
export interface ITimeOfDay {
@@ -42,6 +56,30 @@ export class Duration {
public toSeconds(): number {
return this.minutes * 60 + this.seconds;
}
+
+ public withSeconds(newSeconds: number): Duration {
+ let newMinutes = this.minutes;
+ if (newSeconds >= 60) {
+ newMinutes++;
+ newSeconds = 0;
+ }
+ if (newSeconds < 0) {
+ newMinutes = Math.max(0, newMinutes - 1);
+ newSeconds = 59;
+ }
+ return new Duration(newMinutes, newSeconds);
+ }
+
+ public withMinutes(newMinutes: number): Duration {
+ if (newMinutes < 0) {
+ newMinutes = 0;
+ }
+ return new Duration(newMinutes, this.seconds);
+ }
+
+ public toString(): string {
+ return `${this.minutes}M ${this.seconds}S`;
+ }
}
export interface IProgramItem {
@@ -77,6 +115,8 @@ export abstract class SprinklersDevice {
@observable
public programs: IObservableArray = [] as IObservableArray;
+ public abstract runSection(section: number | Section, duration: Duration);
+
abstract get id(): string;
}
diff --git a/app/style/app.css b/app/style/app.css
index 9099f26..0474802 100644
--- a/app/style/app.css
+++ b/app/style/app.css
@@ -35,7 +35,7 @@
}
-.durationInput--minutes,
-.durationInput--seconds {
- width: 80px;
+.durationInput--minutes > input,
+.durationInput--seconds > input {
+ width: 70px !important;
}
\ No newline at end of file