From 94d368130a753a64498ad7c49b679152b6e0f5e6 Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Sat, 27 May 2017 14:03:13 -0600 Subject: [PATCH] More work on features --- app/script/App.tsx | 152 +++++++++++++++++++++++---------------- app/script/mqtt.ts | 31 +++++++- app/script/sprinklers.ts | 42 ++++++++++- app/style/app.css | 6 +- 4 files changed, 162 insertions(+), 69 deletions(-) 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
+ 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 }) => { - 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
Run Section
- - 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")}/>
    Device {id} - +
    - - - + + +
    ); @@ -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