Browse Source

More work on server; lots of refactoring and good stuff

update-deps
Alex Mikhalev 7 years ago
parent
commit
f23ad08f92
  1. 5
      .vscode/launch.json
  2. 10
      .vscode/tasks.json
  3. 2
      app/state/index.ts
  4. 224
      common/sprinklers.ts
  5. 41
      common/sprinklers/Duration.ts
  6. 9
      common/sprinklers/ISprinklersApi.ts
  7. 47
      common/sprinklers/Program.ts
  8. 25
      common/sprinklers/Section.ts
  9. 49
      common/sprinklers/SectionRunner.ts
  10. 38
      common/sprinklers/SprinklersDevice.ts
  11. 8
      common/sprinklers/index.ts
  12. 172
      common/sprinklers/json/index.ts
  13. 6
      common/sprinklers/mqtt/index.ts
  14. 28
      common/sprinklers/schedule.ts
  15. 2
      package.json
  16. 12
      server/index.ts
  17. 4
      yarn.lock

5
.vscode/launch.json vendored

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

10
.vscode/tasks.json vendored

@ -33,6 +33,16 @@ @@ -33,6 +33,16 @@
"type": "npm",
"script": "start:pretty",
"problemMatcher": []
},
{
"type": "npm",
"script": "start:dev",
"problemMatcher": []
},
{
"type": "npm",
"script": "start:watch",
"problemMatcher": []
}
]
}

2
app/state/index.ts

@ -1,5 +1,5 @@ @@ -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 };

224
common/sprinklers.ts

@ -1,224 +0,0 @@ @@ -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<SectionRun> = 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<Section> = observable.array<Section>();
@observable
programs: IObservableArray<Program> = observable.array<Program>();
@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;
}

41
common/sprinklers/Duration.ts

@ -0,0 +1,41 @@ @@ -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`;
}
}

9
common/sprinklers/ISprinklersApi.ts

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
import { SprinklersDevice } from "./SprinklersDevice";
export interface ISprinklersApi {
start(): void;
getDevice(id: string): SprinklersDevice;
removeDevice(id: string): void;
}

47
common/sprinklers/Program.ts

@ -0,0 +1,47 @@ @@ -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}}`;
}
}

25
common/sprinklers/Section.ts

@ -0,0 +1,25 @@ @@ -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}}`;
}
}

49
common/sprinklers/SectionRunner.ts

@ -0,0 +1,49 @@ @@ -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<SectionRun> = 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}}`;
}
}

38
common/sprinklers/SprinklersDevice.ts

@ -0,0 +1,38 @@ @@ -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<Section> = observable.array<Section>();
@observable
programs: IObservableArray<Program> = observable.array<Program>();
@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} }`;
}
}

8
common/sprinklers/index.ts

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

172
common/sprinklers/json/index.ts

@ -0,0 +1,172 @@ @@ -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]));
}

6
common/mqtt.ts → common/sprinklers/mqtt/index.ts

@ -1,6 +1,6 @@ @@ -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 { @@ -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" });

28
common/sprinklers/schedule.ts

@ -0,0 +1,28 @@ @@ -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;
}

2
package.json

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

12
server/index.ts

@ -3,11 +3,11 @@ import "./configureAlias"; @@ -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"; @@ -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);

4
yarn.lock

@ -27,6 +27,10 @@ @@ -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"

Loading…
Cancel
Save