Browse Source

Improved components structure; better paho-mqtt definitions

update-deps
Alex Mikhalev 8 years ago
parent
commit
10805f9fb1
  1. 287
      app/script/App.tsx
  2. 22
      app/script/components/App.tsx
  3. 43
      app/script/components/DeviceView.tsx
  4. 40
      app/script/components/DurationInput.tsx
  5. 24
      app/script/components/MessagesView.tsx
  6. 66
      app/script/components/ProgramTable.tsx
  7. 68
      app/script/components/RunSectionForm.tsx
  8. 54
      app/script/components/SectionTable.tsx
  9. 7
      app/script/components/index.ts
  10. 6
      app/script/index.tsx
  11. 48
      app/script/mqtt.ts
  12. 444
      app/script/paho-mqtt.d.ts
  13. 4
      app/script/sprinklers.ts
  14. 10
      tslint.json

287
app/script/App.tsx

@ -1,287 +0,0 @@ @@ -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 (
<Table.Row key={index}>
<Table.Cell className="section--number">{"" + (index + 1)}</Table.Cell>
<Table.Cell className="section--name">{name}</Table.Cell>
<Table.Cell className={classNames({
"section--state": true,
"section--state-true": state,
"section--state-false": !state,
})}>{state ?
(<span><FontAwesome name="tint"/> Irrigating</span>)
: "Not irrigating"}
</Table.Cell>
</Table.Row>
);
}
public render() {
return (<Table celled striped>
<Table.Header>
<Table.Row>
<Table.HeaderCell colSpan="3">Sections</Table.HeaderCell>
</Table.Row>
<Table.Row>
<Table.HeaderCell className="section--number">#</Table.HeaderCell>
<Table.HeaderCell className="section--name">Name</Table.HeaderCell>
<Table.HeaderCell className="section--state">State</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{
this.props.sections.map(SectionTable.renderRow)
}
</Table.Body>
</Table>
);
}
}
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 <div className="field durationInput">
<label>Duration</label>
<div className="fields">
<Input type="number" className="field durationInput--minutes"
value={duration.minutes} onChange={this.onMinutesChange}
label="M" labelPosition="right"/>
<Input type="number" className="field durationInput--seconds"
value={duration.seconds} onChange={this.onSecondsChange} max="60"
label="S" labelPosition="right"/>
</div>
</div>;
}
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 <Segment>
<Header>Run Section</Header>
<Form>
<Form.Group>
<Form.Select label="Section" placeholder="Section" options={this.sectionOptions} value={section}
onChange={this.onSectionChange}/>
<DurationInput duration={duration} onDurationChange={this.onDurationChange}/>
{/*Label must be &nbsp; to align it properly*/}
<Form.Button label="&nbsp;" primary onClick={this.run} disabled={!this.isValid}>Run</Form.Button>
</Form.Group>
</Form>
</Segment>;
}
private onSectionChange = (e: SyntheticEvent<HTMLElement>, v: DropdownProps) => {
this.setState({section: v.value as number});
}
private onDurationChange = (newDuration: Duration) => {
this.setState({duration: newDuration});
}
private run = (e: SyntheticEvent<HTMLElement>) => {
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 (
<div>{JSON.stringify(this.props.schedule)}</div>
);
}
}
@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 [
<Table.Row key={i}>
<Table.Cell className="program--number">{"" + (i + 1)}</Table.Cell>
<Table.Cell className="program--name">{name}</Table.Cell>
<Table.Cell className="program--running">{running ? "Running" : "Not running"}</Table.Cell>
<Table.Cell className="program--enabled">{enabled ? "Enabled" : "Not enabled"}</Table.Cell>
</Table.Row>
,
<Table.Row key={i + .5}>
<Table.Cell className="program--sequence" colSpan="4">
<ul>
{sequence.map((item) =>
(<li>Section {item.section + 1 + ""} for&nbsp;
{item.duration.minutes}M {item.duration.seconds}S</li>))}
</ul>
<ScheduleView schedule={schedule}/>
</Table.Cell>
</Table.Row>
,
];
}
public render() {
return (
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell colSpan="7">Programs</Table.HeaderCell>
</Table.Row>
<Table.Row>
<Table.HeaderCell className="program--number">#</Table.HeaderCell>
<Table.HeaderCell className="program--name">Name</Table.HeaderCell>
<Table.HeaderCell className="program--running">Running?</Table.HeaderCell>
<Table.HeaderCell className="program--enabled">Enabled?</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{
Array.prototype.concat.apply([], this.props.programs.map(ProgramTable.renderRow))
}
</Table.Body>
</Table>
);
}
}
const ConnectionState = ({connected}: { connected: boolean }) =>
<span className={classNames({
"device--connectionState": true,
"device--connectionState-connected": connected,
"device--connectionState-disconnected": !connected,
})}>
<FontAwesome name={connected ? "plug" : "chain-broken"}/>
&nbsp;
{connected ? "Connected" : "Disconnected"}
</span>;
@observer
class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void> {
public render() {
const {id, connected, sections, programs} = this.props.device;
return (
<Item>
<Item.Image src={require<string>("app/images/raspberry_pi.png")}/>
<Item.Content>
<Header as="h1">
<span>Device </span><kbd>{id}</kbd>
<ConnectionState connected={connected}/>
</Header>
<Item.Meta>
</Item.Meta>
<SectionTable sections={sections}/>
<RunSectionForm sections={sections}/>
<ProgramTable programs={programs}/>
</Item.Content>
</Item>
);
}
}
@observer
class MessagesView extends React.PureComponent<{uiStore: UiStore}, void> {
public render() {
return <div>
{this.props.uiStore.messages.map(this.renderMessage)}
</div>;
}
private renderMessage = (message: UiMessage, index: number) => {
const {header, content, type} = message;
return <Message header={header} content={content} success={type === UiMessage.Type.Success}
info={type === UiMessage.Type.Info} warning={type === UiMessage.Type.Warning}
error={type === UiMessage.Type.Error} onDismiss={() => 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 <Item.Group divided>
<MessagesView uiStore={this.props.uiStore} />
<DeviceView device={this.props.device}/>
<DevTools />
</Item.Group>;
}
}

22
app/script/components/App.tsx

@ -0,0 +1,22 @@ @@ -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 <Item.Group divided>
<MessagesView uiStore={this.props.uiStore}/>
<DeviceView device={this.props.device}/>
<DevTools />
</Item.Group>;
}
}

