diff --git a/README.md b/README.md index 0e9b63b..a11fc60 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) -Nest plug-in for [Homebridge](https://github.com/nfarina/homebridge) using the native Nest API. See what's new in [release 4.4.6](https://github.com/chrisjshull/homebridge-nest/releases/tag/v4.4.6). +Nest plug-in for [Homebridge](https://github.com/nfarina/homebridge) using the native Nest API. See what's new in [release 4.4.7](https://github.com/chrisjshull/homebridge-nest/releases/tag/v4.4.7). Integrate your Nest Thermostat (including Nest Temperature Sensors), Nest Protect, and Nest x Yale Lock devices into your HomeKit system. Both Nest Accounts (pre-August 2019) and Google Accounts are supported. diff --git a/index.js b/index.js index fcb2dd1..fcb030a 100644 --- a/index.js +++ b/index.js @@ -29,6 +29,99 @@ class NestPlatform { this.log = log; this.api = api; this.accessoryLookup = {}; + this.cachedAccessories = []; + + api.on('didFinishLaunching', async () => { + this.log('Fetching Nest devices.'); + + const generateAccessories = function(data) { + const foundAccessories = []; + + const loadDevices = function(DeviceType) { + const disableFlags = { + 'thermostat': 'Thermostat.Disable', + 'temp_sensor': 'TempSensor.Disable', + 'protect': 'Protect.Disable', + 'home_away_sensor': 'HomeAway.Disable', + 'lock': 'Lock.Disable' + }; + + const devices = (data.devices && data.devices[DeviceType.deviceGroup]) || {}; + for (const deviceId of Object.keys(devices)) { + const device = devices[deviceId]; + const serialNumber = device.serial_number; + if (!this.optionSet(disableFlags[DeviceType.deviceType], serialNumber, deviceId)) { + const structureId = device.structure_id; + if (this.config.structureId && this.config.structureId !== structureId) { + this.log('Skipping device ' + deviceId + ' because it is not in the required structure. Has ' + structureId + ', looking for ' + this.config.structureId + '.'); + continue; + } + const structure = data.structures[structureId]; + const accessory = new DeviceType(this.conn, this.log, device, structure, this); + this.accessoryLookup[deviceId] = accessory; + foundAccessories.push(accessory); + } + } + }.bind(this); + + loadDevices(ThermostatAccessory); + loadDevices(HomeAwayAccessory); + loadDevices(TempSensorAccessory); + loadDevices(ProtectAccessory); + loadDevices(LockAccessory); + + return foundAccessories; + }.bind(this); + + const updateAccessories = function(data, accList) { + accList.map(function(acc) { + const device = data.devices[acc.deviceGroup][acc.deviceId]; + if (device) { + const structureId = device.structure_id; + const structure = data.structures[structureId]; + acc.updateData(device, structure); + } + }); + }; + + const handleUpdates = function(data) { + if (Object.keys(this.accessoryLookup).length > 0) { + updateAccessories(data, this.accessoryLookup); + } + }.bind(this); + + try { + this.conn = await this.setupConnection(this.optionSet('Debug.Verbose'), this.optionSet('Nest.FieldTest.Enable')); + await this.conn.subscribe(handleUpdates); + await this.conn.observe(handleUpdates); + + let initialState = this.conn.apiResponseToObjectTree(this.conn.currentState); + this.accessoryLookup = generateAccessories(initialState); + + this.api.unregisterPlatformAccessories('homebridge-nest', 'Nest', this.cachedAccessories); + this.cachedAccessories = []; + this.api.registerPlatformAccessories('homebridge-nest', 'Nest', this.accessoryLookup.map(el => el.accessory)); + + let accessoriesMounted = this.accessoryLookup.map(el => el.constructor.name); + + if (this.config.readyCallback) { + axios.post(this.config.readyCallback, { + thermostat_count: accessoriesMounted.filter(el => el == 'NestThermostatAccessory').length, + tempsensor_count: accessoriesMounted.filter(el => el == 'NestTempSensorAccessory').length, + protect_count: accessoriesMounted.filter(el => el == 'NestProtectAccessory').length, + lock_count: accessoriesMounted.filter(el => el == 'NestLockAccessory').length + }).catch(() => { }); + } + } catch(err) { + this.log.error(err); + this.log.error('NOTE: Because we couldn\'t connect to the Nest service, your Nest devices in HomeKit will not be responsive.'); + this.cachedAccessories.forEach(el => el.updateReachability(false)); + } + }); + } + + configureAccessory(accessory) { + this.cachedAccessories.push(accessory); } optionSet(key, serialNumber, deviceId) { @@ -52,99 +145,11 @@ class NestPlatform { throw('Unable to authenticate with Google/Nest.'); } } - - async accessories(callback) { - this.log('Fetching Nest devices.'); - - const generateAccessories = function(data) { - const foundAccessories = []; - - const loadDevices = function(DeviceType) { - const disableFlags = { - 'thermostat': 'Thermostat.Disable', - 'temp_sensor': 'TempSensor.Disable', - 'protect': 'Protect.Disable', - 'home_away_sensor': 'HomeAway.Disable', - 'lock': 'Lock.Disable' - }; - - const devices = (data.devices && data.devices[DeviceType.deviceGroup]) || {}; - for (const deviceId of Object.keys(devices)) { - const device = devices[deviceId]; - const serialNumber = device.serial_number; - if (!this.optionSet(disableFlags[DeviceType.deviceType], serialNumber, deviceId)) { - const structureId = device.structure_id; - if (this.config.structureId && this.config.structureId !== structureId) { - this.log('Skipping device ' + deviceId + ' because it is not in the required structure. Has ' + structureId + ', looking for ' + this.config.structureId + '.'); - continue; - } - const structure = data.structures[structureId]; - const accessory = new DeviceType(this.conn, this.log, device, structure, this); - this.accessoryLookup[deviceId] = accessory; - foundAccessories.push(accessory); - } - } - }.bind(this); - - loadDevices(ThermostatAccessory); - loadDevices(HomeAwayAccessory); - loadDevices(TempSensorAccessory); - loadDevices(ProtectAccessory); - loadDevices(LockAccessory); - - return foundAccessories; - }.bind(this); - - const updateAccessories = function(data, accList) { - accList.map(function(acc) { - const device = data.devices[acc.deviceGroup][acc.deviceId]; - if (device) { - const structureId = device.structure_id; - const structure = data.structures[structureId]; - acc.updateData(device, structure); - } - }); - }; - - const handleUpdates = function(data) { - if (Object.keys(this.accessoryLookup).length > 0) { - updateAccessories(data, this.accessoryLookup); - } - }.bind(this); - - try { - this.conn = await this.setupConnection(this.optionSet('Debug.Verbose'), this.optionSet('Nest.FieldTest.Enable')); - await this.conn.subscribe(handleUpdates); - await this.conn.observe(handleUpdates); - - let initialState = this.conn.apiResponseToObjectTree(this.conn.currentState); - this.accessoryLookup = generateAccessories(initialState); - if (callback) { - callback(Array.from(this.accessoryLookup)); - } - - let accessoriesMounted = this.accessoryLookup.map(el => el.constructor.name); - - if (this.config.readyCallback) { - axios.post(this.config.readyCallback, { - thermostat_count: accessoriesMounted.filter(el => el == 'NestThermostatAccessory').length, - tempsensor_count: accessoriesMounted.filter(el => el == 'NestTempSensorAccessory').length, - protect_count: accessoriesMounted.filter(el => el == 'NestProtectAccessory').length, - lock_count: accessoriesMounted.filter(el => el == 'NestLockAccessory').length - }).catch(() => { }); - } - } catch(err) { - this.log.error(err); - if (callback) { - callback([]); - } - } - } } module.exports = function(homebridge) { const exportedTypes = { - Accessory: homebridge.hap.Accessory, + Accessory: homebridge.platformAccessory, Service: homebridge.hap.Service, Characteristic: homebridge.hap.Characteristic, hap: homebridge.hap, diff --git a/lib/nest-connection.js b/lib/nest-connection.js index 1d0e176..9232c90 100644 --- a/lib/nest-connection.js +++ b/lib/nest-connection.js @@ -123,271 +123,287 @@ class Connection { async auth(preemptive) { let req, body; - if (!preemptive) { - this.connected = false; - this.token = null; - } - if (this.config.googleAuth) { - let issueToken = this.config.googleAuth.issueToken; - let cookies = this.config.googleAuth.cookies; + // eslint-disable-next-line + while (true) { + // Will return when authed successfully, or throw when cannot retry + + if (!preemptive) { + this.connected = false; + this.token = null; + } + if (this.config.googleAuth) { + let issueToken = this.config.googleAuth.issueToken; + let cookies = this.config.googleAuth.cookies; + + this.debug('Authenticating via Google.'); + let result; + try { + req = { + method: 'GET', + // followAllRedirects: true, + timeout: API_TIMEOUT_SECONDS * 1000, + url: issueToken, + headers: { + 'Sec-Fetch-Mode': 'cors', + 'User-Agent': NestEndpoints.USER_AGENT_STRING, + 'X-Requested-With': 'XmlHttpRequest', + 'Referer': 'https://accounts.google.com/o/oauth2/iframe', + 'cookie': cookies + } + }; + result = (await axios(req)).data; + let googleAccessToken = result.access_token; + if (result.error) { + this.error('Google authentication was unsuccessful. Make sure you did not log out of your Google account after getting your googleAuth parameters.'); + throw(result); + } + req = { + method: 'POST', + // followAllRedirects: true, + timeout: API_TIMEOUT_SECONDS * 1000, + url: 'https://nestauthproxyservice-pa.googleapis.com/v1/issue_jwt', + data: { + embed_google_oauth_access_token: true, + expire_after: '3600s', + google_oauth_access_token: googleAccessToken, + policy_id: 'authproxy-oauth-policy' + }, + headers: { + 'Authorization': 'Bearer ' + googleAccessToken, + 'User-Agent': NestEndpoints.USER_AGENT_STRING, + 'x-goog-api-key': this.config.googleAuth.apiKey, + 'Referer': 'https://' + NestEndpoints.NEST_API_HOSTNAME + } + }; + result = (await axios(req)).data; + this.config.access_token = result.jwt; + } catch (error) { + error.status = error.response && error.response.status; + this.error('Access token acquisition via googleAuth failed (code ' + (error.status || error.code || error.error) + ').'); + if (error.status == 400) { + // Cookies expired + return false; + } + if ((error.status && error.status >= 500) || ['ECONNREFUSED','ENOTFOUND','ESOCKETTIMEDOUT','ECONNABORTED','ENETUNREACH','EAI_AGAIN','DEPTH_ZERO_SELF_SIGNED_CERT'].includes(error.code)) { + this.error('Retrying in ' + API_AUTH_FAIL_RETRY_DELAY_SECONDS + ' second(s).'); + await Promise.delay(API_AUTH_FAIL_RETRY_DELAY_SECONDS * 1000); + continue; + // return await this.auth(); + } + } + } else if (this.config.authenticator) { + // Call external endpoint to refresh the token + this.debug('Acquiring access token via external authenticator.'); + try { + req = { + method: 'POST', + // followAllRedirects: true, + timeout: API_TIMEOUT_SECONDS * 1000, + url: this.config.authenticator, + data: this.config + }; + let result = (await axios(req)).data; + if (result.status == 'OK' && result.access_token) { + this.config.access_token = result.access_token; + } else { + throw({retry: result.retry, code: result.code}); + } + } catch (error) { + error.status = error.response && error.response.status; + this.error('Access token acquisition failed (code ' + (error.status || error.code) + ').'); + if (error.retry || error.errno) { + this.error('Retrying in ' + API_AUTH_FAIL_RETRY_DELAY_SECONDS + ' second(s).'); + await Promise.delay(API_AUTH_FAIL_RETRY_DELAY_SECONDS * 1000); + continue; + // return await this.auth(); + } + } + } + + let rcKey, rcToken; + if (this.config.access_token) { + if (!this.config.googleAuth) { + this.debug('Authenticating via access token.'); + } - this.debug('Authenticating via Google.'); - let result; - try { req = { method: 'GET', // followAllRedirects: true, timeout: API_TIMEOUT_SECONDS * 1000, - url: issueToken, + url: NestEndpoints.URL_NEST_AUTH, headers: { - 'Sec-Fetch-Mode': 'cors', + 'Authorization': 'Basic ' + this.config.access_token, 'User-Agent': NestEndpoints.USER_AGENT_STRING, - 'X-Requested-With': 'XmlHttpRequest', - 'Referer': 'https://accounts.google.com/o/oauth2/iframe', - 'cookie': cookies + 'cookie': 'G_ENABLED_IDPS=google; eu_cookie_accepted=1; viewer-volume=0.5; cztoken=' + this.config.access_token } }; - result = (await axios(req)).data; - let googleAccessToken = result.access_token; - if (result.error) { - this.error('Google authentication was unsuccessful. Make sure you did not log out of your Google account after getting your googleAuth parameters.'); - throw(result); + } else if (!this.config.googleAuth && !this.config.authenticator) { + this.error('Nest account login by username/password is no longer supported.'); + return false; + + // eslint-disable-next-line + this.debug('Authenticating via Nest account.'); + + if (this.config.recaptchaServer) { + req = { + method: 'GET', + timeout: 3 * API_TIMEOUT_SECONDS * 1000, + url: this.config.recaptchaServer, + // json: true + }; + let result; + try { + result = (await axios(req)).data; + if (result.status != 'OK' || !result.token || !result.key) { + this.debug('Recaptcha service failed:', result); + } else { + rcToken = result.token; + rcKey = result.key; + } + } catch (error) { + // We handle this later + } } + req = { method: 'POST', // followAllRedirects: true, timeout: API_TIMEOUT_SECONDS * 1000, - url: 'https://nestauthproxyservice-pa.googleapis.com/v1/issue_jwt', - data: { - embed_google_oauth_access_token: true, - expire_after: '3600s', - google_oauth_access_token: googleAccessToken, - policy_id: 'authproxy-oauth-policy' - }, + url: NestEndpoints.URL_NEST_AUTH, headers: { - 'Authorization': 'Bearer ' + googleAccessToken, 'User-Agent': NestEndpoints.USER_AGENT_STRING, - 'x-goog-api-key': this.config.googleAuth.apiKey, - 'Referer': 'https://' + NestEndpoints.NEST_API_HOSTNAME + 'content-Type': 'application/json', + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache', + 'Cookie': 'viewer-volume=0.5; _ga=GA1.2.1817575647.1579652723; _gid=GA1.2.18799290.1579652723; _gaexp=GAX1.2.BcN0_xGpR72iDpx328dA9A.18291.1; G_ENABLED_IDPS=google; _gat_UA-19609914-2=1', + 'Host': NestEndpoints.NEST_API_HOSTNAME, + 'hostname': NestEndpoints.NEST_API_HOSTNAME, + 'Origin': 'https://' + NestEndpoints.NEST_API_HOSTNAME, + 'Referer': 'https://' + NestEndpoints.NEST_API_HOSTNAME + '/', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'x-no-cookies': 'true' + }, + data: { + email: this.config.email, + password: this.config.password, + recaptcha: { + token: rcToken, + site_key: rcKey + } } }; - result = (await axios(req)).data; - this.config.access_token = result.jwt; - } catch(error) { - error.status = error.response && error.response.status; - this.error('Access token acquisition via googleAuth failed (code ' + (error.status || error.code) + ').'); - if (['ECONNREFUSED','ESOCKETTIMEDOUT','ECONNABORTED','ENETUNREACH'].includes(error.code)) { - this.error('Retrying in ' + API_AUTH_FAIL_RETRY_DELAY_SECONDS + ' second(s).'); - await Promise.delay(API_AUTH_FAIL_RETRY_DELAY_SECONDS * 1000); - return await this.auth(); - } + } else { + return false; } - } else if (this.config.authenticator) { - // Call external endpoint to refresh the token - this.debug('Acquiring access token via external authenticator.'); + try { - req = { - method: 'POST', - // followAllRedirects: true, - timeout: API_TIMEOUT_SECONDS * 1000, - url: this.config.authenticator, - data: this.config - }; - let result = (await axios(req)).data; - if (result.status == 'OK' && result.access_token) { - this.config.access_token = result.access_token; - } else { - throw({ retry: result.retry, code: result.code }); + if (!this.config.authenticator && !this.config.googleAuth && !this.config.access_token && (!rcToken || !rcKey)) { + // Recaptcha failed - throw + throw({code: 'ENORECAPTCHA'}); } - } catch(error) { + body = (await axios(req)).data; + this.connected = true; + this.token = body.access_token; + this.transport_url = body.urls.transport_url; + this.userid = body.userid; + this.connectionFailures = 0; + this.debug('Authentication successful.'); + } catch (error) { error.status = error.response && error.response.status; - this.error('Access token acquisition failed (code ' + (error.status || error.code) + ').'); - if (error.retry || error.errno) { - this.error('Retrying in ' + API_AUTH_FAIL_RETRY_DELAY_SECONDS + ' second(s).'); - await Promise.delay(API_AUTH_FAIL_RETRY_DELAY_SECONDS * 1000); - return await this.auth(); - } - } - } - - let rcKey, rcToken; - if (this.config.access_token) { - if (!this.config.googleAuth) { - this.debug('Authenticating via access token.'); - } - - req = { - method: 'GET', - // followAllRedirects: true, - timeout: API_TIMEOUT_SECONDS * 1000, - url: NestEndpoints.URL_NEST_AUTH, - headers: { - 'Authorization': 'Basic ' + this.config.access_token, - 'User-Agent': NestEndpoints.USER_AGENT_STRING, - 'cookie': 'G_ENABLED_IDPS=google; eu_cookie_accepted=1; viewer-volume=0.5; cztoken=' + this.config.access_token - } - }; - } else if (!this.config.googleAuth && !this.config.authenticator) { - this.debug('Authenticating via Nest account.'); - - if (this.config.recaptchaServer) { - req = { - method: 'GET', - timeout: 3 * API_TIMEOUT_SECONDS * 1000, - url: this.config.recaptchaServer, - // json: true - }; - let result; - try { - result = (await axios(req)).data; - if (result.status != 'OK' || !result.token || !result.key) { - this.debug('Recaptcha service failed:', result); + if (error.status == 401 && error.response && error.response.data && error.response.data.truncated_phone_number) { + // 2FA required + let getPIN; + + this.log('Your Nest account has 2-factor authentication enabled.'); + if (this.config.pin) { + this.log('Using PIN ' + this.config.pin + ' from config.json.'); + this.log('If authentication fails, check this matches the 6-digit PIN sent to your phone number ending ' + error.response.data.truncated_phone_number + '.'); + getPIN = Promise.resolve(this.config.pin); } else { - rcToken = result.token; - rcKey = result.key; + this.log('Please enter the 6-digit PIN sent to your phone number ending ' + error.response.data.truncated_phone_number + '.'); + getPIN = Prompt('PIN: '); } - } catch(error) { - // We handle this later - } - } - - req = { - method: 'POST', - // followAllRedirects: true, - timeout: API_TIMEOUT_SECONDS * 1000, - url: NestEndpoints.URL_NEST_AUTH, - headers: { - 'User-Agent': NestEndpoints.USER_AGENT_STRING, - 'content-Type': 'application/json', - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache', - 'Cookie': 'viewer-volume=0.5; _ga=GA1.2.1817575647.1579652723; _gid=GA1.2.18799290.1579652723; _gaexp=GAX1.2.BcN0_xGpR72iDpx328dA9A.18291.1; G_ENABLED_IDPS=google; _gat_UA-19609914-2=1', - 'Host': NestEndpoints.NEST_API_HOSTNAME, - 'hostname': NestEndpoints.NEST_API_HOSTNAME, - 'Origin': 'https://' + NestEndpoints.NEST_API_HOSTNAME, - 'Referer': 'https://' + NestEndpoints.NEST_API_HOSTNAME + '/', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'x-no-cookies': 'true' - }, - data: { - email: this.config.email, - password: this.config.password, - recaptcha: { - token: rcToken, - site_key: rcKey + try { + let pin = await getPIN; + let result = (await axios({ + method: 'POST', + // followAllRedirects: true, + timeout: API_TIMEOUT_SECONDS * 1000, + url: NestEndpoints.URL_NEST_VERIFY_PIN, + data: { + pin: pin, + '2fa_token': error.response.data['2fa_token'], + 'cookie': 'G_ENABLED_IDPS=google; eu_cookie_accepted=1; viewer-volume=0.5' + } + })).data; + body = (await axios({ + method: 'GET', + // followAllRedirects: true, + timeout: API_TIMEOUT_SECONDS * 1000, + url: NestEndpoints.URL_NEST_AUTH, + headers: { + 'Authorization': 'Basic ' + result.access_token, + 'User-Agent': NestEndpoints.USER_AGENT_STRING, + 'cookie': 'G_ENABLED_IDPS=google; eu_cookie_accepted=1; viewer-volume=0.5; cztoken=' + result.access_token + } + })).data; + this.connected = true; + this.token = body.access_token; + this.transport_url = body.urls.transport_url; + this.userid = body.userid; + return true; // resolve(true); + } catch (error) { + this.error('Auth failed: 2FA PIN was rejected'); + return false; // resolve(false); } - } - }; - } else { - return false; - } - - try { - if (!this.config.authenticator && !this.config.googleAuth && !this.config.access_token && (!rcToken || !rcKey)) { - // Recaptcha failed - throw - throw({ code: 'ENORECAPTCHA' }); - } - body = (await axios(req)).data; - this.connected = true; - this.token = body.access_token; - this.transport_url = body.urls.transport_url; - this.userid = body.userid; - this.connectionFailures = 0; - this.debug('Authentication successful.'); - } catch(error) { - error.status = error.response && error.response.status; - if (error.status == 401 && error.response && error.response.data && error.response.data.truncated_phone_number) { - // 2FA required - let getPIN; - - this.log('Your Nest account has 2-factor authentication enabled.'); - if (this.config.pin) { - this.log('Using PIN ' + this.config.pin + ' from config.json.'); - this.log('If authentication fails, check this matches the 6-digit PIN sent to your phone number ending ' + error.response.data.truncated_phone_number + '.'); - getPIN = Promise.resolve(this.config.pin); - } else { - this.log('Please enter the 6-digit PIN sent to your phone number ending ' + error.response.data.truncated_phone_number + '.'); - getPIN = Prompt('PIN: '); - } - try { - let pin = await getPIN; - let result = (await axios({ - method: 'POST', - // followAllRedirects: true, - timeout: API_TIMEOUT_SECONDS * 1000, - url: NestEndpoints.URL_NEST_VERIFY_PIN, - data: { - pin: pin, - '2fa_token': error.response.data['2fa_token'], - 'cookie': 'G_ENABLED_IDPS=google; eu_cookie_accepted=1; viewer-volume=0.5' - } - })).data; - body = (await axios({ - method: 'GET', - // followAllRedirects: true, - timeout: API_TIMEOUT_SECONDS * 1000, - url: NestEndpoints.URL_NEST_AUTH, - headers: { - 'Authorization': 'Basic ' + result.access_token, - 'User-Agent': NestEndpoints.USER_AGENT_STRING, - 'cookie': 'G_ENABLED_IDPS=google; eu_cookie_accepted=1; viewer-volume=0.5; cztoken=' + result.access_token + } else if (error.status == 400) { + if (this.config.access_token) { + this.error('Auth failed: access token specified in Homebridge configuration rejected'); + } else { + this.error('Auth failed: Nest rejected the account email/password specified in your Homebridge configuration file. Please check'); + this.connectionFailures++; + if (this.connectionFailures >= 6) { + this.error('Too many failed auth attempts, waiting ' + API_AUTH_FAIL_RETRY_LONG_DELAY_SECONDS + ' seconds'); + await Promise.delay(API_AUTH_FAIL_RETRY_LONG_DELAY_SECONDS * 1000); } - })).data; - this.connected = true; - this.token = body.access_token; - this.transport_url = body.urls.transport_url; - this.userid = body.userid; - return true; // resolve(true); - } catch(error) { - this.error('Auth failed: 2FA PIN was rejected'); + continue; + // return await this.auth(); + } + return false; // resolve(false); + } else if (error.status == 429) { + this.error('Auth failed: rate limit exceeded. Please try again in 60 minutes'); return false; // resolve(false); - } - } else if (error.status == 400) { - if (this.config.access_token) { - this.error('Auth failed: access token specified in Homebridge configuration rejected'); } else { - this.error('Auth failed: Nest rejected the account email/password specified in your Homebridge configuration file. Please check'); - this.connectionFailures++; - if (this.connectionFailures >= 6) { - this.error('Too many failed auth attempts, waiting ' + API_AUTH_FAIL_RETRY_LONG_DELAY_SECONDS + ' seconds'); - await Promise.delay(API_AUTH_FAIL_RETRY_LONG_DELAY_SECONDS * 1000); - } - return await this.auth(); + console.log(error); + this.error('Could not authenticate with Nest (code ' + (error.status || error.code) + '). Retrying in ' + API_AUTH_FAIL_RETRY_DELAY_SECONDS + ' second(s).'); + await Promise.delay(API_AUTH_FAIL_RETRY_DELAY_SECONDS * 1000); + return await this.auth(); // .then(() => this.auth()).then(connected => resolve(connected)); } - return false; // resolve(false); - } else if (error.status == 429) { - this.error('Auth failed: rate limit exceeded. Please try again in 60 minutes'); - return false; // resolve(false); - } else { - console.log(error); - this.error('Could not authenticate with Nest (code ' + (error.status || error.code) + '). Retrying in ' + API_AUTH_FAIL_RETRY_DELAY_SECONDS + ' second(s).'); - await Promise.delay(API_AUTH_FAIL_RETRY_DELAY_SECONDS * 1000); - return await this.auth(); // .then(() => this.auth()).then(connected => resolve(connected)); } - } - let isGoogle = this.config.googleAuth || this.config.authenticator; - // Google tokens expire after 60 minutes (Nest is 30 days), so refresh just before that to make sure we always have a fresh token - if (this.preemptiveReauthTimer) { - clearTimeout(this.preemptiveReauthTimer); - } - this.preemptiveReauthTimer = setTimeout(() => { - this.debug('Initiating pre-emptive reauthentication.'); - this.auth(true).catch(() => { - this.debug('Pre-emptive reauthentication failed.'); - }); - }, (isGoogle ? API_GOOGLE_REAUTH_MINUTES : API_NEST_REAUTH_MINUTES) * 60 * 1000); - - this.associatedStreamers.forEach(streamer => { - try { - streamer.onTheFlyReauthorize(); - } catch(error) { - this.verbose('Warning: attempting to reauthorize with expired streamer', streamer); + let isGoogle = this.config.googleAuth || this.config.authenticator; + // Google tokens expire after 60 minutes (Nest is 30 days), so refresh just before that to make sure we always have a fresh token + if (this.preemptiveReauthTimer) { + clearTimeout(this.preemptiveReauthTimer); } - }); + this.preemptiveReauthTimer = setTimeout(() => { + this.debug('Initiating pre-emptive reauthentication.'); + this.auth(true).catch(() => { + this.debug('Pre-emptive reauthentication failed.'); + }); + }, (isGoogle ? API_GOOGLE_REAUTH_MINUTES : API_NEST_REAUTH_MINUTES) * 60 * 1000); - return true; + this.associatedStreamers.forEach(streamer => { + try { + streamer.onTheFlyReauthorize(); + } catch (error) { + this.verbose('Warning: attempting to reauthorize with expired streamer', streamer); + } + }); + + return true; + } } mergePendingUpdates(unmergedBody) { @@ -569,9 +585,6 @@ class Connection { pendingLength = varint.decode(protoBuffer, 1); pendingLength += varint.decode.bytes + 1; } - if (pendingLength == 2) { - this.verbose(protoMessage.toString('base64')); - } let observeMessage = this.protobufToNestLegacy(protoMessage); this.protobufBody = observeMessage.body; if (notify || observeMessage.hasDeviceInfo) { diff --git a/lib/nest-device-accessory.js b/lib/nest-device-accessory.js index 95c6b8e..42a7232 100644 --- a/lib/nest-device-accessory.js +++ b/lib/nest-device-accessory.js @@ -46,9 +46,13 @@ function NestDeviceAccessory(conn, log, device, structure, platform) { // this.log.debug(this.device); const id = uuid.generate('nest' + '.' + this.deviceType + '.' + this.deviceId); - Accessory.call(this, this.name, id); + this.accessory = new Accessory(this.name, id); this.uuid_base = id; + this.addService = this.accessory._associatedHAPAccessory.addService.bind(this.accessory); + this.getService = this.accessory._associatedHAPAccessory.getService.bind(this.accessory); + this.setPrimaryService = this.accessory._associatedHAPAccessory.setPrimaryService.bind(this.accessory); + this.getService(Service.AccessoryInformation) .setCharacteristic(Characteristic.FirmwareRevision, this.device.software_version) .setCharacteristic(Characteristic.Manufacturer, 'Nest') diff --git a/package.json b/package.json index 1d44a0e..c719b4b 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "prepublishOnly": "npm run lint", "preversion": "npm run lint" }, - "version": "4.4.6", + "version": "4.4.7", "warnings": [ { "code": "ENOTSUP", @@ -44,7 +44,7 @@ "node": ">=7.0.0", "homebridge": ">=0.2.5" }, - "pkgid": "homebridge-nest@4.4.6" + "pkgid": "homebridge-nest@4.4.7" } ] }