Browse Source

Fixed/improved a bunch of stuff

update-deps
Alex Mikhalev 7 years ago
parent
commit
d895e2e3e9
  1. 3
      .vscode/settings.json
  2. 39
      .vscode/tasks.json
  3. 26
      app/script/components/DeviceView.tsx
  4. 24
      app/script/components/DurationInput.tsx
  5. 4
      app/script/components/ProgramTable.tsx
  6. 10
      app/script/components/RunSectionForm.tsx
  7. 10
      app/script/index.tsx
  8. 89
      app/script/mqtt.ts
  9. 73
      app/script/sprinklers.ts
  10. 3
      package.json
  11. 3
      tsconfig.json
  12. 7
      tslint.json
  13. 6
      yarn.lock

3
.vscode/settings.json vendored

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

39
.vscode/tasks.json vendored

@ -1,27 +1,28 @@ @@ -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
}
]
}
}
]
}

26
app/script/components/DeviceView.tsx

@ -1,19 +1,19 @@ @@ -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 }) =>
<span className={classNames({
"device--connectionState": true,
"device--connectionState-connected": connected,
"device--connectionState-disconnected": !connected,
})}>
<FontAwesome name={connected ? "plug" : "chain-broken"}/>
<FontAwesome name={connected ? "plug" : "chain-broken"} />
&nbsp;
{connected ? "Connected" : "Disconnected"}
</span>;
@ -21,22 +21,22 @@ const ConnectionState = ({connected}: { connected: boolean }) => @@ -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 (
<Item>
<Item.Image src={require<string>("app/images/raspberry_pi.png")}/>
<Item.Image src={require<string>("app/images/raspberry_pi.png")} />
<Item.Content>
<Header as="h1">
<span>Device </span><kbd>{id}</kbd>
<ConnectionState connected={connected}/>
<ConnectionState connected={connected} />
</Header>
<Item.Meta>
</Item.Meta>
<SectionRunnerView sectionRunner={sectionRunner}/>
<SectionTable sections={sections}/>
<RunSectionForm sections={sections}/>
<ProgramTable programs={programs}/>
<SectionRunnerView sectionRunner={sectionRunner} />
<SectionTable sections={sections} />
<RunSectionForm sections={sections} />
<ProgramTable programs={programs} />
</Item.Content>
</Item>
);

24
app/script/components/DurationInput.tsx

@ -1,11 +1,11 @@ @@ -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<{ @@ -13,25 +13,25 @@ export default class DurationInput extends React.Component<{
<label>Duration</label>
<div className="fields">
<Input type="number" className="field durationInput--minutes"
value={duration.minutes} onChange={this.onMinutesChange}
label="M" labelPosition="right"/>
value={duration.minutes} onChange={this.onMinutesChange}
label="M" labelPosition="right" />
<Input type="number" className="field durationInput--seconds"
value={duration.seconds} onChange={this.onSecondsChange} max="60"
label="S" labelPosition="right"/>
value={duration.seconds} onChange={this.onSecondsChange} max="60"
label="S" labelPosition="right" />
</div>
</div>;
}
private onMinutesChange = (e, {value}) => {
if (value.length === 0 || isNaN(value)) {
private onMinutesChange = (e: React.SyntheticEvent<any>, { 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<any>, { value }: InputOnChangeData) => {
if (value.length === 0 || isNaN(Number(value))) {
return;
}
const newSeconds = parseInt(value, 10);

4
app/script/components/ProgramTable.tsx

@ -14,7 +14,7 @@ export class ScheduleView extends React.PureComponent<{ schedule: Schedule }, {} @@ -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 @@ -57,7 +57,7 @@ export default class ProgramTable extends React.PureComponent<{ programs: Progra
</Table.Header>
<Table.Body>
{
Array.prototype.concat.apply([], this.props.programs.map(ProgramTable.renderRow))
Array.prototype.concat.apply([], this.props.programs.map(ProgramTable.renderRows))
}
</Table.Body>
</Table>

10
app/script/components/RunSectionForm.tsx

@ -1,7 +1,6 @@ @@ -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<{ @@ -37,7 +36,7 @@ export default class RunSectionForm extends React.Component<{
</Segment>;
}
private onSectionChange = (e: SyntheticEvent<HTMLElement>, v: DropdownProps) => {
private onSectionChange = (e: React.SyntheticEvent<HTMLElement>, v: DropdownProps) => {
this.setState({section: v.value as number});
}
@ -45,10 +44,13 @@ export default class RunSectionForm extends React.Component<{ @@ -45,10 +44,13 @@ export default class RunSectionForm extends React.Component<{
this.setState({duration: newDuration});
}
private run = (e: SyntheticEvent<HTMLElement>) => {
private run = (e: React.SyntheticEvent<HTMLElement>) => {
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));

10
app/script/index.tsx

@ -1,10 +1,10 @@ @@ -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)); @@ -15,14 +15,14 @@ uiStore.addMessage(new Message("asdf", "boo!", Message.Type.Error));
const rootElem = document.getElementById("app");
ReactDOM.render(<AppContainer>
<App device={device} uiStore={uiStore}/>
<App device={device} uiStore={uiStore} />
</AppContainer>, rootElem);
if (module.hot) {
module.hot.accept("./components/App", () => {
const NextApp = require<any>("./components/App").default as typeof App;
ReactDOM.render(<AppContainer>
<NextApp device={device} uiStore={uiStore}/>
<NextApp device={device} uiStore={uiStore} />
</AppContainer>, rootElem);
});
}

89
app/script/mqtt.ts

@ -1,26 +1,26 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -227,7 +239,7 @@ class MqttSprinklersDevice extends SprinklersDevice {
return Math.floor(Math.random() * 1000000000);
}
private makeRequest(topic: string, payload: object | string): Promise<IResponseData> {
private makeRequest(topic: string, payload: object | string = {}): Promise<IResponseData> {
return new Promise<IResponseData>((resolve, reject) => {
const payloadStr = (typeof payload === "string") ?
payload : JSON.stringify(payload);
@ -254,7 +266,7 @@ interface IResponseData { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);
}
}

73
app/script/sprinklers.ts

@ -1,4 +1,4 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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<ISectionRun> = observable([]);
queue: IObservableArray<SectionRun> = 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 { @@ -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 { @@ -154,10 +187,10 @@ export abstract class SprinklersDevice {
connected: boolean = false;
@observable
sections: IObservableArray<Section> = [] as IObservableArray<Section>;
sections: IObservableArray<Section> = observable.array<Section>();
@observable
programs: IObservableArray<Program> = [] as IObservableArray<Program>;
programs: IObservableArray<Program> = observable.array<Program>();
@observable
sectionRunner: SectionRunner;
@ -169,12 +202,16 @@ export abstract class SprinklersDevice { @@ -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;
}

3
package.json

@ -8,7 +8,7 @@ @@ -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 @@ @@ -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",

3
tsconfig.json

@ -5,7 +5,8 @@ @@ -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",

7
tslint.json

@ -26,6 +26,13 @@ @@ -26,6 +26,13 @@
{
"order": "fields-first"
}
],
"object-literal-sort-keys": [
false
],
"no-submodule-imports": false,
"no-unused-variable": [
true
]
},
"rulesDirectory": []

6
yarn.lock

@ -34,6 +34,12 @@ @@ -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"

Loading…
Cancel
Save