43
app/script/components/DeviceView.tsx

@ -0,0 +1,43 @@ @@ -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 }) =>
<span className={classNames({
"device--connectionState": true,
"device--connectionState-connected": connected,
"device--connectionState-disconnected": !connected,
})}>
<FontAwesome name={connected ? "plug" : "chain-broken"}/>
&nbsp;
{connected ? "Connected" : "Disconnected"}
</span>;
@observer
export default class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void> {
render() {
const {id, connected, sections, programs} = this.props.device;
return (
<Item>
<Item.Image src={require<string>("app/images/raspberry_pi.png")}/>
<Item.Content>
<Header as="h1">
<span>Device </span><kbd>{id}</kbd>
<ConnectionState connected={connected}/>
</Header>
<Item.Meta>
</Item.Meta>
<SectionTable sections={sections}/>
<RunSectionForm sections={sections}/>
<ProgramTable programs={programs}/>
</Item.Content>
</Item>
);
}
}

40
app/script/components/DurationInput.tsx

@ -0,0 +1,40 @@ @@ -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 <div className="field durationInput">
<label>Duration</label>
<div className="fields">
<Input type="number" className="field durationInput--minutes"
value={duration.minutes} onChange={this.onMinutesChange}
label="M" labelPosition="right"/>
<Input type="number" className="field durationInput--seconds"
value={duration.seconds} onChange={this.onSecondsChange} max="60"
label="S" labelPosition="right"/>
</div>
</div>;
}
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));
}
}

24
app/script/components/MessagesView.tsx

@ -0,0 +1,24 @@ @@ -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 <div>
{this.props.uiStore.messages.map(this.renderMessage)}
</div>;
}
private renderMessage = (message: UiMessage, index: number) => {
const {header, content, type} = message;
return <Message header={header} content={content} success={type === UiMessage.Type.Success}
info={type === UiMessage.Type.Info} warning={type === UiMessage.Type.Warning}
error={type === UiMessage.Type.Error} onDismiss={() => this.dismiss(index)}/>;
}
private dismiss(index: number) {
this.props.uiStore.messages.splice(index, 1);
}
}

