UNPKG

homebridge-econet-rheem

Version:

Homebridge plugin for control of Rheem and Ruud thermostats and water heaters

198 lines 7.97 kB
import { createHash } from 'crypto'; import mqtt from 'mqtt'; import storage from 'node-persist'; import tls from 'tls'; import { AuthType } from './auth.js'; import { MQTTKey } from './enums.js'; import { EconetApi } from './http.js'; import { strings } from '../i18n/i18n.js'; import { CLEARBLADE_CERT_INTERMEDIATE, CLEARBLADE_CERT_ROOT, CLEARBLADE_HOST, CLEARBLADE_KEY, PLATFORM_NAME } from '../homebridge/settings.js'; import { LogType } from '../tools/log.js'; import { MINUTE, SECOND } from '../tools/time.js'; const BROKER_URL = `mqtts://${CLEARBLADE_HOST}:1884`; const TOPIC_BASE_USER = 'user/%s/device/'; const TOPIC_BASE_DEVICE = 'device/%s/%s/4736/'; const KEEPALIVE = 30; const DELAYS = [5 * SECOND, 10 * SECOND, 30 * SECOND, 2 * MINUTE, 5 * MINUTE]; const IDLE_CONNECTION_TIMER_INTERVAL = 16 * MINUTE; export class MQTT { dependency; options; static INSTANCES = new Map(); static connect(dependency, listener) { let username; let clientId; let topic; switch (dependency.auth.type) { case AuthType.DEVICE: username = dependency.auth.token; topic = TOPIC_BASE_DEVICE.replace('%s', dependency.macAddress).replace('%s', dependency.serialNumber); break; case AuthType.USER: username = dependency.auth.token; clientId = `${dependency.email}${Date.now().toString().replace('.', '').slice(0, 13)}_android`; topic = TOPIC_BASE_USER.replace('%s', dependency.auth.accountId); break; } const options = { username, clientId, password: CLEARBLADE_KEY, ca: [CLEARBLADE_CERT_ROOT, CLEARBLADE_CERT_INTERMEDIATE, ...tls.rootCertificates], rejectUnauthorized: true, keepalive: KEEPALIVE, reconnectPeriod: 0, }; const seed = `${username}|${clientId ?? ''}}`; const id = createHash('sha256').update(seed).digest('hex').slice(0, 32).replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, '$1-$2-$3-$4-$5'); const shortId = id.slice(0, 4); let instance = MQTT.INSTANCES.get(id); if (instance !== undefined) { dependency.log.ifDebug(strings.mqtt.reuse, dependency.parentName, shortId); } else { dependency.log.ifDebug(strings.mqtt.new, dependency.parentName, shortId); instance = new MQTT(dependency, options, topic); MQTT.INSTANCES.set(id, instance); instance.connect(); } instance.listeners.push(listener); return instance; } log; client = undefined; idleTimer; shouldReconnect = false; isReconnecting = false; reconnectCount = 0; listeners = []; topicDesired; topicReported; constructor(dependency, options, topicBase) { this.dependency = dependency; this.options = options; this.log = dependency.log; this.topicDesired = topicBase + 'desired'; this.topicReported = topicBase + 'reported'; } connect() { this.shouldReconnect = true; this.client = mqtt.connect(BROKER_URL, this.options); this.client.on('connect', () => { this.log.ifDebug(strings.mqtt.connected, this.dependency.parentName); this.client?.subscribe(this.topicReported); }); this.client.on('message', (topic, message) => this.messageReceived(topic, message.toString())); this.client.on('close', () => { this.log.ifDebug(strings.mqtt.disconnected, this.dependency.parentName); this.reconnect(); }); this.client.on('error', (error) => { if (error.code !== 'ENOTFOUND') { this.log.warning(`${strings.mqtt.error}: ${error}`, this.dependency.parentName); } }); } teardown() { this.shouldReconnect = false; this.client?.end(true); this.client = undefined; } messageReceived(topic, message) { this.log.ifDebug(`${this.dependency.parentName} ${this.messageReceived.name}() - ${topic}\n${message}`); this.reconnectCount = 0; this.resetIdleTimer(); try { const parsed = JSON.parse(message); for (const listener of this.listeners) { listener.mqttMessageReceived(parsed); } if (this.dependency.debug) { this.saveData(parsed); } } catch (e) { this.log.error(strings.mqtt.parseFailed, this.dependency.parentName, `- ${topic}\n${message}`); } } publish(payload) { if (!this.client || !this.client.connected) { this.log.error(strings.mqtt.notConnected, this.dependency.parentName); return; } const dateTime = new Date().toISOString().replace(/\.\d{3}Z$/, ''); const transactionId = `ANDROID_${dateTime}`; const data = { transactionId, ...payload, }; const message = JSON.stringify(data); this.client.publish(this.topicDesired, message); this.log.ifDebug(`${this.dependency.parentName} ${this.publish.name}() — ${this.topicDesired} ${message}`); return; } async reconnect() { if (!this.shouldReconnect || this.isReconnecting) { return; } this.isReconnecting = true; if (this.client) { this.client.end(true); this.client = undefined; } this.reconnectCount++; if (this.reconnectCount % DELAYS.length === 0) { try { this.log.ifDebug(LogType.WARNING, strings.mqtt.unstable, this.dependency.parentName); await EconetApi.authenticateUser(); } catch (error) { this.log.ifDebug(LogType.ERROR, strings.http.reauthFailed, this.dependency.parentName, JSON.stringify(error)); } } const reconnectDelay = DELAYS[Math.min(this.reconnectCount, DELAYS.length - 1)]; if (reconnectDelay < MINUTE) { this.log.ifDebug(strings.mqtt.reconnectSeconds, this.dependency.parentName, reconnectDelay / SECOND); } else { this.log.ifDebug(strings.mqtt.reconnectMinutes, this.dependency.parentName, reconnectDelay / MINUTE); } setTimeout(() => { this.isReconnecting = false; this.connect(); }, reconnectDelay); } resetIdleTimer() { clearTimeout(this.idleTimer); this.idleTimer = setTimeout(() => { this.log.ifDebug(LogType.WARNING, strings.mqtt.idleConnection, this.dependency.parentName); this.reconnect(); }, IDLE_CONNECTION_TIMER_INTERVAL); } static KNOWN_KEYS = [...Object.keys(MQTTKey), 'device_name', 'serial_number', 'transactionId', '@SIGNAL']; static STORAGE_KEY = `${PLATFORM_NAME}_MQTT`; async saveData(data) { const objectString = await storage.get(MQTT.STORAGE_KEY); const valuesObject = objectString ? JSON.parse(objectString) : {}; let changed = false; for (const [key, valueOrObject] of Object.entries(data)) { if (MQTT.KNOWN_KEYS.includes(key)) { continue; } let valuesArray = valuesObject[key] ?? []; while (valuesArray.length > 4) { valuesArray.shift(); } const value = typeof valueOrObject === 'object' ? JSON.stringify(valueOrObject) : valueOrObject; const valuesSet = new Set(valuesArray); valuesSet.add(value); valuesArray = Array.from(valuesSet); valuesObject[key] = valuesArray; changed = true; } if (changed) { storage.set(MQTT.STORAGE_KEY, JSON.stringify(valuesObject)); } } } //# sourceMappingURL=mqtt.js.map