Added base functionality
Added mobx, react, an mqttws. So far only displays connected status for 1 sprinklers device.
This commit is contained in:
parent
317798c1bd
commit
9148ccb34f
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/node_modules
|
||||
npm-debug*
|
||||
npm-debug*
|
||||
/dist
|
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
27
.vscode/tasks.json
vendored
Normal file
27
.vscode/tasks.json
vendored
Normal file
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
BIN
app/images/raspberry_pi.png
Normal file
BIN
app/images/raspberry_pi.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 216 KiB |
39
app/script/App.tsx
Normal file
39
app/script/App.tsx
Normal file
@ -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 (
|
||||
<Item>
|
||||
<Item.Image src={require("app/images/raspberry_pi.png")} />
|
||||
<Item.Content>
|
||||
<Item.Header>
|
||||
<span>Device </span><kbd>{id}</kbd>
|
||||
</Item.Header>
|
||||
<Item.Meta>
|
||||
<span className={classNames({
|
||||
"device--connected": connected,
|
||||
"device--disconnected": !connected
|
||||
})}>
|
||||
{connected ? "Connected" : "Disconnected"}
|
||||
</span>
|
||||
</Item.Meta>
|
||||
</Item.Content>
|
||||
</Item>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export default class App extends React.Component<{ device: SprinklersDevice }, any> {
|
||||
render() {
|
||||
return <Item.Group divided><Device device={this.props.device} /></Item.Group>
|
||||
}
|
||||
}
|
14
app/script/index.tsx
Normal file
14
app/script/index.tsx
Normal file
@ -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(<App device={device} />, container);
|
120
app/script/mqtt.ts
Normal file
120
app/script/mqtt.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import "paho-mqtt/mqttws31";
|
||||
/// <reference path="./paho-mqtt.d.ts" />
|
||||
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;
|
||||
}
|
||||
}
|
75
app/script/paho-mqtt.d.ts
vendored
Normal file
75
app/script/paho-mqtt.d.ts
vendored
Normal file
@ -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<string>;
|
||||
ports?: Array<number>;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
67
app/script/sprinklers.ts
Normal file
67
app/script/sprinklers.ts
Normal file
@ -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<ProgramItem> = [];
|
||||
}
|
||||
|
||||
export abstract class SprinklersDevice {
|
||||
@observable
|
||||
connected: boolean = false;
|
||||
|
||||
@observable
|
||||
sections: Array<Section> = [];
|
||||
|
||||
@observable
|
||||
programs: Array<Program> = [];
|
||||
|
||||
abstract get id(): string;
|
||||
}
|
||||
|
||||
export interface SprinklersApi {
|
||||
start();
|
||||
getDevice(id: string) : SprinklersDevice;
|
||||
|
||||
removeDevice(id: string)
|
||||
}
|
7
app/style/app.css
Normal file
7
app/style/app.css
Normal file
@ -0,0 +1,7 @@
|
||||
.device--connected {
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
.device--disconnected {
|
||||
color: #FF0000;
|
||||
}
|
33
package.json
33
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"
|
||||
}
|
||||
}
|
||||
|
9
tsconfig.json
Normal file
9
tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"jsx": "react",
|
||||
"experimentalDecorators": true,
|
||||
"target": "es5",
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
}
|
||||
}
|
30
webpack.config.js
Normal file
30
webpack.config.js
Normal file
@ -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"
|
||||
})
|
||||
]
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user