diff --git a/.gitignore b/.gitignore
index 54c353b..dadcda3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/node_modules
-npm-debug*
\ No newline at end of file
+npm-debug*
+/dist
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..b7e67d1
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,14 @@
+{
+ // Use IntelliSense to learn about possible Node.js debug attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Launch Program",
+ "program": "${workspaceRoot}/bin/www"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..c2fe27d
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,27 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "0.1.0",
+ "command": "npm",
+ "isShellCommand": true,
+ "showOutput": "always",
+ "suppressTaskName": true,
+ "tasks": [
+ {
+ "taskName": "install",
+ "args": ["install"]
+ },
+ {
+ "taskName": "update",
+ "args": ["update"]
+ },
+ {
+ "taskName": "start:dev",
+ "args": ["run", "start:dev"]
+ },
+ {
+ "taskName": "test",
+ "args": ["run", "test"]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/images/raspberry_pi.png b/app/images/raspberry_pi.png
new file mode 100644
index 0000000..72a685d
Binary files /dev/null and b/app/images/raspberry_pi.png differ
diff --git a/app/script/App.tsx b/app/script/App.tsx
new file mode 100644
index 0000000..9d1cb09
--- /dev/null
+++ b/app/script/App.tsx
@@ -0,0 +1,39 @@
+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 * as classNames from "classnames";
+
+@observer
+class Device extends React.Component<{ device: SprinklersDevice }, any> {
+ render() {
+ const {id, connected} = this.props.device;
+ return (
+ -
+
+
+
+ Device {id}
+
+
+
+ {connected ? "Connected" : "Disconnected"}
+
+
+
+
+ );
+ }
+}
+
+@observer
+export default class App extends React.Component<{ device: SprinklersDevice }, any> {
+ render() {
+ return
+ }
+}
\ No newline at end of file
diff --git a/app/script/index.tsx b/app/script/index.tsx
new file mode 100644
index 0000000..2f312a8
--- /dev/null
+++ b/app/script/index.tsx
@@ -0,0 +1,14 @@
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+
+import App from "./App";
+import {MqttApiClient} from "./mqtt";
+
+const client = new MqttApiClient();
+client.start();
+const device = client.getDevice("grinklers");
+
+const container = document.createElement("div");
+document.body.appendChild(container);
+
+ReactDOM.render(, container);
\ No newline at end of file
diff --git a/app/script/mqtt.ts b/app/script/mqtt.ts
new file mode 100644
index 0000000..6cc0e9c
--- /dev/null
+++ b/app/script/mqtt.ts
@@ -0,0 +1,120 @@
+import "paho-mqtt/mqttws31";
+///
+import MQTT = Paho.MQTT;
+
+import { EventEmitter } from "events";
+import { SprinklersDevice, SprinklersApi } from "./sprinklers";
+
+
+export class MqttApiClient extends EventEmitter implements SprinklersApi {
+ client: MQTT.Client
+
+ connected: boolean
+
+ devices: { [prefix: string]: MqttSprinklersDevice } = {};
+
+ constructor() {
+ super();
+ this.client = new Paho.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);
+ }
+
+ 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")
+ this.connected = true;
+ for (const prefix in this.devices) {
+ const device = this.devices[prefix];
+ device.doSubscribe();
+ }
+ }
+ })
+ }
+
+ getDevice(prefix: string): SprinklersDevice {
+ if (!this.devices[prefix]) {
+ const device = this.devices[prefix] = new MqttSprinklersDevice(this, prefix);
+ if (this.connected) {
+ device.doSubscribe();
+ }
+ }
+ return this.devices[prefix];
+ }
+
+ removeDevice(prefix: string) {
+ const device = this.devices[prefix];
+ if (!device) return;
+ device.doUnsubscribe();
+ delete this.devices[prefix];
+ }
+
+ private onMessageArrived(m: MQTT.Message) {
+ // console.log("message arrived: ", m)
+ for (const prefix in this.devices) {
+ const device = this.devices[prefix];
+ device.onMessage(m);
+ }
+ }
+
+ private onConnectionLost(e: MQTT.MQTTError) {
+ this.connected = false;
+ }
+}
+
+class MqttSprinklersDevice extends SprinklersDevice {
+ readonly apiClient: MqttApiClient;
+ readonly prefix: string;
+
+ constructor(apiClient: MqttApiClient, prefix: string) {
+ super();
+ this.apiClient = apiClient;
+ this.prefix = prefix;
+ }
+
+ private getSubscriptions() {
+ return [
+ `${this.prefix}/connected`
+ ];
+ }
+
+ doSubscribe() {
+ const c = this.apiClient.client;
+ this.getSubscriptions()
+ .forEach(filter => c.subscribe(filter, { qos: 1 }));
+
+ }
+
+ doUnsubscribe() {
+ const c = this.apiClient.client;
+ this.getSubscriptions()
+ .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)
+ }
+ }
+
+ get id(): string {
+ return this.prefix;
+ }
+}
\ No newline at end of file
diff --git a/app/script/paho-mqtt.d.ts b/app/script/paho-mqtt.d.ts
new file mode 100644
index 0000000..6581e1c
--- /dev/null
+++ b/app/script/paho-mqtt.d.ts
@@ -0,0 +1,75 @@
+declare namespace Paho {
+ namespace MQTT {
+ interface MQTTError { errorCode: string, errorMessage: string }
+ interface WithInvocationContext { invocationContext: object }
+ interface ErrorWithInvocationContext extends MQTTError, WithInvocationContext {}
+ interface OnSubscribeSuccessParams extends WithInvocationContext { grantedQos: number }
+ type OnConnectionLostHandler = (error: MQTTError) => void;
+ type OnMessageHandler = (message: Message) => void;
+ interface ConnectionOptions {
+ timeout?: number;
+ userName?: string;
+ password?: string;
+ willMessage?: Message;
+ keepAliveInterval?: number;
+ cleanSession?: boolean;
+ useSSL?: boolean;
+ invocationContext?: object;
+ onSuccess?: (o: WithInvocationContext) => void;
+ mqttVersion?: number;
+ onFailure?: (e: ErrorWithInvocationContext) => void;
+ hosts?: Array;
+ ports?: Array;
+ }
+ interface SubscribeOptions {
+ qos?: number;
+ invocationContext?: object;
+ onSuccess?: (o: OnSubscribeSuccessParams) => void;
+ onFailure?: (e: ErrorWithInvocationContext) => void;
+ timeout?: number;
+ }
+ interface UnsubscribeOptions {
+ invocationContext?: object;
+ onSuccess?: (o: WithInvocationContext) => void;
+ onFailure?: (e: ErrorWithInvocationContext) => void;
+ timeout?: number;
+ }
+ class Client {
+
+ 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();
+
+ getTraceLog(): Object[];
+ startTrace();
+ stopTrace();
+
+ send(message: Message);
+ subscribe(filter: string, subcribeOptions?: SubscribeOptions);
+ unsubscribe(filter: string, unsubcribeOptions?: UnsubscribeOptions);
+ }
+
+ class Message {
+ constructor(payload: String | ArrayBuffer);
+
+ destinationName: string;
+ readonly duplicate: boolean;
+ readonly payloadBytes: ArrayBuffer;
+ readonly payloadString: string;
+ qos: number;
+ retained: boolean;
+ }
+ }
+}
diff --git a/app/script/sprinklers.ts b/app/script/sprinklers.ts
new file mode 100644
index 0000000..12bc8dd
--- /dev/null
+++ b/app/script/sprinklers.ts
@@ -0,0 +1,67 @@
+import { observable } from "mobx";
+
+class Section {
+ @observable
+ name: string = ""
+
+ @observable
+ state: boolean = false
+}
+
+class TimeOfDay {
+ hour: number
+ minute: number
+ second: number
+ millisecond: number
+
+}
+
+enum Weekday {
+ Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
+}
+
+class Schedule {
+ times: TimeOfDay[] = [];
+ weekdays: Weekday[] = [];
+ from?: Date = null;
+ to?: Date = null;
+}
+
+class ProgramItem {
+ section: number = -1;
+ // duration in milliseconds
+ duration: number = 0;
+}
+
+class Program {
+ @observable
+ name: string = ""
+ @observable
+ enabled: boolean = false
+
+ @observable
+ schedule: Schedule = new Schedule()
+
+ @observable
+ sequence: Array = [];
+}
+
+export abstract class SprinklersDevice {
+ @observable
+ connected: boolean = false;
+
+ @observable
+ sections: Array = [];
+
+ @observable
+ programs: Array = [];
+
+ abstract get id(): string;
+}
+
+export interface SprinklersApi {
+ start();
+ getDevice(id: string) : SprinklersDevice;
+
+ removeDevice(id: string)
+}
\ No newline at end of file
diff --git a/app/style/app.css b/app/style/app.css
new file mode 100644
index 0000000..e827e6d
--- /dev/null
+++ b/app/style/app.css
@@ -0,0 +1,7 @@
+.device--connected {
+ color: #00FF00;
+}
+
+.device--disconnected {
+ color: #FF0000;
+}
\ No newline at end of file
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..a505dbb
--- /dev/null
+++ b/index.js
@@ -0,0 +1 @@
+module.exports = require("./built/")
\ No newline at end of file
diff --git a/package.json b/package.json
index 97ae421..b7af761 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,10 @@
"description": "A frontend for mqtt based IoT sprinklers systems",
"main": "index.js",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "clean": "rm -rf ./dist",
+ "build": "webpack",
+ "start:dev": "webpack-dev-server --content-base ./dist/"
},
"repository": {
"type": "git",
@@ -16,5 +19,31 @@
"bugs": {
"url": "https://github.com/amikhalev/sprinklers3/issues"
},
- "homepage": "https://github.com/amikhalev/sprinklers3#readme"
+ "homepage": "https://github.com/amikhalev/sprinklers3#readme",
+ "dependencies": {
+ "@types/classnames": "0.0.32",
+ "@types/node": "^7.0.13",
+ "@types/react": "^15.0.23",
+ "@types/react-dom": "^15.5.0",
+ "classnames": "^2.2.5",
+ "mobx": "^3.1.9",
+ "mobx-react": "^4.1.8",
+ "paho-mqtt": "^1.0.3",
+ "react": "^15.5.4",
+ "react-dom": "^15.5.4",
+ "semantic-ui-css": "^2.2.10",
+ "semantic-ui-react": "^0.67.0"
+ },
+ "devDependencies": {
+ "awesome-typescript-loader": "^3.1.3",
+ "css-loader": "^0.28.0",
+ "file-loader": "^0.11.1",
+ "html-webpack-plugin": "^2.28.0",
+ "source-map-loader": "^0.2.1",
+ "style-loader": "^0.17.0",
+ "ts-loader": "^2.0.3",
+ "typescript": "^2.3.1",
+ "webpack": "^2.4.1",
+ "webpack-dev-server": "^2.4.4"
+ }
}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..a3d3274
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "sourceMap": true,
+ "jsx": "react",
+ "experimentalDecorators": true,
+ "target": "es5",
+ "typeRoots": ["node_modules/@types"]
+ }
+}
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..c65637c
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,30 @@
+const path = require("path");
+const webpack = require("webpack");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+
+module.exports = {
+ entry: "./app/script/index.tsx",
+ devtool: "sourcemap",
+ output: {
+ path: path.resolve(__dirname, "dist"),
+ filename: "bundle.js"
+ },
+ resolve: {
+ extensions: [".ts", ".tsx", ".js"],
+ alias: {
+ app: path.resolve("./app")
+ }
+ },
+ module: {
+ rules: [
+ { test: /\.tsx?$/, loader: "awesome-typescript-loader" },
+ { test: /\.css$/, loader: "style-loader!css-loader" },
+ { test: /\.(ttf|eot|svg|woff(2)?|png|jpg)(\?[a-z0-9=&.]+)?$/, loader: "file-loader" }
+ ]
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ title: "sprinklers3"
+ })
+ ]
+};
\ No newline at end of file