diff --git a/app/script/App.tsx b/app/script/App.tsx
index 9d1cb09..6d61a88 100644
--- a/app/script/App.tsx
+++ b/app/script/App.tsx
@@ -1,30 +1,62 @@
import * as React from "react";
import { observer } from "mobx-react";
-import { SprinklersDevice } from "./sprinklers";
-import "semantic-ui-css/semantic.min.css";
-import "app/style/app.css";
-import { Item } from "semantic-ui-react";
+import { SprinklersDevice, Section } from "./sprinklers";
+import { Item, Table, Header } 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";
+
@observer
-class Device extends React.Component<{ device: SprinklersDevice }, any> {
+class SectionRow extends React.PureComponent<{ section: Section }, void> {
render() {
- const {id, connected} = this.props.device;
+ const { name, state } = this.props.section;
+ return (
+
+ Section {name}
+ State: {state + ""}
+
+ );
+ }
+}
+
+@observer
+class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void> {
+ render() {
+ const { id, connected, sections } = this.props.device; //src={require("app/images/raspberry_pi.png")}
return (
-
-
+
-
+
-
-
+
+
{connected ? "Connected" : "Disconnected"}
-
+
+
+
+
+
+
+
+ Sections
+
+
+
+ {
+ sections.map((s, i) => )
+ }
+
+
);
@@ -32,8 +64,8 @@ class Device extends React.Component<{ device: SprinklersDevice }, any> {
}
@observer
-export default class App extends React.Component<{ device: SprinklersDevice }, any> {
+export default class App extends React.PureComponent<{ device: SprinklersDevice }, any> {
render() {
- return
+ return
}
}
\ No newline at end of file
diff --git a/app/script/index.tsx b/app/script/index.tsx
index 2f312a8..0879362 100644
--- a/app/script/index.tsx
+++ b/app/script/index.tsx
@@ -1,14 +1,26 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
+import { AppContainer } from "react-hot-loader";
import App from "./App";
-import {MqttApiClient} from "./mqtt";
+import { MqttApiClient } from "./mqtt";
const client = new MqttApiClient();
client.start();
const device = client.getDevice("grinklers");
-const container = document.createElement("div");
-document.body.appendChild(container);
+const rootElem = document.createElement("div");
+document.body.appendChild(rootElem);
-ReactDOM.render(, container);
\ No newline at end of file
+ReactDOM.render(
+
+, rootElem);
+
+if (module.hot) {
+ module.hot.accept("./App", () => {
+ const NextApp = require("./App").default;
+ ReactDOM.render(
+
+ , rootElem);
+ })
+}
\ No newline at end of file
diff --git a/app/script/mqtt.ts b/app/script/mqtt.ts
index 6cc0e9c..5fe6189 100644
--- a/app/script/mqtt.ts
+++ b/app/script/mqtt.ts
@@ -3,7 +3,7 @@ import "paho-mqtt/mqttws31";
import MQTT = Paho.MQTT;
import { EventEmitter } from "events";
-import { SprinklersDevice, SprinklersApi } from "./sprinklers";
+import { SprinklersDevice, SprinklersApi, Section } from "./sprinklers";
export class MqttApiClient extends EventEmitter implements SprinklersApi {
@@ -42,6 +42,9 @@ export class MqttApiClient extends EventEmitter implements SprinklersApi {
}
getDevice(prefix: string): SprinklersDevice {
+ if (/\//.test(prefix)) {
+ throw new Error("Prefix cannot contain a /");
+ }
if (!this.devices[prefix]) {
const device = this.devices[prefix] = new MqttSprinklersDevice(this, prefix);
if (this.connected) {
@@ -60,10 +63,15 @@ export class MqttApiClient extends EventEmitter implements SprinklersApi {
private onMessageArrived(m: MQTT.Message) {
// console.log("message arrived: ", m)
- for (const prefix in this.devices) {
- const device = this.devices[prefix];
- device.onMessage(m);
+ 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];
+ if (!device) {
+ console.warn(`recieved message for unknown device. prefix: ${prefix}`);
+ return;
}
+ device.onMessage(topic, m.payloadString);
}
private onConnectionLost(e: MQTT.MQTTError) {
@@ -83,15 +91,17 @@ class MqttSprinklersDevice extends SprinklersDevice {
private getSubscriptions() {
return [
- `${this.prefix}/connected`
+ `${this.prefix}/connected`,
+ `${this.prefix}/sections`,
+ `${this.prefix}/sections/+/#`
];
- }
+ }
doSubscribe() {
const c = this.apiClient.client;
this.getSubscriptions()
.forEach(filter => c.subscribe(filter, { qos: 1 }));
-
+
}
doUnsubscribe() {
@@ -100,21 +110,51 @@ class MqttSprinklersDevice extends SprinklersDevice {
.forEach(filter => c.unsubscribe(filter));
}
- onMessage(m: MQTT.Message) {
- const postfix = m.destinationName.replace(`${this.prefix}/`, "");
- if (postfix === m.destinationName)
- return;
- switch (postfix) {
- case "connected":
- this.connected = (m.payloadString == "true");
- console.log(`MqttSprinklersDevice with prefix ${this.prefix}: ${this.connected}`)
- break;
- default:
- console.warn(`MqttSprinklersDevice recieved invalid message`, m)
+ /**
+ * Updates this device with the specified message
+ * @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");
+ // console.log(`MqttSprinklersDevice with prefix ${this.prefix}: ${this.connected}`)
+ } else if ((matches = topic.match(/^sections(?:\/(\d+)(?:\/?(.+))?)?$/)) != 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];
+ if (!section) {
+ this.sections[secNum] = section = new MqttSection();
+ }
+ (section as MqttSection).onMessage(subTopic, payload);
+ }
+ } else {
+ console.warn(`MqttSprinklersDevice recieved invalid topic: ${topic}`)
}
}
get id(): string {
return this.prefix;
}
+}
+
+interface SectionJSON {
+ name: string;
+ pin: number;
+}
+
+class MqttSection extends Section {
+ onMessage(topic: string, payload: string) {
+ if (topic == "state") {
+ this.state = (payload == "true");
+ } else if (topic == null) {
+ const json = JSON.parse(payload) as SectionJSON;
+ this.name = json.name;
+ }
+ }
}
\ No newline at end of file
diff --git a/app/script/sprinklers.ts b/app/script/sprinklers.ts
index 12bc8dd..dfe83be 100644
--- a/app/script/sprinklers.ts
+++ b/app/script/sprinklers.ts
@@ -1,6 +1,6 @@
import { observable } from "mobx";
-class Section {
+export class Section {
@observable
name: string = ""
diff --git a/app/style/app.css b/app/style/app.css
index e827e6d..26eda8d 100644
--- a/app/style/app.css
+++ b/app/style/app.css
@@ -1,7 +1,15 @@
-.device--connected {
- color: #00FF00;
+.device--connectedState-connected {
+ color: #13D213;
}
-.device--disconnected {
- color: #FF0000;
+.device--connectedState-disconnected {
+ color: #D20000;
+}
+
+.section--name {
+ width: 200px;
+}
+
+.section--state {
+
}
\ No newline at end of file
diff --git a/package.json b/package.json
index b7af761..0fcc631 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"test": "echo \"Error: no test specified\" && exit 1",
"clean": "rm -rf ./dist",
"build": "webpack",
- "start:dev": "webpack-dev-server --content-base ./dist/"
+ "start:dev": "webpack-dev-server"
},
"repository": {
"type": "git",
@@ -25,20 +25,25 @@
"@types/node": "^7.0.13",
"@types/react": "^15.0.23",
"@types/react-dom": "^15.5.0",
+ "@types/react-fontawesome": "^1.5.0",
"classnames": "^2.2.5",
+ "font-awesome": "^4.7.0",
"mobx": "^3.1.9",
"mobx-react": "^4.1.8",
"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"
},
"devDependencies": {
+ "@types/webpack-env": "^1.13.0",
"awesome-typescript-loader": "^3.1.3",
"css-loader": "^0.28.0",
"file-loader": "^0.11.1",
"html-webpack-plugin": "^2.28.0",
+ "react-hot-loader": "^3.0.0-beta.6",
"source-map-loader": "^0.2.1",
"style-loader": "^0.17.0",
"ts-loader": "^2.0.3",
diff --git a/tsconfig.json b/tsconfig.json
index a3d3274..f1b25c0 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,5 +5,9 @@
"experimentalDecorators": true,
"target": "es5",
"typeRoots": ["node_modules/@types"]
- }
+ },
+ "files": [
+ "./node_modules/@types/webpack-env/index.d.ts",
+ "./app/script/index.tsx"
+ ]
}
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
index c65637c..a4b0b6a 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -3,7 +3,10 @@ const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
- entry: "./app/script/index.tsx",
+ entry: [
+ "react-hot-loader/patch",
+ "./app/script/index.tsx"
+ ],
devtool: "sourcemap",
output: {
path: path.resolve(__dirname, "dist"),
@@ -17,7 +20,7 @@ module.exports = {
},
module: {
rules: [
- { test: /\.tsx?$/, loader: "awesome-typescript-loader" },
+ { test: /\.tsx?$/, loaders: ["react-hot-loader/webpack", "awesome-typescript-loader"] },
{ test: /\.css$/, loader: "style-loader!css-loader" },
{ test: /\.(ttf|eot|svg|woff(2)?|png|jpg)(\?[a-z0-9=&.]+)?$/, loader: "file-loader" }
]
@@ -25,6 +28,16 @@ module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: "sprinklers3"
- })
- ]
+ }),
+ new webpack.NamedModulesPlugin(),
+ new webpack.HotModuleReplacementPlugin()
+ ],
+ externals: {
+ "react": "React",
+ "react-dom": "ReactDOM"
+ },
+ devServer: {
+ hot: true,
+ contentBase: "./dist"
+ }
};
\ No newline at end of file