Skip to content
  • Watch
    Notifications
Permalink
Branch: master
Find file Copy path
Find file Copy path
15 contributors

Users who have contributed to this file

@gboudreau @bauzer714 @chawkinsuf @rbrenton @MikeKemmerer @oyarzun @rkaramchedu @philharle @andyg5000 @scottarver @rossking @thomric2 @themoosman @beavel @t1n1wall
1427 lines (1338 sloc) 67.7 KB

Code navigation is available!

Navigate your code with ease. Click on function and method calls to jump to their definitions or references in the same repository. Learn more

<?php
defined('DATE_FORMAT') OR define('DATE_FORMAT', 'Y-m-d');
defined('DATETIME_FORMAT') OR define('DATETIME_FORMAT', DATE_FORMAT . ' H:i:s');
define('TARGET_TEMP_MODE_COOL', 'cool');
define('TARGET_TEMP_MODE_HEAT', 'heat');
define('TARGET_TEMP_MODE_RANGE', 'range');
define('TARGET_TEMP_MODE_OFF', 'off');
define('ECO_MODE_MANUAL', 'manual-eco');
define('ECO_MODE_SCHEDULE', 'schedule');
define('FAN_MODE_AUTO', 'auto');
define('FAN_MODE_ON', 'on');
define('FAN_MODE_EVERY_DAY_ON', 'on');
define('FAN_MODE_EVERY_DAY_OFF', 'auto');
define('FAN_MODE_MINUTES_PER_HOUR', 'duty-cycle');
define('FAN_MODE_MINUTES_PER_HOUR_15', FAN_MODE_MINUTES_PER_HOUR . ',900');
define('FAN_MODE_MINUTES_PER_HOUR_30', FAN_MODE_MINUTES_PER_HOUR . ',1800');
define('FAN_MODE_MINUTES_PER_HOUR_45', FAN_MODE_MINUTES_PER_HOUR . ',2700');
define('FAN_MODE_MINUTES_PER_HOUR_ALWAYS_ON', 'on,3600');
define('FAN_MODE_TIMER', '');
define('FAN_TIMER_15M', ',900');
define('FAN_TIMER_30M', ',1800');
define('FAN_TIMER_45M', ',2700');
define('FAN_TIMER_1H', ',3600');
define('FAN_TIMER_2H', ',7200');
define('FAN_TIMER_4H', ',14400');
define('FAN_TIMER_8H', ',28800');
define('FAN_TIMER_12H', ',43200');
define('AWAY_MODE_ON', TRUE);
define('AWAY_MODE_OFF', FALSE);
define('DUALFUEL_BREAKPOINT_ALWAYS_PRIMARY', 'always-primary');
define('DUALFUEL_BREAKPOINT_ALWAYS_ALT', 'always-alt');
define('DEVICE_WITH_NO_NAME', 'Not Set');
define('DEVICE_TYPE_THERMOSTAT', 'thermostat');
define('DEVICE_TYPE_PROTECT', 'protect');
define('DEVICE_TYPE_SENSOR', 'sensor');
define('NESTAPI_DEVICE_TYPE_SENSOR', 'kryptonite.');
define('NESTAPI_ERROR_UNDER_MAINTENANCE', 1000);
define('NESTAPI_ERROR_EMPTY_RESPONSE', 1001);
define('NESTAPI_ERROR_NOT_JSON_RESPONSE', 1002);
define('NESTAPI_ERROR_API_JSON_ERROR', 1003);
define('NESTAPI_ERROR_API_OTHER_ERROR', 1004);
/**
* Unofficial Nest API
*
* This is an unofficial PHP class that will allow you to monitor and control your Nest Learning Thermostat, and Nest Protect.
*
* @category Algorithm
* @package PommePause\Nest\API
* @author Guillaume Boudreau <guillaume@pommepause.com>
* @license GNU LESSER GENERAL PUBLIC LICENSE Version 3
* @link https://github.com/gboudreau/nest-api/
* @link https://nest.com/
*/
class Nest
{
const USER_AGENT = 'Nest/5.0.0.23 (iOScom.nestlabs.jasper.release) os=11.0';
const PROTOCOL_VERSION = 1;
const LOGIN_URL = 'https://home.nest.com/session';
protected $days_maps = array('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun');
protected $transport_url;
protected $access_token;
protected $user;
protected $userid;
protected $cookie_file;
protected $cache_file;
protected $cache_expiration;
protected $last_status;
/**
* Constructor
*
* @param string|null $username Your Nest username.
* @param string|null $password Your Nest password.
* @param string|null $issue_token Issue-token URL
* @param string|null $cookies Google cookies
*
* @throws InvalidArgumentException|UnexpectedValueException|RuntimeException
*/
public function __construct($username = NULL, $password = NULL, $issue_token = NULL, $cookies = NULL) {
if ($issue_token === NULL && defined('ISSUE_TOKEN')) {
$issue_token = ISSUE_TOKEN;
}
if ($cookies === NULL && defined('COOKIES')) {
$cookies = COOKIES;
}
if (!empty($issue_token)) {
$this->issue_token = $issue_token;
if (empty($cookies)) {
throw new InvalidArgumentException('Google login requires issue_token and cookie.');
}
$this->cookies = $cookies;
$this->cookie_file = sys_get_temp_dir() . '/nest_php_cookies_' . md5($this->issue_token);
$this->cache_file = sys_get_temp_dir() . '/nest_php_cache_' . md5($this->issue_token);
} else {
if ($username === NULL && defined('USERNAME')) {
$username = USERNAME;
}
if ($password === NULL && defined('PASSWORD')) {
$password = PASSWORD;
}
if ($username === NULL || $password === NULL) {
throw new InvalidArgumentException('Nest credentials were not provided.');
}
$this->username = $username;
$this->password = $password;
$this->cookie_file = sys_get_temp_dir() . '/nest_php_cookies_' . md5($username . $password);
$this->cache_file = sys_get_temp_dir() . '/nest_php_cache_' . md5($username . $password);
}
static::secureTouch($this->cookie_file);
static::secureTouch($this->cache_file);
// Attempt to load the cache
$this->loadCache();
// Log in, if needed
$this->login();
}
/**
* Get the outside temperature & humidity, given a location (zip/postal code & optional country code).
*
* @param string $postal_code Zip or postal code
* @param string $country_code (Optional) Country code
*
* @return stdClass
*
* @throws RuntimeException
*/
public function getWeather($postal_code, $country_code = NULL) {
try {
$url = "https://home.nest.com/api/0.1/weather/forecast/$postal_code";
if (!empty($country_code)) {
$url .= ",$country_code";
}
$weather = $this->doGET($url);
} catch (RuntimeException $ex) {
// NESTAPI_ERROR_NOT_JSON_RESPONSE is kinda normal. The forecast API will often return a '502 Bad Gateway' response... meh.
if ($ex->getCode() != NESTAPI_ERROR_NOT_JSON_RESPONSE) {
throw new RuntimeException("Unexpected issue fetching forecast.", $ex->getCode(), $ex);
}
}
return (object) array(
'outside_temperature' => isset($weather->now->current_temperature) ? $this->temperatureInUserScale((float) $weather->now->current_temperature) : NULL,
'outside_humidity' => isset($weather->now->current_humidity) ? $weather->now->current_humidity : NULL
);
}
/**
* Get a list of all the locations configured in the Nest account.
*
* @return array
*/
public function getUserLocations() {
$this->prepareForGet();
$structures = (array) $this->last_status->structure;
$user_structures = array();
$class_name = get_class($this);
$topaz = isset($this->last_status->topaz) ? $this->last_status->topaz : array();
$kryptonite = isset($this->last_status->kryptonite) ? $this->last_status->kryptonite : array();
foreach ($structures as $struct_id => $structure) {
// Nest Protects at this location (structure)
$protects = array();
$sensors = array();
foreach ($topaz as $protect) {
if ($protect->structure_id == $struct_id) {
$protects[] = $protect->serial_number;
}
}
foreach ($kryptonite as $serial_number => $sensor) {
if ($sensor->structure_id == $struct_id) {
$sensors[] = $serial_number;
}
}
if (empty($protects) && empty($sensors) && empty($structure->devices)) {
continue;
}
$weather_data = $this->getWeather($structure->postal_code, $structure->country_code);
$user_structures[] = (object) array(
'name' => isset($structure->name)?$structure->name:'',
'address' => !empty($structure->street_address) ? $structure->street_address : NULL,
'city' => $structure->location,
'postal_code' => $structure->postal_code,
'country' => $structure->country_code,
'outside_temperature' => $weather_data->outside_temperature,
'outside_humidity' => $weather_data->outside_humidity,
'away' => $structure->away,
'away_last_changed' => !empty($structure->away_timestamp) ? date(DATETIME_FORMAT, $structure->away_timestamp) : NULL,
'thermostats' => array_map(array($class_name, 'cleanDevices'), $structure->devices),
'protects' => $protects,
'sensors' => $sensors,
);
}
return $user_structures;
}
/**
* Get the schedule details for the specified device.
*
* @param string $serial_number The device (thermostat or protect) serial number. Defaults to the first device of the account.
*
* @return array Returns as array, one element for each day of the week for which there has at least one scheduled event.
* Array keys are a textual representation of a day, three letters, as returned by `date('D')`.
* Array values are arrays of scheduled temperatures, including a time (in minutes after midnight),
* and a mode (one of the TARGET_TEMP_MODE_* defines).
*/
public function getDeviceSchedule($serial_number = NULL) {
$this->prepareForGet();
$serial_number = $this->getDefaultSerial($serial_number);
$schedule_days = $this->last_status->schedule->{$serial_number}->days;
$schedule = array();
foreach ((array)$schedule_days as $day => $scheduled_events) {
$events = array();
foreach ($scheduled_events as $scheduled_event) {
if ($scheduled_event->entry_type == 'setpoint') {
$events[(int)$scheduled_event->time] = (object) array(
'time' => $scheduled_event->time/60, // in minutes
'target_temperature' => $scheduled_event->type == 'RANGE' ? array($this->temperatureInUserScale((float)$scheduled_event->{'temp-min'}), $this->temperatureInUserScale((float)$scheduled_event->{'temp-max'})) : $this->temperatureInUserScale((float) $scheduled_event->temp),
'mode' => $scheduled_event->type == 'HEAT' ? TARGET_TEMP_MODE_HEAT : ($scheduled_event->type == 'COOL' ? TARGET_TEMP_MODE_COOL : TARGET_TEMP_MODE_RANGE)
);
}
}
if (!empty($events)) {
ksort($events);
$schedule[(int) $day] = array_values($events);
}
}
ksort($schedule);
$sorted_schedule = array();
foreach ($schedule as $day => $events) {
$sorted_schedule[$this->days_maps[(int) $day]] = $events;
}
return $sorted_schedule;
}
/**
* Get the next scheduled event.
*
* @param string $serial_number The device (thermostat or protect) serial number. Defaults to the first device of the account.
*
* @return stdClass|bool Returns the next scheduled event, or FALSE is there is none.
*/
public function getNextScheduledEvent($serial_number = NULL) {
$schedule = $this->getDeviceSchedule($serial_number);
$next_event = FALSE;
$time = date('H') * 60 + date('i');
for ($i = 0, $day = date('D'); $i++ < 7; $day = date('D', strtotime("+ $i days"))) {
if (isset($schedule[$day])) {
foreach ($schedule[$day] as $event) {
if ($event->time > $time) {
return $event;
}
}
}
$time = 0;
}
return $next_event;
}
/**
* Get the specified device (thermostat or protect) information.
*
* @param string $serial_number The device (thermostat, sensor, or protect) serial number. Defaults to the first device of the account.
*
* @return stdClass
*/
public function getDeviceInfo($serial_number = NULL) {
$this->prepareForGet();
$serial_number = $this->getDefaultSerial($serial_number);
$topaz = isset($this->last_status->topaz) ? $this->last_status->topaz : array();
$kryptonite = isset($this->last_status->kryptonite) ? $this->last_status->kryptonite : array();
foreach ($topaz as $protect) {
if ($serial_number == $protect->serial_number) {
// The specified device is a Nest Protect
$infos = (object) array(
'co_status' => $protect->co_status == 0 ? "OK" : $protect->co_status,
'co_previous_peak' => isset($protect->co_previous_peak) ? $protect->co_previous_peak : NULL,
'co_sequence_number' => $protect->co_sequence_number,
'smoke_status' => $protect->smoke_status == 0 ? "OK" : $protect->smoke_status,
'smoke_sequence_number' => $protect->smoke_sequence_number,
'model' => $protect->model,
'software_version' => $protect->software_version,
'line_power_present' => $protect->line_power_present,
'battery_level' => $protect->battery_level,
'battery_health_state' => $protect->battery_health_state == 0 ? "OK" : $protect->battery_health_state,
'wired_or_battery' => isset($protect->wired_or_battery) ? $protect->wired_or_battery : NULL,
'born_on_date' => isset($protect->device_born_on_date_utc_secs) ? date(DATE_FORMAT, $protect->device_born_on_date_utc_secs) : NULL,
'replace_by_date' => date(DATE_FORMAT, $protect->replace_by_date_utc_secs),
'last_update' => date(DATETIME_FORMAT, $protect->{'$timestamp'}/1000),
'last_manual_test' => $protect->latest_manual_test_start_utc_secs == 0 ? NULL : date(DATETIME_FORMAT, $protect->latest_manual_test_start_utc_secs),
'ntp_green_led_brightness' => isset($protect->ntp_green_led_brightness) ? $protect->ntp_green_led_brightness : NULL,
'tests_passed' => array(
'led' => $protect->component_led_test_passed,
'pir' => $protect->component_pir_test_passed,
'temp' => $protect->component_temp_test_passed,
'smoke' => $protect->component_smoke_test_passed,
'heat' => $protect->component_heat_test_passed,
'wifi' => $protect->component_wifi_test_passed,
'als' => $protect->component_als_test_passed,
'co' => $protect->component_co_test_passed,
'us' => $protect->component_us_test_passed,
'hum' => $protect->component_hum_test_passed,
'speaker' => isset($protect->component_speaker_test_passed) ? $protect->component_speaker_test_passed : NULL,
'buzzer' => isset($protect->component_buzzer_test_passed) ? $protect->component_buzzer_test_passed : NULL,
),
'nest_features' => array(
'night_time_promise' => !empty($protect->ntp_green_led_enable) ? $protect->ntp_green_led_enable : 0,
'night_light' => !empty($protect->night_light_enable) ? $protect->night_light_enable : 0,
'auto_away' => !empty($protect->auto_away) ? $protect->auto_away : 0,
'heads_up' => !empty($protect->heads_up_enable) ? $protect->heads_up_enable : 0,
'steam_detection' => !empty($protect->steam_detection_enable) ? $protect->steam_detection_enable : 0,
'home_alarm_link' => !empty($protect->home_alarm_link_capable) ? $protect->home_alarm_link_capable : 0,
'wired_led_enable' => !empty($protect->wired_led_enable) ? $protect->wired_led_enable : 0,
),
'serial_number' => $protect->serial_number,
'location' => $protect->structure_id,
'network' => (object) array(
'online' => $protect->component_wifi_test_passed,
'local_ip' => $protect->wifi_ip_address,
'mac_address' => $protect->wifi_mac_address
),
'name' => !empty($protect->description) ? $protect->description : DEVICE_WITH_NO_NAME,
'where' => $this->getWhereById($protect->spoken_where_id),
'color' => isset($protect->device_external_color) ? $protect->device_external_color : NULL,
);
return $infos;
}
}
foreach ($kryptonite as $sensor_serial => $sensor) {
if ($serial_number == $sensor_serial) {
// The specified device is a Nest Sensor
$infos = (object) array(
'temperature' => $this->temperatureInUserScale((float) $sensor->current_temperature),
'battery_level' => $sensor->battery_level,
'last_status' => date(DATETIME_FORMAT, $sensor->last_updated_at),
'location' => $sensor->structure_id,
'where' => $this->getWhereById($sensor->where_id),
);
return $infos;
}
}
list(, $structure) = explode('.', $this->last_status->link->{$serial_number}->structure);
$structure_away = $this->last_status->structure->{$structure}->away;
$mode = strtolower($this->last_status->device->{$serial_number}->current_schedule_mode);
$target_mode = $this->last_status->shared->{$serial_number}->target_temperature_type;
$eco_mode = $this->last_status->device->{$serial_number}->eco->mode; // manual-eco, auto-eco, schedule
if ($target_mode == TARGET_TEMP_MODE_OFF) {
$target_temperatures = FALSE; // No target due to it being off
$mode = TARGET_TEMP_MODE_OFF;
} elseif ($eco_mode !== "schedule") {
// We are in eco, thus not actively using the schedule
if ($this->last_status->device->{$serial_number}->away_temperature_low_enabled && $this->last_status->device->{$serial_number}->away_temperature_high_enabled) {
// We have both low and high temp eco temperatures
$mode = TARGET_TEMP_MODE_RANGE;
$target_temperatures = array(
$this->temperatureInUserScale((float) $this->last_status->device->{$serial_number}->away_temperature_low),
$this->temperatureInUserScale((float) $this->last_status->device->{$serial_number}->away_temperature_high)
);
} elseif ($this->last_status->device->{$serial_number}->away_temperature_low_enabled) {
// We have only an eco temp low, i.e. we're heating
$mode = TARGET_TEMP_MODE_HEAT;
$target_temperatures = $this->temperatureInUserScale((float) $this->last_status->device->{$serial_number}->away_temperature_low);
} elseif ($this->last_status->device->{$serial_number}->away_temperature_high_enabled) {
// We have only an eco temp high, i.e. we're cooling
$mode = TARGET_TEMP_MODE_COOL;
$target_temperatures = $this->temperatureInUserScale((float) $this->last_status->device->{$serial_number}->away_temperature_high);
} else {
// We're in eco with no away temperatures set, i.e. we're technically off (safety temps would still kick in)
$mode = TARGET_TEMP_MODE_OFF;
$target_temperatures = FALSE;
}
} elseif ($target_mode === 'range') {
$target_temperatures = array(
$this->temperatureInUserScale((float) $this->last_status->shared->{$serial_number}->target_temperature_low),
$this->temperatureInUserScale((float) $this->last_status->shared->{$serial_number}->target_temperature_high)
);
} else {
// It is either heat or cool mode
$target_temperatures = $this->temperatureInUserScale((float) $this->last_status->shared->{$serial_number}->target_temperature);
}
$current_modes = array();
$current_modes[] = $mode;
if ($eco_mode !== "schedule") {
$current_modes[] = $eco_mode;
}
if ($structure_away) {
$current_modes[] = 'away';
}
//Process sensors associated to this thermostat
$sensors = (object)array('all' => array(), 'active' => array(), 'active_temperatures' => array());
foreach ($this->last_status->rcs_settings->{$serial_number}->associated_rcs_sensors as $sensor_serial) {
$sensor_parsed_serial_number = str_replace(NESTAPI_DEVICE_TYPE_SENSOR, '', $sensor_serial);
$current_sensor = $this->getDeviceInfo($sensor_parsed_serial_number);
$current_sensor->is_active = in_array($sensor_serial, $this->last_status->rcs_settings->{$serial_number}->active_rcs_sensors);
if ($current_sensor->is_active) {
$sensors->active[] = $current_sensor;
$sensors->active_temperatures[] = $current_sensor->temperature;
}
$sensors->all[] = $current_sensor;
}
$infos = (object) array(
'current_state' => (object) array(
'mode' => implode(',', $current_modes),
'temperature' => $this->temperatureInUserScale((float) $this->last_status->shared->{$serial_number}->current_temperature),
'backplate_temperature' => $this->temperatureInUserScale((float) $this->last_status->device->{$serial_number}->backplate_temperature),
'humidity' => $this->last_status->device->{$serial_number}->current_humidity,
'ac' => $this->last_status->shared->{$serial_number}->hvac_ac_state,
'heat' => $this->last_status->shared->{$serial_number}->hvac_heater_state,
'alt_heat' => $this->last_status->shared->{$serial_number}->hvac_alt_heat_state,
'fan' => $this->last_status->shared->{$serial_number}->hvac_fan_state,
'hot_water' => isset($this->last_status->device->{$serial_number}->has_hot_water_control) ? $this->last_status->device->{$serial_number}->hot_water_active : NULL,
'auto_away' => $this->last_status->shared->{$serial_number}->auto_away, // -1 when disabled, 0 when enabled (thermostat can set auto-away), >0 when enabled and active (thermostat is currently in auto-away mode)
'manual_away' => $structure_away, //Leaving this for others - but manual away really doesn't exist anymore and should be removed eventually
'structure_away' => $structure_away,
'leaf' => $this->last_status->device->{$serial_number}->leaf,
'battery_level' => $this->last_status->device->{$serial_number}->battery_level,
'active_stages' => (object) array(
'heat' => (object) array(
'stage1' => $this->last_status->shared->{$serial_number}->hvac_heater_state,
'stage2' => $this->last_status->shared->{$serial_number}->hvac_heat_x2_state,
'stage3' => $this->last_status->shared->{$serial_number}->hvac_heat_x3_state,
'alt' => $this->last_status->shared->{$serial_number}->hvac_alt_heat_state,
'alt_stage2' => $this->last_status->shared->{$serial_number}->hvac_alt_heat_x2_state,
'aux' => $this->last_status->shared->{$serial_number}->hvac_aux_heater_state,
'emergency' => $this->last_status->shared->{$serial_number}->hvac_emer_heat_state,
),
'cool' => (object) array(
'stage1' => $this->last_status->shared->{$serial_number}->hvac_ac_state,
'stage2' => $this->last_status->shared->{$serial_number}->hvac_cool_x2_state,
'stage3' => $this->last_status->shared->{$serial_number}->hvac_cool_x3_state,
),
),
'eco_mode' => $eco_mode,
'eco_temperatures_assist_enabled' => $this->last_status->device->{$serial_number}->auto_away_enable,
'eco_temperatures' => (object) array(
'low' => ($this->last_status->device->{$serial_number}->away_temperature_low_enabled) ? $this->temperatureInUserScale((float)$this->last_status->device->{$serial_number}->away_temperature_low) : FALSE,
'high' => ($this->last_status->device->{$serial_number}->away_temperature_high_enabled) ? $this->temperatureInUserScale((float)$this->last_status->device->{$serial_number}->away_temperature_high) : FALSE,
),
'safety_temperatures' => (object) array(
'low' => ($this->last_status->device->{$serial_number}->lower_safety_temp_enabled) ? $this->temperatureInUserScale((float)$this->last_status->device->{$serial_number}->lower_safety_temp) : FALSE,
'high' => ($this->last_status->device->{$serial_number}->upper_safety_temp_enabled) ? $this->temperatureInUserScale((float)$this->last_status->device->{$serial_number}->upper_safety_temp) : FALSE,
),
),
'target' => (object) array(
'mode' => $target_mode,
'temperature' => $target_temperatures,
'time_to_target' => $this->last_status->device->{$serial_number}->time_to_target
),
'sensors' => $sensors,
'serial_number' => $this->last_status->device->{$serial_number}->serial_number,
'scale' => $this->last_status->device->{$serial_number}->temperature_scale,
'location' => $structure,
'network' => $this->getDeviceNetworkInfo($serial_number),
'name' => !empty($this->last_status->shared->{$serial_number}->name) ? $this->last_status->shared->{$serial_number}->name : DEVICE_WITH_NO_NAME,
'auto_cool' => ((int) $this->last_status->device->{$serial_number}->leaf_threshold_cool === 0) ? FALSE : ceil($this->temperatureInUserScale((float) $this->last_status->device->{$serial_number}->leaf_threshold_cool)),
'auto_heat' => ((int) $this->last_status->device->{$serial_number}->leaf_threshold_heat === 1000) ? FALSE : floor($this->temperatureInUserScale((float) $this->last_status->device->{$serial_number}->leaf_threshold_heat)),
'where' => isset($this->last_status->device->{$serial_number}->where_id) ? $this->getWhereById($this->last_status->device->{$serial_number}->where_id) : "",
);
if ($this->last_status->device->{$serial_number}->has_humidifier) {
$infos->current_state->humidifier = $this->last_status->device->{$serial_number}->humidifier_state;
$infos->target->humidity = $this->last_status->device->{$serial_number}->target_humidity;
$infos->target->humidity_enabled = $this->last_status->device->{$serial_number}->target_humidity_enabled;
}
return $infos;
}
/**
* Get the last 10 days energy report.
*
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool
*/
public function getEnergyLatest($serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$payload = array(
'objects' => array(
array('object_key' => "energy_latest.$serial_number")
)
);
$url = '/v5/subscribe';
return $this->doPOST($url, json_encode($payload));
}
/**
* Change the thermostat target mode and temperature
*
* @param string $mode One of the TARGET_TEMP_MODE_* constants.
* @param float|array $temperature Target temperature; specify a float when setting $mode = TARGET_TEMP_MODE_HEAT or TARGET_TEMP_MODE_COLD, and a array of two float values when setting $mode = TARGET_TEMP_MODE_RANGE. Not needed when setting $mode = TARGET_TEMP_MODE_OFF. Send NULL if you want to keep the previous temperature(s) value(s).
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setTargetTemperatureMode($mode, $temperature = NULL, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
if ($temperature !== NULL) {
if ($mode == TARGET_TEMP_MODE_RANGE) {
if (!is_array($temperature) || count($temperature) != 2 || !is_numeric($temperature[0]) || !is_numeric($temperature[1])) {
echo "Error: when using TARGET_TEMP_MODE_RANGE, you need to set the target temperatures (second argument of setTargetTemperatureMode) using an array of two numeric values.\n";
return FALSE;
}
$temp_low = $this->temperatureInCelsius($temperature[0], $serial_number);
$temp_high = $this->temperatureInCelsius($temperature[1], $serial_number);
$data = json_encode(array('target_change_pending' => TRUE, 'target_temperature_low' => $temp_low, 'target_temperature_high' => $temp_high));
$set_temp_result = $this->doPOST("/v2/put/shared." . $serial_number, $data);
} elseif ($mode != TARGET_TEMP_MODE_OFF) {
// heat or cool
if (!is_numeric($temperature)) {
echo "Error: when using TARGET_TEMP_MODE_HEAT or TARGET_TEMP_MODE_COLD, you need to set the target temperature (second argument of setTargetTemperatureMode) using an numeric value.\n";
return FALSE;
}
$temperature = $this->temperatureInCelsius($temperature, $serial_number);
$data = json_encode(array('target_change_pending' => TRUE, 'target_temperature' => $temperature));
$set_temp_result = $this->doPOST("/v2/put/shared." . $serial_number, $data);
}
}
$data = json_encode(array('target_change_pending' => TRUE, 'target_temperature_type' => $mode));
return $this->doPOST("/v2/put/shared." . $serial_number, $data);
}
/**
* Change the thermostat target temperature, when the thermostat is not using a range.
*
* @param float $temperature Target temperature.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setTargetTemperature($temperature, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$temperature = $this->temperatureInCelsius($temperature, $serial_number);
$data = json_encode(array('target_change_pending' => TRUE, 'target_temperature' => $temperature));
return $this->doPOST("/v2/put/shared." . $serial_number, $data);
}
/**
* Change the thermostat target temperatures, when the thermostat is using a range.
*
* @param float $temp_low Target low temperature.
* @param float $temp_high Target high temperature.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setTargetTemperatures($temp_low, $temp_high, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$temp_low = $this->temperatureInCelsius($temp_low, $serial_number);
$temp_high = $this->temperatureInCelsius($temp_high, $serial_number);
$data = json_encode(array('target_change_pending' => TRUE, 'target_temperature_low' => $temp_low, 'target_temperature_high' => $temp_high));
return $this->doPOST("/v2/put/shared." . $serial_number, $data);
}
/**
* Set the thermostat to use ECO mode ($mode = ECO_MODE_MANUAL) or not ($mode = ECO_MODE_SCHEDULE).
*
* @param string $mode One of the ECO_MODE_* constants.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setEcoMode($mode, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$data = array();
$data['mode'] = $mode;
$data['touched_by'] = 4;
$data['mode_update_timestamp'] = time();
$data = json_encode(array('eco' => $data));
return $this->doPOST("/v2/put/device." . $serial_number, $data);
}
/**
* (Deprecated) Change the thermostat away temperatures. This method is an alias for setEcoTemperatures().
*
* @param float $temp_low Away low temperature.
* @param float $temp_high Away high temperature.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*
* @deprecated
* @see Nest::setEcoTemperatures()
*/
public function setAwayTemperatures($temp_low, $temp_high, $serial_number = NULL) {
return $this->setEcoTemperatures($temp_low, $temp_high, $serial_number);
}
/**
* Change the thermostat ECO temperatures.
*
* @param float|bool $temp_low ECO low temperature. Use FALSE to turn it Off (only the safety minimum temperature will apply).
* @param float|bool $temp_high ECO high temperature. Use FALSE to turn it Off (only the safety maximum temperature will apply).
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setEcoTemperatures($temp_low, $temp_high, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$temp_low = $this->temperatureInCelsius($temp_low, $serial_number);
$temp_high = $this->temperatureInCelsius($temp_high, $serial_number);
$data = array();
if ($temp_low === FALSE) {
$data['away_temperature_low_enabled'] = FALSE;
} elseif ($temp_low != NULL) {
$data['away_temperature_low_enabled'] = TRUE;
$data['away_temperature_low'] = $temp_low;
}
if ($temp_high === FALSE) {
$data['away_temperature_high_enabled'] = FALSE;
} elseif ($temp_high != NULL) {
$data['away_temperature_high_enabled'] = TRUE;
$data['away_temperature_high'] = $temp_high;
}
$data = json_encode($data);
return $this->doPOST("/v2/put/device." . $serial_number, $data);
}
/**
* Change the thermostat safety temperatures.
*
* @param float|bool $temp_low Safety low temperature. Use FALSE to turn it Off (not recommended)
* @param float|bool $temp_high Safety high temperature. Use FALSE to turn it Off (not recommended)
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setSafetyTemperatures($temp_low, $temp_high, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$temp_low = $this->temperatureInCelsius($temp_low, $serial_number);
$temp_high = $this->temperatureInCelsius($temp_high, $serial_number);
$data = array();
if ($temp_low === FALSE) {
$data['lower_safety_temp_enabled'] = FALSE;
} elseif ($temp_low != NULL) {
$data['lower_safety_temp_enabled'] = TRUE;
$data['lower_safety_temp'] = $temp_low;
}
if ($temp_high === FALSE) {
$data['upper_safety_temp_enabled'] = FALSE;
} elseif ($temp_high != NULL) {
$data['upper_safety_temp_enabled'] = TRUE;
$data['upper_safety_temp'] = $temp_high;
}
$data = json_encode($data);
return $this->doPOST("/v2/put/device." . $serial_number, $data);
}
/**
* Set the thermostat-controlled fan mode.
*
* @param string|array $mode One of the following constants: FAN_MODE_AUTO, FAN_MODE_ON, FAN_MODE_EVERY_DAY_ON or FAN_MODE_EVERY_DAY_OFF.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*
* @throws InvalidArgumentException
*/
public function setFanMode($mode, $serial_number = NULL) {
$duty_cycle = NULL;
$timer = NULL;
if (is_array($mode)) {
$modes = $mode;
$mode = $modes[0];
if (count($modes) > 1) {
if ($mode == FAN_MODE_MINUTES_PER_HOUR) {
$duty_cycle = (int) $modes[1];
} else {
$timer = (int) $modes[1];
}
} else {
throw new InvalidArgumentException("setFanMode(array \$mode[, ...]) needs at least a mode and a value in the \$mode array.");
}
} elseif (!is_string($mode)) {
throw new InvalidArgumentException("setFanMode() can only take a string or an array as it's first parameter.");
}
return $this->_setFanMode($mode, $duty_cycle, $timer, $serial_number);
}
/**
* Set the thermostat-controlled fan to be ON for a specific number of minutes each hour.
*
* @param string|array $mode One of the FAN_MODE_MINUTES_PER_HOUR_* constants.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setFanModeMinutesPerHour($mode, $serial_number = NULL) {
$modes = explode(',', $mode);
$mode = $modes[0];
$duty_cycle = $modes[1];
return $this->_setFanMode($mode, $duty_cycle, NULL, $serial_number);
}
/**
* Set the thermostat-controlled fan to be ON using a timer.
*
* @param string|array $mode One of the FAN_TIMER_* constants.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setFanModeOnWithTimer($mode, $serial_number = NULL) {
$modes = explode(',', $mode);
$mode = $modes[0];
$timer = (int) $modes[1];
return $this->_setFanMode($mode, NULL, $timer, $serial_number);
}
/**
* Cancels the timer for the thermostat-controlled fan.
*
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function cancelFanModeOnWithTimer($serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$data = json_encode(array('fan_timer_timeout' => 0));
return $this->doPOST("/v2/put/device." . $serial_number, $data);
}
/**
* Set the thermostat-controlled fan to run only between the specified hours.
*
* @param int $start_hour When the fan should start.
* @param int $end_hour When the fan should stop.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setFanEveryDaySchedule($start_hour, $end_hour, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$data = json_encode(array('fan_duty_start_time' => $start_hour*3600, 'fan_duty_end_time' => $end_hour*3600));
return $this->doPOST("/v2/put/device." . $serial_number, $data);
}
/**
* Turn off the thermostat (no heating, cooling or fan).
*
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function turnOff($serial_number = NULL) {
return $this->setTargetTemperatureMode(TARGET_TEMP_MODE_OFF, 0, $serial_number);
}
/**
* Change the location (structure) to away or present. Can also set the specified thermostat to use ECO temperatures, when enabling Away mode.
*
* @param string $away_mode AWAY_MODE_ON or AWAY_MODE_OFF
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
* @param bool $eco_when_away Specify if you want to use Eco temperatures or not, when using AWAY_MODE_ON. Default to TRUE.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setAway($away_mode, $serial_number = NULL, $eco_when_away = TRUE) {
$serial_number = $this->getDefaultSerial($serial_number);
$data = json_encode(array('away' => $away_mode, 'away_timestamp' => time(), 'away_setter' => 0));
$structure_id = $this->getDeviceInfo($serial_number)->location;
if ($away_mode == AWAY_MODE_ON && $eco_when_away) {
$this->setEcoMode(ECO_MODE_MANUAL, $serial_number);
} else {
$this->setEcoMode(ECO_MODE_SCHEDULE, $serial_number);
}
return $this->doPOST("/v2/put/structure." . $structure_id, $data);
}
/**
* (Deprecated) Enable or disable Nest Sense Auto-Away.
*
* @param bool $enabled True to enable auto-away.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*
* @deprecated Nest Sense Auto-Away is not available anymore. This now controls if the thermostat should use Eco temperatures if it detects you are away.
* @see Nest::useEcoTempWhenAway()
*/
public function setAutoAwayEnabled($enabled, $serial_number = NULL) {
return $this->useEcoTempWhenAway($enabled, $serial_number);
}
/**
* Enable or disable using Eco temperatures when you're Away.
*
* @param bool $enabled True to enable Eco temperatures when Away.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function useEcoTempWhenAway($enabled, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$data = json_encode(array('auto_away_enable' => $enabled));
return $this->doPOST("/v2/put/device." . $serial_number, $data);
}
/**
* Change the dual-fuel breakpoint temperature.
*
* @param float|string $breakpoint DUALFUEL_BREAKPOINT_ALWAYS_PRIMARY, DUALFUEL_BREAKPOINT_ALWAYS_ALT, or a temperature: thermostat will force usage of alt-heating when the outside temperature is below this value.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setDualFuelBreakpoint($breakpoint, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
if (!is_string($breakpoint)) {
$breakpoint = $this->temperatureInCelsius($breakpoint, $serial_number);
$data = json_encode(array('dual_fuel_breakpoint_override' => 'none', 'dual_fuel_breakpoint' => $breakpoint));
} else {
$data = json_encode(array('dual_fuel_breakpoint_override' => $breakpoint));
}
return $this->doPOST("/v2/put/device." . $serial_number, $data);
}
/**
* Enable or disable Nest Sense Humidifier.
*
* @param bool $enabled True to enable auto-away.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function enableHumidifier($enabled, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$data = json_encode(array('target_humidity_enabled' => ((boolean)$enabled)));
return $this->doPOST("/v2/put/device." . $serial_number, $data);
}
/**
* Change the dual-fuel breakpoint temperature.
*
* @param float $humidity The target humidity value.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setHumidity($humidity, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$data = json_encode(array('target_humidity' => ((double)$humidity)));
return $this->doPOST("/v2/put/device." . $serial_number, $data);
}
/**
* Convert a temperature value from the device-prefered scale to Celsius.
*
* @param float $temperature The temperature to convert.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return float Temperature in Celsius.
*/
public function temperatureInCelsius($temperature, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$temp_scale = $this->getDeviceTemperatureScale($serial_number);
if ($temp_scale == 'F') {
return ($temperature - 32) / 1.8;
}
return $temperature;
}
/**
* Convert a temperature value from Celsius to the device-preferred scale.
*
* @param float $temperature_in_celsius The temperature to convert.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return float Temperature in device-preferred scale.
*/
public function temperatureInUserScale($temperature_in_celsius, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$temp_scale = $this->getDeviceTemperatureScale($serial_number);
if ($temp_scale == 'F') {
return ($temperature_in_celsius * 1.8) + 32;
}
return $temperature_in_celsius;
}
/**
* Get the thermostat preferred scale: Celsius or Fahrenheit.
*
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return string 'F' or 'C'
*/
public function getDeviceTemperatureScale($serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
return $this->last_status->device->{$serial_number}->temperature_scale;
}
/**
* Get all the devices of a specific type from the user's account.
*
* @param string $type DEVICE_TYPE_THERMOSTAT or DEVICE_TYPE_PROTECT or DEVICE_TYPE_SENSOR
*
* @return array Devices
*/
public function getDevices($type = DEVICE_TYPE_THERMOSTAT) {
$this->prepareForGet();
if ($type == DEVICE_TYPE_PROTECT) {
$protects = array();
$topaz = isset($this->last_status->topaz) ? $this->last_status->topaz : array();
foreach ($topaz as $protect) {
$protects[] = $protect->serial_number;
}
return $protects;
}
elseif ($type == DEVICE_TYPE_SENSOR) {
return isset($this->last_status->kryptonite) ? array_keys(get_object_vars($this->last_status->kryptonite)) : array();
}
$devices_serials = array();
foreach ($this->last_status->user->{$this->userid}->structures as $structure) {
list(, $structure_id) = explode('.', $structure);
foreach ($this->last_status->structure->{$structure_id}->devices as $device) {
list(, $device_serial) = explode('.', $device);
$devices_serials[] = $device_serial;
}
}
return $devices_serials;
}
/**
* Either return the parameter as-is, or, if empty, return the serial number of the first device found in the user's account.
*
* @param string $serial_number Serial number will be returned, if specified.
*
* @return string Serial number of the first defined device.
*/
protected function getDefaultSerial($serial_number) {
if (empty($serial_number)) {
$devices_serials = $this->getDevices();
if (count($devices_serials) == 0) {
$devices_serials = $this->getDevices(DEVICE_TYPE_PROTECT);
}
$serial_number = $devices_serials[0];
}
return $serial_number;
}
/**
* Return the device information for the default device.
*
* @return stdClass
*/
public function getDefaultDevice() {
$serial_number = $this->getDefaultSerial(NULL);
return $this->last_status->device->{$serial_number};
}
/**
* Get the specified device network information.
*
* @param string $serial_number The device (thermostat or protect) serial number. Defaults to the first device of the account.
*
* @return stdClass
*/
protected function getDeviceNetworkInfo($serial_number = NULL) {
$this->prepareForGet();
$serial_number = $this->getDefaultSerial($serial_number);
$connection_info = $this->last_status->track->{$serial_number};
return (object) array(
'online' => $connection_info->online,
'last_connection' => date(DATETIME_FORMAT, $connection_info->last_connection/1000),
'last_connection_UTC' => gmdate(DATETIME_FORMAT, $connection_info->last_connection/1000),
'wan_ip' => @$connection_info->last_ip,
'local_ip' => $this->last_status->device->{$serial_number}->local_ip,
'mac_address' => $this->last_status->device->{$serial_number}->mac_address
);
}
/**
* Boost hot water.
*
* @param int $seconds Duration of boost.
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
*/
public function setHotWaterBoost($boost_in_seconds = 30, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$data = json_encode(array('hot_water_boost_time_to_end' => time() + $boost_in_seconds));
return $this->doPOST("/v2/put/device." . $serial_number, $data);
}
/**
* Cancel hot water boost. Sets boost timer to zero
*
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return stdClass|bool The object returned by the API call, or FALSE on error.
* */
public function cancelHotWaterBoost($serial_number = NULL) {
return $this->setHotWaterBoost(0,$serial_number);
}
/**
* Get hot water status
*
* @param string $serial_number The thermostat serial number. Defaults to the first device of the account.
*
* @return string.
*/
public function getHotWaterStatus($serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
if ($this->last_status->device->{$serial_number}->has_hot_water_control) {
return ($this->last_status->device->{$serial_number}->hot_water_active ? "On" : "Off");
} else {
return "device has no hot water control";
}
return "error";
}
/* Helper functions */
public function clearStatusCache() {
unset($this->last_status);
}
/**
* Load all status information from server.
*
* @param boolean $retry If needed, rety loading the status from the server a second time.
*
* @return \stdClass
*
* @throws RuntimeException
*/
public function getStatus($retry = TRUE) {
$url = "/v3/mobile/" . $this->user;
$status = $this->doGET($url);
if (!is_object($status)) {
throw new RuntimeException("Error: Couldn't get status from NEST API: $status");
}
if (@$status->cmd == 'REINIT_STATE') {
if ($retry) {
@unlink($this->cookie_file);
@unlink($this->cache_file);
$this->login();
return $this->getStatus(FALSE);
}
throw new RuntimeException("Error: HTTP request to $url returned cmd = REINIT_STATE. Retrying failed.");
}
$this->last_status = $status;
$this->saveCache();
return $status;
}
public static function cleanDevices($device) {
list(, $device_id) = explode('.', $device);
return $device_id;
}
protected function _setFanMode($mode, $fan_duty_cycle = NULL, $timer = NULL, $serial_number = NULL) {
$serial_number = $this->getDefaultSerial($serial_number);
$data = array();
if (!empty($mode)) {
$data['fan_mode'] = $mode;
}
if (!empty($fan_duty_cycle)) {
$data['fan_duty_cycle'] = (int) $fan_duty_cycle;
}
if (!empty($timer)) {
$data['fan_timer_duration'] = $timer;
$data['fan_timer_timeout'] = time() + $timer;
}
return $this->doPOST("/v2/put/device." . $serial_number, json_encode($data));
}
protected function prepareForGet() {
if (!isset($this->last_status)) {
$this->getStatus();
}
}
/**
* Login
*
* @param bool $retry Should retry (once)?
*
* @return void
*
* @throws UnexpectedValueException|RuntimeException
*/
protected function login($retry = TRUE) {
if ($this->use_cache()) {
// No need to login; we'll use cached values for authentication.
return;
}
if (!empty($this->issue_token)) {
// Get a Bearer token using the Google cookies and issue_token
$headers = array(
'Sec-Fetch-Mode: cors',
'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36',
'X-Requested-With: XmlHttpRequest',
'Referer: https://accounts.google.com/o/oauth2/iframe',
'Cookie: ' . $this->cookies,
);
try {
$result = $this->doGET($this->issue_token, $headers);
} catch (RuntimeException $ex) {
if ($retry) {
// Delete cookie and cache files, and retry
@unlink($this->cookie_file);
@unlink($this->cache_file);
$this->login(FALSE);
return;
}
}
if (!isset($result->access_token)) {
throw new UnexpectedValueException("Response to login request doesn't contain required access token. Response: " . json_encode($result));
}
// Use Bearer token to get an access token, and user ID
$headers = array(
'Authorization: Bearer ' . $result->access_token,
'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36',
'X-Goog-API-Key: AIzaSyAdkSIMNc51XGNEAYWasX9UOWkS5P6sZE4', // Nest website's (public) API key,
'Referer: https://home.nest.com',
);
$params = array(
'embed_google_oauth_access_token' => TRUE,
'expire_after' => '3600s',
'google_oauth_access_token' => $result->access_token,
'policy_id' => 'authproxy-oauth-policy',
);
$result = $this->doPOST("https://nestauthproxyservice-pa.googleapis.com/v1/issue_jwt", $params, $headers);
if (empty($result->claims->subject->nestId->id)) {
throw new RuntimeException("Response to login request doesn't contain required User ID. Response: " . json_encode($result));
}
if (empty($result->jwt)) {
throw new RuntimeException("Response to login request doesn't contain required (JWT) access token. Response: " . json_encode($result));
}
$this->userid = $result->claims->subject->nestId->id;
$this->access_token = $result->jwt;
$this->cache_expiration = strtotime($result->claims->expirationTime);
// Get user
$params = array(
'known_bucket_types' => array("user"),
'known_bucket_versions' => array(),
);
$result = $this->doPOST("https://home.nest.com/api/0.1/user/{$this->userid}/app_launch", json_encode($params), array('Content-type: text/json'));
if (empty($result->service_urls->urls->transport_url)) {
throw new RuntimeException("Response to login request doesn't contain required transport_url. Response: " . json_encode($result));
}
$this->transport_url = $result->service_urls->urls->transport_url;
foreach ($result->updated_buckets as $bucket) {
if (strpos($bucket->object_key, 'user.') === 0) {
$this->user = $bucket->object_key;
break;
}
}
if (empty($this->user)) {
$this->user = "user.{$this->userid}"; // meh; no need to get it from API; it's simple enough!
}
} else {
$result = $this->doPOST(self::LOGIN_URL, array('username' => $this->username, 'password' => $this->password));
if (!isset($result->urls)) {
throw new RuntimeException("Response to login request doesn't contain required transport URL. Response: " . json_encode($result));
}
$this->transport_url = $result->urls->transport_url;
$this->access_token = $result->access_token;
$this->userid = $result->userid;
$this->user = $result->user;
$this->cache_expiration = strtotime($result->expires_in);
}
$this->saveCache();
}
protected function use_cache() {
return file_exists($this->cookie_file) && file_exists($this->cache_file) && !empty($this->cache_expiration) && $this->cache_expiration > time();
}
protected function loadCache() {
if (!file_exists($this->cache_file)) {
return;
}
$vars = @unserialize(file_get_contents($this->cache_file));
if ($vars === FALSE) {
return;
}
$this->transport_url = $vars['transport_url'];
$this->access_token = $vars['access_token'];
$this->user = $vars['user'];
$this->userid = $vars['userid'];
$this->cache_expiration = $vars['cache_expiration'];
// Let's not load this from the disk cache; otherwise, prepareForGet() would always skip getStatus()
// $this->last_status = $vars['last_status'];
}
protected function saveCache() {
$vars = array(
'transport_url' => $this->transport_url,
'access_token' => $this->access_token,
'user' => $this->user,
'userid' => $this->userid,
'cache_expiration' => $this->cache_expiration,
'last_status' => @$this->last_status
);
file_put_contents($this->cache_file, serialize($vars));
}
/**
* Obtain the Nest "where name" by the where id
*
* @param string $device_where_id device where id
*
* @return string of the where name or value of parameter
*
*/
protected function getWhereById($device_where_id) {
foreach($this->last_status->where as $structure) {
foreach($structure->wheres as $where) {
if($where->where_id === $device_where_id) {
return $where->name;
}
}
}
return $device_where_id;
}
/**
* Send a GET HTTP request.
*
* @param string $url URL
* @param array $headers HTTP headers
*
* @return stdClass|bool JSON-decoded object, or boolean if no response was returned.
*
* @throws RuntimeException
*/
protected function doGET($url, $headers = array()) {
return $this->doRequest('GET', $url, NULL, TRUE, $headers);
}
/**
* Send a POST HTTP request.
*
* @param string $url URL
* @param array|string $data_fields Data to send via POST.
* @param array $headers HTTP headers
*
* @return stdClass|bool JSON-decoded object, or boolean if no response was returned.
*
* @throws RuntimeException
*/
protected function doPOST($url, $data_fields, $headers = array()) {
return $this->doRequest('POST', $url, $data_fields, TRUE, $headers);
}
/**
* Send a HTTP request.
*
* @param string $method HTTP method: GET or POST
* @param string $url URL
* @param array|string $data_fields Data to send via POST.
* @param bool $with_retry Retry if request fails?
* @param array $headers HTTP headers
*
* @return stdClass|bool JSON-decoded object, or boolean if no response was returned.
*
* @throws RuntimeException
*/
protected function doRequest($method, $url, $data_fields = NULL, $with_retry = TRUE, $headers = array()) {
$ch = curl_init();
if ($url[0] == '/') {
$url = $this->transport_url . $url;
}
$headers[] = 'X-nl-protocol-version: ' . self::PROTOCOL_VERSION;
if (isset($this->userid)) {
$headers[] = 'X-nl-user-id: ' . $this->userid;
$headers[] = 'Authorization: Basic ' . $this->access_token;
}
if (is_array($data_fields)) {
$data = array();
foreach ($data_fields as $k => $v) {
$data[] = "$k=" . urlencode($v);
}
$data = implode('&', $data);
} elseif (is_string($data_fields)) {
$data = $data_fields;
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_AUTOREFERER, TRUE);
curl_setopt($ch, CURLOPT_USERAGENT, self::USER_AGENT);
curl_setopt($ch, CURLOPT_COOKIEJAR, $this->cookie_file);
curl_setopt($ch, CURLOPT_COOKIEFILE, $this->cookie_file);
if ($method == 'POST') {
if (!isset($data)) {
throw new RuntimeException("Error: You need to specify \$data when sending a POST.");
}
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
$headers[] = 'Content-length: ' . strlen($data);
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE); // for security this should always be set to true.
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // for security this should always be set to 2.
curl_setopt($ch, CURLOPT_SSLVERSION, 1); // Nest servers now require TLSv1; won't work with SSLv2 or even SSLv3!
// Update cacert.pem (valid CA certificates list) from the cURL website once a month
$curl_cainfo = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'cacert.pem';
$last_month = time()-30*24*60*60;
if (!file_exists($curl_cainfo) || filemtime($curl_cainfo) < $last_month || filesize($curl_cainfo) < 100000) {
$certs = static::getCURLCerts();
if ($certs) {
file_put_contents($curl_cainfo, $certs);
}
}
if (file_exists($curl_cainfo) && filesize($curl_cainfo) > 100000) {
curl_setopt($ch, CURLOPT_CAINFO, $curl_cainfo);
}
$response = curl_exec($ch);
$info = curl_getinfo($ch);
if ($info['http_code'] == 401 || (!$response && curl_errno($ch) != 0)) {
if ($with_retry) {
// Received 401; let's re-login then try again this same request
@unlink($this->cookie_file);
@unlink($this->cache_file);
if ($info['http_code'] == 401) {
$this->login();
}
return $this->doRequest($method, $url, $data_fields, FALSE);
} else {
if (curl_errno($ch) != 0) {
throw new RuntimeException("Error: HTTP request to $url returned a cURL error: [" . curl_errno($ch) . "] " . curl_error($ch), curl_errno($ch));
} else {
throw new RuntimeException("Error: HTTP request to $url returned an HTTP error code " . $info['http_code'] . ". Response: " . str_replace(array("\n","\r"), '', $response), $info['http_code']);
}
}
}
$json = json_decode($response);
if (!is_object($json) && ($method == 'GET' || $url == self::LOGIN_URL)) {
if (strpos($response, "currently performing maintenance on your Nest account") !== FALSE) {
throw new RuntimeException("Error: Account is under maintenance; API temporarily unavailable.", NESTAPI_ERROR_UNDER_MAINTENANCE);
}
if (empty($response)) {
throw new RuntimeException("Error: Received empty response from request to $url.", NESTAPI_ERROR_EMPTY_RESPONSE);
}
throw new RuntimeException("Error: Response from request to $url is not valid JSON data. Response: " . str_replace(array("\n","\r"), '', $response), NESTAPI_ERROR_NOT_JSON_RESPONSE);
}
if ($info['http_code'] == 400) {
if (!is_object($json)) {
throw new RuntimeException("Error: HTTP 400 from request to $url. Response: " . str_replace(array("\n","\r"), '', $response), NESTAPI_ERROR_API_OTHER_ERROR);
}
throw new RuntimeException("Error: HTTP 400 from request to $url. JSON error: $json->error - $json->error_description", NESTAPI_ERROR_API_JSON_ERROR);
}
// No body returned; return a boolean value that confirms a 200 OK was returned.
if ($info['download_content_length'] == 0) {
return $info['http_code'] == 200;
}
return $json;
}
/**
* Get latest CA certs from curl.haxx.se
*
* @return string
*/
protected static function getCURLCerts() {
$url = 'https://curl.haxx.se/ca/cacert.pem';
$certs = @file_get_contents($url);
if (!$certs) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE); // for security this should always be set to true.
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // for security this should always be set to 2.
$response = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($info['http_code'] == 200) {
$certs = $response;
}
}
return $certs;
}
/**
* Create a temporary file in the system temp folder.
*
* @param string $fname Filename
*
* @return void
*/
protected static function secureTouch($fname) {
if (file_exists($fname)) {
return;
}
$temp = tempnam(sys_get_temp_dir(), 'NEST');
rename($temp, $fname);
}
}
You can’t perform that action at this time.