Browse Source
Added mobx, react, an mqttws. So far only displays connected status for 1 sprinklers device.update-deps
Alex Mikhalev
8 years ago
14 changed files with 436 additions and 3 deletions
@ -1,2 +1,3 @@ |
|||||||
/node_modules |
/node_modules |
||||||
npm-debug* |
npm-debug* |
||||||
|
/dist |
@ -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" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -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"] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
After Width: | Height: | Size: 216 KiB |
@ -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> |
||||||
|
} |
||||||
|
} |
@ -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); |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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) |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
.device--connected { |
||||||
|
color: #00FF00; |
||||||
|
} |
||||||
|
|
||||||
|
.device--disconnected { |
||||||
|
color: #FF0000; |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"sourceMap": true, |
||||||
|
"jsx": "react", |
||||||
|
"experimentalDecorators": true, |
||||||
|
"target": "es5", |
||||||
|
"typeRoots": ["node_modules/@types"] |
||||||
|
} |
||||||
|
} |
@ -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…
Reference in new issue