diff --git a/app/components/DeviceView.tsx b/app/components/DeviceView.tsx index c2fae5f..6ba218f 100644 --- a/app/components/DeviceView.tsx +++ b/app/components/DeviceView.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import FontAwesome = require("react-fontawesome"); import { Header, Item } from "semantic-ui-react"; -import { injectState, State } from "@app/state"; +import { injectState, MqttApiState } from "@app/state"; import { SprinklersDevice } from "@common/sprinklers"; import { ProgramTable, RunSectionForm, SectionRunnerView, SectionTable } from "."; @@ -24,7 +24,7 @@ const ConnectionState = ({ connected }: { connected: boolean }) => { interface DeviceViewProps { deviceId: string; - state: State; + state: MqttApiState; } class DeviceView extends React.Component { diff --git a/app/components/MessageTest.tsx b/app/components/MessageTest.tsx index 944d79a..1c802c7 100644 --- a/app/components/MessageTest.tsx +++ b/app/components/MessageTest.tsx @@ -1,10 +1,10 @@ import * as React from "react"; import { Button, Segment } from "semantic-ui-react"; -import { injectState, State } from "@app/state"; +import { injectState, MqttApiState } from "@app/state"; import { getRandomId } from "@common/utils"; -class MessageTest extends React.Component<{ state: State }> { +class MessageTest extends React.Component<{ state: MqttApiState }> { render() { return ( diff --git a/app/components/MessagesView.tsx b/app/components/MessagesView.tsx index 1c3d889..4975e39 100644 --- a/app/components/MessagesView.tsx +++ b/app/components/MessagesView.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { Message, MessageProps, TransitionGroup } from "semantic-ui-react"; -import { injectState, State, UiMessage, UiStore } from "@app/state/"; +import { injectState, MqttApiState, UiMessage, UiStore } from "@app/state/"; @observer class MessageView extends React.Component<{ @@ -33,7 +33,7 @@ class MessageView extends React.Component<{ } } -class MessagesView extends React.Component<{ state: State }> { +class MessagesView extends React.Component<{ state: MqttApiState }> { render() { const { uiStore } = this.props.state; const messages = uiStore.messages.map((message) => ( diff --git a/app/index.tsx b/app/index.tsx index 22f22a6..385b7d2 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -3,12 +3,13 @@ import * as ReactDOM from "react-dom"; import { AppContainer } from "react-hot-loader"; import App from "@app/components/App"; -import { ProvideState, State } from "@app/state"; +import { ProvideState, MqttApiState, WebApiState } from "@app/state"; import log, { setLogger } from "@common/logger"; setLogger(log.child({ name: "sprinklers3/app" })); -const state = new State(); +// const state = new MqttApiState(); +const state = new WebApiState(); state.start(); const rootElem = document.getElementById("app"); diff --git a/app/state/index.ts b/app/state/index.ts index 3951eee..d25dd00 100644 --- a/app/state/index.ts +++ b/app/state/index.ts @@ -1,17 +1,13 @@ import { ISprinklersApi } from "@common/sprinklers"; import { MqttApiClient } from "@common/sprinklers/mqtt"; +import { WebApiClient } from "./web"; import { UiMessage, UiStore } from "./ui"; export { UiMessage, UiStore }; export * from "./inject"; -export interface IState { - sprinklersApi: ISprinklersApi; - uiStore: UiStore; -} - -export class State implements IState { - sprinklersApi: ISprinklersApi = new MqttApiClient(`ws://${location.hostname}:1884`); +export abstract class StateBase { + abstract readonly sprinklersApi: ISprinklersApi; uiStore = new UiStore(); constructor() { @@ -23,6 +19,14 @@ export class State implements IState { } } +export class MqttApiState extends StateBase { + sprinklersApi = new MqttApiClient(`ws://${location.hostname}:1884`); +} + +export class WebApiState extends StateBase { + sprinklersApi = new WebApiClient(); +} + // const state = new State(); // export default state; diff --git a/app/state/inject.tsx b/app/state/inject.tsx index a424066..1aec037 100644 --- a/app/state/inject.tsx +++ b/app/state/inject.tsx @@ -1,10 +1,10 @@ import * as PropTypes from "prop-types"; import * as React from "react"; -import { State } from "@app/state"; +import { StateBase } from "@app/state"; interface IProvidedStateContext { - providedState: State; + providedState: StateBase; } const providedStateContextTypes: PropTypes.ValidationMap = { @@ -12,7 +12,7 @@ const providedStateContextTypes: PropTypes.ValidationMap = { }; export class ProvideState extends React.Component<{ - state: State, + state: StateBase, }> implements React.ChildContextProvider { static childContextTypes = providedStateContextTypes; @@ -30,7 +30,7 @@ export class ProvideState extends React.Component<{ type Diff = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T]; type Omit = {[P in Diff]: T[P]}; -export function injectState

(Component: React.ComponentType

) { +export function injectState

(Component: React.ComponentType

) { return class extends React.Component> { static contextTypes = providedStateContextTypes; context: IProvidedStateContext; diff --git a/app/state/web.ts b/app/state/web.ts new file mode 100644 index 0000000..cd547e4 --- /dev/null +++ b/app/state/web.ts @@ -0,0 +1,46 @@ +import { update } from "serializr"; + +import * as s from "@common/sprinklers"; +import * as schema from "@common/sprinklers/json"; + +export class WebSprinklersDevice extends s.SprinklersDevice { + get id() { + return "grinklers"; + } + async runSection(section: number | s.Section, duration: s.Duration): Promise<{}> { + return {}; + } + async runProgram(program: number | s.Program): Promise<{}> { + return {}; + } + async cancelSectionRunById(id: number): Promise<{}> { + return {}; + } + async pauseSectionRunner(): Promise<{}> { + return {}; + } + async unpauseSectionRunner(): Promise<{}> { + return {}; + } +} + +export class WebApiClient implements s.ISprinklersApi { + start() { + // NOT IMPLEMENTED + } + + getDevice(name: string): s.SprinklersDevice { + const device = new WebSprinklersDevice(); + fetch("/api/grinklers") + .then((res) => res.json()) + .then((json) => { + update(schema.sprinklersDeviceSchema, device, json); + }) + .catch((e) => alert(e)); + return device; + } + + removeDevice(name: string) { + // NOT IMPLEMENTED + } +} diff --git a/common/sprinklers/SprinklersDevice.ts b/common/sprinklers/SprinklersDevice.ts index 5247bcc..1e94c0d 100644 --- a/common/sprinklers/SprinklersDevice.ts +++ b/common/sprinklers/SprinklersDevice.ts @@ -10,6 +10,10 @@ export abstract class SprinklersDevice { @observable programs: Program[] = []; @observable sectionRunner: SectionRunner; + constructor() { + this.sectionRunner = new (this.sectionRunnerConstructor)(this); + } + abstract get id(): string; abstract runSection(section: number | Section, duration: Duration): Promise<{}>; abstract runProgram(program: number | Program): Promise<{}>; @@ -17,9 +21,15 @@ export abstract class SprinklersDevice { abstract pauseSectionRunner(): Promise<{}>; abstract unpauseSectionRunner(): Promise<{}>; - abstract get sectionConstructor(): typeof Section; - abstract get sectionRunnerConstructor(): typeof SectionRunner; - abstract get programConstructor(): typeof Program; + get sectionConstructor(): typeof Section { + return Section; + } + get sectionRunnerConstructor(): typeof SectionRunner { + return SectionRunner; + } + get programConstructor(): typeof Program { + return Program; + } toString(): string { return `SprinklersDevice{id="${this.id}", connected=${this.connected}, ` + diff --git a/common/sprinklers/json/index.ts b/common/sprinklers/json/index.ts index e44503d..a5d195e 100644 --- a/common/sprinklers/json/index.ts +++ b/common/sprinklers/json/index.ts @@ -8,15 +8,28 @@ import * as s from ".."; export const durationSchema: PropSchema = { serializer: (duration: s.Duration | null) => duration != null ? duration.toSeconds() : null, - deserializer: (json: any) => - typeof json === "number" ? s.Duration.fromSeconds(json) : null, + deserializer: (json: any, done) => { + if (typeof json === "number") { + done(null, s.Duration.fromSeconds(json)); + } else { + done(new Error(`Duration expects a number, not ${json}`), undefined); + } + }, }; export const dateSchema: PropSchema = { serializer: (jsDate: Date | null) => jsDate != null ? jsDate.toISOString() : null, - deserializer: (json: any) => typeof json === "string" ? - new Date(json) : null, + deserializer: (json: any, done) => { + if (json === null) { + done(null, null); + } + try { + done(null, new Date(json)); + } catch (e) { + done(e, undefined); + } + }, }; export const dateOfYearSchema: ModelSchema = { diff --git a/server/index.ts b/server/index.ts index 633ce32..4255f69 100644 --- a/server/index.ts +++ b/server/index.ts @@ -13,13 +13,17 @@ const mqttClient = new mqtt.MqttApiClient("mqtt://localhost:1883"); mqttClient.start(); import { sprinklersDeviceSchema } from "@common/sprinklers/json"; -import { autorun } from "mobx"; +import { autorunAsync } from "mobx"; import { serialize } from "serializr"; const device = mqttClient.getDevice("grinklers"); +autorunAsync(() => { + const j = serialize(sprinklersDeviceSchema, device); + log.info({ device: j }); +}, 0); + app.get("/api/grinklers", (req, res) => { const j = serialize(sprinklersDeviceSchema, device); - log.trace(j); res.send(j); });