diff --git a/app/script/App.tsx b/app/script/App.tsx
index 8955811..6b2630b 100644
--- a/app/script/App.tsx
+++ b/app/script/App.tsx
@@ -6,30 +6,41 @@ import FontAwesome = require("react-fontawesome");
import * as classNames from "classnames";
import "semantic-ui-css/semantic.css";
-import "font-awesome/css/font-awesome.css"
+import "font-awesome/css/font-awesome.css";
import "app/style/app.css";
+/* tslint:disable:object-literal-sort-keys */
+
@observer
class SectionTable extends React.PureComponent<{ sections: Section[] }, void> {
- static renderRow(section: Section, index: number) {
+ private static renderRow(section: Section, index: number) {
const { name, state } = section;
return (
- Section {name}
- State: {state + ""}
+ {"" + (index + 1)}
+ {name}
+ {state ?
+ ( Irrigating)
+ : "Not irrigating"}
+
);
}
- render() {
+ public render() {
return (
Sections
- Name
- State
+ #
+ Name
+ State
@@ -44,26 +55,28 @@ class SectionTable extends React.PureComponent<{ sections: Section[] }, void> {
@observer
class ProgramTable extends React.PureComponent<{ programs: Program[] }, void> {
- static renderRow(program: Program, i: number) {
+ private static renderRow(program: Program, i: number) {
const { name, running } = program;
return (
- Program {name}
- running: {running + ""}
+ {"" + (i + 1)}
+ {name}
+ {running ? "Running" : "Not running"}
);
}
- render() {
+ public render() {
return (
-
+
Programs
- Name
- Running
+ #
+ Name
+ Running
@@ -76,9 +89,20 @@ class ProgramTable extends React.PureComponent<{ programs: Program[] }, void> {
}
}
+const ConnectionState = ({ connected }: { connected: boolean }) =>
+
+
+
+ {connected ? "Connected" : "Disconnected"}
+ ;
+
@observer
class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void> {
- render() {
+ public render() {
const { id, connected, sections, programs } = this.props.device;
return (
-
@@ -86,15 +110,7 @@ class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void>
Device {id}
-
-
-
- {connected ? "Connected" : "Disconnected"}
-
+
@@ -109,7 +125,7 @@ class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void>
@observer
export default class App extends React.PureComponent<{ device: SprinklersDevice }, any> {
- render() {
- return
+ public render() {
+ return ;
}
-}
\ No newline at end of file
+}
diff --git a/app/script/index.tsx b/app/script/index.tsx
index f9a1040..f957fd9 100644
--- a/app/script/index.tsx
+++ b/app/script/index.tsx
@@ -21,5 +21,5 @@ if (module.hot) {
ReactDOM.render(
, rootElem);
- })
-}
\ No newline at end of file
+ });
+}
diff --git a/app/script/mqtt.ts b/app/script/mqtt.ts
index 69044c8..12a3e3e 100644
--- a/app/script/mqtt.ts
+++ b/app/script/mqtt.ts
@@ -1,47 +1,48 @@
-///
import "paho-mqtt/mqttws31";
import MQTT = Paho.MQTT;
import { EventEmitter } from "events";
-import { SprinklersDevice, SprinklersApi, Section, Program } from "./sprinklers";
-
+import * as objectAssign from "object-assign";
+import {
+ SprinklersDevice, SprinklersApi, Section, Program, ProgramItem, Schedule, TimeOfDay, Weekday,
+} from "./sprinklers";
export class MqttApiClient extends EventEmitter implements SprinklersApi {
- client: MQTT.Client
+ private static newClientId() {
+ return "sprinklers3-MqttApiClient-" + Math.round(Math.random() * 1000);
+ }
- connected: boolean
+ public client: MQTT.Client;
- devices: { [prefix: string]: MqttSprinklersDevice } = {};
+ public connected: boolean;
+
+ public devices: { [prefix: string]: MqttSprinklersDevice } = {};
constructor() {
super();
this.client = new MQTT.Client(location.hostname, 1884, MqttApiClient.newClientId());
- this.client.onMessageArrived = m => this.onMessageArrived(m);
- this.client.onConnectionLost = e => this.onConnectionLost(e);
- }
-
- static newClientId() {
- return "sprinklers3-MqttApiClient-" + Math.round(Math.random() * 1000);
+ this.client.onMessageArrived = (m) => this.onMessageArrived(m);
+ this.client.onConnectionLost = (e) => this.onConnectionLost(e);
}
- start() {
+ public start() {
console.log("connecting to mqtt with client id %s", this.client.clientId);
this.client.connect({
onFailure: (e) => {
console.log("mqtt error: ", e.errorMessage);
},
onSuccess: () => {
- console.log("mqtt connected")
+ console.log("mqtt connected");
this.connected = true;
- for (const prefix in this.devices) {
+ for (const prefix of Object.keys(this.devices)) {
const device = this.devices[prefix];
device.doSubscribe();
}
- }
- })
+ },
+ });
}
- getDevice(prefix: string): SprinklersDevice {
+ public getDevice(prefix: string): SprinklersDevice {
if (/\//.test(prefix)) {
throw new Error("Prefix cannot contain a /");
}
@@ -54,16 +55,18 @@ export class MqttApiClient extends EventEmitter implements SprinklersApi {
return this.devices[prefix];
}
- removeDevice(prefix: string) {
+ public removeDevice(prefix: string) {
const device = this.devices[prefix];
- if (!device) return;
+ if (!device) {
+ return;
+ }
device.doUnsubscribe();
delete this.devices[prefix];
}
private onMessageArrived(m: MQTT.Message) {
// console.log("message arrived: ", m)
- const topicIdx = m.destinationName.indexOf('/'); // find the first /
+ const topicIdx = m.destinationName.indexOf("/"); // find the first /
const prefix = m.destinationName.substr(0, topicIdx); // assume prefix does not contain a /
const topic = m.destinationName.substr(topicIdx + 1);
const device = this.devices[prefix];
@@ -80,8 +83,8 @@ export class MqttApiClient extends EventEmitter implements SprinklersApi {
}
class MqttSprinklersDevice extends SprinklersDevice {
- readonly apiClient: MqttApiClient;
- readonly prefix: string;
+ public readonly apiClient: MqttApiClient;
+ public readonly prefix: string;
constructor(apiClient: MqttApiClient, prefix: string) {
super();
@@ -89,27 +92,16 @@ class MqttSprinklersDevice extends SprinklersDevice {
this.prefix = prefix;
}
- private getSubscriptions() {
- return [
- `${this.prefix}/connected`,
- `${this.prefix}/sections`,
- `${this.prefix}/sections/+/#`,
- `${this.prefix}/programs`,
- `${this.prefix}/programs/+/#`
- ];
- }
-
- doSubscribe() {
+ public doSubscribe() {
const c = this.apiClient.client;
this.getSubscriptions()
- .forEach(filter => c.subscribe(filter, { qos: 1 }));
-
+ .forEach((filter) => c.subscribe(filter, { qos: 1 }));
}
- doUnsubscribe() {
+ public doUnsubscribe() {
const c = this.apiClient.client;
this.getSubscriptions()
- .forEach(filter => c.unsubscribe(filter));
+ .forEach((filter) => c.unsubscribe(filter));
}
/**
@@ -117,78 +109,119 @@ class MqttSprinklersDevice extends SprinklersDevice {
* @param topic The topic, with prefix removed
* @param payload The payload string
*/
- onMessage(topic: string, payload: string) {
- var matches;
- if (topic == "connected") {
- this.connected = (payload == "true");
+ public onMessage(topic: string, payload: string) {
+ if (topic === "connected") {
+ this.connected = (payload === "true");
// console.log(`MqttSprinklersDevice with prefix ${this.prefix}: ${this.connected}`)
- } else if ((matches = topic.match(/^sections(?:\/(\d+)(?:\/?(.+))?)?$/)) != null) {
- const [topic, secStr, subTopic] = matches;
+ return;
+ }
+ let matches = topic.match(/^sections(?:\/(\d+)(?:\/?(.+))?)?$/);
+ if (matches != null) {
+ const [_topic, secStr, subTopic] = matches;
// console.log(`section: ${secStr}, topic: ${subTopic}, payload: ${payload}`);
if (!secStr) { // new number of sections
this.sections = new Array(Number(payload));
} else {
const secNum = Number(secStr);
- var section = this.sections[secNum];
+ let section = this.sections[secNum];
if (!section) {
this.sections[secNum] = section = new MqttSection();
}
(section as MqttSection).onMessage(subTopic, payload);
}
- } else if ((matches = topic.match(/^programs(?:\/(\d+)(?:\/?(.+))?)?$/)) != null) {
- const [topic, progStr, subTopic] = matches;
+ return;
+ }
+ matches = topic.match(/^programs(?:\/(\d+)(?:\/?(.+))?)?$/);
+ if (matches != null) {
+ const [_topic, progStr, subTopic] = matches;
// console.log(`program: ${progStr}, topic: ${subTopic}, payload: ${payload}`);
if (!progStr) { // new number of programs
this.programs = new Array(Number(payload));
} else {
const progNum = Number(progStr);
- var program = this.programs[progNum];
+ let program = this.programs[progNum];
if (!program) {
this.programs[progNum] = program = new MqttProgram();
}
(program as MqttProgram).onMessage(subTopic, payload);
}
} else {
- console.warn(`MqttSprinklersDevice recieved invalid topic: ${topic}`)
+ console.warn(`MqttSprinklersDevice recieved invalid topic: ${topic}`);
}
}
get id(): string {
return this.prefix;
}
+
+ private getSubscriptions() {
+ return [
+ `${this.prefix}/connected`,
+ `${this.prefix}/sections`,
+ `${this.prefix}/sections/+/#`,
+ `${this.prefix}/programs`,
+ `${this.prefix}/programs/+/#`,
+ ];
+ }
}
-interface SectionJSON {
+interface ISectionJSON {
name: string;
pin: number;
}
class MqttSection extends Section {
- onMessage(topic: string, payload: string) {
- if (topic == "state") {
- this.state = (payload == "true");
+ public onMessage(topic: string, payload: string) {
+ if (topic === "state") {
+ this.state = (payload === "true");
} else if (topic == null) {
- const json = JSON.parse(payload) as SectionJSON;
+ const json = JSON.parse(payload) as ISectionJSON;
this.name = json.name;
}
}
}
-interface ProgramJSON {
+interface IScheduleJSON {
+ times: TimeOfDay[];
+ weekdays: number[];
+ from?: string;
+ to?: string;
+}
+
+function scheduleFromJSON(json: IScheduleJSON): Schedule {
+ const sched = new Schedule();
+ sched.times = json.times;
+ sched.weekdays = json.weekdays;
+ sched.from = json.from == null ? null : new Date(json.from);
+ sched.to = json.to == null ? null : new Date(json.to);
+ return sched;
+}
+
+interface IProgramJSON {
name: string;
enabled: boolean;
- // sequence: Array;
- // sched: Schedule;
+ sequence: ProgramItem[];
+ sched: IScheduleJSON;
}
class MqttProgram extends Program {
- onMessage(topic: string, payload: string) {
- if (topic == "running") {
- this.running = (payload == "true");
+ public onMessage(topic: string, payload: string) {
+ if (topic === "running") {
+ this.running = (payload === "true");
} else if (topic == null) {
- const json = JSON.parse(payload) as Partial;
- this.name = json.name;
- this.enabled = json.enabled;
+ const json = JSON.parse(payload) as Partial;
+ if (json.name != null) {
+ this.name = json.name;
+ }
+ if (json.enabled != null) {
+ this.enabled = json.enabled;
+ }
+ if (json.sequence != null) {
+ this.sequence = json.sequence;
+ }
+ if (json.sched != null) {
+ this.schedule = scheduleFromJSON(json.sched);
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/app/script/paho-mqtt.d.ts b/app/script/paho-mqtt.d.ts
index 6581e1c..0fcf695 100644
--- a/app/script/paho-mqtt.d.ts
+++ b/app/script/paho-mqtt.d.ts
@@ -1,9 +1,11 @@
+/* tslint:disable:interface-name */
+
declare namespace Paho {
namespace MQTT {
- interface MQTTError { errorCode: string, errorMessage: string }
- interface WithInvocationContext { invocationContext: object }
+ interface MQTTError { errorCode: string; errorMessage: string; }
+ interface WithInvocationContext { invocationContext: object; }
interface ErrorWithInvocationContext extends MQTTError, WithInvocationContext {}
- interface OnSubscribeSuccessParams extends WithInvocationContext { grantedQos: number }
+ interface OnSubscribeSuccessParams extends WithInvocationContext { grantedQos: number; }
type OnConnectionLostHandler = (error: MQTTError) => void;
type OnMessageHandler = (message: Message) => void;
interface ConnectionOptions {
@@ -18,8 +20,8 @@ declare namespace Paho {
onSuccess?: (o: WithInvocationContext) => void;
mqttVersion?: number;
onFailure?: (e: ErrorWithInvocationContext) => void;
- hosts?: Array;
- ports?: Array;
+ hosts?: string[];
+ ports?: number[];
}
interface SubscribeOptions {
qos?: number;
@@ -35,41 +37,41 @@ declare namespace Paho {
timeout?: number;
}
class Client {
+ public readonly clientId: string;
+ public readonly host: string;
+ public readonly path: string;
+ public readonly port: number;
+
+ public onConnectionLost: OnConnectionLostHandler;
+ public onMessageArrived: OnMessageHandler;
+ public onMessageDelivered: OnMessageHandler;
+ // tslint:disable unified-signatures
constructor(host: string, port: number, path: string, clientId: string);
constructor(host: string, port: number, clientId: string);
constructor(hostUri: string, clientId: string);
- readonly clientId: string;
- readonly host: string;
- readonly path: string;
- readonly port: number;
-
- onConnectionLost: OnConnectionLostHandler;
- onMessageArrived: OnMessageHandler;
- onMessageDelivered: OnMessageHandler;
-
- connect(connectionOptions?: ConnectionOptions);
- disconnect();
+ public connect(connectionOptions?: ConnectionOptions);
+ public disconnect();
- getTraceLog(): Object[];
- startTrace();
- stopTrace();
+ public getTraceLog(): object[];
+ public startTrace();
+ public stopTrace();
- send(message: Message);
- subscribe(filter: string, subcribeOptions?: SubscribeOptions);
- unsubscribe(filter: string, unsubcribeOptions?: UnsubscribeOptions);
+ public send(message: Message);
+ public subscribe(filter: string, subcribeOptions?: SubscribeOptions);
+ public unsubscribe(filter: string, unsubcribeOptions?: UnsubscribeOptions);
}
class Message {
- constructor(payload: String | ArrayBuffer);
+ public destinationName: string;
+ public readonly duplicate: boolean;
+ public readonly payloadBytes: ArrayBuffer;
+ public readonly payloadString: string;
+ public qos: number;
+ public retained: boolean;
- destinationName: string;
- readonly duplicate: boolean;
- readonly payloadBytes: ArrayBuffer;
- readonly payloadString: string;
- qos: number;
- retained: boolean;
+ constructor(payload: string | ArrayBuffer);
}
}
}
diff --git a/app/script/sprinklers.ts b/app/script/sprinklers.ts
index c5c7692..c63f43f 100644
--- a/app/script/sprinklers.ts
+++ b/app/script/sprinklers.ts
@@ -2,69 +2,69 @@ import { observable } from "mobx";
export class Section {
@observable
- name: string = ""
+ public name: string = "";
@observable
- state: boolean = false
+ public state: boolean = false;
}
-class TimeOfDay {
- hour: number
- minute: number
- second: number
- millisecond: number
-
+export interface ITimeOfDay {
+ hour: number;
+ minute: number;
+ second: number;
+ millisecond: number;
}
-enum Weekday {
- Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
+export enum Weekday {
+ Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday,
}
-class Schedule {
- times: TimeOfDay[] = [];
- weekdays: Weekday[] = [];
- from?: Date = null;
- to?: Date = null;
+export class Schedule {
+ public times: ITimeOfDay[] = [];
+ public weekdays: Weekday[] = [];
+ public from?: Date = null;
+ public to?: Date = null;
}
-class ProgramItem {
- section: number = -1;
- // duration in milliseconds
- duration: number = 0;
+export interface IProgramItem {
+ // the section number
+ section: number;
+ // duration in seconds
+ duration: number;
}
export class Program {
@observable
- name: string = ""
+ public name: string = "";
@observable
- enabled: boolean = false
+ public enabled: boolean = false;
@observable
- schedule: Schedule = new Schedule()
+ public schedule: Schedule = new Schedule();
@observable
- sequence: Array = [];
+ public sequence: IProgramItem[] = [];
@observable
- running: boolean = false;
+ public running: boolean = false;
}
export abstract class SprinklersDevice {
@observable
- connected: boolean = false;
+ public connected: boolean = false;
@observable
- sections: Array = [];
+ public sections: Section[] = [];
@observable
- programs: Array = [];
+ public programs: Program[] = [];
abstract get id(): string;
}
-export interface SprinklersApi {
+export interface ISprinklersApi {
start();
- getDevice(id: string) : SprinklersDevice;
+ getDevice(id: string): SprinklersDevice;
- removeDevice(id: string)
-}
\ No newline at end of file
+ removeDevice(id: string);
+}
diff --git a/app/style/app.css b/app/style/app.css
index f6c6571..3ce7729 100644
--- a/app/style/app.css
+++ b/app/style/app.css
@@ -1,19 +1,36 @@
-.device--connectedState-connected {
+.device--connectionState {
+ margin-left: 10px;
+ font-size: 18px;
+ font-weight: 100;
+}
+
+.device--connectionState-connected {
color: #13D213;
}
-.device--connectedState-disconnected {
+.device--connectionState-disconnected {
color: #D20000;
}
-.section--name {
- width: 200px;
+.section--number,
+.program--number {
+ width: 20px
+}
+
+.section--name,
+.program--name {
+ width: 150px;
+ white-space: nowrap;
}
.section--state {
}
-.program--name {
- width: 200px;
+.section--state-true {
+ color: green;
+}
+
+.section--state-false {
+
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 555da75..f8d2f31 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,8 @@
"test": "echo \"Error: no test specified\" && exit 1",
"clean": "rm -rf ./dist ./build",
"build": "webpack --config ./webpack/prod.config.js",
- "start:dev": "webpack-dev-server --config ./webpack/dev.config.js"
+ "start:dev": "webpack-dev-server --config ./webpack/dev.config.js",
+ "lint": "tslint app/script/**/* || :"
},
"repository": {
"type": "git",
@@ -23,6 +24,7 @@
"dependencies": {
"@types/classnames": "0.0.32",
"@types/node": "^7.0.13",
+ "@types/object-assign": "^4.0.30",
"@types/react": "^15.0.23",
"@types/react-dom": "^15.5.0",
"@types/react-fontawesome": "^1.5.0",
@@ -30,12 +32,13 @@
"font-awesome": "^4.7.0",
"mobx": "^3.1.9",
"mobx-react": "^4.1.8",
+ "object-assign": "^4.1.1",
"paho-mqtt": "^1.0.3",
"react": "^15.5.4",
"react-dom": "^15.5.4",
"react-fontawesome": "^1.6.1",
"semantic-ui-css": "^2.2.10",
- "semantic-ui-react": "^0.67.0"
+ "semantic-ui-react": "^0.67.2"
},
"devDependencies": {
"@types/webpack-env": "^1.13.0",
diff --git a/tsconfig.json b/tsconfig.json
index f1b25c0..9013f45 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,6 +8,7 @@
},
"files": [
"./node_modules/@types/webpack-env/index.d.ts",
+ "./app/script/paho-mqtt.d.ts",
"./app/script/index.tsx"
]
}
\ No newline at end of file
diff --git a/tslint.json b/tslint.json
new file mode 100644
index 0000000..e1a4e5d
--- /dev/null
+++ b/tslint.json
@@ -0,0 +1,25 @@
+{
+ "defaultSeverity": "error",
+ "extends": [
+ "tslint:recommended"
+ ],
+ "jsRules": {},
+ "rules": {
+ "no-console": [
+ false
+ ],
+ "max-classes-per-file": [
+ false
+ ],
+ "ordered-imports": [
+ false
+ ],
+ "variable-name": [
+ "allow-leading-underscore"
+ ],
+ "no-namespace": [
+ "allow-declarations"
+ ]
+ },
+ "rulesDirectory": []
+}
\ No newline at end of file