66
app/script/components/ProgramTable.tsx

@ -0,0 +1,66 @@ @@ -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 (
<div>{JSON.stringify(this.props.schedule)}</div>
);
}
}
@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 [
<Table.Row key={i}>
<Table.Cell className="program--number">{"" + (i + 1)}</Table.Cell>
<Table.Cell className="program--name">{name}</Table.Cell>
<Table.Cell className="program--running">{running ? "Running" : "Not running"}</Table.Cell>
<Table.Cell className="program--enabled">{enabled ? "Enabled" : "Not enabled"}</Table.Cell>
</Table.Row>
,
<Table.Row key={i + .5}>
<Table.Cell className="program--sequence" colSpan="4">
<ul>
{sequence.map((item) =>
(<li>Section {item.section + 1 + ""} for&nbsp;
{item.duration.minutes}M {item.duration.seconds}S</li>))}
</ul>
<ScheduleView schedule={schedule}/>
</Table.Cell>
</Table.Row>
,
];
}
render() {
return (
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell colSpan="7">Programs</Table.HeaderCell>
</Table.Row>
<Table.Row>
<Table.HeaderCell className="program--number">#</Table.HeaderCell>
<Table.HeaderCell className="program--name">Name</Table.HeaderCell>
<Table.HeaderCell className="program--running">Running?</Table.HeaderCell>
<Table.HeaderCell className="program--enabled">Enabled?</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{
Array.prototype.concat.apply([], this.props.programs.map(ProgramTable.renderRow))
}
</Table.Body>
</Table>
);
}
}

68
app/script/components/RunSectionForm.tsx

@ -0,0 +1,68 @@ @@ -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 <Segment>
<Header>Run Section</Header>
<Form>
<Form.Group>
<Form.Select label="Section" placeholder="Section" options={this.sectionOptions} value={section}
onChange={this.onSectionChange}/>
<DurationInput duration={duration} onDurationChange={this.onDurationChange}/>
{/*Label must be &nbsp; to align it properly*/}
<Form.Button label="&nbsp;" primary onClick={this.run} disabled={!this.isValid}>Run</Form.Button>
</Form.Group>
</Form>
</Segment>;
}
private onSectionChange = (e: SyntheticEvent<HTMLElement>, v: DropdownProps) => {
this.setState({section: v.value as number});
}
private onDurationChange = (newDuration: Duration) => {
this.setState({duration: newDuration});
}
private run = (e: SyntheticEvent<HTMLElement>) => {
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,
}));
}
}

54
app/script/components/SectionTable.tsx

@ -0,0 +1,54 @@ @@ -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 (
<Table.Row key={index}>
<Table.Cell className="section--number">{"" + (index + 1)}</Table.Cell>
<Table.Cell className="section--name">{name}</Table.Cell>
<Table.Cell className={classNames({
"section--state": true,
"section--state-true": state,
"section--state-false": !state,
})}>{state ?
(<span><FontAwesome name="tint"/> Irrigating</span>)
: "Not irrigating"}
</Table.Cell>
</Table.Row>
);
}
render() {
return (<Table celled striped>
<Table.Header>
<Table.Row>
<Table.HeaderCell colSpan="3">Sections</Table.HeaderCell>
</Table.Row>
<Table.Row>
<Table.HeaderCell className="section--number">#</Table.HeaderCell>
<Table.HeaderCell className="section--name">Name</Table.HeaderCell>
<Table.HeaderCell className="section--state">State</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{
this.props.sections.map(SectionTable.renderRow)
}
</Table.Body>
</Table>
);
}
}

7
app/script/components/index.ts

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

6
app/script/index.tsx

