From 10805f9fb16e7eaf208dd048f5fd79efad83a1f2 Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Tue, 20 Jun 2017 08:45:25 -0600 Subject: [PATCH] Improved components structure; better paho-mqtt definitions --- app/script/App.tsx | 287 --------------- app/script/components/App.tsx | 22 ++ app/script/components/DeviceView.tsx | 43 +++ app/script/components/DurationInput.tsx | 40 ++ app/script/components/MessagesView.tsx | 24 ++ app/script/components/ProgramTable.tsx | 66 ++++ app/script/components/RunSectionForm.tsx | 68 ++++ app/script/components/SectionTable.tsx | 54 +++ app/script/components/index.ts | 7 + app/script/index.tsx | 6 +- app/script/mqtt.ts | 48 +-- app/script/paho-mqtt.d.ts | 444 +++++++++++++++++++++-- app/script/sprinklers.ts | 4 + tslint.json | 44 +-- 14 files changed, 783 insertions(+), 374 deletions(-) delete mode 100644 app/script/App.tsx create mode 100644 app/script/components/App.tsx create mode 100644 app/script/components/DeviceView.tsx create mode 100644 app/script/components/DurationInput.tsx create mode 100644 app/script/components/MessagesView.tsx create mode 100644 app/script/components/ProgramTable.tsx create mode 100644 app/script/components/RunSectionForm.tsx create mode 100644 app/script/components/SectionTable.tsx create mode 100644 app/script/components/index.ts diff --git a/app/script/App.tsx b/app/script/App.tsx deleted file mode 100644 index 3e95878..0000000 --- a/app/script/App.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import * as React from "react"; -import {SyntheticEvent} from "react"; -import {computed} from "mobx"; -import DevTools from "mobx-react-devtools"; -import {observer} from "mobx-react"; -import {SprinklersDevice, Section, Program, Duration, Schedule} from "./sprinklers"; -import {Item, Table, Header, Segment, Form, Input, Button, DropdownItemProps, DropdownProps, Message} from "semantic-ui-react"; -import FontAwesome = require("react-fontawesome"); -import * as classNames from "classnames"; - -import "semantic-ui-css/semantic.css"; -import "font-awesome/css/font-awesome.css"; -import "app/style/app.css"; -import {Message as UiMessage, UiStore} from "./ui"; - -/* tslint:disable:object-literal-sort-keys */ - -@observer -class SectionTable extends React.PureComponent<{ sections: Section[] }, void> { - private static renderRow(section: Section, index: number) { - if (!section) { - return null; - } - const {name, state} = section; - return ( - - {"" + (index + 1)} - {name} - {state ? - ( Irrigating) - : "Not irrigating"} - - - ); - } - - public render() { - return ( - - - Sections - - - # - Name - State - - - - { - this.props.sections.map(SectionTable.renderRow) - } - -
- ); - } -} - -class DurationInput extends React.Component<{ - duration: Duration, - onDurationChange?: (newDuration: Duration) => void; -}, void> { - public render() { - const duration = this.props.duration; - // const editing = this.props.onDurationChange != null; - return
- -
- - -
-
; - } - - private onMinutesChange = (e, {value}) => { - if (value.length === 0 || isNaN(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)) { - return; - } - const newSeconds = parseInt(value, 10); - this.props.onDurationChange(this.props.duration.withSeconds(newSeconds)); - } -} - -@observer -class RunSectionForm extends React.Component<{ - sections: Section[], -}, { - duration: Duration, - section: number | "", -}> { - constructor() { - super(); - this.state = { - duration: new Duration(1, 1), - section: "", - }; - } - - public render() { - const {section, duration} = this.state; - return -
Run Section
-
- - - - {/*Label must be   to align it properly*/} - Run - -
-
; - } - - private onSectionChange = (e: SyntheticEvent, v: DropdownProps) => { - this.setState({section: v.value as number}); - } - - private onDurationChange = (newDuration: Duration) => { - this.setState({duration: newDuration}); - } - - private run = (e: SyntheticEvent) => { - e.preventDefault(); - const section: Section = this.props.sections[this.state.section]; - console.log(`should run 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)); - } - - private get isValid(): boolean { - return typeof this.state.section === "number"; - } - - @computed - private get sectionOptions(): DropdownItemProps[] { - return this.props.sections.map((s, i) => ({ - text: s ? s.name : null, - value: i, - })); - } -} - -@observer -class ScheduleView extends React.PureComponent<{ schedule: Schedule }, void> { - public render() { - return ( -
{JSON.stringify(this.props.schedule)}
- ); - } -} - -@observer -class ProgramTable extends React.PureComponent<{ programs: Program[] }, void> { - private static renderRow(program: Program, i: number): JSX.Element[] { - if (!program) { - return null; - } - const {name, running, enabled, schedule, sequence} = program; - return [ - - {"" + (i + 1)} - {name} - {running ? "Running" : "Not running"} - {enabled ? "Enabled" : "Not enabled"} - - , - - -
    - {sequence.map((item) => - (
  • Section {item.section + 1 + ""} for  - {item.duration.minutes}M {item.duration.seconds}S
  • ))} -
- -
-
- , - ]; - } - - public render() { - return ( - - - - Programs - - - # - Name - Running? - Enabled? - - - - { - Array.prototype.concat.apply([], this.props.programs.map(ProgramTable.renderRow)) - } - -
- ); - } -} - -const ConnectionState = ({connected}: { connected: boolean }) => - - -   - {connected ? "Connected" : "Disconnected"} - ; - -@observer -class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void> { - public render() { - const {id, connected, sections, programs} = this.props.device; - return ( - - ("app/images/raspberry_pi.png")}/> - -
- Device {id} - -
- - - - - - -
-
- ); - } -} - -@observer -class MessagesView extends React.PureComponent<{uiStore: UiStore}, void> { - public render() { - return
- {this.props.uiStore.messages.map(this.renderMessage)} -
; - } - - private renderMessage = (message: UiMessage, index: number) => { - const {header, content, type} = message; - return this.dismiss(index)}/>; - } - - private dismiss(index: number) { - this.props.uiStore.messages.splice(index, 1); - } -} - -@observer -export default class App extends React.PureComponent<{ device: SprinklersDevice, uiStore: UiStore }, any> { - public render() { - return - - - - ; - } -} diff --git a/app/script/components/App.tsx b/app/script/components/App.tsx new file mode 100644 index 0000000..3c09f1e --- /dev/null +++ b/app/script/components/App.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import DevTools from "mobx-react-devtools"; +import {observer} from "mobx-react"; +import {SprinklersDevice} from "../sprinklers"; +import {Item} from "semantic-ui-react"; +import {UiStore} from "../ui"; +import {MessagesView, DeviceView} from "."; + +import "semantic-ui-css/semantic.css"; +import "font-awesome/css/font-awesome.css"; +import "app/style/app.css"; + +@observer +export default class App extends React.PureComponent<{ device: SprinklersDevice, uiStore: UiStore }, any> { + render() { + return + + + + ; + } +} diff --git a/app/script/components/DeviceView.tsx b/app/script/components/DeviceView.tsx new file mode 100644 index 0000000..a40aa15 --- /dev/null +++ b/app/script/components/DeviceView.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +import {observer} from "mobx-react"; +import {Item, Header} from "semantic-ui-react"; +import FontAwesome = require("react-fontawesome"); +import * as classNames from "classnames"; + +import {SprinklersDevice} from "../sprinklers"; +import {SectionTable, RunSectionForm, ProgramTable} from "."; + +const ConnectionState = ({connected}: { connected: boolean }) => + + +   + {connected ? "Connected" : "Disconnected"} + ; + +@observer +export default class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void> { + render() { + const {id, connected, sections, programs} = this.props.device; + return ( + + ("app/images/raspberry_pi.png")}/> + +
+ Device {id} + +
+ + + + + + +
+
+ ); + } +} \ No newline at end of file diff --git a/app/script/components/DurationInput.tsx b/app/script/components/DurationInput.tsx new file mode 100644 index 0000000..23f28ee --- /dev/null +++ b/app/script/components/DurationInput.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; +import {Duration} from "../sprinklers"; +import {Input} from "semantic-ui-react"; + +export default class DurationInput extends React.Component<{ + duration: Duration, + onDurationChange?: (newDuration: Duration) => void; +}, void> { + render() { + const duration = this.props.duration; + // const editing = this.props.onDurationChange != null; + return
+ +
+ + +
+
; + } + + private onMinutesChange = (e, {value}) => { + if (value.length === 0 || isNaN(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)) { + return; + } + const newSeconds = parseInt(value, 10); + this.props.onDurationChange(this.props.duration.withSeconds(newSeconds)); + } +} diff --git a/app/script/components/MessagesView.tsx b/app/script/components/MessagesView.tsx new file mode 100644 index 0000000..b50579a --- /dev/null +++ b/app/script/components/MessagesView.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import {observer} from "mobx-react"; +import {UiStore, Message as UiMessage} from "../ui"; +import {Message} from "semantic-ui-react"; + +@observer +export default class MessagesView extends React.PureComponent<{ uiStore: UiStore }, void> { + render() { + return
+ {this.props.uiStore.messages.map(this.renderMessage)} +
; + } + + private renderMessage = (message: UiMessage, index: number) => { + const {header, content, type} = message; + return this.dismiss(index)}/>; + } + + private dismiss(index: number) { + this.props.uiStore.messages.splice(index, 1); + } +} diff --git a/app/script/components/ProgramTable.tsx b/app/script/components/ProgramTable.tsx new file mode 100644 index 0000000..ca38080 --- /dev/null +++ b/app/script/components/ProgramTable.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; +import {observer} from "mobx-react"; +import {Program, Schedule} from "../sprinklers"; +import {Table} from "semantic-ui-react"; + +@observer +export class ScheduleView extends React.PureComponent<{ schedule: Schedule }, void> { + render() { + return ( +
{JSON.stringify(this.props.schedule)}
+ ); + } +} + +@observer +export default class ProgramTable extends React.PureComponent<{ programs: Program[] }, void> { + private static renderRow(program: Program, i: number): JSX.Element[] { + if (!program) { + return null; + } + const {name, running, enabled, schedule, sequence} = program; + return [ + + {"" + (i + 1)} + {name} + {running ? "Running" : "Not running"} + {enabled ? "Enabled" : "Not enabled"} + + , + + +
    + {sequence.map((item) => + (
  • Section {item.section + 1 + ""} for  + {item.duration.minutes}M {item.duration.seconds}S
  • ))} +
+ +
+
+ , + ]; + } + + render() { + return ( + + + + Programs + + + # + Name + Running? + Enabled? + + + + { + Array.prototype.concat.apply([], this.props.programs.map(ProgramTable.renderRow)) + } + +
+ ); + } +} \ No newline at end of file diff --git a/app/script/components/RunSectionForm.tsx b/app/script/components/RunSectionForm.tsx new file mode 100644 index 0000000..59e8a7f --- /dev/null +++ b/app/script/components/RunSectionForm.tsx @@ -0,0 +1,68 @@ +import * as React from "react"; +import {SyntheticEvent} from "react"; +import {computed} from "mobx"; +import {observer} from "mobx-react"; +import {Duration, Section} from "../sprinklers"; +import {Segment, Header, Form, DropdownProps, DropdownItemProps} from "semantic-ui-react"; +import {DurationInput} from "."; + +@observer +export default class RunSectionForm extends React.Component<{ + sections: Section[], +}, { + duration: Duration, + section: number | "", +}> { + constructor() { + super(); + this.state = { + duration: new Duration(1, 1), + section: "", + }; + } + + render() { + const {section, duration} = this.state; + return +
Run Section
+
+ + + + {/*Label must be   to align it properly*/} + Run + +
+
; + } + + private onSectionChange = (e: SyntheticEvent, v: DropdownProps) => { + this.setState({section: v.value as number}); + } + + private onDurationChange = (newDuration: Duration) => { + this.setState({duration: newDuration}); + } + + private run = (e: SyntheticEvent) => { + e.preventDefault(); + const section: Section = this.props.sections[this.state.section]; + console.log(`should run 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)); + } + + private get isValid(): boolean { + return typeof this.state.section === "number"; + } + + @computed + private get sectionOptions(): DropdownItemProps[] { + return this.props.sections.map((s, i) => ({ + text: s ? s.name : null, + value: i, + })); + } +} \ No newline at end of file diff --git a/app/script/components/SectionTable.tsx b/app/script/components/SectionTable.tsx new file mode 100644 index 0000000..d7d762b --- /dev/null +++ b/app/script/components/SectionTable.tsx @@ -0,0 +1,54 @@ +import * as React from "react"; +import {observer} from "mobx-react"; +import * as classNames from "classnames"; +import {Table} from "semantic-ui-react"; +import FontAwesome = require("react-fontawesome"); + +import {Section} from "../sprinklers"; + +/* tslint:disable:object-literal-sort-keys */ + +@observer +export default class SectionTable extends React.PureComponent<{ sections: Section[] }, void> { + private static renderRow(section: Section, index: number) { + if (!section) { + return null; + } + const {name, state} = section; + return ( + + {"" + (index + 1)} + {name} + {state ? + ( Irrigating) + : "Not irrigating"} + + + ); + } + + render() { + return ( + + + Sections + + + # + Name + State + + + + { + this.props.sections.map(SectionTable.renderRow) + } + +
+ ); + } +} \ No newline at end of file diff --git a/app/script/components/index.ts b/app/script/components/index.ts new file mode 100644 index 0000000..1476b6b --- /dev/null +++ b/app/script/components/index.ts @@ -0,0 +1,7 @@ +export {default as App} from "./App"; +export {default as DeviceView} from "./DeviceView"; +export {default as DurationInput} from "./DurationInput"; +export {default as MessagesView} from "./MessagesView"; +export {default as ProgramTable} from "./ProgramTable"; +export {default as RunSectionForm} from "./RunSectionForm"; +export {default as SectionTable} from "./SectionTable"; \ No newline at end of file diff --git a/app/script/index.tsx b/app/script/index.tsx index a9c5903..8527c91 100644 --- a/app/script/index.tsx +++ b/app/script/index.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; import { AppContainer } from "react-hot-loader"; -import App from "./App"; +import App from "./components/App"; import { MqttApiClient } from "./mqtt"; import {Message, UiStore} from "./ui"; @@ -19,8 +19,8 @@ ReactDOM.render( , rootElem); if (module.hot) { - module.hot.accept("./App", () => { - const NextApp = require("./App").default; + module.hot.accept("./components/App", () => { + const NextApp = require("./components/App").default; ReactDOM.render( , rootElem); diff --git a/app/script/mqtt.ts b/app/script/mqtt.ts index 4c5778e..39a05c9 100644 --- a/app/script/mqtt.ts +++ b/app/script/mqtt.ts @@ -1,9 +1,9 @@ -import "paho-mqtt/mqttws31"; +import "paho-mqtt"; import MQTT = Paho.MQTT; import {EventEmitter} from "events"; import { - SprinklersDevice, ISprinklersApi, Section, Program, IProgramItem, Schedule, ITimeOfDay, Weekday, Duration, + SprinklersDevice, ISprinklersApi, Section, Program, Schedule, ITimeOfDay, Duration, } from "./sprinklers"; import {checkedIndexOf} from "./utils"; import * as Promise from "bluebird"; @@ -13,11 +13,11 @@ export class MqttApiClient extends EventEmitter implements ISprinklersApi { return "sprinklers3-MqttApiClient-" + Math.round(Math.random() * 1000); } - public client: MQTT.Client; + client: MQTT.Client; - public connected: boolean; + connected: boolean; - public devices: { [prefix: string]: MqttSprinklersDevice } = {}; + devices: { [prefix: string]: MqttSprinklersDevice } = {}; constructor() { super(); @@ -27,7 +27,7 @@ export class MqttApiClient extends EventEmitter implements ISprinklersApi { // (this.client as any).trace = (m => console.log(m)); } - public start() { + start() { console.log("connecting to mqtt with client id %s", this.client.clientId); this.client.connect({ onFailure: (e) => { @@ -44,7 +44,7 @@ export class MqttApiClient extends EventEmitter implements ISprinklersApi { }); } - public getDevice(prefix: string): SprinklersDevice { + getDevice(prefix: string): SprinklersDevice { if (/\//.test(prefix)) { throw new Error("Prefix cannot contain a /"); } @@ -57,7 +57,7 @@ export class MqttApiClient extends EventEmitter implements ISprinklersApi { return this.devices[prefix]; } - public removeDevice(prefix: string) { + removeDevice(prefix: string) { const device = this.devices[prefix]; if (!device) { return; @@ -93,8 +93,8 @@ export class MqttApiClient extends EventEmitter implements ISprinklersApi { } class MqttSprinklersDevice extends SprinklersDevice { - public readonly apiClient: MqttApiClient; - public readonly prefix: string; + readonly apiClient: MqttApiClient; + readonly prefix: string; private responseCallbacks: { [rid: number]: ResponseCallback; @@ -106,13 +106,13 @@ class MqttSprinklersDevice extends SprinklersDevice { this.prefix = prefix; } - public doSubscribe() { + doSubscribe() { const c = this.apiClient.client; this.subscriptions .forEach((filter) => c.subscribe(filter, {qos: 1})); } - public doUnsubscribe() { + doUnsubscribe() { const c = this.apiClient.client; this.subscriptions .forEach((filter) => c.unsubscribe(filter)); @@ -123,7 +123,7 @@ class MqttSprinklersDevice extends SprinklersDevice { * @param topic The topic, with prefix removed * @param payload The payload string */ - public onMessage(topic: string, payload: string) { + onMessage(topic: string, payload: string) { if (topic === "connected") { this.connected = (payload === "true"); // console.log(`MqttSprinklersDevice with prefix ${this.prefix}: ${this.connected}`) @@ -131,6 +131,7 @@ class MqttSprinklersDevice extends SprinklersDevice { } let matches = topic.match(/^sections(?:\/(\d+)(?:\/?(.+))?)?$/); if (matches != null) { + //noinspection JSUnusedLocalSymbols const [_topic, secStr, subTopic] = matches; // console.log(`section: ${secStr}, topic: ${subTopic}, payload: ${payload}`); if (!secStr) { // new number of sections @@ -147,6 +148,7 @@ class MqttSprinklersDevice extends SprinklersDevice { } matches = topic.match(/^programs(?:\/(\d+)(?:\/?(.+))?)?$/); if (matches != null) { + //noinspection JSUnusedLocalSymbols const [_topic, progStr, subTopic] = matches; // console.log(`program: ${progStr}, topic: ${subTopic}, payload: ${payload}`); if (!progStr) { // new number of programs @@ -163,6 +165,7 @@ class MqttSprinklersDevice extends SprinklersDevice { } matches = topic.match(/^responses\/(\d+)$/); if (matches != null) { + //noinspection JSUnusedLocalSymbols const [_topic, respIdStr] = matches; console.log(`response: ${respIdStr}`); const respId = parseInt(respIdStr, 10); @@ -180,7 +183,7 @@ class MqttSprinklersDevice extends SprinklersDevice { return this.prefix; } - public runSection(section: Section | number, duration: Duration) { + runSection(section: Section | number, duration: Duration) { const sectionNum = checkedIndexOf(section, this.sections, "Section"); return this.makeRequest(`sections/${sectionNum}/run`, { @@ -188,23 +191,20 @@ class MqttSprinklersDevice extends SprinklersDevice { } as IRunSectionJSON); } - public runProgram(program: Program | number) { + runProgram(program: Program | number) { const programNum = checkedIndexOf(program, this.programs, "Program"); return this.makeRequest(`programs/${programNum}/run`, {}); } + //noinspection JSMethodCanBeStatic private nextRequestId(): number { return Math.floor(Math.random() * 1000000000); } private makeRequest(topic: string, payload: object | string): Promise { return new Promise((resolve, reject) => { - let payloadStr: string; - if (typeof payload === "string") { - payloadStr = payload; - } else { - payloadStr = JSON.stringify(payload); - } + const payloadStr = (typeof payload === "string") ? + payload : JSON.stringify(payload); const message = new MQTT.Message(payloadStr); message.destinationName = this.prefix + "/" + topic; const requestId = this.nextRequestId(); @@ -250,7 +250,7 @@ interface IRunSectionJSON { } class MqttSection extends Section { - public onMessage(topic: string, payload: string) { + onMessage(topic: string, payload: string) { if (topic === "state") { this.state = (payload === "true"); } else if (topic == null) { @@ -289,7 +289,7 @@ interface IProgramJSON { } class MqttProgram extends Program { - public onMessage(topic: string, payload: string) { + onMessage(topic: string, payload: string) { if (topic === "running") { this.running = (payload === "true"); } else if (topic == null) { @@ -298,7 +298,7 @@ class MqttProgram extends Program { } } - public updateFromJSON(json: Partial) { + updateFromJSON(json: Partial) { if (json.name != null) { this.name = json.name; } diff --git a/app/script/paho-mqtt.d.ts b/app/script/paho-mqtt.d.ts index 0fcf695..7df5863 100644 --- a/app/script/paho-mqtt.d.ts +++ b/app/script/paho-mqtt.d.ts @@ -1,76 +1,444 @@ -/* tslint:disable:interface-name */ +// Type definitions for paho-mqtt 1.0 +// Project: https://github.com/eclipse/paho.mqtt.javascript#readme +// Definitions by: Alex Mikhalev +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped declare namespace Paho { + /** + * Send and receive messages using web browsers. + *

+ * This programming interface lets a JavaScript client application use the MQTT V3.1 or + * V3.1.1 protocol to connect to an MQTT-supporting messaging server. + * + * The function supported includes: + *

    + *
  1. Connecting to and disconnecting from a server. The server is identified by its host name and port number. + *
  2. Specifying options that relate to the communications link with the server, + * for example the frequency of keep-alive heartbeats, and whether SSL/TLS is required. + *
  3. Subscribing to and receiving messages from MQTT Topics. + *
  4. Publishing messages to MQTT Topics. + *
+ *

+ * The API consists of two main objects: + *

+ *
{@link Paho.MQTT.Client}
+ *
This contains methods that provide the functionality of the API, + * including provision of callbacks that notify the application when a message + * arrives from or is delivered to the messaging server, + * or when the status of its connection to the messaging server changes.
+ *
{@link Paho.MQTT.Message}
+ *
This encapsulates the payload of the message along with various attributes + * associated with its delivery, in particular the destination to which it has + * been (or is about to be) sent.
+ *
+ *

+ * The programming interface validates parameters passed to it, and will throw + * an Error containing an error message intended for developer use, if it detects + * an error with any parameter. + *

+ * + * @namespace Paho.MQTT + */ namespace MQTT { - interface MQTTError { errorCode: string; errorMessage: string; } - interface WithInvocationContext { invocationContext: object; } - interface ErrorWithInvocationContext extends MQTTError, WithInvocationContext {} - interface OnSubscribeSuccessParams extends WithInvocationContext { grantedQos: number; } + /** + * The Quality of Service used to deliver a message. + *

+ *
0 Best effort (default).
+ *
1 At least once.
+ *
2 Exactly once.
+ *
+ */ + type Qos = 0 | 1 | 2; + + interface MQTTError { + /** A number indicating the nature of the error. */ + errorCode: number; + + /** Text describing the error */ + errorMessage: string; + } + interface WithInvocationContext { + /** + * invocationContext as passed in with the corresponding field in the connectOptions or + * subscribeOptions. + */ + invocationContext: any; + } + interface ErrorWithInvocationContext extends MQTTError, WithInvocationContext { + } + interface OnSubscribeSuccessParams extends WithInvocationContext { + grantedQos: Qos; + } + + /** + * Called when the connect acknowledgement has been received from the server. + * @param o + * A single response object parameter is passed to the onSuccess callback containing the following fields: + *
  • invocationContext as passed in with the corresponding field in the connectOptions. + */ + type OnSuccessCallback = (o: WithInvocationContext) => void; + + /** + * Called when the subscribe acknowledgement has been received from the server. + * @param o + * A single response object parameter is passed to the onSuccess callback containing the following fields: + *
  • invocationContext as passed in with the corresponding field in the connectOptions. + */ + type OnSubscribeSuccessCallback = (o: OnSubscribeSuccessParams) => void; + + /** + * Called when the connect request has failed or timed out. + * @param e + * A single response object parameter is passed to the onFailure callback containing the following fields: + *
  • invocationContext as passed in with the corresponding field in the connectOptions. + *
  • errorCode a number indicating the nature of the error. + *
  • errorMessage text describing the error. + */ + type OnFailureCallback = (e: ErrorWithInvocationContext) => void; + + /** + * Called when a connection has been lost. + * @param error A single response object parameter is passed to the onConnectionLost callback containing the + * following fields: + *
  • errorCode + *
  • errorMessage + */ type OnConnectionLostHandler = (error: MQTTError) => void; + + /** + * Called when a message was delivered or has arrived. + * @param message The {@link Paho.MQTT.Message} that was delivered or has arrived. + */ type OnMessageHandler = (message: Message) => void; + + /** + * Attributes used with a connection. + */ interface ConnectionOptions { + /** + * If the connect has not succeeded within this number of seconds, it is deemed to have failed. + * @default The default is 30 seconds. + */ timeout?: number; + /** Authentication username for this connection. */ userName?: string; + /** Authentication password for this connection. */ password?: string; + /** Sent by the server when the client disconnects abnormally. */ willMessage?: Message; + /** + * The server disconnects this client if there is no activity for this number of seconds. + * @default The default value of 60 seconds is assumed if not set. + */ keepAliveInterval?: number; + /** + * If true(default) the client and server persistent state is deleted on successful connect. + * @default true + */ cleanSession?: boolean; + /** If present and true, use an SSL Websocket connection. */ useSSL?: boolean; - invocationContext?: object; - onSuccess?: (o: WithInvocationContext) => void; - mqttVersion?: number; - onFailure?: (e: ErrorWithInvocationContext) => void; + /** Passed to the onSuccess callback or onFailure callback. */ + invocationContext?: any; + /** + * Called when the connect acknowledgement has been received from the server. + */ + onSuccess?: OnSuccessCallback; + /** + * Specifies the mqtt version to use when connecting + *
    + *
    3 - MQTT 3.1
    + *
    4 - MQTT 3.1.1 (default)
    + *
    + * @default 4 + */ + mqttVersion?: 3 | 4; + /** + * Called when the connect request has failed or timed out. + */ + onFailure?: OnFailureCallback; + /** + * If present this contains either a set of hostnames or fully qualified + * WebSocket URIs (ws://example.com:1883/mqtt), that are tried in order in place of the host and port + * paramater on the construtor. The hosts are tried one at at time in order until one of then succeeds. + */ hosts?: string[]; + /** + * If present the set of ports matching the hosts. If hosts contains URIs, this property is not used. + */ ports?: number[]; } + + /** + * Used to control a subscription + */ interface SubscribeOptions { - qos?: number; - invocationContext?: object; - onSuccess?: (o: OnSubscribeSuccessParams) => void; - onFailure?: (e: ErrorWithInvocationContext) => void; + /** the maximum qos of any publications sent as a result of making this subscription. */ + qos?: Qos; + /** passed to the onSuccess callback or onFailure callback. */ + invocationContext?: any; + /** called when the subscribe acknowledgement has been received from the server. */ + onSuccess?: OnSubscribeSuccessCallback; + /** called when the subscribe request has failed or timed out. */ + onFailure?: OnFailureCallback; + /** + * timeout which, if present, determines the number of seconds after which the onFailure calback is called. + * The presence of a timeout does not prevent the onSuccess callback from being called when the subscribe + * completes. + */ timeout?: number; } + interface UnsubscribeOptions { - invocationContext?: object; - onSuccess?: (o: WithInvocationContext) => void; - onFailure?: (e: ErrorWithInvocationContext) => void; + /** passed to the onSuccess callback or onFailure callback. */ + invocationContext?: any; + /** called when the unsubscribe acknowledgement has been received from the server. */ + onSuccess?: OnSuccessCallback; + /** called when the unsubscribe request has failed or timed out. */ + onFailure?: OnFailureCallback; + /** + * timeout which, if present, determines the number of seconds after which the onFailure calback is called. + * The presence of a timeout does not prevent the onSuccess callback from being called when the unsubscribe + * completes. + */ timeout?: number; } + + interface TraceElement { + severity: "Debug"; + message: string; + } + + type TraceFunction = (element: TraceElement) => void; + + /** + * The JavaScript application communicates to the server using a {@link Paho.MQTT.Client} object. + * + * Most applications will create just one Client object and then call its connect() method, + * however applications can create more than one Client object if they wish. + * In this case the combination of host, port and clientId attributes must be different for each Client object. + * + * The send, subscribe and unsubscribe methods are implemented as asynchronous JavaScript methods + * (even though the underlying protocol exchange might be synchronous in nature). + * This means they signal their completion by calling back to the application, + * via Success or Failure callback functions provided by the application on the method in question. + * Such callbacks are called at most once per method invocation and do not persist beyond the lifetime + * of the script that made the invocation. + * + * In contrast there are some callback functions, most notably {@link onMessageArrived}, + * that are defined on the {@link Paho.MQTT.Client} object. + * These may get called multiple times, and aren't directly related to specific method invocations made by the + * client. + * + */ class Client { - public readonly clientId: string; - public readonly host: string; - public readonly path: string; - public readonly port: number; + /** read only used when connecting to the server. */ + readonly clientId: string; + + /** read only the server's DNS hostname or dotted decimal IP address. */ + readonly host: string; + + /** read only the server's path. */ + readonly path: string; - public onConnectionLost: OnConnectionLostHandler; - public onMessageArrived: OnMessageHandler; - public onMessageDelivered: OnMessageHandler; + /** read only the server's port. */ + readonly port: number; - // tslint:disable unified-signatures + /** function called with trace information, if set */ + trace?: TraceFunction; + + /** + * called when a connection has been lost. after a connect() method has succeeded. + * Establish the call back used when a connection has been lost. The connection may be + * lost because the client initiates a disconnect or because the server or network + * cause the client to be disconnected. The disconnect call back may be called without + * the connectionComplete call back being invoked if, for example the client fails to + * connect. + * A single response object parameter is passed to the onConnectionLost callback containing the following + * fields: + *
  • errorCode + *
  • errorMessage + */ + onConnectionLost: OnConnectionLostHandler; + + /** + * called when a message has been delivered. + * All processing that this Client will ever do has been completed. So, for example, + * in the case of a Qos=2 message sent by this client, the PubComp flow has been received from the server + * and the message has been removed from persistent storage before this callback is invoked. + * Parameters passed to the onMessageDelivered callback are: + *
  • {@link Paho.MQTT.Message} that was delivered. + */ + onMessageDelivered: OnMessageHandler; + + /** + * called when a message has arrived in this Paho.MQTT.client. + * Parameters passed to the onMessageArrived callback are: + *
  • {@link Paho.MQTT.Message} that has arrived. + */ + onMessageArrived: OnMessageHandler; + + /* tslint:disable:unified-signatures */ + /* these cannot actually be neatly unified */ + + /** + * @param host - the address of the messaging server as a DNS name or dotted decimal IP address. + * @param port - the port number to connect to + * @param path - the path on the host to connect to - only used if host is not a URI. Default: '/mqtt'. + * @param clientId - the Messaging client identifier, between 1 and 23 characters in length. + */ constructor(host: string, port: number, path: string, clientId: string); + + /** + * @param host - the address of the messaging server as a DNS name or dotted decimal IP address. + * @param port - the port number to connect to + * @param clientId - the Messaging client identifier, between 1 and 23 characters in length. + */ constructor(host: string, port: number, clientId: string); + + /** + * @param hostUri - the address of the messaging server as a fully qualified WebSocket URI + * @param clientId - the Messaging client identifier, between 1 and 23 characters in length. + */ constructor(hostUri: string, clientId: string); - public connect(connectionOptions?: ConnectionOptions); - public disconnect(); + /* tslint:enable:unified-signatures */ + + /** + * Connect this Messaging client to its server. + * @throws {InvalidState} if the client is not in disconnected state. The client must have received + * connectionLost or disconnected before calling connect for a second or subsequent time. + */ + connect(connectionOptions?: ConnectionOptions): void; + + /** + * Normal disconnect of this Messaging client from its server. + * + * @throws {InvalidState} if the client is already disconnected. + */ + disconnect(): void; + + /** + * @returns True if the client is currently connected + */ + isConnected(): boolean; - public getTraceLog(): object[]; - public startTrace(); - public stopTrace(); + /** + * Get the contents of the trace log. + * + * @return {Object[]} tracebuffer containing the time ordered trace records. + */ + getTraceLog(): any[]; - public send(message: Message); - public subscribe(filter: string, subcribeOptions?: SubscribeOptions); - public unsubscribe(filter: string, unsubcribeOptions?: UnsubscribeOptions); + /** + * Start tracing. + */ + startTrace(): void; + + /** + * Stop tracing. + */ + stopTrace(): void; + + /** + * Send a message to the consumers of the destination in the Message. + * + * @param {Paho.MQTT.Message} message - mandatory The {@link Paho.MQTT.Message} object to be sent. + * @throws {InvalidState} if the client is not connected. + */ + send(message: Message): void; + + /** + * Send a message to the consumers of the destination in the Message. + * + * @param {string} topic - mandatory The name of the destination to which the message is to be sent. + * @param {string|ArrayBuffer} payload - The message data to be sent. + * @param {number} qos The Quality of Service used to deliver the message. + *
    + *
    0 Best effort (default). + *
    1 At least once. + *
    2 Exactly once. + *
    + * @param {Boolean} retained If true, the message is to be retained by the server and delivered to both + * current and future subscriptions. If false the server only delivers the message to current subscribers, + * this is the default for new Messages. A received message has the retained boolean set to true if the + * message was published with the retained boolean set to true and the subscrption was made after the + * message has been published. + * @throws {InvalidState} if the client is not connected. + */ + send(topic: string, payload: string | ArrayBuffer, qos?: Qos, retained?: boolean): void; + + /** + * Subscribe for messages, request receipt of a copy of messages sent to the destinations described by the + * filter. + * + * @param filter A filter describing the destinations to receive messages from. + * @param subcribeOptions Used to control the subscription + * @throws {InvalidState} if the client is not in connected state. + */ + subscribe(filter: string, subcribeOptions?: SubscribeOptions): void; + + /** + * Unsubscribe for messages, stop receiving messages sent to destinations described by the filter. + * + * @param filter - describing the destinations to receive messages from. + * @param unsubscribeOptions - used to control the subscription + * @throws {InvalidState} if the client is not in connected state. + */ + unsubscribe(filter: string, unsubcribeOptions?: UnsubscribeOptions): void; } + /** + * An application message, sent or received. + */ class Message { - public destinationName: string; - public readonly duplicate: boolean; - public readonly payloadBytes: ArrayBuffer; - public readonly payloadString: string; - public qos: number; - public retained: boolean; + /** + * mandatory The name of the destination to which the message is to be sent + * (for messages about to be sent) or the name of the destination from which the message has been received. + * (for messages received by the onMessage function). + */ + destinationName?: string; + + /** + * read only If true, this message might be a duplicate of one which has already been received. + * This is only set on messages received from the server. + */ + readonly duplicate: boolean; + + /** read only The payload as an ArrayBuffer. */ + readonly payloadBytes: ArrayBuffer; + + /** + * read only The payload as a string if the payload consists of valid UTF-8 characters. + * @throw {Error} if the payload is not valid UTF-8 + */ + readonly payloadString: string; + + /** + * The Quality of Service used to deliver the message. + *
    + *
    0 Best effort (default). + *
    1 At least once. + *
    2 Exactly once. + *
    + * + * @default 0 + */ + qos: number; + + /** + * If true, the message is to be retained by the server and delivered to both current and future + * subscriptions. If false the server only delivers the message to current subscribers, this is the default + * for new Messages. A received message has the retained boolean set to true if the message was published + * with the retained boolean set to true and the subscription was made after the message has been published. + * + * @default false + */ + retained: boolean; + /** + * @param {String|ArrayBuffer} payload The message data to be sent. + */ constructor(payload: string | ArrayBuffer); } } diff --git a/app/script/sprinklers.ts b/app/script/sprinklers.ts index 5df50c4..fa04fe9 100644 --- a/app/script/sprinklers.ts +++ b/app/script/sprinklers.ts @@ -120,6 +120,10 @@ export class Program { } } +export class SectionRunner { + +} + export abstract class SprinklersDevice { @observable public connected: boolean = false; diff --git a/tslint.json b/tslint.json index e1a4e5d..0b61947 100644 --- a/tslint.json +++ b/tslint.json @@ -1,25 +1,25 @@ { - "defaultSeverity": "error", - "extends": [ - "tslint:recommended" + "defaultSeverity": "error", + "extends": [ + "tslint:latest" + ], + "jsRules": {}, + "rules": { + "no-console": [ + false ], - "jsRules": {}, - "rules": { - "no-console": [ - false - ], - "max-classes-per-file": [ - false - ], - "ordered-imports": [ - false - ], - "variable-name": [ - "allow-leading-underscore" - ], - "no-namespace": [ - "allow-declarations" - ] - }, - "rulesDirectory": [] + "max-classes-per-file": [ + false + ], + "ordered-imports": false, + "variable-name": [ + "allow-leading-underscore" + ], + "no-namespace": [ + "allow-declarations" + ], + "interface-name": false, + "member-access": [ true, "no-public" ] + }, + "rulesDirectory": [] } \ No newline at end of file