Browse Source

Added base functionality

Added mobx, react, an mqttws. So far only displays connected status for
1 sprinklers device.
update-deps
Alex Mikhalev 8 years ago
parent
commit
9148ccb34f
  1. 3
      .gitignore
  2. 14
      .vscode/launch.json
  3. 27
      .vscode/tasks.json
  4. BIN
      app/images/raspberry_pi.png
  5. 39
      app/script/App.tsx
  6. 14
      app/script/index.tsx
  7. 120
      app/script/mqtt.ts
  8. 75
      app/script/paho-mqtt.d.ts
  9. 67
      app/script/sprinklers.ts
  10. 7
      app/style/app.css
  11. 1
      index.js
  12. 33
      package.json
  13. 9
      tsconfig.json
  14. 30
      webpack.config.js

3
.gitignore vendored

@ -1,2 +1,3 @@ @@ -1,2 +1,3 @@
/node_modules
npm-debug*
npm-debug*
/dist

14
.vscode/launch.json vendored

@ -0,0 +1,14 @@ @@ -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

@ -0,0 +1,27 @@ @@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

39
app/script/App.tsx

@ -0,0 +1,39 @@ @@ -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

@ -0,0 +1,14 @@ @@ -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

@ -0,0 +1,120 @@ @@ -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

@ -0,0 +1,75 @@ @@ -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

@ -0,0 +1,67 @@ @@ -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

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
.device--connected {
color: #00FF00;
}
.device--disconnected {
color: #FF0000;
}

1
index.js

@ -0,0 +1 @@ @@ -0,0 +1 @@
module.exports = require("./built/")

33
package.json

@ -5,7 +5,10 @@ @@ -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 @@ @@ -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

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
{
"compilerOptions": {
"sourceMap": true,
"jsx": "react",
"experimentalDecorators": true,
"target": "es5",
"typeRoots": ["node_modules/@types"]
}
}

30
webpack.config.js

@ -0,0 +1,30 @@ @@ -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…
Cancel
Save