[ 'read_id' ], 'public' => [] ]; /** * Sync thermostats. */ public function sync() { // Get the thermostat list from ecobee with sensors. Keep this identical to // ecobee_sensor->sync() to leverage caching. $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 */ /** * '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 { /** * This will force the device sync to use the secondary method that uses * the undocumented API calls instead of the normal GET "registered" * thermostats. This fixes an issue where beestat never sees shared * thermostats if there is at least one registered thermostat. */ $user = $this->api('user', 'get', $this->session->get_user_id()); if( isset($user['settings']['app']) === true && isset($user['settings']['app']['prefer_secondary_device_sync']) === true && $user['settings']['app']['prefer_secondary_device_sync'] === true ) { throw new cora\exception('No thermostats found.', 10511, false, null, false); } $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']; } } } if(count($serial_numbers) > 0) { try { $response = $this->api( 'ecobee', 'ecobee_api', [ 'method' => 'GET', 'endpoint' => 'thermostat', 'arguments' => [ 'body' => json_encode([ 'selection' => array_merge( [ 'selectionType' => 'thermostats', 'selectionMatch' => implode(',', $serial_numbers), ], $include ) ]) ] ] ); } catch(cora\exception $e) { /** * For some reason, I can get a serial number in the /homes data * and still get no results from the /thermostat endpoint. Likely * due to two data sources not being in sync. Catch that exception * and let the code continue so any existing thermostats still get * inactivated. * * Also have to fabricate the $response a bit. */ if($e->getCode() === 10511) { $response = [ 'thermostatList' => [] ]; } else { throw new cora\exception($e->getMessage(), $e->getCode(), $e->getReportable(), $e->getExtraInfo(), $e->getRollback()); } } /** * At this point, $response will either be populated with results, * or have an empty thermostatList attribute. The code can continue * on as it will inactivate any thermostats that were not found. */ } } else { throw new cora\exception($e->getMessage(), $e->getCode(), $e->getReportable(), $e->getExtraInfo(), $e->getRollback()); } } // Loop over the returned thermostats and create/update them as necessary. $thermostat_ids_to_keep = []; $email_addresses = []; foreach($response['thermostatList'] as $api_thermostat) { $ecobee_thermostat = $this->get( [ 'identifier' => $api_thermostat['identifier'] ] ); if ($ecobee_thermostat !== null) { // Thermostat exists. $thermostat = $this->api( 'thermostat', 'get', [ 'attributes' => [ 'ecobee_thermostat_id' => $ecobee_thermostat['ecobee_thermostat_id'] ] ] ); } else { // Thermostat does not exist. $ecobee_thermostat = $this->create([ 'identifier' => $api_thermostat['identifier'] ]); $thermostat = $this->api( 'thermostat', 'create', [ 'attributes' => [ 'ecobee_thermostat_id' => $ecobee_thermostat['ecobee_thermostat_id'], 'alerts' => [] ] ] ); } $thermostat_ids_to_keep[] = $thermostat['thermostat_id']; $ecobee_thermostat = $this->update( [ 'ecobee_thermostat_id' => $ecobee_thermostat['ecobee_thermostat_id'], 'name' => $api_thermostat['name'], 'identifier' => $api_thermostat['identifier'], 'utc_time' => $api_thermostat['utcTime'], 'model_number' => $api_thermostat['modelNumber'], 'runtime' => $api_thermostat['runtime'], 'extended_runtime' => $api_thermostat['extendedRuntime'], 'electricity' => $api_thermostat['electricity'], 'settings' => $api_thermostat['settings'], 'location' => $api_thermostat['location'], 'program' => $api_thermostat['program'], 'events' => $api_thermostat['events'], 'device' => $api_thermostat['devices'], 'technician' => $api_thermostat['technician'], 'utility' => $api_thermostat['utility'], 'management' => $api_thermostat['management'], 'alerts' => $api_thermostat['alerts'], 'weather' => $api_thermostat['weather'], 'house_details' => $api_thermostat['houseDetails'], 'oem_cfg' => $api_thermostat['oemCfg'], 'equipment_status' => trim($api_thermostat['equipmentStatus']) !== '' ? explode(',', $api_thermostat['equipmentStatus']) : [], 'notification_settings' => $api_thermostat['notificationSettings'], 'privacy' => $api_thermostat['privacy'], 'version' => $api_thermostat['version'], 'remote_sensors' => $api_thermostat['remoteSensors'], 'audio' => $api_thermostat['audio'], 'inactive' => 0 ] ); foreach($api_thermostat['notificationSettings']['emailAddresses'] as $email_address) { if(preg_match('/.+@.+\..+/', $email_address) === 1) { $email_addresses[] = trim(strtolower($email_address)); } } // Grab a bunch of attributes from the ecobee_thermostat and attach them // to the thermostat. $attributes = []; $attributes['name'] = $api_thermostat['name']; $attributes['inactive'] = 0; // Temperature. Ignore values outside possible ranges available. if( ($api_thermostat['runtime']['actualTemperature'] / 10) > 120 || ($api_thermostat['runtime']['actualTemperature'] / 10) < -10 ) { $attributes['temperature'] = null; } else { $attributes['temperature'] = ($api_thermostat['runtime']['actualTemperature'] / 10); } $attributes['temperature_unit'] = $api_thermostat['settings']['useCelsius'] === true ? '°C' : '°F'; // There are some instances where ecobee gives invalid humidity values. if( $api_thermostat['runtime']['actualHumidity'] > 100 || $api_thermostat['runtime']['actualHumidity'] < 0 ) { $attributes['humidity'] = null; } else { $attributes['humidity'] = $api_thermostat['runtime']['actualHumidity']; } // Heat setpoint. Ignore values outside possible ranges available. if( ($api_thermostat['runtime']['desiredHeat'] / 10) > 120 || ($api_thermostat['runtime']['desiredHeat'] / 10) < 45 ) { $attributes['setpoint_heat'] = null; } else { $attributes['setpoint_heat'] = ($api_thermostat['runtime']['desiredHeat'] / 10); } // Cool setpoint. Ignore values outside possible ranges available. if( ($api_thermostat['runtime']['desiredCool'] / 10) > 120 || ($api_thermostat['runtime']['desiredCool'] / 10) < -10 ) { $attributes['setpoint_cool'] = null; } else { $attributes['setpoint_cool'] = ($api_thermostat['runtime']['desiredCool'] / 10); } $attributes['first_connected'] = $api_thermostat['runtime']['firstConnected']; $address = $this->get_address($thermostat, $ecobee_thermostat); $attributes['address_id'] = $address['address_id']; $attributes['property'] = $this->get_property($thermostat, $ecobee_thermostat); $attributes['filters'] = $this->get_filters($thermostat, $ecobee_thermostat); $attributes['weather'] = $this->get_weather($thermostat, $ecobee_thermostat); $attributes['settings'] = $this->get_settings($thermostat, $ecobee_thermostat); $attributes['time_zone'] = $this->get_time_zone($thermostat, $ecobee_thermostat); $attributes['program'] = $this->get_program($thermostat, $ecobee_thermostat); $detected_system_type = $this->get_detected_system_type($thermostat, $ecobee_thermostat); if($thermostat['system_type'] === null) { $attributes['system_type'] = [ 'reported' => [ 'heat' => [ 'equipment' => null, 'stages' => null ], 'auxiliary_heat' => [ 'equipment' => null, 'stages' => null ], 'cool' => [ 'equipment' => null, 'stages' => null ] ], 'detected' => $detected_system_type ]; } else { $attributes['system_type'] = [ 'reported' => $thermostat['system_type']['reported'], 'detected' => $detected_system_type ]; } $attributes['running_equipment'] = $this->get_running_equipment( $thermostat, $ecobee_thermostat, $attributes['system_type'] ); $attributes['alerts'] = $this->get_alerts( $thermostat, $ecobee_thermostat, $attributes['system_type'] ); $this->api( 'thermostat', 'update', [ 'attributes' => array_merge( ['thermostat_id' => $thermostat['thermostat_id']], $attributes ) ] ); } // Update the email_address on the user. if(count($email_addresses) > 0) { $email_address_counts = array_count_values($email_addresses); arsort($email_address_counts); $email_address = array_keys(array_slice($email_address_counts, 0, 1, true))[0]; $this->api('user', 'update', [ 'attributes' => [ 'user_id' => $this->session->get_user_id(), 'email_address' => $email_address ] ]); } // Inactivate any ecobee_thermostats that were no longer returned. $thermostats = $this->api('thermostat', 'read', ['attributes' => ['inactive' => false]]); $ecobee_thermostat_ids_to_return = []; foreach($thermostats as $thermostat) { if(in_array($thermostat['thermostat_id'], $thermostat_ids_to_keep) === false) { $this->update( [ 'ecobee_thermostat_id' => $thermostat['ecobee_thermostat_id'], 'inactive' => true ] ); $this->api( 'thermostat', 'update', [ 'attributes' => [ 'thermostat_id' => $thermostat['thermostat_id'], 'inactive' => true ], ] ); } else { $ecobee_thermostat_ids_to_return[] = $thermostat['ecobee_thermostat_id']; } } if (count($ecobee_thermostat_ids_to_return) === 0) { return []; } else { return $this->read_id(['ecobee_thermostat_id' => $ecobee_thermostat_ids_to_return]); } } /** * Get the address for the given thermostat. * * @param array $thermostat * @param array $ecobee_thermostat * * @return array */ private function get_address($thermostat, $ecobee_thermostat) { $address = []; if(isset($ecobee_thermostat['location']['streetAddress']) === true) { $address['line_1'] = $ecobee_thermostat['location']['streetAddress']; } if(isset($ecobee_thermostat['location']['city']) === true) { $address['locality'] = $ecobee_thermostat['location']['city']; } if(isset($ecobee_thermostat['location']['provinceState']) === true) { $address['administrative_area'] = $ecobee_thermostat['location']['provinceState']; } if(isset($ecobee_thermostat['location']['postalCode']) === true) { $address['postal_code'] = $ecobee_thermostat['location']['postalCode']; } if( isset($ecobee_thermostat['location']['country']) === true && trim($ecobee_thermostat['location']['country']) !== '' ) { if(preg_match('/(^USA?$)|(united.?states)/i', $ecobee_thermostat['location']['country']) === 1) { $country = 'USA'; } else { $country = $ecobee_thermostat['location']['country']; } } else { // If all else fails, assume USA. $country = 'USA'; } return $this->api( 'address', 'search', [ 'address' => $address, 'country' => $country ] ); } /** * Get details about the thermostat's property. * * @param array $thermostat * @param array $ecobee_thermostat * * @return array */ private function get_property($thermostat, $ecobee_thermostat) { $property = []; /** * Example values from ecobee: "0", "apartment", "Apartment", "Condo", * "condominium", "detached", "Detached", "I don't know", "loft", "Multi * Plex", "multiPlex", "other", "Other", "rowHouse", "Semi-Detached", * "semiDetached", "townhouse", "Townhouse" */ $property['structure_type'] = null; if(isset($ecobee_thermostat['house_details']['style']) === true) { $structure_type = $ecobee_thermostat['house_details']['style']; if(preg_match('/^detached$/i', $structure_type) === 1) { $property['structure_type'] = 'detached'; } else if(preg_match('/apartment/i', $structure_type) === 1) { $property['structure_type'] = 'apartment'; } else if(preg_match('/^condo/i', $structure_type) === 1) { $property['structure_type'] = 'condominium'; } else if(preg_match('/^loft/i', $structure_type) === 1) { $property['structure_type'] = 'loft'; } else if(preg_match('/multi[^a-z]?plex/i', $structure_type) === 1) { $property['structure_type'] = 'multiplex'; } else if(preg_match('/(town|row)(house|home)/i', $structure_type) === 1) { $property['structure_type'] = 'townhouse'; } else if(preg_match('/semi[^a-z]?detached/i', $structure_type) === 1) { $property['structure_type'] = 'semi-detached'; } } /** * Example values from ecobee: "0", "1", "2", "3", "4", "5", "8", "9", "10" */ $property['stories'] = null; if(isset($ecobee_thermostat['house_details']['numberOfFloors']) === true) { $stories = $ecobee_thermostat['house_details']['numberOfFloors']; if(ctype_digit((string) $stories) === true && $stories > 0) { $property['stories'] = (int) $stories; } } /** * Example values from ecobee: "0", "5", "500", "501", "750", "1000", * "1001", "1050", "1200", "1296", "1400", "1500", "1501", "1600", "1750", * "1800", "1908", "2000", "2400", "2450", "2500", "2600", "2750", "2800", * "2920", "3000", "3200", "3437", "3500", "3600", "4000", "4500", "5000", * "5500", "5600", "6000", "6500", "6800", "7000", "7500", "7800", "8000", * "9000", "9500", "10000" */ $property['square_feet'] = null; if(isset($ecobee_thermostat['house_details']['size']) === true) { $square_feet = $ecobee_thermostat['house_details']['size']; if(ctype_digit((string) $square_feet) === true && $square_feet > 0) { $property['square_feet'] = round($square_feet / 500) * 500; } } /** * Example values from ecobee: 0, 1, 10, 100, 101, 102, 103, 104, 105, * 106, 107, 108, 109, 11, 110, 111, 112, 113, 114, 115, 116, 117, 118, * 119, 12, 120, 121, 122, 123, 124, 125, 126, 127, 128, 13, 14, 15, 16, * 17, 18, 19, 1930, 1931, 1941, 1950, 1951, 1960, 1961, 1970, 1971, 1980, * 1981, 1990, 1991, 2, 20, 2000, 2001, 2010, 2011, 2020, 2021, 21, 22, * 23, 24, 25, 26, 27, 28, 29, 3, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, * 4, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 5, 50, 51, 52, 53, 54, 55, * 56, 57, 58, 59, 6, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 7, 70, 71, * 72, 73, 74, 75, 76, 77, 78, 79, 8, 80, 81, 82, 83, 84, 85, 86, 87, 88, * 89, 9, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99 */ $property['age'] = null; if(isset($ecobee_thermostat['house_details']['age']) === true) { $age = $ecobee_thermostat['house_details']['age']; if(ctype_digit((string) $age) === true) { $property['age'] = (int) $age; // Fix for #378 - the ecobee app stores year, the website stores age. if($property['age'] > 1000) { $property['age'] = date('Y') - $property['age']; } } } return $property; } /** * Get details about the different filters and things. * * @param array $thermostat * @param array $ecobee_thermostat * * @return array */ private function get_filters($thermostat, $ecobee_thermostat) { $filters = []; $supported_types = [ 'furnaceFilter' => [ 'key' => 'furnace', 'sum_column' => 'sum_fan' ], 'humidifierFilter' => [ 'key' => 'humidifier', 'sum_column' => 'sum_humidifier' ], 'dehumidifierFilter' => [ 'key' => 'dehumidifier', 'sum_column' => 'sum_dehumidifier' ], 'ventilator' => [ 'key' => 'ventilator', 'sum_column' => 'sum_ventilator' ], 'uvLamp' => [ 'key' => 'uv_lamp', 'sum_column' => 'sum_fan' ] ]; $sums = []; $min_timestamp = INF; if(isset($ecobee_thermostat['notification_settings']['equipment']) === true) { foreach($ecobee_thermostat['notification_settings']['equipment'] as $notification) { if($notification['enabled'] === true && isset($supported_types[$notification['type']]) === true) { $key = $supported_types[$notification['type']]['key']; $sum_column = $supported_types[$notification['type']]['sum_column']; $filters[$key] = [ 'last_changed' => $notification['filterLastChanged'], 'life' => $notification['filterLife'], 'life_units' => $notification['filterLifeUnits'] ]; $sums[] = 'sum(case when `date` > "' . $notification['filterLastChanged'] . '" then `' . $sum_column . '` else 0 end) `' . $key . '`'; $min_timestamp = min($min_timestamp, strtotime($notification['filterLastChanged'])); } } } if(count($filters) > 0) { $query = ' select ' . implode(',', $sums) . ' from runtime_thermostat_summary where `user_id` = "' . $this->session->get_user_id() . '" and `thermostat_id` = "' . $thermostat['thermostat_id'] . '" and `date` >= "' . date('Y-m-d', $min_timestamp) . '" '; $result = $this->database->query($query); $row = $result->fetch_assoc(); foreach($row as $key => $value) { $filters[$key]['runtime'] = (int) $value; } } return $filters; } /** * Get whatever the alerts should be set to. * * @param array $thermostat * @param array $ecobee_thermostat * @param array $system_type * * @return array */ private function get_alerts($thermostat, $ecobee_thermostat, $system_type) { // Get a list of all ecobee thermostat alerts $new_alerts = []; foreach($ecobee_thermostat['alerts'] as $ecobee_thermostat_alert) { $alert = []; $alert['timestamp'] = date( 'Y-m-d H:i:s', strtotime($ecobee_thermostat_alert['date'] . ' ' . $ecobee_thermostat_alert['time']) ); $alert['text'] = $ecobee_thermostat_alert['text']; $alert['code'] = $ecobee_thermostat_alert['alertNumber']; $alert['details'] = 'N/A'; $alert['source'] = 'thermostat'; $alert['dismissed'] = false; $alert['guid'] = $this->get_alert_guid($alert); $new_alerts[$alert['guid']] = $alert; } // Has heat or cool if($system_type['reported']['heat'] !== null) { $system_type_heat = $system_type['reported']['heat']['equipment']; } else { $system_type_heat = $system_type['detected']['heat']['equipment']; } if($system_type['reported']['auxiliary_heat'] !== null) { $system_type_auxiliary_heat = $system_type['reported']['auxiliary_heat']['equipment']; } else { $system_type_auxiliary_heat = $system_type['detected']['auxiliary_heat']['equipment']; } $has_heat = ( $system_type_heat !== 'none' || $system_type_auxiliary_heat !== 'none' ); if($system_type['reported']['cool'] !== null) { $system_type_cool = $system_type['reported']['cool']['equipment']; } else { $system_type_cool = $system_type['detected']['cool']['equipment']; } $has_cool = ($system_type_cool !== 'none'); // Cool Differential Temperature if( $has_cool === true && $ecobee_thermostat['settings']['stage1CoolingDifferentialTemp'] / 10 === 0.5 ) { $alert = [ 'timestamp' => date('Y-m-d H:i:s'), 'text' => 'Cool Differential Temperature is set to 0.5°F; we recommend at least 1.0°F', 'details' => 'Low values for this setting will generally not cause any harm, but they do contribute to short cycling and decreased efficiency.', 'code' => 100000, 'source' => 'beestat', 'dismissed' => false ]; $alert['guid'] = $this->get_alert_guid($alert); $new_alerts[$alert['guid']] = $alert; } // Heat Differential Temperature if( $has_heat === true && $ecobee_thermostat['settings']['stage1HeatingDifferentialTemp'] / 10 === 0.5 ) { $alert = [ 'timestamp' => date('Y-m-d H:i:s'), 'text' => 'Heat Differential Temperature is set to 0.5°F; we recommend at least 1.0°F', 'details' => 'Low values for this setting will generally not cause any harm, but they do contribute to short cycling and decreased efficiency.', 'code' => 100001, 'source' => 'beestat', 'dismissed' => false ]; $alert['guid'] = $this->get_alert_guid($alert); $new_alerts[$alert['guid']] = $alert; } // Get the guids for easy comparison $new_guids = array_column($new_alerts, 'guid'); $existing_guids = array_column($thermostat['alerts'], 'guid'); $guids_to_add = array_diff($new_guids, $existing_guids); $guids_to_remove = array_diff($existing_guids, $new_guids); // Remove any removed alerts $final_alerts = $thermostat['alerts']; foreach($final_alerts as $key => $thermostat_alert) { if(in_array($thermostat_alert['guid'], $guids_to_remove) === true) { unset($final_alerts[$key]); } } // Add any new alerts foreach($guids_to_add as $guid) { $final_alerts[] = $new_alerts[$guid]; } return array_values($final_alerts); } /** * Get the GUID for an alert. Basically if the text and the source are the * same then it's considered the same alert. Timestamp could be included for * ecobee alerts but since beestat alerts are constantly re-generated the * timestamp always changes. * * @param array $alert * * @return string */ private function get_alert_guid($alert) { return sha1($alert['text'] . $alert['source']); } /** * Try and detect the type of HVAC system. * * @param array $thermostat * @param array $ecobee_thermostat * * @return array System type for each of heat, cool, and aux. */ private function get_detected_system_type($thermostat, $ecobee_thermostat) { $detected_system_type = []; $settings = $ecobee_thermostat['settings']; $devices = $ecobee_thermostat['device']; // Get a list of all outputs. These get their type set when they get // connected to a wire so it's a pretty reliable way to see what's hooked // up. $outputs = []; foreach($devices as $device) { foreach($device['outputs'] as $output) { if($output['type'] !== 'none') { $outputs[] = $output['type']; } } } // Heat $detected_system_type['heat'] = [ 'equipment' => null, 'stages' => null ]; if($settings['heatPumpGroundWater'] === true) { $detected_system_type['heat']['equipment'] = 'geothermal'; } else if($settings['hasHeatPump'] === true) { $detected_system_type['heat']['equipment'] = 'compressor'; } else if($settings['hasBoiler'] === true) { $detected_system_type['heat']['equipment'] = 'boiler'; } else if(in_array('heat1', $outputs) === true) { // This is the fastest way I was able to determine this. The further north // you are the less likely you are to use electric heat. if($thermostat['address_id'] !== null) { $address = $this->api('address', 'get', $thermostat['address_id']); if( isset($address['normalized']['metadata']['latitude']) === true && $address['normalized']['metadata']['latitude'] > 30 ) { $detected_system_type['heat']['equipment'] = 'gas'; } else { $detected_system_type['heat']['equipment'] = 'electric'; } } else { $detected_system_type['heat']['equipment'] = 'electric'; } } else { $detected_system_type['heat']['equipment'] = 'none'; } // Rudimentary aux heat guess. It's pretty good overall but not as good as // heat/cool. $detected_system_type['auxiliary_heat'] = [ 'equipment' => null, 'stages' => null ]; if( $detected_system_type['heat']['equipment'] === 'gas' || $detected_system_type['heat']['equipment'] === 'boiler' || $detected_system_type['heat']['equipment'] === 'oil' || $detected_system_type['heat']['equipment'] === 'electric' ) { $detected_system_type['auxiliary_heat']['equipment'] = 'none'; } else if($detected_system_type['heat']['equipment'] === 'compressor') { $detected_system_type['auxiliary_heat']['equipment'] = 'electric'; } // Cool $detected_system_type['cool'] = [ 'equipment' => null, 'stages' => null ]; if($settings['heatPumpGroundWater'] === true) { $detected_system_type['cool']['equipment'] = 'geothermal'; } else if(in_array('compressor1', $outputs) === true) { $detected_system_type['cool']['equipment'] = 'compressor'; } else { $detected_system_type['cool']['equipment'] = 'none'; } /** * Stages. For whatever reason, heat stages seem wrong. They appear to * match the number of "furnace" stages on the ecobee. Attempt to fix this * by assuming that if heat and cool are both a compressor then pick the * max of these two for both. */ if( $detected_system_type['heat']['equipment'] === 'compressor' && $detected_system_type['cool']['equipment'] === 'compressor' ) { $stages = max( $ecobee_thermostat['settings']['coolStages'], $ecobee_thermostat['settings']['heatStages'] ); $detected_system_type['heat']['stages'] = $stages; $detected_system_type['cool']['stages'] = $stages; } else { $detected_system_type['heat']['stages'] = $ecobee_thermostat['settings']['heatStages']; $detected_system_type['cool']['stages'] = $ecobee_thermostat['settings']['coolStages']; } return $detected_system_type; } /** * Get the current weather status. * * @param array $thermostat * @param array $ecobee_thermostat * * @return array */ private function get_weather($thermostat, $ecobee_thermostat) { $weather = [ 'dew_point' => null, 'barometric_pressure' => null, 'humidity_relative' => null, 'temperature_high' => null, 'temperature_low' => null, 'temperature' => null, 'wind_bearing' => null, 'wind_speed' => null, 'condition' => null ]; if( isset($ecobee_thermostat['weather']['forecasts']) === true && isset($ecobee_thermostat['weather']['forecasts'][0]) === true ) { $ecobee_weather = $ecobee_thermostat['weather']['forecasts'][0]; if(isset($ecobee_weather['dewpoint']) === true) { $weather['dew_point'] = ($ecobee_weather['dewpoint'] / 10); } // Returned in MB (divide by 33.864 to get inHg) if(isset($ecobee_weather['pressure']) === true) { $weather['barometric_pressure'] = $ecobee_weather['pressure']; } if(isset($ecobee_weather['relativeHumidity']) === true) { $weather['humidity_relative'] = $ecobee_weather['relativeHumidity']; } if(isset($ecobee_weather['tempHigh']) === true) { $weather['temperature_high'] = ($ecobee_weather['tempHigh'] / 10); } if(isset($ecobee_weather['tempLow']) === true) { $weather['temperature_low'] = ($ecobee_weather['tempLow'] / 10); } if(isset($ecobee_weather['temperature']) === true) { $weather['temperature'] = ($ecobee_weather['temperature'] / 10); } if(isset($ecobee_weather['windBearing']) === true) { $weather['wind_bearing'] = $ecobee_weather['windBearing']; } // mph if(isset($ecobee_weather['windSpeed']) === true) { $weather['wind_speed'] = $ecobee_weather['windSpeed']; } if(isset($ecobee_weather['weatherSymbol']) === true) { switch($ecobee_weather['weatherSymbol']) { case 0: $weather['condition'] = 'sunny'; break; case 1: $weather['condition'] = 'few_clouds'; break; case 2: $weather['condition'] = 'partly_cloudy'; break; case 3: $weather['condition'] = 'mostly_cloudy'; break; case 4: $weather['condition'] = 'overcast'; break; case 5: $weather['condition'] = 'drizzle'; break; case 6: $weather['condition'] = 'rain'; break; case 7: $weather['condition'] = 'freezing_rain'; break; case 8: $weather['condition'] = 'showers'; break; case 9: $weather['condition'] = 'hail'; break; case 10: $weather['condition'] = 'snow'; break; case 11: $weather['condition'] = 'flurries'; break; case 12: $weather['condition'] = 'freezing_snow'; break; case 13: $weather['condition'] = 'blizzard'; break; case 14: $weather['condition'] = 'pellets'; break; case 15: $weather['condition'] = 'thunderstorm'; break; case 16: $weather['condition'] = 'windy'; break; case 17: $weather['condition'] = 'tornado'; break; case 18: $weather['condition'] = 'fog'; break; case 19: $weather['condition'] = 'haze'; break; case 20: $weather['condition'] = 'smoke'; break; case 21: $weather['condition'] = 'dust'; break; } } } return $weather; } /** * Get certain settings. * * @param array $thermostat * @param array $ecobee_thermostat * * @return array */ private function get_settings($thermostat, $ecobee_thermostat) { $settings = []; if(isset($ecobee_thermostat['settings']['stage1CoolingDifferentialTemp']) === true) { $settings['differential_cool'] = ($ecobee_thermostat['settings']['stage1CoolingDifferentialTemp'] / 10); } if(isset($ecobee_thermostat['settings']['stage1HeatingDifferentialTemp']) === true) { $settings['differential_heat'] = ($ecobee_thermostat['settings']['stage1HeatingDifferentialTemp'] / 10); } return $settings; } /** * Get program. This is just a clone of the ecobee_thermostat program with a * slight modification to the temperature values. Divide by 10 since this * data is returned directly by the API. * * @param array $thermostat * @param array $ecobee_thermostat * * @return array */ private function get_program($thermostat, $ecobee_thermostat) { $program = $ecobee_thermostat['program']; if(isset($program['climates']) === true) { foreach($program['climates'] as &$climate) { $climate['coolTemp'] = $climate['coolTemp'] / 10; $climate['heatTemp'] = $climate['heatTemp'] / 10; } } return $program; } /** * Get the currently running equipment. Just looks at the ecobee list and * translates it a bit. * * @param array $thermostat * @param array $ecobee_thermostat * @param array $system_type * * @return array */ private function get_running_equipment($thermostat, $ecobee_thermostat, $system_type) { $running_equipment = []; foreach($ecobee_thermostat['equipment_status'] as $equipment) { switch($equipment) { case 'heatPump': $running_equipment[] = 'heat_1'; break; case 'heatPump2': $running_equipment[] = 'heat_2'; break; case 'heatPump3': $running_equipment[] = 'heat_3'; break; case 'compCool1': $running_equipment[] = 'cool_1'; break; case 'compCool2': $running_equipment[] = 'cool_2'; break; case 'auxHeat1': if ($system_type['detected']['heat']['equipment'] === 'compressor') { $running_equipment[] = 'auxiliary_heat_1'; } else { $running_equipment[] = 'heat_1'; } break; case 'auxHeat2': if ($system_type['detected']['heat']['equipment'] === 'compressor') { $running_equipment[] = 'auxiliary_heat_2'; } else { $running_equipment[] = 'heat_2'; } break; case 'auxHeat3': if ($system_type['detected']['heat']['equipment'] === 'compressor') { $running_equipment[] = 'auxiliary_heat_3'; } else { $running_equipment[] = 'heat_3'; } break; case 'fan': $running_equipment[] = 'fan'; break; case 'humidifier': $running_equipment[] = 'humidifier'; break; case 'dehumidifier': $running_equipment[] = 'dehumidifier'; break; case 'ventilator': $running_equipment[] = 'ventilator'; break; case 'economizer': $running_equipment[] = 'economizer'; break; case 'compHotWater': $running_equipment[] = 'heat_1'; break; case 'auxHotWater': $running_equipment[] = 'auxiliary_heat_1'; break; default: throw new \Exception('Unknown equipment running.', 10800); break; } } return $running_equipment; } /** * Get the current time zone. It's usually set. If not set use the offset * minutes to find it. Worst case default to the most common time zone. * * @param array $thermostat * @param array $ecobee_thermostat * * @return The time zone. */ private function get_time_zone($thermostat, $ecobee_thermostat) { $time_zone = $ecobee_thermostat['location']['timeZone']; if (in_array($time_zone, timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)) === true) { return $time_zone; } else if ($ecobee_thermostat['location']['timeZoneOffsetMinutes'] !== '') { $offset_seconds = $ecobee_thermostat['location']['timeZoneOffsetMinutes'] * 60; $time_zone = timezone_name_from_abbr('', $offset_seconds, 1); // Workaround for bug #44780 if ($time_zone === false) { $time_zone = timezone_name_from_abbr('', $offset_seconds, 0); } return $time_zone; } else { return 'America/New_York'; } } }