From 42fea4fb9771c2df59393b550bd577a524510ff1 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Tue, 6 Nov 2018 10:11:10 -0500 Subject: [PATCH] Add services to set/update and cancel Nest ETA (#17836) * Add service to cancel ETA * Update test requirements * Change service name and update logging * Reformat logging to verify structures --- homeassistant/components/nest/__init__.py | 129 +++++++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 94 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index bb0e6247de3..5f584d20c55 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -12,10 +12,12 @@ import threading import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.climate import ( + ATTR_AWAY_MODE, SERVICE_SET_AWAY_MODE) from homeassistant.const import ( - CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, - CONF_MONITORED_CONDITIONS, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + CONF_BINARY_SENSORS, CONF_FILENAME, CONF_MONITORED_CONDITIONS, + CONF_SENSORS, CONF_STRUCTURE, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send, \ @@ -25,11 +27,13 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN from . import local_auth -REQUIREMENTS = ['python-nest==4.0.3'] +REQUIREMENTS = ['python-nest==4.0.4'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) +SERVICE_CANCEL_ETA = 'cancel_eta' +SERVICE_SET_ETA = 'set_eta' DATA_NEST = 'nest' DATA_NEST_CONFIG = 'nest_config' @@ -40,27 +44,18 @@ NEST_CONFIG_FILE = 'nest.conf' CONF_CLIENT_ID = 'client_id' CONF_CLIENT_SECRET = 'client_secret' -ATTR_HOME_MODE = 'home_mode' -ATTR_STRUCTURE = 'structure' -ATTR_TRIP_ID = 'trip_id' ATTR_ETA = 'eta' ATTR_ETA_WINDOW = 'eta_window' +ATTR_STRUCTURE = 'structure' +ATTR_TRIP_ID = 'trip_id' -HOME_MODE_AWAY = 'away' -HOME_MODE_HOME = 'home' +AWAY_MODE_AWAY = 'away' +AWAY_MODE_HOME = 'home' SENSOR_SCHEMA = vol.Schema({ vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list) }) -AWAY_SCHEMA = vol.Schema({ - vol.Required(ATTR_HOME_MODE): vol.In([HOME_MODE_AWAY, HOME_MODE_HOME]), - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_TRIP_ID): cv.string, - vol.Optional(ATTR_ETA): cv.time_period, - vol.Optional(ATTR_ETA_WINDOW): cv.time_period -}) - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_CLIENT_ID): cv.string, @@ -71,6 +66,23 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) +SET_AWAY_MODE_SCHEMA = vol.Schema({ + vol.Required(ATTR_AWAY_MODE): vol.In([AWAY_MODE_AWAY, AWAY_MODE_HOME]), + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]) +}) + +SET_ETA_SCHEMA = vol.Schema({ + vol.Required(ATTR_ETA): cv.time_period, + vol.Optional(ATTR_TRIP_ID): cv.string, + vol.Optional(ATTR_ETA_WINDOW): cv.time_period, + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]) +}) + +CANCEL_ETA_SCHEMA = vol.Schema({ + vol.Required(ATTR_TRIP_ID): cv.string, + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]) +}) + def nest_update_event_broker(hass, nest): """ @@ -134,40 +146,83 @@ async def async_setup_entry(hass, entry): hass.async_create_task(hass.config_entries.async_forward_entry_setup( entry, component)) - def set_mode(service): - """ - Set the home/away mode for a Nest structure. + def validate_structures(target_structures): + all_structures = [structure.name for structure in nest.structures] + for target in target_structures: + if target not in all_structures: + _LOGGER.info("Invalid structure: %s", target) - You can set optional eta information when set mode to away. - """ + def set_away_mode(service): + """Set the away mode for a Nest structure.""" if ATTR_STRUCTURE in service.data: - structures = service.data[ATTR_STRUCTURE] + target_structures = service.data[ATTR_STRUCTURE] + validate_structures(target_structures) else: - structures = hass.data[DATA_NEST].local_structure + target_structures = hass.data[DATA_NEST].local_structure for structure in nest.structures: - if structure.name in structures: - _LOGGER.info("Setting mode for %s", structure.name) - structure.away = service.data[ATTR_HOME_MODE] + if structure.name in target_structures: + _LOGGER.info("Setting away mode for: %s to: %s", + structure.name, service.data[ATTR_AWAY_MODE]) + structure.away = service.data[ATTR_AWAY_MODE] + + def set_eta(service): + """Set away mode to away and include ETA for a Nest structure.""" + if ATTR_STRUCTURE in service.data: + target_structures = service.data[ATTR_STRUCTURE] + validate_structures(target_structures) + else: + target_structures = hass.data[DATA_NEST].local_structure + + for structure in nest.structures: + if structure.name in target_structures: + if structure.thermostats: + _LOGGER.info("Setting away mode for: %s to: %s", + structure.name, AWAY_MODE_AWAY) + structure.away = AWAY_MODE_AWAY - if service.data[ATTR_HOME_MODE] == HOME_MODE_AWAY \ - and ATTR_ETA in service.data: now = datetime.utcnow() + trip_id = service.data.get( + ATTR_TRIP_ID, "trip_{}".format(int(now.timestamp()))) eta_begin = now + service.data[ATTR_ETA] eta_window = service.data.get(ATTR_ETA_WINDOW, timedelta(minutes=1)) eta_end = eta_begin + eta_window - trip_id = service.data.get( - ATTR_TRIP_ID, "trip_{}".format(int(now.timestamp()))) - _LOGGER.info("Setting eta for %s, eta window starts at " - "%s ends at %s", trip_id, eta_begin, eta_end) + _LOGGER.info("Setting ETA for trip: %s, " + "ETA window starts at: %s and ends at: %s", + trip_id, eta_begin, eta_end) structure.set_eta(trip_id, eta_begin, eta_end) - else: - _LOGGER.error("Invalid structure %s", - service.data[ATTR_STRUCTURE]) + else: + _LOGGER.info("No thermostats found in structure: %s, " + "unable to set ETA", structure.name) + + def cancel_eta(service): + """Cancel ETA for a Nest structure.""" + if ATTR_STRUCTURE in service.data: + target_structures = service.data[ATTR_STRUCTURE] + validate_structures(target_structures) + else: + target_structures = hass.data[DATA_NEST].local_structure + + for structure in nest.structures: + if structure.name in target_structures: + if structure.thermostats: + trip_id = service.data[ATTR_TRIP_ID] + _LOGGER.info("Cancelling ETA for trip: %s", trip_id) + structure.cancel_eta(trip_id) + else: + _LOGGER.info("No thermostats found in structure: %s, " + "unable to cancel ETA", structure.name) hass.services.async_register( - DOMAIN, 'set_mode', set_mode, schema=AWAY_SCHEMA) + DOMAIN, SERVICE_SET_AWAY_MODE, set_away_mode, + schema=SET_AWAY_MODE_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_SET_ETA, set_eta, schema=SET_ETA_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_CANCEL_ETA, cancel_eta, schema=CANCEL_ETA_SCHEMA) @callback def start_up(event): diff --git a/requirements_all.txt b/requirements_all.txt index 478a3193d16..10c8a2e365f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1189,7 +1189,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.4 # homeassistant.components.nest -python-nest==4.0.3 +python-nest==4.0.4 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1cf0de72104..42c80cea71b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -196,7 +196,7 @@ pyspcwebgw==0.4.0 python-forecastio==1.4.0 # homeassistant.components.nest -python-nest==4.0.3 +python-nest==4.0.4 # homeassistant.components.sensor.whois pythonwhois==2.4.3