|
|
|
@ -1,47 +1,48 @@
@@ -1,47 +1,48 @@
|
|
|
|
|
/// <reference path="./paho-mqtt.d.ts" />
|
|
|
|
|
import "paho-mqtt/mqttws31"; |
|
|
|
|
import MQTT = Paho.MQTT; |
|
|
|
|
|
|
|
|
|
import { EventEmitter } from "events"; |
|
|
|
|
import { SprinklersDevice, SprinklersApi, Section, Program } from "./sprinklers"; |
|
|
|
|
|
|
|
|
|
import * as objectAssign from "object-assign"; |
|
|
|
|
import { |
|
|
|
|
SprinklersDevice, SprinklersApi, Section, Program, ProgramItem, Schedule, TimeOfDay, Weekday, |
|
|
|
|
} from "./sprinklers"; |
|
|
|
|
|
|
|
|
|
export class MqttApiClient extends EventEmitter implements SprinklersApi { |
|
|
|
|
client: MQTT.Client |
|
|
|
|
private static newClientId() { |
|
|
|
|
return "sprinklers3-MqttApiClient-" + Math.round(Math.random() * 1000); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
connected: boolean |
|
|
|
|
public client: MQTT.Client; |
|
|
|
|
|
|
|
|
|
devices: { [prefix: string]: MqttSprinklersDevice } = {}; |
|
|
|
|
public connected: boolean; |
|
|
|
|
|
|
|
|
|
public devices: { [prefix: string]: MqttSprinklersDevice } = {}; |
|
|
|
|
|
|
|
|
|
constructor() { |
|
|
|
|
super(); |
|
|
|
|
this.client = new MQTT.Client(location.hostname, 1884, MqttApiClient.newClientId()); |
|
|
|
|
this.client.onMessageArrived = m => this.onMessageArrived(m); |
|
|
|
|
this.client.onConnectionLost = e => this.onConnectionLost(e); |
|
|
|
|
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() { |
|
|
|
|
public 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") |
|
|
|
|
console.log("mqtt connected"); |
|
|
|
|
this.connected = true; |
|
|
|
|
for (const prefix in this.devices) { |
|
|
|
|
for (const prefix of Object.keys(this.devices)) { |
|
|
|
|
const device = this.devices[prefix]; |
|
|
|
|
device.doSubscribe(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
getDevice(prefix: string): SprinklersDevice { |
|
|
|
|
public getDevice(prefix: string): SprinklersDevice { |
|
|
|
|
if (/\//.test(prefix)) { |
|
|
|
|
throw new Error("Prefix cannot contain a /"); |
|
|
|
|
} |
|
|
|
@ -54,16 +55,18 @@ export class MqttApiClient extends EventEmitter implements SprinklersApi {
@@ -54,16 +55,18 @@ export class MqttApiClient extends EventEmitter implements SprinklersApi {
|
|
|
|
|
return this.devices[prefix]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
removeDevice(prefix: string) { |
|
|
|
|
public removeDevice(prefix: string) { |
|
|
|
|
const device = this.devices[prefix]; |
|
|
|
|
if (!device) return; |
|
|
|
|
if (!device) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
device.doUnsubscribe(); |
|
|
|
|
delete this.devices[prefix]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private onMessageArrived(m: MQTT.Message) { |
|
|
|
|
// console.log("message arrived: ", m)
|
|
|
|
|
const topicIdx = m.destinationName.indexOf('/'); // find the first /
|
|
|
|
|
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]; |
|
|
|
@ -80,8 +83,8 @@ export class MqttApiClient extends EventEmitter implements SprinklersApi {
@@ -80,8 +83,8 @@ export class MqttApiClient extends EventEmitter implements SprinklersApi {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class MqttSprinklersDevice extends SprinklersDevice { |
|
|
|
|
readonly apiClient: MqttApiClient; |
|
|
|
|
readonly prefix: string; |
|
|
|
|
public readonly apiClient: MqttApiClient; |
|
|
|
|
public readonly prefix: string; |
|
|
|
|
|
|
|
|
|
constructor(apiClient: MqttApiClient, prefix: string) { |
|
|
|
|
super(); |
|
|
|
@ -89,27 +92,16 @@ class MqttSprinklersDevice extends SprinklersDevice {
@@ -89,27 +92,16 @@ class MqttSprinklersDevice extends SprinklersDevice {
|
|
|
|
|
this.prefix = prefix; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private getSubscriptions() { |
|
|
|
|
return [ |
|
|
|
|
`${this.prefix}/connected`, |
|
|
|
|
`${this.prefix}/sections`, |
|
|
|
|
`${this.prefix}/sections/+/#`, |
|
|
|
|
`${this.prefix}/programs`, |
|
|
|
|
`${this.prefix}/programs/+/#` |
|
|
|
|
]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
doSubscribe() { |
|
|
|
|
public doSubscribe() { |
|
|
|
|
const c = this.apiClient.client; |
|
|
|
|
this.getSubscriptions() |
|
|
|
|
.forEach(filter => c.subscribe(filter, { qos: 1 })); |
|
|
|
|
|
|
|
|
|
.forEach((filter) => c.subscribe(filter, { qos: 1 })); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
doUnsubscribe() { |
|
|
|
|
public doUnsubscribe() { |
|
|
|
|
const c = this.apiClient.client; |
|
|
|
|
this.getSubscriptions() |
|
|
|
|
.forEach(filter => c.unsubscribe(filter)); |
|
|
|
|
.forEach((filter) => c.unsubscribe(filter)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
@ -117,78 +109,119 @@ class MqttSprinklersDevice extends SprinklersDevice {
@@ -117,78 +109,119 @@ class MqttSprinklersDevice extends SprinklersDevice {
|
|
|
|
|
* @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"); |
|
|
|
|
public onMessage(topic: string, payload: string) { |
|
|
|
|
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; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
let matches = topic.match(/^sections(?:\/(\d+)(?:\/?(.+))?)?$/); |
|
|
|
|
if (matches != 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]; |
|
|
|
|
let section = this.sections[secNum]; |
|
|
|
|
if (!section) { |
|
|
|
|
this.sections[secNum] = section = new MqttSection(); |
|
|
|
|
} |
|
|
|
|
(section as MqttSection).onMessage(subTopic, payload); |
|
|
|
|
} |
|
|
|
|
} else if ((matches = topic.match(/^programs(?:\/(\d+)(?:\/?(.+))?)?$/)) != null) { |
|
|
|
|
const [topic, progStr, subTopic] = matches; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
matches = topic.match(/^programs(?:\/(\d+)(?:\/?(.+))?)?$/); |
|
|
|
|
if (matches != null) { |
|
|
|
|
const [_topic, progStr, subTopic] = matches; |
|
|
|
|
// console.log(`program: ${progStr}, topic: ${subTopic}, payload: ${payload}`);
|
|
|
|
|
if (!progStr) { // new number of programs
|
|
|
|
|
this.programs = new Array(Number(payload)); |
|
|
|
|
} else { |
|
|
|
|
const progNum = Number(progStr); |
|
|
|
|
var program = this.programs[progNum]; |
|
|
|
|
let program = this.programs[progNum]; |
|
|
|
|
if (!program) { |
|
|
|
|
this.programs[progNum] = program = new MqttProgram(); |
|
|
|
|
} |
|
|
|
|
(program as MqttProgram).onMessage(subTopic, payload); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
console.warn(`MqttSprinklersDevice recieved invalid topic: ${topic}`) |
|
|
|
|
console.warn(`MqttSprinklersDevice recieved invalid topic: ${topic}`); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
get id(): string { |
|
|
|
|
return this.prefix; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private getSubscriptions() { |
|
|
|
|
return [ |
|
|
|
|
`${this.prefix}/connected`, |
|
|
|
|
`${this.prefix}/sections`, |
|
|
|
|
`${this.prefix}/sections/+/#`, |
|
|
|
|
`${this.prefix}/programs`, |
|
|
|
|
`${this.prefix}/programs/+/#`, |
|
|
|
|
]; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
interface SectionJSON { |
|
|
|
|
interface ISectionJSON { |
|
|
|
|
name: string; |
|
|
|
|
pin: number; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class MqttSection extends Section { |
|
|
|
|
onMessage(topic: string, payload: string) { |
|
|
|
|
if (topic == "state") { |
|
|
|
|
this.state = (payload == "true"); |
|
|
|
|
public onMessage(topic: string, payload: string) { |
|
|
|
|
if (topic === "state") { |
|
|
|
|
this.state = (payload === "true"); |
|
|
|
|
} else if (topic == null) { |
|
|
|
|
const json = JSON.parse(payload) as SectionJSON; |
|
|
|
|
const json = JSON.parse(payload) as ISectionJSON; |
|
|
|
|
this.name = json.name; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
interface ProgramJSON { |
|
|
|
|
interface IScheduleJSON { |
|
|
|
|
times: TimeOfDay[]; |
|
|
|
|
weekdays: number[]; |
|
|
|
|
from?: string; |
|
|
|
|
to?: string; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function scheduleFromJSON(json: IScheduleJSON): Schedule { |
|
|
|
|
const sched = new Schedule(); |
|
|
|
|
sched.times = json.times; |
|
|
|
|
sched.weekdays = json.weekdays; |
|
|
|
|
sched.from = json.from == null ? null : new Date(json.from); |
|
|
|
|
sched.to = json.to == null ? null : new Date(json.to); |
|
|
|
|
return sched; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
interface IProgramJSON { |
|
|
|
|
name: string; |
|
|
|
|
enabled: boolean; |
|
|
|
|
// sequence: Array<ProgramItem>;
|
|
|
|
|
// sched: Schedule;
|
|
|
|
|
sequence: ProgramItem[]; |
|
|
|
|
sched: IScheduleJSON; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class MqttProgram extends Program { |
|
|
|
|
onMessage(topic: string, payload: string) { |
|
|
|
|
if (topic == "running") { |
|
|
|
|
this.running = (payload == "true"); |
|
|
|
|
public onMessage(topic: string, payload: string) { |
|
|
|
|
if (topic === "running") { |
|
|
|
|
this.running = (payload === "true"); |
|
|
|
|
} else if (topic == null) { |
|
|
|
|
const json = JSON.parse(payload) as Partial<ProgramJSON>; |
|
|
|
|
this.name = json.name; |
|
|
|
|
this.enabled = json.enabled; |
|
|
|
|
const json = JSON.parse(payload) as Partial<IProgramJSON>; |
|
|
|
|
if (json.name != null) { |
|
|
|
|
this.name = json.name; |
|
|
|
|
} |
|
|
|
|
if (json.enabled != null) { |
|
|
|
|
this.enabled = json.enabled; |
|
|
|
|
} |
|
|
|
|
if (json.sequence != null) { |
|
|
|
|
this.sequence = json.sequence; |
|
|
|
|
} |
|
|
|
|
if (json.sched != null) { |
|
|
|
|
this.schedule = scheduleFromJSON(json.sched); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |