Much better serialization/deserialization for sprinklers
This commit is contained in:
parent
52ba25c038
commit
e5cc6b86e7
1
.vscode/launch.json
vendored
1
.vscode/launch.json
vendored
@ -11,6 +11,7 @@
|
|||||||
"env": {
|
"env": {
|
||||||
"NODE_ENV": "development"
|
"NODE_ENV": "development"
|
||||||
},
|
},
|
||||||
|
"console": "integratedTerminal",
|
||||||
"program": "${workspaceRoot}/dist/server/index.js"
|
"program": "${workspaceRoot}/dist/server/index.js"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -135,7 +135,7 @@ let logger: pino.Logger = pino({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function setLogger(newLogger: pino.Logger) {
|
export function setLogger(newLogger: pino.Logger) {
|
||||||
logger = newLogger;
|
exports.default = logger = newLogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default logger;
|
export default logger;
|
||||||
|
@ -9,7 +9,7 @@ export class ProgramItem {
|
|||||||
// duration of the run
|
// duration of the run
|
||||||
readonly duration: Duration;
|
readonly duration: Duration;
|
||||||
|
|
||||||
constructor(section: number, duration: Duration) {
|
constructor(section: number = 0, duration: Duration = new Duration()) {
|
||||||
this.section = section;
|
this.section = section;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,10 @@ export abstract class SprinklersDevice {
|
|||||||
abstract pauseSectionRunner(): Promise<{}>;
|
abstract pauseSectionRunner(): Promise<{}>;
|
||||||
abstract unpauseSectionRunner(): Promise<{}>;
|
abstract unpauseSectionRunner(): Promise<{}>;
|
||||||
|
|
||||||
|
abstract get sectionConstructor(): typeof Section;
|
||||||
|
abstract get sectionRunnerConstructor(): typeof SectionRunner;
|
||||||
|
abstract get programConstructor(): typeof Program;
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
return `SprinklersDevice{id="${this.id}", connected=${this.connected}, ` +
|
return `SprinklersDevice{id="${this.id}", connected=${this.connected}, ` +
|
||||||
`sections=[${this.sections}], ` +
|
`sections=[${this.sections}], ` +
|
||||||
|
@ -1,175 +1,108 @@
|
|||||||
|
/* tslint:disable:ordered-imports */
|
||||||
import { assign, pick } from "lodash";
|
import { assign, pick } from "lodash";
|
||||||
|
import {
|
||||||
|
createSimpleSchema, createModelSchema, primitive, object, date, custom,
|
||||||
|
ModelSchema, PropSchema,
|
||||||
|
} from "serializr";
|
||||||
|
import list from "./list";
|
||||||
import * as s from "..";
|
import * as s from "..";
|
||||||
|
|
||||||
export interface ISectionJSON {
|
export const durationSchema: PropSchema = {
|
||||||
name: string;
|
serializer: (duration: s.Duration | null) =>
|
||||||
state: boolean;
|
duration != null ? duration.toSeconds() : null,
|
||||||
}
|
deserializer: (json: any) =>
|
||||||
const sectionProps = ["name", "state"];
|
typeof json === "number" ? s.Duration.fromSeconds(json) : null,
|
||||||
|
};
|
||||||
|
|
||||||
export function sectionToJSON(sec: s.Section): ISectionJSON {
|
export const dateSchema: PropSchema = {
|
||||||
return pick(sec, sectionProps);
|
serializer: (jsDate: Date | null) => jsDate != null ?
|
||||||
}
|
jsDate.toISOString() : null,
|
||||||
|
deserializer: (json: any) => typeof json === "string" ?
|
||||||
|
new Date(json) : null,
|
||||||
|
};
|
||||||
|
|
||||||
export function sectionFromJSON(sec: s.Section, json: ISectionJSON) {
|
export const dateOfYearSchema: ModelSchema<s.DateOfYear> = {
|
||||||
assign(sec, pick(json, sectionProps));
|
factory: () => new s.DateOfYear(),
|
||||||
}
|
props: {
|
||||||
|
year: primitive(),
|
||||||
|
month: primitive(), // this only works if it is represented as a # from 0-12
|
||||||
|
day: primitive(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export interface ITimeOfDayJSON {
|
export const timeOfDaySchema: ModelSchema<s.TimeOfDay> = {
|
||||||
hour: number;
|
factory: () => new s.TimeOfDay(),
|
||||||
minute: number;
|
props: {
|
||||||
second: number;
|
hour: primitive(),
|
||||||
millisecond: number;
|
minute: primitive(),
|
||||||
}
|
second: primitive(),
|
||||||
const timeOfDayProps = ["hour", "minute", "second", "millisecond"];
|
millisecond: primitive(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function timeOfDayToJSON(timeOfDay: s.TimeOfDay): ITimeOfDayJSON {
|
export const sectionSchema: ModelSchema<s.Section> = {
|
||||||
return pick(timeOfDay, timeOfDayProps);
|
factory: (c) => new (c.parentContext.target as s.SprinklersDevice).sectionConstructor(
|
||||||
}
|
c.parentContext.target, c.json.id),
|
||||||
|
props: {
|
||||||
|
name: primitive(),
|
||||||
|
state: primitive(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function timeOfDayFromJSON(timeOfDay: s.TimeOfDay, json: ITimeOfDayJSON) {
|
export const sectionRunSchema: ModelSchema<s.SectionRun> = {
|
||||||
assign(timeOfDay, pick(json, timeOfDayProps));
|
factory: (c) => new s.SectionRun(c.json.id),
|
||||||
}
|
props: {
|
||||||
|
id: primitive(),
|
||||||
|
section: primitive(),
|
||||||
|
duration: durationSchema,
|
||||||
|
startTime: dateSchema,
|
||||||
|
endTime: dateSchema,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export interface IScheduleJSON {
|
export const sectionRunnerSchema: ModelSchema<s.SectionRunner> = {
|
||||||
times: ITimeOfDayJSON[];
|
factory: (c) => new (c.parentContext.target as s.SprinklersDevice).sectionRunnerConstructor(
|
||||||
weekdays: number[];
|
c.parentContext.target),
|
||||||
from?: string;
|
props: {
|
||||||
to?: string;
|
queue: list(object(sectionRunSchema)),
|
||||||
}
|
current: object(sectionRunSchema),
|
||||||
const scheduleProps = ["weekdays", "from", "to"];
|
paused: primitive(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function scheduleToJSON(schedule: s.Schedule): IScheduleJSON {
|
export const scheduleSchema: ModelSchema<s.Schedule> = {
|
||||||
return {
|
factory: () => new s.Schedule(),
|
||||||
...pick(schedule, scheduleProps),
|
props: {
|
||||||
times: schedule.times.map(timeOfDayToJSON),
|
times: list(object(timeOfDaySchema)),
|
||||||
};
|
weekdays: list(primitive()),
|
||||||
}
|
from: object(dateOfYearSchema),
|
||||||
|
to: object(dateOfYearSchema),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function scheduleFromJSON(schedule: s.Schedule, json: IScheduleJSON) {
|
export const programItemSchema: ModelSchema<s.ProgramItem> = {
|
||||||
assign(schedule, pick(json, scheduleProps));
|
factory: () => new s.ProgramItem(),
|
||||||
schedule.times.length = json.times.length;
|
props: {
|
||||||
schedule.times.forEach((timeOfDay, i) =>
|
section: primitive(),
|
||||||
timeOfDayFromJSON(timeOfDay, json.times[i]));
|
duration: durationSchema,
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export interface IProgramItemJSON {
|
export const programSchema: ModelSchema<s.Program> = {
|
||||||
section: number;
|
factory: (c) => new (c.parentContext.target as s.SprinklersDevice).programConstructor(
|
||||||
duration: number;
|
c.parentContext.target, c.json.id),
|
||||||
}
|
props: {
|
||||||
const programItemProps = ["section"];
|
name: primitive(),
|
||||||
|
enabled: primitive(),
|
||||||
|
schedule: object(scheduleSchema),
|
||||||
|
sequence: list(object(programItemSchema)),
|
||||||
|
running: primitive(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function programItemToJSON(programItem: s.ProgramItem): IProgramItemJSON {
|
export const sprinklersDeviceSchema = createSimpleSchema({
|
||||||
return {
|
connected: primitive(),
|
||||||
...pick(programItem, programItemProps),
|
sections: list(object(sectionSchema)),
|
||||||
duration: programItem.duration.toSeconds(),
|
sectionRunner: object(sectionRunnerSchema),
|
||||||
};
|
programs: list(object(programSchema)),
|
||||||
}
|
});
|
||||||
|
|
||||||
export function programItemFromJSON(json: IProgramItemJSON): s.ProgramItem {
|
|
||||||
const duration = s.Duration.fromSeconds(json.duration);
|
|
||||||
return new s.ProgramItem(json.section, 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 = json.sequence.map((programItemJson) =>
|
|
||||||
programItemFromJSON(programItemJson));
|
|
||||||
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),
|
|
||||||
duration: sectionRun.duration.toSeconds(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sectionRunFromJSON(sectionRun: s.SectionRun, json: ISectionRunJSON) {
|
|
||||||
assign(sectionRun, pick(json, sectionRunProps));
|
|
||||||
sectionRun.duration = s.Duration.fromSeconds(json.duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
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]));
|
|
||||||
}
|
|
||||||
|
65
common/sprinklers/json/list.ts
Normal file
65
common/sprinklers/json/list.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { primitive, PropSchema } from "serializr";
|
||||||
|
|
||||||
|
function invariant(cond: boolean, message?: string) {
|
||||||
|
if (!cond) {
|
||||||
|
throw new Error("[serializr] " + (message || "Illegal State"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPropSchema(thing: any) {
|
||||||
|
return thing && thing.serializer && thing.deserializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAliasedPropSchema(propSchema: any) {
|
||||||
|
return typeof propSchema === "object" && !!propSchema.jsonname;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parallel(ar: any[], processor: (item: any, done: any) => void, cb: any) {
|
||||||
|
if (ar.length === 0) {
|
||||||
|
return void cb(null, []);
|
||||||
|
}
|
||||||
|
let left = ar.length;
|
||||||
|
const resultArray: any[] = [];
|
||||||
|
let failed = false;
|
||||||
|
const processorCb = (idx: number, err: any, result: any) => {
|
||||||
|
if (err) {
|
||||||
|
if (!failed) {
|
||||||
|
failed = true;
|
||||||
|
cb(err);
|
||||||
|
}
|
||||||
|
} else if (!failed) {
|
||||||
|
resultArray[idx] = result;
|
||||||
|
if (--left === 0) {
|
||||||
|
cb(null, resultArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ar.forEach((value, idx) => processor(value, processorCb.bind(null, idx)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function list(propSchema: PropSchema): PropSchema {
|
||||||
|
propSchema = propSchema || primitive();
|
||||||
|
invariant(isPropSchema(propSchema), "expected prop schema as first argument");
|
||||||
|
invariant(!isAliasedPropSchema(propSchema), "provided prop is aliased, please put aliases first");
|
||||||
|
return {
|
||||||
|
serializer(ar) {
|
||||||
|
invariant(ar && typeof ar.length === "number" && typeof ar.map === "function",
|
||||||
|
"expected array (like) object");
|
||||||
|
return ar.map(propSchema.serializer);
|
||||||
|
},
|
||||||
|
deserializer(jsonArray, done, context) {
|
||||||
|
if (jsonArray === null) { // sometimes go will return null in place of empty array
|
||||||
|
return void done(null, []);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(jsonArray)) {
|
||||||
|
return void done("[serializr] expected JSON array", null);
|
||||||
|
}
|
||||||
|
parallel(
|
||||||
|
jsonArray,
|
||||||
|
(item: any, itemDone: (err: any, targetPropertyValue: any) => void) =>
|
||||||
|
propSchema.deserializer(item, itemDone, context, undefined),
|
||||||
|
done,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -1,23 +1,15 @@
|
|||||||
|
import { cloneDeep } from "lodash";
|
||||||
import * as mqtt from "mqtt";
|
import * as mqtt from "mqtt";
|
||||||
|
import { deserialize, update } from "serializr";
|
||||||
|
|
||||||
import logger from "@common/logger";
|
import logger from "@common/logger";
|
||||||
import {
|
import * as s from "@common/sprinklers";
|
||||||
Duration,
|
import * as schema from "@common/sprinklers/json";
|
||||||
ISprinklersApi,
|
|
||||||
Program,
|
|
||||||
ProgramItem,
|
|
||||||
Schedule,
|
|
||||||
Section,
|
|
||||||
SectionRun,
|
|
||||||
SectionRunner,
|
|
||||||
SprinklersDevice,
|
|
||||||
TimeOfDay,
|
|
||||||
} from "@common/sprinklers";
|
|
||||||
import { checkedIndexOf } from "@common/utils";
|
import { checkedIndexOf } from "@common/utils";
|
||||||
|
|
||||||
const log = logger.child({ source: "mqtt" });
|
const log = logger.child({ source: "mqtt" });
|
||||||
|
|
||||||
export class MqttApiClient implements ISprinklersApi {
|
export class MqttApiClient implements s.ISprinklersApi {
|
||||||
readonly mqttUri: string;
|
readonly mqttUri: string;
|
||||||
client: mqtt.Client;
|
client: mqtt.Client;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
@ -54,7 +46,7 @@ export class MqttApiClient implements ISprinklersApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getDevice(prefix: string): SprinklersDevice {
|
getDevice(prefix: string): s.SprinklersDevice {
|
||||||
if (/\//.test(prefix)) {
|
if (/\//.test(prefix)) {
|
||||||
throw new Error("Prefix cannot contain a /");
|
throw new Error("Prefix cannot contain a /");
|
||||||
}
|
}
|
||||||
@ -84,7 +76,8 @@ export class MqttApiClient implements ISprinklersApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private processMessage(topic: string, payload: Buffer, packet: mqtt.Packet) {
|
private processMessage(topic: string, payloadBuf: Buffer, packet: mqtt.Packet) {
|
||||||
|
const payload = payloadBuf.toString("utf8");
|
||||||
log.trace({ topic, payload }, "message arrived: ");
|
log.trace({ topic, payload }, "message arrived: ");
|
||||||
const topicIdx = topic.indexOf("/"); // find the first /
|
const topicIdx = topic.indexOf("/"); // find the first /
|
||||||
const prefix = topic.substr(0, topicIdx); // assume prefix does not contain a /
|
const prefix = topic.substr(0, topicIdx); // assume prefix does not contain a /
|
||||||
@ -108,7 +101,7 @@ const subscriptions = [
|
|||||||
"/section_runner",
|
"/section_runner",
|
||||||
];
|
];
|
||||||
|
|
||||||
class MqttSprinklersDevice extends SprinklersDevice {
|
class MqttSprinklersDevice extends s.SprinklersDevice {
|
||||||
readonly apiClient: MqttApiClient;
|
readonly apiClient: MqttApiClient;
|
||||||
readonly prefix: string;
|
readonly prefix: string;
|
||||||
|
|
||||||
@ -127,6 +120,10 @@ class MqttSprinklersDevice extends SprinklersDevice {
|
|||||||
return this.prefix;
|
return this.prefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get sectionConstructor() { return MqttSection; }
|
||||||
|
get sectionRunnerConstructor() { return MqttSectionRunner; }
|
||||||
|
get programConstructor() { return MqttProgram; }
|
||||||
|
|
||||||
doSubscribe() {
|
doSubscribe() {
|
||||||
const topics = subscriptions.map((filter) => this.prefix + filter);
|
const topics = subscriptions.map((filter) => this.prefix + filter);
|
||||||
this.apiClient.client.subscribe(topics, { qos: 1 });
|
this.apiClient.client.subscribe(topics, { qos: 1 });
|
||||||
@ -142,8 +139,7 @@ class MqttSprinklersDevice extends SprinklersDevice {
|
|||||||
* @param topic The topic, with prefix removed
|
* @param topic The topic, with prefix removed
|
||||||
* @param payload The payload buffer
|
* @param payload The payload buffer
|
||||||
*/
|
*/
|
||||||
onMessage(topic: string, payloadBuf: Buffer) {
|
onMessage(topic: string, payload: string) {
|
||||||
const payload = payloadBuf.toString("utf8");
|
|
||||||
if (topic === "connected") {
|
if (topic === "connected") {
|
||||||
this.connected = (payload === "true");
|
this.connected = (payload === "true");
|
||||||
log.trace(`MqttSprinklersDevice with prefix ${this.prefix}: ${this.connected}`);
|
log.trace(`MqttSprinklersDevice with prefix ${this.prefix}: ${this.connected}`);
|
||||||
@ -207,7 +203,7 @@ class MqttSprinklersDevice extends SprinklersDevice {
|
|||||||
log.warn({ topic }, "MqttSprinklersDevice recieved invalid message");
|
log.warn({ topic }, "MqttSprinklersDevice recieved invalid message");
|
||||||
}
|
}
|
||||||
|
|
||||||
runSection(section: Section | number, duration: Duration) {
|
runSection(section: s.Section | number, duration: s.Duration) {
|
||||||
const sectionNum = checkedIndexOf(section, this.sections, "Section");
|
const sectionNum = checkedIndexOf(section, this.sections, "Section");
|
||||||
const payload: IRunSectionJSON = {
|
const payload: IRunSectionJSON = {
|
||||||
duration: duration.toSeconds(),
|
duration: duration.toSeconds(),
|
||||||
@ -215,9 +211,9 @@ class MqttSprinklersDevice extends SprinklersDevice {
|
|||||||
return this.makeRequest(`sections/${sectionNum}/run`, payload);
|
return this.makeRequest(`sections/${sectionNum}/run`, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
runProgram(program: Program | number) {
|
runProgram(program: s.Program | number) {
|
||||||
const programNum = checkedIndexOf(program, this.programs, "Program");
|
const programNum = checkedIndexOf(program, this.programs, "Program");
|
||||||
return this.makeRequest(`programs/${programNum}/run`, {});
|
return this.makeRequest(`programs/${programNum}/run`);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelSectionRunById(id: number) {
|
cancelSectionRunById(id: number) {
|
||||||
@ -273,120 +269,40 @@ interface IRunSectionJSON {
|
|||||||
duration: number;
|
duration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MqttSection extends Section {
|
class MqttSection extends s.Section {
|
||||||
onMessage(topic: string, payload: string) {
|
onMessage(topic: string, payload: string) {
|
||||||
if (topic === "state") {
|
if (topic === "state") {
|
||||||
this.state = (payload === "true");
|
this.state = (payload === "true");
|
||||||
} else if (topic == null) {
|
} else if (topic == null) {
|
||||||
const json = JSON.parse(payload) as ISectionJSON;
|
this.updateFromJSON(JSON.parse(payload));
|
||||||
this.name = json.name;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFromJSON(json: any) {
|
||||||
|
update(schema.sectionSchema, this, json);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ITimeOfDayJSON {
|
class MqttProgram extends s.Program {
|
||||||
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: ITimeOfDayJSON[];
|
|
||||||
weekdays: number[];
|
|
||||||
from?: string;
|
|
||||||
to?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleFromJSON(json: IScheduleJSON): Schedule {
|
|
||||||
const sched = new Schedule();
|
|
||||||
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);
|
|
||||||
return sched;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProgramItemJSON {
|
|
||||||
section: number;
|
|
||||||
duration: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProgramJSON {
|
|
||||||
name: string;
|
|
||||||
enabled: boolean;
|
|
||||||
sequence: IProgramItemJSON[];
|
|
||||||
sched: IScheduleJSON;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MqttProgram extends Program {
|
|
||||||
onMessage(topic: string, payload: string) {
|
onMessage(topic: string, payload: string) {
|
||||||
if (topic === "running") {
|
if (topic === "running") {
|
||||||
this.running = (payload === "true");
|
this.running = (payload === "true");
|
||||||
} else if (topic == null) {
|
} else if (topic == null) {
|
||||||
const json = JSON.parse(payload) as Partial<IProgramJSON>;
|
this.updateFromJSON(JSON.parse(payload));
|
||||||
this.updateFromJSON(json);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFromJSON(json: Partial<IProgramJSON>) {
|
updateFromJSON(json: any) {
|
||||||
if (json.name != null) {
|
update(schema.programSchema, this, json);
|
||||||
this.name = json.name;
|
|
||||||
}
|
|
||||||
if (json.enabled != null) {
|
|
||||||
this.enabled = json.enabled;
|
|
||||||
}
|
|
||||||
if (json.sequence != null) {
|
|
||||||
this.sequence = json.sequence.map((item) => (new ProgramItem(
|
|
||||||
item.section,
|
|
||||||
Duration.fromSeconds(item.duration),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
if (json.sched != null) {
|
|
||||||
this.schedule = scheduleFromJSON(json.sched);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISectionRunJSON {
|
class MqttSectionRunner extends s.SectionRunner {
|
||||||
id: number;
|
|
||||||
section: number;
|
|
||||||
duration: number;
|
|
||||||
startTime?: number;
|
|
||||||
pauseTime?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sectionRunFromJSON(json: ISectionRunJSON) {
|
|
||||||
const run = new SectionRun(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: ISectionRunJSON[];
|
|
||||||
current: ISectionRunJSON | null;
|
|
||||||
paused: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MqttSectionRunner extends SectionRunner {
|
|
||||||
onMessage(payload: string) {
|
onMessage(payload: string) {
|
||||||
const json = JSON.parse(payload) as ISectionRunnerJSON;
|
this.updateFromJSON(JSON.parse(payload));
|
||||||
this.updateFromJSON(json);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFromJSON(json: ISectionRunnerJSON) {
|
updateFromJSON(json: any) {
|
||||||
this.queue.length = 0;
|
update(schema.sectionRunnerSchema, this, json);
|
||||||
if (json.queue && json.queue.length) { // null means empty queue
|
|
||||||
this.queue.push.apply(this.queue, json.queue.map(sectionRunFromJSON));
|
|
||||||
}
|
|
||||||
this.current = json.current == null ? null : sectionRunFromJSON(json.current);
|
|
||||||
this.paused = json.paused;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ export class TimeOfDay {
|
|||||||
readonly second: number;
|
readonly second: number;
|
||||||
readonly millisecond: number;
|
readonly millisecond: number;
|
||||||
|
|
||||||
constructor(hour: number, minute: number = 0, second: number = 0, millisecond: number = 0) {
|
constructor(hour: number = 0, minute: number = 0, second: number = 0, millisecond: number = 0) {
|
||||||
this.hour = hour;
|
this.hour = hour;
|
||||||
this.minute = minute;
|
this.minute = minute;
|
||||||
this.second = second;
|
this.second = second;
|
||||||
@ -22,9 +22,40 @@ export enum Weekday {
|
|||||||
Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday,
|
Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Month {
|
||||||
|
January = 1,
|
||||||
|
February = 2,
|
||||||
|
March = 3,
|
||||||
|
April = 4,
|
||||||
|
May = 5,
|
||||||
|
June = 6,
|
||||||
|
July = 7,
|
||||||
|
August = 8,
|
||||||
|
September = 9,
|
||||||
|
October = 10,
|
||||||
|
November = 11,
|
||||||
|
December = 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DateOfYear {
|
||||||
|
readonly day: number;
|
||||||
|
readonly month: Month;
|
||||||
|
readonly year: number;
|
||||||
|
|
||||||
|
constructor(day: number = 0, month: Month = Month.January, year: number = 0) {
|
||||||
|
this.day = day;
|
||||||
|
this.month = month;
|
||||||
|
this.year = year;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `${Month[this.month]} ${this.day}, ${this.year}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class Schedule {
|
export class Schedule {
|
||||||
@observable times: TimeOfDay[] = [];
|
@observable times: TimeOfDay[] = [];
|
||||||
@observable weekdays: Weekday[] = [];
|
@observable weekdays: Weekday[] = [];
|
||||||
@observable from: Date | null = null;
|
@observable from: DateOfYear | null = null;
|
||||||
@observable to: Date | null = null;
|
@observable to: DateOfYear | null = null;
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/amikhalev/sprinklers3#readme",
|
"homepage": "https://github.com/amikhalev/sprinklers3#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/async": "^2.0.43",
|
||||||
"@types/chalk": "^0.4.31",
|
"@types/chalk": "^0.4.31",
|
||||||
"@types/classnames": "^2.2.0",
|
"@types/classnames": "^2.2.0",
|
||||||
"@types/core-js": "^0.9.43",
|
"@types/core-js": "^0.9.43",
|
||||||
@ -45,6 +46,7 @@
|
|||||||
"@types/react-dom": "^15.5.0",
|
"@types/react-dom": "^15.5.0",
|
||||||
"@types/react-fontawesome": "^1.5.0",
|
"@types/react-fontawesome": "^1.5.0",
|
||||||
"@types/react-hot-loader": "^3.0.4",
|
"@types/react-hot-loader": "^3.0.4",
|
||||||
|
"async": "^2.5.0",
|
||||||
"autoprefixer": "^7.1.4",
|
"autoprefixer": "^7.1.4",
|
||||||
"case-sensitive-paths-webpack-plugin": "^2.1.1",
|
"case-sensitive-paths-webpack-plugin": "^2.1.1",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
@ -70,6 +72,7 @@
|
|||||||
"react-fontawesome": "^1.6.1",
|
"react-fontawesome": "^1.6.1",
|
||||||
"semantic-ui-css": "^2.2.10",
|
"semantic-ui-css": "^2.2.10",
|
||||||
"semantic-ui-react": "^0.73.0",
|
"semantic-ui-react": "^0.73.0",
|
||||||
|
"serializr": "^1.1.13",
|
||||||
"tslint-loader": "^3.5.3",
|
"tslint-loader": "^3.5.3",
|
||||||
"uglifyjs-webpack-plugin": "^0.4.6",
|
"uglifyjs-webpack-plugin": "^0.4.6",
|
||||||
"url-loader": "^0.5.9"
|
"url-loader": "^0.5.9"
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import log, { setLogger } from "@common/logger";
|
import log, { setLogger } from "@common/logger";
|
||||||
setLogger(log.child({
|
setLogger(log.child({
|
||||||
name: "sprinklers3/server",
|
name: "sprinklers3/server",
|
||||||
level: "trace",
|
level: "debug",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
/* tslint:disable:ordered-imports */
|
||||||
import "./configureAlias";
|
import "./configureAlias";
|
||||||
|
|
||||||
import "env";
|
import "env";
|
||||||
|
import "./configureLogger";
|
||||||
|
|
||||||
import log from "@common/logger";
|
import log from "@common/logger";
|
||||||
import * as mqtt from "@common/sprinklers/mqtt";
|
import * as mqtt from "@common/sprinklers/mqtt";
|
||||||
@ -11,12 +12,13 @@ const mqttClient = new mqtt.MqttApiClient("mqtt://localhost:1883");
|
|||||||
|
|
||||||
mqttClient.start();
|
mqttClient.start();
|
||||||
|
|
||||||
import * as sjson from "@common/sprinklers/json";
|
import { sprinklersDeviceSchema } from "@common/sprinklers/json";
|
||||||
import { autorun } from "mobx";
|
import { autorun } from "mobx";
|
||||||
|
import { serialize } from "serializr";
|
||||||
const device = mqttClient.getDevice("grinklers");
|
const device = mqttClient.getDevice("grinklers");
|
||||||
|
|
||||||
app.get("/api/grinklers", (req, res) => {
|
app.get("/api/grinklers", (req, res) => {
|
||||||
const j = sjson.sprinklersDeviceToJSON(device);
|
const j = serialize(sprinklersDeviceSchema, device);
|
||||||
log.trace(j);
|
log.trace(j);
|
||||||
res.send(j);
|
res.send(j);
|
||||||
});
|
});
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -2,6 +2,10 @@
|
|||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@types/async@^2.0.43":
|
||||||
|
version "2.0.43"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.43.tgz#0d6fce7e11a582b4251a4bf5439399428c79e387"
|
||||||
|
|
||||||
"@types/chalk@^0.4.31":
|
"@types/chalk@^0.4.31":
|
||||||
version "0.4.31"
|
version "0.4.31"
|
||||||
resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-0.4.31.tgz#a31d74241a6b1edbb973cf36d97a2896834a51f9"
|
resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-0.4.31.tgz#a31d74241a6b1edbb973cf36d97a2896834a51f9"
|
||||||
@ -334,7 +338,7 @@ async@^1.5.2:
|
|||||||
version "1.5.2"
|
version "1.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
||||||
|
|
||||||
async@^2.1.2, async@^2.4.1:
|
async@^2.1.2, async@^2.4.1, async@^2.5.0:
|
||||||
version "2.5.0"
|
version "2.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
|
resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4706,6 +4710,10 @@ send@0.15.4:
|
|||||||
range-parser "~1.2.0"
|
range-parser "~1.2.0"
|
||||||
statuses "~1.3.1"
|
statuses "~1.3.1"
|
||||||
|
|
||||||
|
serializr@^1.1.13:
|
||||||
|
version "1.1.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/serializr/-/serializr-1.1.13.tgz#21b20f220fcf94caaecd86eb09e438cde6532b25"
|
||||||
|
|
||||||
serve-index@^1.7.2:
|
serve-index@^1.7.2:
|
||||||
version "1.9.0"
|
version "1.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.0.tgz#d2b280fc560d616ee81b48bf0fa82abed2485ce7"
|
resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.0.tgz#d2b280fc560d616ee81b48bf0fa82abed2485ce7"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user