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 ( - + - +
Device {id} - - - + +   {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