@ -2,7 +2,7 @@ import * as React from "react"; @@ -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(<AppContainer> @@ -19,8 +19,8 @@ ReactDOM.render(<AppContainer>
</AppContainer>, rootElem);
if (module.hot) {
module.hot.accept("./App", () => {
const NextApp = require<any>("./App").default;
module.hot.accept("./components/App", () => {
const NextApp = require<any>("./components/App").default;
ReactDOM.render(<AppContainer>
<App device={device} uiStore={uiStore} />
</AppContainer>, rootElem);

48
app/script/mqtt.ts

@ -1,9 +1,9 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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<IResponseData> {
return new Promise<IResponseData>((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 { @@ -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 { @@ -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 { @@ -298,7 +298,7 @@ class MqttProgram extends Program {
}
}
public updateFromJSON(json: Partial<IProgramJSON>) {
updateFromJSON(json: Partial<IProgramJSON>) {
if (json.name != null) {
this.name = json.name;
}

444
app/script/paho-mqtt.d.ts vendored

@ -1,76 +1,444 @@ @@ -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 <https://github.com/amikhalev>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
declare namespace Paho {
/**
* Send and receive messages using web browsers.
* <p>
* 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:
* <ol>
* <li>Connecting to and disconnecting from a server. The server is identified by its host name and port number.
* <li>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.
* <li>Subscribing to and receiving messages from MQTT Topics.
* <li>Publishing messages to MQTT Topics.
* </ol>
* <p>
* The API consists of two main objects:
* <dl>
* <dt><b>{@link Paho.MQTT.Client}</b></dt>
* <dd>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.</dd>
* <dt><b>{@link Paho.MQTT.Message}</b></dt>
* <dd>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.</dd>
* </dl>
* <p>
* 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.
* <p>
*
* @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.
* <dl>
* <dt>0 Best effort (default).</dt>
* <dt>1 At least once.</dt>
* <dt>2 Exactly once.</dt>
* </dl>
*/
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 {
/**
* <code>invocationContext</code> 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:
* <li><code>invocationContext</code> 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:
* <li><code>invocationContext</code> 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:
* <li><code>invocationContext</code> as passed in with the corresponding field in the connectOptions.
* <li><code>errorCode</code> a number indicating the nature of the error.
* <li><code>errorMessage</code> 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:
* <li>errorCode
* <li>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
* <dl>
* <dt>3 - MQTT 3.1</dt>
* <dt>4 - MQTT 3.1.1 (default)</dt>
* </dl>
* @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;
/** <i>read only</i> used when connecting to the server. */
readonly clientId: string;
/** <i>read only</i> the server's DNS hostname or dotted decimal IP address. */
readonly host: string;
/** <i>read only</i> the server's path. */
readonly path: string;
public onConnectionLost: OnConnectionLostHandler;
public onMessageArrived: OnMessageHandler;
public onMessageDelivered: OnMessageHandler;
/** <i>read only</i> 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:
* <li>errorCode
* <li>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:
* <li>{@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:
* <li> {@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 - <b>mandatory</b> 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 - <b>mandatory</b> 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.
* <dl>
* <dt>0 Best effort (default).
* <dt>1 At least once.
* <dt>2 Exactly once.
* </dl>
* @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;
/**
* <b>mandatory</b> 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;
/**
* <i>read only</i> 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;
/** <i>read only</i> The payload as an ArrayBuffer. */
readonly payloadBytes: ArrayBuffer;
/**
* <i>read only</i> 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.
* <dl>
* <dt>0 Best effort (default).
* <dt>1 At least once.
* <dt>2 Exactly once.
* </dl>
*
* @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);
}
}

4
app/script/sprinklers.ts

@ -120,6 +120,10 @@ export class Program { @@ -120,6 +120,10 @@ export class Program {
}
}
export class SectionRunner {
}
export abstract class SprinklersDevice {
@observable
public connected: boolean = false;

10
tslint.json

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
"tslint:latest"
],
"jsRules": {},
"rules": {
@ -11,15 +11,15 @@ @@ -11,15 +11,15 @@
"max-classes-per-file": [
false
],
"ordered-imports": [
false
],
"ordered-imports": false,
"variable-name": [
"allow-leading-underscore"
],
"no-namespace": [
"allow-declarations"
]
],
"interface-name": false,
"member-access": [ true, "no-public" ]
},
"rulesDirectory": []
}
Loading…
Cancel
Save