UNPKG

@maxsaber/homebridge-govee

Version:

Homebridge plugin to integrate Govee devices into HomeKit.

209 lines (172 loc) 7.39 kB
import { Buffer } from 'node:buffer' import btClient from '@abandonware/noble' import { base64ToHex, generateCodeFromHexValues, hexToTwoItems, sleep, } from '../utils/functions.js' import platformLang from '../utils/lang-en.js' /* The necessary commands to send and functions are taken from and credit to: https://www.npmjs.com/package/govee-led-client */ export default class { constructor(platform) { this.connectedTo = false this.log = platform.log this.platform = platform this.stateChange = false // Can only scan/connect/send if the noble stateChange is 'poweredOn' btClient.on('stateChange', (state) => { this.stateChange = state this.log.debug('[BLE] stateChange: %s.', state) }) // Event listener for noble scanning start btClient.on('scanStart', () => { this.log.debug('[BLE] %s.', platformLang.bleStart) }) // Event listener for noble scanning stop btClient.on('scanStop', () => { this.log.debug('[BLE] %s.', platformLang.bleStop) }) // Event and log noble warnings btClient.on('warning', (message) => { this.log.debugWarn('[BLE] %s.', message) }) // Event handler for discovering bluetooth devices // This should only be each and every time a device update is sent btClient.on('discover', (device) => { // Log the address found can be useful for debugging what's working this.log.debug('[BLE] found device [%s] [%s].', device.address, device.advertisement.localName) // Look for the device to update at the time if (!this.accessory || this.accessory.context.bleAddress !== device.address) { return } // Found the device so stop scanning btClient.stopScanning() // Make the device global as needed in other functions this.device = device // Log that the device has been discovered this.accessory.logDebug(platformLang.onlineBT) // Remove previous listeners that may still be intact this.device.removeAllListeners() // Add a listener for device disconnect this.device.on('disconnect', (reason) => { // Log the disconnection if (this.accessory) { this.accessory.logDebug(`${platformLang.offlineBTConn} [${reason || 'unknown'}]`) } else { this.log.debug( '[BLE] [%s] %s [%s].', this.device ? this.device.address : 'unknown', platformLang.offlineBTConn, reason || 'unknown', ) } // Un-define the variables used within the class this.device = undefined this.connectedTo = false this.controlChar = undefined this.accessory = undefined }) // Reset adapter btClient.reset() // Connect to the device this.accessory.logDebug('attempting to connect') this.device.connect((error) => { if (error) { this.accessory.logWarn(`could not connect as ${error}`) return } // Update the currently-connect-to variable this.connectedTo = this.accessory.context.bleAddress // Log the connection this.accessory.logDebug(platformLang.onlineBTConn) // Find the noble characteristic we need for controlling the device this.accessory.logDebug('finding device characteristics') device.discoverAllServicesAndCharacteristics((error2, services, characteristics) => { if (error2) { this.accessory.logWarn(`could not find device characteristics as ${error2}`) return } this.accessory.logDebug('found some device characteristics') Object.values(characteristics).forEach((char) => { // Make sure we found the characteristic and make it global for the sendUpdate function const formattedChar = char.uuid.replace(/-/g, '') if (formattedChar === '000102030405060708090a0b0c0d2b11') { this.controlChar = char this.accessory.logDebug(`found correct characteristic [${formattedChar}]`) } else { this.accessory.logDebug(`found different characteristic [${formattedChar}]`) } }) if (!this.controlChar) { this.accessory.logWarn('could not find control characteristic') } }) }) }) } async updateDevice(accessory, params) { // This is called by the platform on sending a device update via bluetooth accessory.logDebug(`starting update with params [${JSON.stringify(params)}]`) // Check the noble state is ready for bluetooth action if (this.stateChange !== 'poweredOn') { throw new Error(`${platformLang.bleWrongState} [${this.stateChange}]`) } // This is used to time out the request later on if it's taking too much time // 7 seconds, to take into account AWS control failing as well as the ~10 second HK limit let doIt = true accessory.logDebug('starting timer') setTimeout(() => { doIt = false }, 7000) // Check if we are already connected to a device - and disconnect if (this.device) { if (this.connectedTo && this.connectedTo !== accessory.context.bleAddress) { accessory.logDebug(`disconnecting from [${this.connectedTo}] to connect to [${accessory.context.bleAddress}]`) await this.device.disconnectAsync() accessory.logDebug('disconnect successful') } } // Make global the accessory in question which we are sending an update to this.accessory = accessory // Start the bluetooth scan to discover this accessory // Service UUID for future reference 000102030405060708090a0b0c0d1910 accessory.logDebug('starting scan') await btClient.startScanningAsync() accessory.logDebug('scanning started') // We want to wait for the .on('discover') function to find the accessory and the characteristic accessory.logDebug('starting loop') while (true) { if (!doIt) { accessory.logWarn(`could not find device [${accessory.context.bleAddress}]`) throw new Error(platformLang.bleTimeout) } // Once the characteristic (this.controlChar) has been found then break the loop if (this.connectedTo === accessory.context.bleAddress && this.controlChar) { accessory.logDebug('found correct characteristic so breaking loop') break } // Repeat this process every 200ms until the characteristic is available await sleep(200) } // We can be sent either: // - a base64 action code (with params.cmd === 'ptReal') // - an array containing a varied amount of already-hex values const finalBuffer = params.cmd === 'ptReal' ? Buffer.from(hexToTwoItems(base64ToHex(params.data)).map(byte => `0x${byte}`)) : generateCodeFromHexValues([0x33, params.cmd, params.data], true) // Log the request if in debug mode accessory.logDebug(`[BLE] ${platformLang.sendingUpdate} [${finalBuffer.toString('hex')}]`) // Send the data to the device await this.controlChar.writeAsync(finalBuffer, true) // Maybe a slight await here helps (for an unknown reason) await sleep(100) // Disconnect from device await this.device.disconnectAsync() // Maybe a slight await here helps (for an unknown reason) await sleep(100) } }