UNPKG

homebridge-mqtt-ceiling-fan-remote

Version:

A remote using http commands to control a hunter ceiling fan via a tasmotized sonoff rfbridge for homebridge written in Typescript

260 lines 13.7 kB
import { PLATFORM_NAME, PLUGIN_NAME, SCHEMA_VERSION } from './settings.js'; import { CeilingFanRemote } from './platformAccessory.js'; import os from 'os'; import mqtt from 'mqtt'; import DebounceQueue from './debounceQueue.js'; function binaryCommand(remote, command = -1) { const room = `1${remote.padStart(40, '0')}0`; const rfrawCommand = `0${command.toString(2).padStart(10, '0')}1`; const rfrawInverseCommand = rfrawCommand.split('').map(i => Math.abs(parseInt(i) - 1)).join(''); const output = `${room}${rfrawCommand}${rfrawInverseCommand}`; return output; } function hexCommand(remote, command = -1) { const bin = binaryCommand(remote, command); const nibbles = 8 * Math.ceil(bin.length / 8); const _bin = bin.padEnd(nibbles, '0'); let output = ''; for (let i = 0; i < nibbles; i += 8) { const byte = parseInt(_bin.substring(i, i + 8), 2).toString(16).padStart(2, '0'); output = `${output}${byte}`; } return `AAA8${((nibbles / 8) + 1).toString(16).padStart(2, '0')}00${output}55`.toUpperCase(); } const commands = { Brightness: { 0: 266, 12: 10, 13: 10, 25: 11, 37: 12, 38: 12, 50: 13, 62: 14, 63: 14, 75: 15, 87: 72, 88: 72, 100: 74 }, Speed: { 0: 98, 1: 2, 2: 32, 3: 66 } }; /** * HomebridgePlatform * This class is the main constructor for your plugin, this is where you should * parse the user config and discover/register accessories with Homebridge. */ export class CeilingFanRemotePlatform { log; config; api; Service; Characteristic; // this is used to track restored cached accessories accessories = []; rooms = []; remotes = {}; initializedRemotes = []; remoteCommands = {}; mqttClient; rfbridgeResultsTopic = ''; rfbridgeBootTopic = ''; debounceQueue = new DebounceQueue(); constructor(log, config, api) { this.log = log; this.config = config; this.api = api; this.Service = api.hap.Service; this.Characteristic = api.hap.Characteristic; this.log.debug(Date.now() + ' Initializing ceiling fan platform'); if (!(this.config._version && this.config._version === SCHEMA_VERSION)) { this.log.info(`Schema version has been updated. You're currently using version ${this.config._version ? this.config._version : '0.0.0'} The latest version is ${SCHEMA_VERSION} Please check your configuration and save to upgrade to the latest version.`); } else { const connectUrl = `${this.config.mqtt.protocol}://${this.config.mqtt.host}:${this.config.mqtt.port}`; const connectionParams = { clientId: `${PLATFORM_NAME}_${os.hostname()}`, clean: false, connectTimeout: 4000, reconnectPeriod: 1000, }; if (this.config.mqtt.user) { connectionParams.username = this.config.mqtt.user; } if (this.config.mqtt.password) { connectionParams.password = this.config.mqtt.password; } this.rfbridgeResultsTopic = `tele/${this.config.rfbridge.topic}/RESULT`; this.rfbridgeBootTopic = `tele/${this.config.rfbridge.topic}/INFO3`; this.mqttClient = mqtt.connect(connectUrl, connectionParams); const connectCallback = () => { this.log.debug(Date.now() + ' MQTT Connected'); if (this.mqttClient) { this.mqttClient.subscribe([this.rfbridgeResultsTopic, this.rfbridgeBootTopic], () => { this.log.debug(Date.now() + ` Subscribed to topic '${this.rfbridgeResultsTopic}'`); }); //Make sure that code sniffing is on this.mqttClient.publish(`cmnd/${this.config.rfbridge.topic}/rfraw`, 'AAA655'); } }; this.mqttClient.on('connect', connectCallback); this.mqttClient.on('reconnect', connectCallback); } // When this event is fired it means Homebridge has restored all cached accessories from disk. // Dynamic Platform plugins should only register new accessories after this event was fired, // in order to ensure they weren't added to homebridge already. This event can also be used // to start discovery of new accessories. this.api.on('didFinishLaunching', () => { this.log.debug(Date.now() + ' Executed didFinishLaunching callback'); // run the method to discover / register your devices as accessories this.initialize(); this.log.debug(Date.now() + ' Finished initializing ceiling fan platform'); }); } /** * This function is invoked when homebridge restores cached accessories from disk at startup. * It should be used to setup event handlers for characteristics and update respective values. */ configureAccessory(accessory) { this.log.info('Loading accessory from cache:', accessory.displayName); // add the restored accessory to the accessories cache so we can track if it has already been registered this.accessories.push(accessory); } removeUnusedAccessoriesFromCache() { //Remove any cached remotes that aren't in the config anymore. this.accessories.forEach(existingAccessory => { this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]); this.log.info('Removing existing accessory from cache:', existingAccessory.displayName); }); } initialize() { this.log.debug(Date.now() + ' Initializing.'); if (!(this.config._version && this.config._version === SCHEMA_VERSION)) { this.removeUnusedAccessoriesFromCache(); } else if (this.config.rooms && Array.isArray(this.config.rooms)) { // loop over the discovered devices and register each one if it has not already been registered for (const roomConfig of this.config.rooms) { if (!roomConfig._id) { roomConfig._id = Date.now(); } const uuid = this.api.hap.uuid.generate(roomConfig._id.toString()); if (this.initializedRemotes.indexOf(uuid) < 0) { this.initializedRemotes.push(uuid); // see if an accessory with the same uuid has already been registered and restored from // the cached devices we stored in the `configureAccessory` method above const existingAccessoryIndex = this.accessories.findIndex(accessory => accessory.UUID === uuid); const remote = (() => { if (existingAccessoryIndex >= 0) { // the accessory already exists const existingAccessory = this.accessories.splice(existingAccessoryIndex, 1)[0]; //Check if the display name has been updated if (roomConfig.name === existingAccessory.displayName) { this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); } else { this.log.info(`Restoring existing accessory from cache: ${roomConfig.name} (formerly ${existingAccessory.displayName})`); existingAccessory.displayName = roomConfig.name; existingAccessory._associatedHAPAccessory.displayName = roomConfig.name; } // Update the context to make sure the current settings are in the context existingAccessory.context.config = roomConfig; this.api.updatePlatformAccessories([existingAccessory]); return new CeilingFanRemote(this, existingAccessory); //this.remotes[remoteConfig.remote_id] = new CeilingFanRemote(this, existingAccessory); } else { // the accessory does not yet exist, so we need to create it this.log.info('Adding new accessory:', roomConfig.name); // create a new accessory const accessory = new this.api.platformAccessory(roomConfig.name, uuid); accessory.context.config = roomConfig; //this.remotes[remoteConfig.remote_id] = new CeilingFanRemote(this, accessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); return new CeilingFanRemote(this, accessory); } })(); this.rooms.push(remote); for (const remote_id of roomConfig.remote_ids) { if (!(this.remotes[remote_id] && Array.isArray(this.remotes[remote_id]))) { this.remotes[remote_id] = []; } this.remotes[remote_id].push(remote); } } else { this.log.error(`Already initialized a room with id ${roomConfig._id}. Room ID must be unique.`); } } if (this.mqttClient) { this.mqttClient.on('message', (topic, payload) => { if (topic === this.rfbridgeResultsTopic) { //Parse rfraw data; const rfraw = JSON.parse(payload.toString()).RfRaw; if (rfraw && rfraw.Data) { const message = rfraw.Data; this.log.debug(Date.now() + ' Received Message:', topic, message); if (message.substring(2, 4) === 'A6' && message.substring(6, 8) === this.config.rfbridge.protocol) { this.log.debug(Date.now() + ' Parsing Message:', message); const uartPayload = message.substring(8, message.length - 2); const bytes = uartPayload.match(/../g); if (bytes) { const binaryString = bytes.map(byte => { return parseInt(byte, 16).toString(2).padStart(8, '0'); }).join(''); const parsedBinary = binaryString.match(/^1(.{40})00(.{10})11(.{10})0.*/); if (parsedBinary !== null) { // const command = parsedBinary[2]; // const iCommand = parsedBinary[3]; // this.log.debug(Date.now()+' to binary -> ', binaryString, {room, command, iCommand, commandNum: parseInt(command, 2)}); const remote = parsedBinary[1]; const command = parseInt(parsedBinary[2], 2); if (this.remotes[remote]) { this.remotes[remote].forEach(accessory => { this.log.debug(Date.now() + ` Sending command (${command}) to ${accessory.config.name}`); accessory.update(command); }); } } } } } } else if (topic === this.rfbridgeBootTopic) { //Make sure that code sniffing is on if (this.mqttClient) { this.mqttClient.publish(`cmnd/${this.config.rfbridge.topic}/rfraw`, 'AAA655'); } } }); } //Add event listener to remotes for transmitting changes to mqtt Object.values(this.rooms).forEach(room => { room.addListener('update', props => { const command = commands[props.parameter][props.value]; this.log.debug(Date.now() + ` Recieved update from ${room.config.name}: ${JSON.stringify(props)}`, command); if (command && this.mqttClient) { const mqttClient = this.mqttClient; const commandCallback = () => { this.log.debug(Date.now() + ` Sending command: ${room.config.name} ${props.parameter} (${props.value})`, hexCommand(props.remote, command)); mqttClient.publish(`cmnd/${this.config.rfbridge.topic}/rfraw`, hexCommand(props.remote, command)); }; commandCallback.bind(this); this.debounceQueue.queue(`${props.remote}/${props.parameter}`, commandCallback); } }); }); //Remove any cached remotes that aren't in the config anymore. this.removeUnusedAccessoriesFromCache(); } } } //# sourceMappingURL=platform.js.map