UNPKG

@maxsaber/homebridge-govee

Version:

Homebridge plugin to integrate Govee devices into HomeKit.

241 lines (215 loc) 7.33 kB
import { Buffer } from 'node:buffer' import axios from 'axios' import platformConsts from '../utils/constants.js' import { parseError, sleep } from '../utils/functions.js' import platformLang from '../utils/lang-en.js' export default class { constructor(platform) { // Create variables usable by the class this.log = platform.log this.password = platform.config.password this.token = platform.accountToken this.tokenTTR = platform.accountTokenTTR this.username = platform.config.username // May need changing from time to time this.appVersion = '5.6.01' this.userAgent = `GoveeHome/${this.appVersion} (com.ihoment.GoVeeSensor; build:2; iOS 16.5.0) Alamofire/5.6.4` // Create a client id generated from Govee username which should remain constant let clientSuffix = platform.api.hap.uuid.generate(this.username).replace(/-/g, '') // 32 chars clientSuffix = clientSuffix.substring(0, clientSuffix.length - 2) // 30 chars this.clientId = `hb${clientSuffix}` // 32 chars } async login() { try { // Perform the HTTP request const res = await axios({ url: 'https://app2.govee.com/account/rest/account/v1/login', method: 'post', data: { email: this.username, password: this.password, client: this.clientId, }, timeout: 30000, }) // Check to see we got a response if (!res.data) { throw new Error(platformLang.noToken) } // Check to see we got a needed response if (!res.data.client || !res.data.client.token) { if (res.data.message && res.data.message.replace(/\s+/g, '') === 'Incorrectpassword') { if (this.base64Tried) { throw new Error(res.data.message || platformLang.noToken) } else { this.base64Tried = true this.password = Buffer.from(this.password, 'base64') .toString('utf8') .replace(/\r\n|\n|\r/g, '') .trim() return await this.login() } } throw new Error(res.data.message || platformLang.noToken) } // Also grab an access token specifically for the get tap to run endpoint const ttrRes = await axios({ url: 'https://community-api.govee.com/os/v1/login', method: 'post', data: { email: this.username, password: this.password, }, timeout: 30000, }) // Make the token available in other functions this.token = res.data.client.token this.tokenTTR = ttrRes.data.data.token // Mark this request complete if in debug mode this.log.debug('[HTTP] %s.', platformLang.loginSuccess) // Also grab the iot data const iotRes = await axios({ url: 'https://app2.govee.com/app/v1/account/iot/key', method: 'get', headers: { 'Authorization': `Bearer ${this.token}`, 'appVersion': this.appVersion, 'clientId': this.clientId, 'clientType': 1, 'iotVersion': 0, 'timestamp': Date.now(), 'User-Agent': this.userAgent, }, }) // Return the account token and topic for AWS return { accountId: res.data.client.accountId, client: this.clientId, endpoint: iotRes.data.data.endpoint, iot: iotRes.data.data.p12, iotPass: iotRes.data.data.p12Pass, token: res.data.client.token, tokenTTR: this.tokenTTR, topic: res.data.client.topic, } } catch (err) { if (err.code && platformConsts.httpRetryCodes.includes(err.code)) { // Retry if another attempt could be successful this.log.warn('[HTTP] %s [login() - %s].', platformLang.httpRetry, err.code) await sleep(30000) return this.login() } throw err } } async logout() { try { await axios({ url: 'https://app2.govee.com/account/rest/account/v1/logout', method: 'post', headers: { 'Authorization': `Bearer ${this.token}`, 'appVersion': this.appVersion, 'clientId': this.clientId, 'clientType': 1, 'iotVersion': 0, 'timestamp': Date.now(), 'User-Agent': this.userAgent, }, }) } catch (err) { // Logout is only called on homebridge shutdown, so we can just log the error this.log.warn('[HTTP] %s %s.', platformLang.logoutFail, parseError(err)) } } async getDevices(isSync = true) { try { // Make sure we do have the account token if (!this.token) { throw new Error(platformLang.noTokenExists) } // Use the token received to get a device list const res = await axios({ url: 'https://app2.govee.com/device/rest/devices/v1/list', method: 'post', headers: { 'Authorization': `Bearer ${this.token}`, 'appVersion': this.appVersion, 'clientId': this.clientId, 'clientType': 1, 'iotVersion': 0, 'timestamp': Date.now(), 'User-Agent': this.userAgent, }, timeout: 30000, }) // Check to see we got a response if (!res.data || !res.data.devices) { throw new Error(platformLang.noDevices) } // Return the device list return res.data.devices || [] } catch (err) { if (!isSync && err.code && platformConsts.httpRetryCodes.includes(err.code)) { // Retry if another attempt could be successful (only on init, not sync) this.log.warn('[HTTP] %s [getDevices() - %s].', platformLang.httpRetry, err.code) await sleep(30000) return this.getDevices() } throw err } } async getTapToRuns() { // Build and send the request const res = await axios({ url: 'https://app2.govee.com/bff-app/v1/exec-plat/home', method: 'get', headers: { 'Authorization': `Bearer ${this.tokenTTR}`, 'appVersion': this.appVersion, 'clientId': this.clientId, 'clientType': 1, 'iotVersion': 0, 'timestamp': Date.now(), 'User-Agent': this.userAgent, }, timeout: 10000, }) // Check to see we got a response if (!res?.data?.data?.components) { throw new Error('not a valid response') } return res.data.data.components } async getLeakDeviceWarning(deviceId, deviceSku) { // Make sure we do have the account token if (!this.token) { throw new Error(platformLang.noTokenExists) } // Build and send the request const res = await axios({ url: 'https://app2.govee.com/leak/rest/device/v1/warnMessage', method: 'post', headers: { 'Authorization': `Bearer ${this.token}`, 'appVersion': this.appVersion, 'clientId': this.clientId, 'clientType': 1, 'iotVersion': 0, 'timestamp': Date.now(), 'User-Agent': this.userAgent, }, data: { device: deviceId.replaceAll(':', ''), limit: 50, sku: deviceSku, }, timeout: 10000, }) // Check to see we got a response if (!res?.data?.data) { throw new Error(platformLang.noDevices) } return res.data.data } }