diff --git a/api/ecobee.php b/api/ecobee.php index 5aff73d..e7a2fd1 100644 --- a/api/ecobee.php +++ b/api/ecobee.php @@ -163,6 +163,11 @@ class ecobee extends external_api { } $curl['url'] = 'https://api.ecobee.com' . $full_endpoint; + // Allow a completely custom endpoint if desired. + if(str_starts_with($endpoint, 'https://') === true) { + $curl['url'] = $endpoint; + } + if ($method === 'GET') { $curl['url'] .= '?' . http_build_query($arguments); } @@ -221,6 +226,13 @@ class ecobee extends external_api { $this->api('ecobee_token', 'delete', $ecobee_token['ecobee_token_id']); throw new cora\exception('Ecobee access was revoked by user.', 10508, false, null, false); } + else if (isset($response['status']) === true && $response['status']['code'] === 9) { + // Invalid selection. No thermostats in selection. Ensure permissions and selection. + if($this::$log_mysql !== 'all') { + $this->log_mysql($curl_response, true); + } + throw new cora\exception('No thermostats found.', 10511, false, null, false); + } else if (isset($response['status']) === true && $response['status']['code'] === 3) { if ( isset($response['status']['message']) === true && diff --git a/api/ecobee_sensor.php b/api/ecobee_sensor.php index cb87c0f..1d150f9 100644 --- a/api/ecobee_sensor.php +++ b/api/ecobee_sensor.php @@ -58,81 +58,169 @@ class ecobee_sensor extends cora\crud { public function sync() { // Get the thermostat list from ecobee with sensors. Keep this identical to // ecobee_thermostat->sync() to leverage caching. - $response = $this->api( - 'ecobee', - 'ecobee_api', - [ - 'method' => 'GET', - 'endpoint' => 'thermostat', - 'arguments' => [ - 'body' => json_encode([ - 'selection' => [ - 'selectionType' => 'registered', - 'selectionMatch' => '', - 'includeRuntime' => true, - 'includeExtendedRuntime' => true, - 'includeElectricity' => true, - 'includeSettings' => true, - 'includeLocation' => true, - 'includeProgram' => true, - 'includeEvents' => true, - 'includeDevice' => true, - 'includeTechnician' => true, - 'includeUtility' => true, - 'includeManagement' => true, - 'includeAlerts' => true, - 'includeWeather' => true, - 'includeHouseDetails' => true, - 'includeOemCfg' => true, - 'includeEquipmentStatus' => true, - 'includeNotificationSettings' => true, - 'includeVersion' => true, - 'includePrivacy' => true, - 'includeAudio' => true, - 'includeSensors' => true + $include = [ + 'includeRuntime' => true, + 'includeExtendedRuntime' => true, + 'includeElectricity' => true, + 'includeSettings' => true, + 'includeLocation' => true, + 'includeProgram' => true, + 'includeEvents' => true, + 'includeDevice' => true, + 'includeTechnician' => true, + 'includeUtility' => true, + 'includeManagement' => true, + 'includeAlerts' => true, + 'includeWeather' => true, + 'includeHouseDetails' => true, + 'includeOemCfg' => true, + 'includeEquipmentStatus' => true, + 'includeNotificationSettings' => true, + 'includeVersion' => true, + 'includePrivacy' => true, + 'includeAudio' => true, + 'includeSensors' => true - /** - * 'includeReminders' => true - * - * While documented, this is not available for general API use - * unless you are a technician user. - * - * The reminders and the includeReminders flag are something extra - * for ecobee Technicians. It allows them to set and receive - * reminders with more detail than the usual alert reminder type. - * These reminders are only available to Technician users, which - * is why you aren't seeing any new information when you set that - * flag to true. Thanks for pointing out the lack of documentation - * regarding this. We'll get this updated as soon as possible. - * - * - * https://getsatisfaction.com/api/topics/what-does-includereminders-do-when-calling-get-thermostat?rfm=1 - */ + /** + * 'includeReminders' => true + * + * While documented, this is not available for general API use unless + * you are a technician user. + * + * The reminders and the includeReminders flag are something extra for + * ecobee Technicians. It allows them to set and receive reminders with + * more detail than the usual alert reminder type. These reminders are + * only available to Technician users, which is why you aren't seeing + * any new information when you set that flag to true. Thanks for + * pointing out the lack of documentation regarding this. We'll get this + * updated as soon as possible. + * + * + * https://getsatisfaction.com/api/topics/what-does-includereminders-do-when-calling-get-thermostat?rfm=1 + */ - /** - * 'includeSecuritySettings' => true - * - * While documented, this is not made available for general API - * use unless you are a utility. If you try to include this an - * "Authentication failed" error will be returned. - * - * Special accounts such as Utilities are permitted an alternate - * method of authorization using implicit authorization. This - * method permits the Utility application to authorize against - * their own specific account without the requirement of a PIN. - * This method is limited to special contractual obligations and - * is not available for 3rd party applications who are not - * Utilities. - * - * https://www.ecobee.com/home/developer/api/documentation/v1/objects/SecuritySettings.shtml - * https://www.ecobee.com/home/developer/api/documentation/v1/auth/auth-intro.shtml - * - */ - ] - ]) + /** + * 'includeSecuritySettings' => true + * + * While documented, this is not made available for general API use + * unless you are a utility. If you try to include this an + * "Authentication failed" error will be returned. + * + * Special accounts such as Utilities are permitted an alternate method + * of authorization using implicit authorization. This method permits + * the Utility application to authorize against their own specific + * account without the requirement of a PIN. This method is limited to + * special contractual obligations and is not available for 3rd party + * applications who are not Utilities. + * + * + * https://www.ecobee.com/home/developer/api/documentation/v1/objects/SecuritySettings.shtml + * https://www.ecobee.com/home/developer/api/documentation/v1/auth/auth-intro.shtml + */ + ]; + + try { + $response = $this->api( + 'ecobee', + 'ecobee_api', + [ + 'method' => 'GET', + 'endpoint' => 'thermostat', + 'arguments' => [ + 'body' => json_encode([ + 'selection' => array_merge( + [ + 'selectionType' => 'registered', + 'selectionMatch' => '' + ], + $include + ) + ]) + ] ] - ] - ); + ); + if(count($response['thermostatList']) === 0) { + throw new cora\exception('No thermostats found.', 10511, false, null, false); + } + } catch(cora\exception $e) { + // If no thermostats found (ie. not the owner of any homes that contain a thermostat) + if($e->getCode() === 10511) { + $homes = $this->api( + 'ecobee', + 'ecobee_api', + [ + 'method' => 'GET', + 'endpoint' => 'https://home.hm-prod.ecobee.com/homes', + 'arguments' => [ + ] + ] + ); + + $home_ids = array_column($homes['homes'], 'homeID'); + + $serial_numbers = []; + foreach($home_ids as $home_id) { + $devices = $this->api( + 'ecobee', + 'ecobee_api', + [ + 'method' => 'GET', + 'endpoint' => 'https://home.hm-prod.ecobee.com/home/' . $home_id . '/devices', + 'arguments' => [ + ] + ] + ); + + /** + * This is a select distinct from ecobee_thermostat. Ideally it + * would be possible to send *all* serial numbers from the devices + * call to the GET->thermostat API call, but that throws an error if + * you include a serial number for something that's not a + * thermostat. So I have to keep this array to identify valid serial + * numbers. + */ + $model_numbers = [ + 'athenaSmart', + 'apolloSmart', + 'idtSmart', + 'nikeSmart', + 'siSmart', + 'corSmart', + 'vulcanSmart', + 'aresSmart', + 'artemisSmart' + ]; + + foreach($devices['devices'] as $device) { + if(in_array($device['modelNumber'], $model_numbers) === true) { + $serial_numbers[] = $device['serialNumber']; + } + } + } + + $response = $this->api( + 'ecobee', + 'ecobee_api', + [ + 'method' => 'GET', + 'endpoint' => 'thermostat', + 'arguments' => [ + 'body' => json_encode([ + 'selection' => array_merge( + [ + 'selectionType' => 'thermostats', + 'selectionMatch' => implode(',', $serial_numbers), + ], + $include + ) + ]) + ] + ] + ); + } else { + throw $e; + } + } // Loop over the returned sensors and create/update them as necessary. $sensor_ids_to_keep = []; diff --git a/api/ecobee_thermostat.php b/api/ecobee_thermostat.php index c8c7417..7839ac7 100644 --- a/api/ecobee_thermostat.php +++ b/api/ecobee_thermostat.php @@ -21,81 +21,169 @@ class ecobee_thermostat extends cora\crud { public function sync() { // Get the thermostat list from ecobee with sensors. Keep this identical to // ecobee_sensor->sync() to leverage caching. - $response = $this->api( - 'ecobee', - 'ecobee_api', - [ - 'method' => 'GET', - 'endpoint' => 'thermostat', - 'arguments' => [ - 'body' => json_encode([ - 'selection' => [ - 'selectionType' => 'registered', - 'selectionMatch' => '', - 'includeRuntime' => true, - 'includeExtendedRuntime' => true, - 'includeElectricity' => true, - 'includeSettings' => true, - 'includeLocation' => true, - 'includeProgram' => true, - 'includeEvents' => true, - 'includeDevice' => true, - 'includeTechnician' => true, - 'includeUtility' => true, - 'includeManagement' => true, - 'includeAlerts' => true, - 'includeWeather' => true, - 'includeHouseDetails' => true, - 'includeOemCfg' => true, - 'includeEquipmentStatus' => true, - 'includeNotificationSettings' => true, - 'includeVersion' => true, - 'includePrivacy' => true, - 'includeAudio' => true, - 'includeSensors' => true + $include = [ + 'includeRuntime' => true, + 'includeExtendedRuntime' => true, + 'includeElectricity' => true, + 'includeSettings' => true, + 'includeLocation' => true, + 'includeProgram' => true, + 'includeEvents' => true, + 'includeDevice' => true, + 'includeTechnician' => true, + 'includeUtility' => true, + 'includeManagement' => true, + 'includeAlerts' => true, + 'includeWeather' => true, + 'includeHouseDetails' => true, + 'includeOemCfg' => true, + 'includeEquipmentStatus' => true, + 'includeNotificationSettings' => true, + 'includeVersion' => true, + 'includePrivacy' => true, + 'includeAudio' => true, + 'includeSensors' => true - /** - * 'includeReminders' => true - * - * While documented, this is not available for general API use - * unless you are a technician user. - * - * The reminders and the includeReminders flag are something extra - * for ecobee Technicians. It allows them to set and receive - * reminders with more detail than the usual alert reminder type. - * These reminders are only available to Technician users, which - * is why you aren't seeing any new information when you set that - * flag to true. Thanks for pointing out the lack of documentation - * regarding this. We'll get this updated as soon as possible. - * - * - * https://getsatisfaction.com/api/topics/what-does-includereminders-do-when-calling-get-thermostat?rfm=1 - */ + /** + * 'includeReminders' => true + * + * While documented, this is not available for general API use unless + * you are a technician user. + * + * The reminders and the includeReminders flag are something extra for + * ecobee Technicians. It allows them to set and receive reminders with + * more detail than the usual alert reminder type. These reminders are + * only available to Technician users, which is why you aren't seeing + * any new information when you set that flag to true. Thanks for + * pointing out the lack of documentation regarding this. We'll get this + * updated as soon as possible. + * + * + * https://getsatisfaction.com/api/topics/what-does-includereminders-do-when-calling-get-thermostat?rfm=1 + */ - /** - * 'includeSecuritySettings' => true - * - * While documented, this is not made available for general API - * use unless you are a utility. If you try to include this an - * "Authentication failed" error will be returned. - * - * Special accounts such as Utilities are permitted an alternate - * method of authorization using implicit authorization. This - * method permits the Utility application to authorize against - * their own specific account without the requirement of a PIN. - * This method is limited to special contractual obligations and - * is not available for 3rd party applications who are not - * Utilities. - * - * https://www.ecobee.com/home/developer/api/documentation/v1/objects/SecuritySettings.shtml - * https://www.ecobee.com/home/developer/api/documentation/v1/auth/auth-intro.shtml - * - */ - ] - ]) + /** + * 'includeSecuritySettings' => true + * + * While documented, this is not made available for general API use + * unless you are a utility. If you try to include this an + * "Authentication failed" error will be returned. + * + * Special accounts such as Utilities are permitted an alternate method + * of authorization using implicit authorization. This method permits + * the Utility application to authorize against their own specific + * account without the requirement of a PIN. This method is limited to + * special contractual obligations and is not available for 3rd party + * applications who are not Utilities. + * + * + * https://www.ecobee.com/home/developer/api/documentation/v1/objects/SecuritySettings.shtml + * https://www.ecobee.com/home/developer/api/documentation/v1/auth/auth-intro.shtml + */ + ]; + + try { + $response = $this->api( + 'ecobee', + 'ecobee_api', + [ + 'method' => 'GET', + 'endpoint' => 'thermostat', + 'arguments' => [ + 'body' => json_encode([ + 'selection' => array_merge( + [ + 'selectionType' => 'registered', + 'selectionMatch' => '' + ], + $include + ) + ]) + ] ] - ] - ); + ); + if(count($response['thermostatList']) === 0) { + throw new cora\exception('No thermostats found.', 10511, false, null, false); + } + } catch(cora\exception $e) { + // If no thermostats found (ie. not the owner of any homes that contain a thermostat) + if($e->getCode() === 10511) { + $homes = $this->api( + 'ecobee', + 'ecobee_api', + [ + 'method' => 'GET', + 'endpoint' => 'https://home.hm-prod.ecobee.com/homes', + 'arguments' => [ + ] + ] + ); + + $home_ids = array_column($homes['homes'], 'homeID'); + + $serial_numbers = []; + foreach($home_ids as $home_id) { + $devices = $this->api( + 'ecobee', + 'ecobee_api', + [ + 'method' => 'GET', + 'endpoint' => 'https://home.hm-prod.ecobee.com/home/' . $home_id . '/devices', + 'arguments' => [ + ] + ] + ); + + /** + * This is a select distinct from ecobee_thermostat. Ideally it + * would be possible to send *all* serial numbers from the devices + * call to the GET->thermostat API call, but that throws an error if + * you include a serial number for something that's not a + * thermostat. So I have to keep this array to identify valid serial + * numbers. + */ + $model_numbers = [ + 'athenaSmart', + 'apolloSmart', + 'idtSmart', + 'nikeSmart', + 'siSmart', + 'corSmart', + 'vulcanSmart', + 'aresSmart', + 'artemisSmart' + ]; + + foreach($devices['devices'] as $device) { + if(in_array($device['modelNumber'], $model_numbers) === true) { + $serial_numbers[] = $device['serialNumber']; + } + } + } + + $response = $this->api( + 'ecobee', + 'ecobee_api', + [ + 'method' => 'GET', + 'endpoint' => 'thermostat', + 'arguments' => [ + 'body' => json_encode([ + 'selection' => array_merge( + [ + 'selectionType' => 'thermostats', + 'selectionMatch' => implode(',', $serial_numbers), + ], + $include + ) + ]) + ] + ] + ); + } else { + throw $e; + } + } // Loop over the returned thermostats and create/update them as necessary. $thermostat_ids_to_keep = [];