From f23ad08f92421d1127fb1ae9063a3e6de8c4ac84 Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Wed, 4 Oct 2017 13:22:10 -0600 Subject: [PATCH] More work on server; lots of refactoring and good stuff --- .vscode/launch.json | 5 +- .vscode/tasks.json | 10 + app/state/index.ts | 2 +- common/sprinklers.ts | 224 ------------------- common/sprinklers/Duration.ts | 41 ++++ common/sprinklers/ISprinklersApi.ts | 9 + common/sprinklers/Program.ts | 47 ++++ common/sprinklers/Section.ts | 25 +++ common/sprinklers/SectionRunner.ts | 49 ++++ common/sprinklers/SprinklersDevice.ts | 38 ++++ common/sprinklers/index.ts | 8 + common/sprinklers/json/index.ts | 172 ++++++++++++++ common/{mqtt.ts => sprinklers/mqtt/index.ts} | 6 +- common/sprinklers/schedule.ts | 28 +++ package.json | 2 + server/index.ts | 12 +- yarn.lock | 4 + 17 files changed, 451 insertions(+), 231 deletions(-) delete mode 100644 common/sprinklers.ts create mode 100644 common/sprinklers/Duration.ts create mode 100644 common/sprinklers/ISprinklersApi.ts create mode 100644 common/sprinklers/Program.ts create mode 100644 common/sprinklers/Section.ts create mode 100644 common/sprinklers/SectionRunner.ts create mode 100644 common/sprinklers/SprinklersDevice.ts create mode 100644 common/sprinklers/index.ts create mode 100644 common/sprinklers/json/index.ts rename common/{mqtt.ts => sprinklers/mqtt/index.ts} (99%) create mode 100644 common/sprinklers/schedule.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index b7e67d1..c55cc47 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,10 @@ "type": "node", "request": "launch", "name": "Launch Program", - "program": "${workspaceRoot}/bin/www" + "env": { + "NODE_ENV": "development" + }, + "program": "${workspaceRoot}/dist/server/index.js" } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 4a8b52d..320e680 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -33,6 +33,16 @@ "type": "npm", "script": "start:pretty", "problemMatcher": [] + }, + { + "type": "npm", + "script": "start:dev", + "problemMatcher": [] + }, + { + "type": "npm", + "script": "start:watch", + "problemMatcher": [] } ] } \ No newline at end of file diff --git a/app/state/index.ts b/app/state/index.ts index 77e9893..3951eee 100644 --- a/app/state/index.ts +++ b/app/state/index.ts @@ -1,5 +1,5 @@ -import { MqttApiClient } from "@common/mqtt"; import { ISprinklersApi } from "@common/sprinklers"; +import { MqttApiClient } from "@common/sprinklers/mqtt"; import { UiMessage, UiStore } from "./ui"; export { UiMessage, UiStore }; diff --git a/common/sprinklers.ts b/common/sprinklers.ts deleted file mode 100644 index e86b937..0000000 --- a/common/sprinklers.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { IObservableArray, observable } from "mobx"; - -export abstract class Section { - device: SprinklersDevice; - - @observable - name: string = ""; - - @observable - state: boolean = false; - - constructor(device: SprinklersDevice) { - this.device = device; - } - - run(duration: Duration) { - return this.device.runSection(this, duration); - } - - toString(): string { - return `Section{name="${this.name}", state=${this.state}}`; - } -} - -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 { - Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, -} - -export class Schedule { - times: TimeOfDay[] = []; - weekdays: Weekday[] = []; - from: Date | null = null; - to: Date | null = null; -} - -export class Duration { - minutes: number = 0; - seconds: number = 0; - - constructor(minutes: number = 0, seconds: number = 0) { - this.minutes = minutes; - this.seconds = seconds; - } - - static fromSeconds(seconds: number): Duration { - return new Duration(Math.floor(seconds / 60), seconds % 60); - } - - toSeconds(): number { - return this.minutes * 60 + this.seconds; - } - - 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); - } - - withMinutes(newMinutes: number): Duration { - if (newMinutes < 0) { - newMinutes = 0; - } - return new Duration(newMinutes, this.seconds); - } - - toString(): string { - return `${this.minutes}M ${this.seconds}S`; - } -} - -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 { - device: SprinklersDevice; - - @observable - name: string = ""; - @observable - enabled: boolean = false; - - @observable - schedule: Schedule = new Schedule(); - - @observable - sequence: ProgramItem[] = []; - - @observable - running: boolean = false; - - constructor(device: SprinklersDevice) { - this.device = device; - } - - run() { - return this.device.runProgram(this); - } - - toString(): string { - return `Program{name="${this.name}", enabled=${this.enabled}, schedule=${this.schedule}, - sequence=${this.sequence}, running=${this.running}}`; - } -} - -export class SectionRun { - id: number; - section: number; - 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([]); - - @observable - current: SectionRun | null = null; - - @observable - paused: boolean = false; - - constructor(device: SprinklersDevice) { - this.device = device; - } - - cancelRunById(id: number): Promise<{}> { - return this.device.cancelSectionRunById(id); - } - - toString(): string { - return `SectionRunner{queue="${this.queue}", current="${this.current}", paused=${this.paused}}`; - } -} - -export abstract class SprinklersDevice { - @observable - connected: boolean = false; - - @observable - sections: IObservableArray
= observable.array
(); - - @observable - programs: IObservableArray = observable.array(); - - @observable - sectionRunner: SectionRunner; - - abstract get id(): string; - - abstract runSection(section: number | Section, duration: Duration): Promise<{}>; - - abstract runProgram(program: number | Program): Promise<{}>; - - abstract cancelSectionRunById(id: number): Promise<{}>; - - abstract pauseSectionRunner(): Promise<{}>; - - abstract unpauseSectionRunner(): Promise<{}>; - - toString(): string { - return `SprinklersDevice{id="${this.id}", connected=${this.connected}, - sections=${this.sections}, - programs=${this.programs}, - sectionRunner=${this.sectionRunner} }`; - } -} - -export interface ISprinklersApi { - start(): void; - - getDevice(id: string): SprinklersDevice; - - removeDevice(id: string): void; -} diff --git a/common/sprinklers/Duration.ts b/common/sprinklers/Duration.ts new file mode 100644 index 0000000..6c64e30 --- /dev/null +++ b/common/sprinklers/Duration.ts @@ -0,0 +1,41 @@ +export class Duration { + minutes: number = 0; + seconds: number = 0; + + constructor(minutes: number = 0, seconds: number = 0) { + this.minutes = minutes; + this.seconds = seconds; + } + + static fromSeconds(seconds: number): Duration { + return new Duration(Math.floor(seconds / 60), seconds % 60); + } + + toSeconds(): number { + return this.minutes * 60 + this.seconds; + } + + 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); + } + + withMinutes(newMinutes: number): Duration { + if (newMinutes < 0) { + newMinutes = 0; + } + return new Duration(newMinutes, this.seconds); + } + + toString(): string { + return `${this.minutes}M ${this.seconds}S`; + } +} diff --git a/common/sprinklers/ISprinklersApi.ts b/common/sprinklers/ISprinklersApi.ts new file mode 100644 index 0000000..07a95e2 --- /dev/null +++ b/common/sprinklers/ISprinklersApi.ts @@ -0,0 +1,9 @@ +import { SprinklersDevice } from "./SprinklersDevice"; + +export interface ISprinklersApi { + start(): void; + + getDevice(id: string): SprinklersDevice; + + removeDevice(id: string): void; +} diff --git a/common/sprinklers/Program.ts b/common/sprinklers/Program.ts new file mode 100644 index 0000000..fb937e9 --- /dev/null +++ b/common/sprinklers/Program.ts @@ -0,0 +1,47 @@ +import { observable } from "mobx"; +import { Duration } from "./Duration"; +import { Schedule } from "./schedule"; +import { SprinklersDevice } from "./SprinklersDevice"; + +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 { + device: SprinklersDevice; + + @observable + name: string = ""; + @observable + enabled: boolean = false; + + @observable + schedule: Schedule = new Schedule(); + + @observable + sequence: ProgramItem[] = []; + + @observable + running: boolean = false; + + constructor(device: SprinklersDevice) { + this.device = device; + } + + run() { + return this.device.runProgram(this); + } + + toString(): string { + return `Program{name="${this.name}", enabled=${this.enabled}, schedule=${this.schedule}, + sequence=${this.sequence}, running=${this.running}}`; + } +} diff --git a/common/sprinklers/Section.ts b/common/sprinklers/Section.ts new file mode 100644 index 0000000..854ebad --- /dev/null +++ b/common/sprinklers/Section.ts @@ -0,0 +1,25 @@ +import { observable } from "mobx"; +import { Duration } from "./Duration"; +import { SprinklersDevice } from "./SprinklersDevice"; + +export class Section { + device: SprinklersDevice; + + @observable + name: string = ""; + + @observable + state: boolean = false; + + constructor(device: SprinklersDevice) { + this.device = device; + } + + run(duration: Duration) { + return this.device.runSection(this, duration); + } + + toString(): string { + return `Section{name="${this.name}", state=${this.state}}`; + } +} diff --git a/common/sprinklers/SectionRunner.ts b/common/sprinklers/SectionRunner.ts new file mode 100644 index 0000000..e79d30b --- /dev/null +++ b/common/sprinklers/SectionRunner.ts @@ -0,0 +1,49 @@ +import { IObservableArray, observable } from "mobx"; +import { Duration } from "./Duration"; +import { SprinklersDevice } from "./SprinklersDevice"; + +export class SectionRun { + id: number; + section: number; + 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([]); + + @observable + current: SectionRun | null = null; + + @observable + paused: boolean = false; + + constructor(device: SprinklersDevice) { + this.device = device; + } + + cancelRunById(id: number): Promise<{}> { + return this.device.cancelSectionRunById(id); + } + + toString(): string { + return `SectionRunner{queue="${this.queue}", current="${this.current}", paused=${this.paused}}`; + } +} diff --git a/common/sprinklers/SprinklersDevice.ts b/common/sprinklers/SprinklersDevice.ts new file mode 100644 index 0000000..3ef1f4b --- /dev/null +++ b/common/sprinklers/SprinklersDevice.ts @@ -0,0 +1,38 @@ +import { IObservableArray, observable } from "mobx"; +import { Duration } from "./Duration"; +import { Program } from "./Program"; +import { Section } from "./Section"; +import { SectionRunner } from "./SectionRunner"; + +export abstract class SprinklersDevice { + @observable + connected: boolean = false; + + @observable + sections: IObservableArray
= observable.array
(); + + @observable + programs: IObservableArray = observable.array(); + + @observable + sectionRunner: SectionRunner; + + abstract get id(): string; + + abstract runSection(section: number | Section, duration: Duration): Promise<{}>; + + abstract runProgram(program: number | Program): Promise<{}>; + + abstract cancelSectionRunById(id: number): Promise<{}>; + + abstract pauseSectionRunner(): Promise<{}>; + + abstract unpauseSectionRunner(): Promise<{}>; + + toString(): string { + return `SprinklersDevice{id="${this.id}", connected=${this.connected}, + sections=${this.sections}, + programs=${this.programs}, + sectionRunner=${this.sectionRunner} }`; + } +} diff --git a/common/sprinklers/index.ts b/common/sprinklers/index.ts new file mode 100644 index 0000000..3424c58 --- /dev/null +++ b/common/sprinklers/index.ts @@ -0,0 +1,8 @@ +import { IObservableArray, observable } from "mobx"; +export * from "./Duration"; +export * from "./ISprinklersApi"; +export * from "./Program"; +export * from "./schedule"; +export * from "./Section"; +export * from "./SectionRunner"; +export * from "./SprinklersDevice"; diff --git a/common/sprinklers/json/index.ts b/common/sprinklers/json/index.ts new file mode 100644 index 0000000..636fdd5 --- /dev/null +++ b/common/sprinklers/json/index.ts @@ -0,0 +1,172 @@ +import { assign, pick } from "lodash"; +import * as s from ".."; + +export interface ISectionJSON { + name: string; + state: boolean; +} +const sectionProps = ["name", "state"]; + +export function sectionToJSON(sec: s.Section): ISectionJSON { + return pick(sec, sectionProps); +} + +export function sectionFromJSON(sec: s.Section, json: ISectionJSON) { + assign(sec, pick(json, sectionProps)); +} + +export interface ITimeOfDayJSON { + hour: number; + minute: number; + second: number; + millisecond: number; +} +const timeOfDayProps = ["hour", "minute", "second", "millisecond"]; + +export function timeOfDayToJSON(timeOfDay: s.TimeOfDay): ITimeOfDayJSON { + return pick(timeOfDay, timeOfDayProps); +} + +export function timeOfDayFromJSON(timeOfDay: s.TimeOfDay, json: ITimeOfDayJSON) { + assign(timeOfDay, pick(json, timeOfDayProps)); +} + +export interface IScheduleJSON { + times: ITimeOfDayJSON[]; + weekdays: number[]; + from?: string; + to?: string; +} +const scheduleProps = ["weekdays", "from", "to"]; + +export function scheduleToJSON(schedule: s.Schedule): IScheduleJSON { + return { + ...pick(schedule, scheduleProps), + times: schedule.times.map(timeOfDayToJSON), + }; +} + +export function scheduleFromJSON(schedule: s.Schedule, json: IScheduleJSON) { + assign(schedule, pick(json, scheduleProps)); + schedule.times.length = json.times.length; + schedule.times.forEach((timeOfDay, i) => + timeOfDayFromJSON(timeOfDay, json.times[i])); +} + +export interface IProgramItemJSON { + section: number; + duration: number; +} +const programItemProps = ["section"]; + +export function programItemToJSON(programItem: s.ProgramItem): IProgramItemJSON { + return { + ...pick(programItem, programItemProps), + duration: programItem.duration.toSeconds(), + }; +} + +export function programItemFromJSON(programItem: s.ProgramItem, json: IProgramItemJSON) { + assign(programItem, pick(json, programItemProps)); + programItem.duration = s.Duration.fromSeconds(json.duration); +} + +export interface IProgramJSON { + name: string; + enabled: boolean; + sequence: IProgramItemJSON[]; + schedule: IScheduleJSON; + running: boolean; +} +const programProps = ["name", "enabled", "running"]; + +export function programToJSON(program: s.Program): IProgramJSON { + return { + ...pick(program, programProps), + sequence: program.sequence.map(programItemToJSON), + schedule: scheduleToJSON(program.schedule), + }; +} + +export function programFromJSON(program: s.Program, json: IProgramJSON) { + assign(program, pick(json, programProps)); + program.sequence.length = json.sequence.length; + program.sequence.forEach((programItem, i) => + programItemFromJSON(programItem, json.sequence[i])); + scheduleFromJSON(program.schedule, json.schedule); +} + +export interface ISectionRunJSON { + id: number; + section: number; + duration: number; + startTime?: number; + pauseTime?: number; +} +const sectionRunProps = ["id", "section", "duration", "startTime", "pauseTime"]; + +export function sectionRunToJSON(sectionRun: s.SectionRun): ISectionRunJSON { + return pick(sectionRun, sectionRunProps); +} + +export function sectionRunFromJSON(sectionRun: s.SectionRun, json: ISectionRunJSON) { + assign(sectionRun, pick(json, sectionRunProps)); +} + +interface ISectionRunnerJSON { + queue: ISectionRunJSON[]; + current: ISectionRunJSON | null; + paused: boolean; +} +const sectionRunnerProps = ["paused"]; + +export function sectionRunnerToJSON(sectionRunner: s.SectionRunner): ISectionRunnerJSON { + return { + ...pick(sectionRunner, sectionRunnerProps), + queue: sectionRunner.queue.map(sectionRunToJSON), + current: sectionRunner.current ? sectionRunToJSON(sectionRunner.current) : null, + }; +} + +export function sectionRunnerFromJSON(sectionRunner: s.SectionRunner, json: ISectionRunnerJSON) { + assign(sectionRunner, pick(json, sectionRunnerProps)); + sectionRunner.queue.length = json.queue.length; + sectionRunner.queue.forEach((sectionRun, i) => + sectionRunFromJSON(sectionRun, json.queue[i])); + if (json.current == null) { + sectionRunner.current = null; + } else { + if (sectionRunner.current == null) { + sectionRunner.current = new s.SectionRun(); + } + sectionRunFromJSON(sectionRunner.current, json.current); + } +} + +interface ISprinklersDeviceJSON { + connected: boolean; + sections: ISectionJSON[]; + sectionRunner: ISectionRunnerJSON; + programs: IProgramJSON[]; +} +const sprinklersDeviceProps = ["connected"]; + +export function sprinklersDeviceToJSON(sprinklersDevice: s.SprinklersDevice): ISprinklersDeviceJSON { + return { + ...pick(sprinklersDevice, sprinklersDeviceProps), + sections: sprinklersDevice.sections.map(sectionToJSON), + sectionRunner: sectionRunnerToJSON(sprinklersDevice.sectionRunner), + programs: sprinklersDevice.programs.map(programToJSON), + }; +} + +export function sprinklersDeviceFromJSON(sprinklersDevice: s.SprinklersDevice, json: ISprinklersDeviceJSON) { + assign(sprinklersDevice, pick(json, sprinklersDeviceProps)); + sprinklersDevice.sections.length = json.sections.length; + sprinklersDevice.sections.forEach((section, i) => + sectionFromJSON(section, json.sections[i])); + sectionRunnerFromJSON(sprinklersDevice.sectionRunner, json.sectionRunner); + sprinklersDevice.programs.length = json.programs.length; + sprinklersDevice.programs.forEach((program, i) => + programFromJSON(program, json.programs[i])); +} diff --git a/common/mqtt.ts b/common/sprinklers/mqtt/index.ts similarity index 99% rename from common/mqtt.ts rename to common/sprinklers/mqtt/index.ts index c12fc12..91e01e8 100644 --- a/common/mqtt.ts +++ b/common/sprinklers/mqtt/index.ts @@ -1,6 +1,6 @@ import * as mqtt from "mqtt"; -import logger from "./logger"; +import logger from "@common/logger"; import { Duration, ISprinklersApi, @@ -12,8 +12,8 @@ import { SectionRunner, SprinklersDevice, TimeOfDay, -} from "./sprinklers"; -import { checkedIndexOf } from "./utils"; +} from "@common/sprinklers"; +import { checkedIndexOf } from "@common/utils"; const log = logger.child({ source: "mqtt" }); diff --git a/common/sprinklers/schedule.ts b/common/sprinklers/schedule.ts new file mode 100644 index 0000000..b13973b --- /dev/null +++ b/common/sprinklers/schedule.ts @@ -0,0 +1,28 @@ +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 { + Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, +} + +export class Schedule { + times: TimeOfDay[] = []; + weekdays: Weekday[] = []; + from: Date | null = null; + to: Date | null = null; +} diff --git a/package.json b/package.json index 45d69e1..73815bc 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@types/classnames": "^2.2.0", "@types/core-js": "^0.9.43", "@types/express": "^4.0.37", + "@types/lodash": "^4.14.77", "@types/mqtt": "^0.0.34", "@types/node": "^8.0.6", "@types/object-assign": "^4.0.30", @@ -52,6 +53,7 @@ "express-pino-logger": "^2.0.0", "extract-text-webpack-plugin": "^3.0.0", "font-awesome": "^4.7.0", + "lodash": "^4.17.4", "mobx": "^3.1.11", "mobx-react": "^4.2.1", "module-alias": "^2.0.1", diff --git a/server/index.ts b/server/index.ts index 1c56a39..2210c85 100644 --- a/server/index.ts +++ b/server/index.ts @@ -3,11 +3,11 @@ import "./configureAlias"; import "env"; import log from "@common/logger"; -import * as mqtt from "@common/mqtt"; +import * as mqtt from "@common/sprinklers/mqtt"; import { Server } from "http"; import app from "./app"; -const mqttClient = new mqtt.MqttApiClient("mqtt://localhost:1882"); +const mqttClient = new mqtt.MqttApiClient("mqtt://localhost:1883"); mqttClient.start(); @@ -15,6 +15,14 @@ import { autorun } from "mobx"; const device = mqttClient.getDevice("grinklers"); autorun(() => log.info("device: ", device.toString())); +import * as json from "@common/sprinklers/json"; + +app.get("/grinklers", (req, res) => { + const j = json.sprinklersDeviceToJSON(device); + console.dir(device); + res.send(j); +}); + const server = new Server(app); const port = +(process.env.PORT || 8080); diff --git a/yarn.lock b/yarn.lock index f8624df..021e371 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,6 +27,10 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" +"@types/lodash@^4.14.77": + version "4.14.77" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.77.tgz#0bc699413e84d6ed5d927ca30ea0f0a890b42d75" + "@types/mime@*": version "1.3.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.1.tgz#2cf42972d0931c1060c7d5fa6627fce6bd876f2f"