2019-02-14 04:35:12 +00:00
|
|
|
"""Support for Nest devices."""
|
2020-10-21 08:17:49 +00:00
|
|
|
|
|
|
|
import asyncio
|
2019-12-09 11:08:51 +00:00
|
|
|
from datetime import datetime, timedelta
|
2016-04-12 04:52:19 +00:00
|
|
|
import logging
|
2018-08-16 11:46:43 +00:00
|
|
|
import threading
|
2016-04-12 04:52:19 +00:00
|
|
|
|
2020-10-21 08:17:49 +00:00
|
|
|
from google_nest_sdm.event import EventCallback, EventMessage
|
|
|
|
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
|
2019-10-17 13:03:50 +00:00
|
|
|
from nest import Nest
|
2019-12-09 11:08:51 +00:00
|
|
|
from nest.nest import APIError, AuthorizationError
|
2016-04-01 14:31:11 +00:00
|
|
|
import voluptuous as vol
|
2016-09-01 20:08:03 +00:00
|
|
|
|
2018-08-09 11:24:14 +00:00
|
|
|
from homeassistant import config_entries
|
2020-10-21 08:17:49 +00:00
|
|
|
from homeassistant.config_entries import ConfigEntry
|
2017-04-30 05:04:49 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_BINARY_SENSORS,
|
2020-05-30 15:27:20 +00:00
|
|
|
CONF_CLIENT_ID,
|
|
|
|
CONF_CLIENT_SECRET,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_FILENAME,
|
|
|
|
CONF_MONITORED_CONDITIONS,
|
|
|
|
CONF_SENSORS,
|
|
|
|
CONF_STRUCTURE,
|
|
|
|
EVENT_HOMEASSISTANT_START,
|
|
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
|
|
)
|
2020-10-21 08:17:49 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
|
|
|
from homeassistant.helpers import (
|
|
|
|
aiohttp_client,
|
|
|
|
config_entry_oauth2_flow,
|
|
|
|
config_validation as cv,
|
|
|
|
)
|
2019-12-09 11:08:51 +00:00
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
|
2018-06-03 01:54:48 +00:00
|
|
|
from homeassistant.helpers.entity import Entity
|
2016-01-14 04:05:47 +00:00
|
|
|
|
2020-10-21 08:17:49 +00:00
|
|
|
from . import api, config_flow, local_auth
|
|
|
|
from .const import (
|
|
|
|
API_URL,
|
|
|
|
DATA_SDM,
|
|
|
|
DOMAIN,
|
|
|
|
OAUTH2_AUTHORIZE,
|
|
|
|
OAUTH2_TOKEN,
|
|
|
|
SIGNAL_NEST_UPDATE,
|
|
|
|
)
|
2018-06-13 15:14:52 +00:00
|
|
|
|
2016-11-28 00:18:47 +00:00
|
|
|
_CONFIGURING = {}
|
2016-09-01 20:08:03 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2016-01-14 04:05:47 +00:00
|
|
|
|
2020-10-21 08:17:49 +00:00
|
|
|
CONF_PROJECT_ID = "project_id"
|
|
|
|
CONF_SUBSCRIBER_ID = "subscriber_id"
|
|
|
|
|
|
|
|
# Configuration for the legacy nest API
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_CANCEL_ETA = "cancel_eta"
|
|
|
|
SERVICE_SET_ETA = "set_eta"
|
|
|
|
|
|
|
|
DATA_NEST = "nest"
|
|
|
|
DATA_NEST_CONFIG = "nest_config"
|
|
|
|
|
|
|
|
NEST_CONFIG_FILE = "nest.conf"
|
|
|
|
|
|
|
|
ATTR_ETA = "eta"
|
|
|
|
ATTR_ETA_WINDOW = "eta_window"
|
|
|
|
ATTR_STRUCTURE = "structure"
|
|
|
|
ATTR_TRIP_ID = "trip_id"
|
|
|
|
|
|
|
|
AWAY_MODE_AWAY = "away"
|
|
|
|
AWAY_MODE_HOME = "home"
|
|
|
|
|
|
|
|
ATTR_AWAY_MODE = "away_mode"
|
|
|
|
SERVICE_SET_AWAY_MODE = "set_away_mode"
|
|
|
|
|
|
|
|
SENSOR_SCHEMA = vol.Schema(
|
|
|
|
{vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list)}
|
|
|
|
)
|
|
|
|
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
DOMAIN: vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_CLIENT_ID): cv.string,
|
|
|
|
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
2020-10-21 08:17:49 +00:00
|
|
|
# Required to use the new API (optional for compatibility)
|
|
|
|
vol.Optional(CONF_PROJECT_ID): cv.string,
|
|
|
|
vol.Optional(CONF_SUBSCRIBER_ID): cv.string,
|
|
|
|
# Config that only currently works on the old API
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Optional(CONF_STRUCTURE): vol.All(cv.ensure_list, [cv.string]),
|
|
|
|
vol.Optional(CONF_SENSORS): SENSOR_SCHEMA,
|
|
|
|
vol.Optional(CONF_BINARY_SENSORS): SENSOR_SCHEMA,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
extra=vol.ALLOW_EXTRA,
|
|
|
|
)
|
|
|
|
|
2020-10-21 08:17:49 +00:00
|
|
|
PLATFORMS = ["sensor"]
|
|
|
|
|
|
|
|
# Services for the legacy API
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
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]),
|
|
|
|
}
|
|
|
|
)
|
2018-11-06 15:11:10 +00:00
|
|
|
|
2016-04-12 04:52:19 +00:00
|
|
|
|
2020-10-21 08:17:49 +00:00
|
|
|
async def async_setup(hass: HomeAssistant, config: dict):
|
|
|
|
"""Set up Nest components with dispatch between old/new flows."""
|
|
|
|
hass.data[DOMAIN] = {}
|
|
|
|
|
|
|
|
if DOMAIN not in config:
|
|
|
|
return True
|
|
|
|
|
|
|
|
if CONF_PROJECT_ID not in config[DOMAIN]:
|
|
|
|
return await async_setup_legacy(hass, config)
|
|
|
|
|
|
|
|
if CONF_SUBSCRIBER_ID not in config[DOMAIN]:
|
|
|
|
_LOGGER.error("Configuration option '{CONF_SUBSCRIBER_ID}' required")
|
|
|
|
return False
|
|
|
|
|
|
|
|
# For setup of ConfigEntry below
|
|
|
|
hass.data[DOMAIN][DATA_NEST_CONFIG] = config[DOMAIN]
|
|
|
|
project_id = config[DOMAIN][CONF_PROJECT_ID]
|
|
|
|
config_flow.NestFlowHandler.register_sdm_api(hass)
|
|
|
|
config_flow.NestFlowHandler.async_register_implementation(
|
|
|
|
hass,
|
|
|
|
config_entry_oauth2_flow.LocalOAuth2Implementation(
|
|
|
|
hass,
|
|
|
|
DOMAIN,
|
|
|
|
config[DOMAIN][CONF_CLIENT_ID],
|
|
|
|
config[DOMAIN][CONF_CLIENT_SECRET],
|
|
|
|
OAUTH2_AUTHORIZE.format(project_id=project_id),
|
|
|
|
OAUTH2_TOKEN,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
class SignalUpdateCallback(EventCallback):
|
|
|
|
"""An EventCallback invoked when new events arrive from subscriber."""
|
|
|
|
|
|
|
|
def __init__(self, hass: HomeAssistant):
|
|
|
|
"""Initialize EventCallback."""
|
|
|
|
self._hass = hass
|
|
|
|
|
|
|
|
def handle_event(self, event_message: EventMessage):
|
|
|
|
"""Process an incoming EventMessage."""
|
|
|
|
_LOGGER.debug("Update %s @ %s", event_message.event_id, event_message.timestamp)
|
|
|
|
traits = event_message.resource_update_traits
|
|
|
|
if traits:
|
|
|
|
_LOGGER.debug("Trait update %s", traits.keys())
|
|
|
|
events = event_message.resource_update_events
|
|
|
|
if events:
|
|
|
|
_LOGGER.debug("Event Update %s", events.keys())
|
|
|
|
|
|
|
|
if not event_message.resource_update_traits:
|
|
|
|
# Note: Currently ignoring events like camera motion
|
|
|
|
return
|
|
|
|
# This event triggered an update to a device that changed some
|
|
|
|
# properties which the DeviceManager should already have received.
|
|
|
|
# Send a signal to refresh state of all listening devices.
|
|
|
|
dispatcher_send(self._hass, SIGNAL_NEST_UPDATE)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|
|
|
"""Set up Nest from a config entry with dispatch between old/new flows."""
|
|
|
|
|
|
|
|
if DATA_SDM not in entry.data:
|
|
|
|
return await async_setup_legacy_entry(hass, entry)
|
|
|
|
|
|
|
|
implementation = (
|
|
|
|
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
|
|
|
hass, entry
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
config = hass.data[DOMAIN][DATA_NEST_CONFIG]
|
|
|
|
|
|
|
|
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
|
|
|
auth = api.AsyncConfigEntryAuth(
|
|
|
|
aiohttp_client.async_get_clientsession(hass),
|
|
|
|
session,
|
|
|
|
API_URL,
|
|
|
|
)
|
|
|
|
subscriber = GoogleNestSubscriber(
|
|
|
|
auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID]
|
|
|
|
)
|
|
|
|
subscriber.set_update_callback(SignalUpdateCallback(hass))
|
|
|
|
hass.loop.create_task(subscriber.start_async())
|
|
|
|
hass.data[DOMAIN][entry.entry_id] = subscriber
|
|
|
|
|
|
|
|
for component in PLATFORMS:
|
|
|
|
hass.async_create_task(
|
|
|
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
|
|
|
)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|
|
|
"""Unload a config entry."""
|
|
|
|
if DATA_SDM not in entry.data:
|
|
|
|
# Legacy API
|
|
|
|
return True
|
|
|
|
|
|
|
|
subscriber = hass.data[DOMAIN][entry.entry_id]
|
|
|
|
subscriber.stop_async()
|
|
|
|
unload_ok = all(
|
|
|
|
await asyncio.gather(
|
|
|
|
*[
|
|
|
|
hass.config_entries.async_forward_entry_unload(entry, component)
|
|
|
|
for component in PLATFORMS
|
|
|
|
]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if unload_ok:
|
|
|
|
hass.data[DOMAIN].pop(entry.entry_id)
|
|
|
|
|
|
|
|
return unload_ok
|
|
|
|
|
|
|
|
|
2018-08-16 11:46:43 +00:00
|
|
|
def nest_update_event_broker(hass, nest):
|
2018-06-01 14:44:58 +00:00
|
|
|
"""
|
|
|
|
Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data.
|
|
|
|
|
2020-10-21 08:17:49 +00:00
|
|
|
Used for the legacy nest API.
|
|
|
|
|
2018-08-16 11:46:43 +00:00
|
|
|
Runs in its own thread.
|
2018-06-01 14:44:58 +00:00
|
|
|
"""
|
2019-02-14 04:35:12 +00:00
|
|
|
_LOGGER.debug("Listening for nest.update_event")
|
2018-08-16 11:46:43 +00:00
|
|
|
|
|
|
|
while hass.is_running:
|
|
|
|
nest.update_event.wait()
|
|
|
|
|
|
|
|
if not hass.is_running:
|
|
|
|
break
|
|
|
|
|
|
|
|
nest.update_event.clear()
|
2019-02-14 04:35:12 +00:00
|
|
|
_LOGGER.debug("Dispatching nest data update")
|
2018-08-16 11:46:43 +00:00
|
|
|
dispatcher_send(hass, SIGNAL_NEST_UPDATE)
|
|
|
|
|
2019-02-14 04:35:12 +00:00
|
|
|
_LOGGER.debug("Stop listening for nest.update_event")
|
2018-06-01 14:44:58 +00:00
|
|
|
|
|
|
|
|
2020-10-21 08:17:49 +00:00
|
|
|
async def async_setup_legacy(hass, config):
|
|
|
|
"""Set up Nest components using the legacy nest API."""
|
2018-06-13 15:14:52 +00:00
|
|
|
if DOMAIN not in config:
|
2018-08-02 22:36:37 +00:00
|
|
|
return True
|
2016-11-28 00:18:47 +00:00
|
|
|
|
2018-06-13 15:14:52 +00:00
|
|
|
conf = config[DOMAIN]
|
|
|
|
|
|
|
|
local_auth.initialize(hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET])
|
|
|
|
|
|
|
|
filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE)
|
|
|
|
access_token_cache_file = hass.config.path(filename)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.async_create_task(
|
|
|
|
hass.config_entries.flow.async_init(
|
|
|
|
DOMAIN,
|
|
|
|
context={"source": config_entries.SOURCE_IMPORT},
|
|
|
|
data={"nest_conf_path": access_token_cache_file},
|
|
|
|
)
|
|
|
|
)
|
2018-06-13 15:14:52 +00:00
|
|
|
|
|
|
|
# Store config to be used during entry setup
|
|
|
|
hass.data[DATA_NEST_CONFIG] = conf
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2020-10-21 08:17:49 +00:00
|
|
|
async def async_setup_legacy_entry(hass, entry):
|
|
|
|
"""Set up Nest from legacy config entry."""
|
2018-06-13 15:14:52 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
nest = Nest(access_token=entry.data["tokens"]["access_token"])
|
2016-11-28 00:18:47 +00:00
|
|
|
|
|
|
|
_LOGGER.debug("proceeding with setup")
|
2018-06-13 15:14:52 +00:00
|
|
|
conf = hass.data.get(DATA_NEST_CONFIG, {})
|
2020-10-21 08:17:49 +00:00
|
|
|
hass.data[DATA_NEST] = NestLegacyDevice(hass, conf, nest)
|
2020-10-16 11:31:16 +00:00
|
|
|
if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize):
|
2018-06-25 20:06:00 +00:00
|
|
|
return False
|
2016-11-28 00:18:47 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
for component in "climate", "camera", "sensor", "binary_sensor":
|
|
|
|
hass.async_create_task(
|
|
|
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
|
|
|
)
|
2017-01-17 08:12:15 +00:00
|
|
|
|
2018-11-06 15:11:10 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
def set_away_mode(service):
|
|
|
|
"""Set the away mode 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:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info(
|
|
|
|
"Setting away mode for: %s to: %s",
|
|
|
|
structure.name,
|
|
|
|
service.data[ATTR_AWAY_MODE],
|
|
|
|
)
|
2018-11-06 15:11:10 +00:00
|
|
|
structure.away = service.data[ATTR_AWAY_MODE]
|
2018-06-12 05:28:16 +00:00
|
|
|
|
2018-11-06 15:11:10 +00:00
|
|
|
def set_eta(service):
|
|
|
|
"""Set away mode to away and include ETA for a Nest structure."""
|
2018-06-01 14:44:58 +00:00
|
|
|
if ATTR_STRUCTURE in service.data:
|
2018-11-06 15:11:10 +00:00
|
|
|
target_structures = service.data[ATTR_STRUCTURE]
|
|
|
|
validate_structures(target_structures)
|
2018-06-01 14:44:58 +00:00
|
|
|
else:
|
2018-11-06 15:11:10 +00:00
|
|
|
target_structures = hass.data[DATA_NEST].local_structure
|
2018-06-01 14:44:58 +00:00
|
|
|
|
|
|
|
for structure in nest.structures:
|
2018-11-06 15:11:10 +00:00
|
|
|
if structure.name in target_structures:
|
|
|
|
if structure.thermostats:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info(
|
|
|
|
"Setting away mode for: %s to: %s",
|
|
|
|
structure.name,
|
|
|
|
AWAY_MODE_AWAY,
|
|
|
|
)
|
2018-11-06 15:11:10 +00:00
|
|
|
structure.away = AWAY_MODE_AWAY
|
2018-06-12 05:28:16 +00:00
|
|
|
|
|
|
|
now = datetime.utcnow()
|
2018-11-06 15:11:10 +00:00
|
|
|
trip_id = service.data.get(
|
2020-04-05 15:48:55 +00:00
|
|
|
ATTR_TRIP_ID, f"trip_{int(now.timestamp())}"
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-06-12 05:28:16 +00:00
|
|
|
eta_begin = now + service.data[ATTR_ETA]
|
2019-07-31 19:25:30 +00:00
|
|
|
eta_window = service.data.get(ATTR_ETA_WINDOW, timedelta(minutes=1))
|
2018-06-12 05:28:16 +00:00
|
|
|
eta_end = eta_begin + eta_window
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info(
|
|
|
|
"Setting ETA for trip: %s, "
|
|
|
|
"ETA window starts at: %s and ends at: %s",
|
|
|
|
trip_id,
|
|
|
|
eta_begin,
|
|
|
|
eta_end,
|
|
|
|
)
|
2018-06-12 05:28:16 +00:00
|
|
|
structure.set_eta(trip_id, eta_begin, eta_end)
|
2018-11-06 15:11:10 +00:00
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info(
|
2020-01-02 19:17:10 +00:00
|
|
|
"No thermostats found in structure: %s, unable to set ETA",
|
2019-07-31 19:25:30 +00:00
|
|
|
structure.name,
|
|
|
|
)
|
2018-11-06 15:11:10 +00:00
|
|
|
|
|
|
|
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:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info(
|
|
|
|
"No thermostats found in structure: %s, "
|
|
|
|
"unable to cancel ETA",
|
|
|
|
structure.name,
|
|
|
|
)
|
2018-11-06 15:11:10 +00:00
|
|
|
|
|
|
|
hass.services.async_register(
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN, SERVICE_SET_AWAY_MODE, set_away_mode, schema=SET_AWAY_MODE_SCHEMA
|
|
|
|
)
|
2018-11-06 15:11:10 +00:00
|
|
|
|
|
|
|
hass.services.async_register(
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN, SERVICE_SET_ETA, set_eta, schema=SET_ETA_SCHEMA
|
|
|
|
)
|
2017-01-17 08:12:15 +00:00
|
|
|
|
2018-06-01 14:44:58 +00:00
|
|
|
hass.services.async_register(
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN, SERVICE_CANCEL_ETA, cancel_eta, schema=CANCEL_ETA_SCHEMA
|
|
|
|
)
|
2017-01-17 08:12:15 +00:00
|
|
|
|
2018-08-16 11:46:43 +00:00
|
|
|
@callback
|
2018-06-01 14:44:58 +00:00
|
|
|
def start_up(event):
|
|
|
|
"""Start Nest update event listener."""
|
2018-08-16 11:46:43 +00:00
|
|
|
threading.Thread(
|
2019-07-31 19:25:30 +00:00
|
|
|
name="Nest update listener",
|
2018-08-16 11:46:43 +00:00
|
|
|
target=nest_update_event_broker,
|
2019-07-31 19:25:30 +00:00
|
|
|
args=(hass, nest),
|
2018-08-16 11:46:43 +00:00
|
|
|
).start()
|
2018-06-01 14:44:58 +00:00
|
|
|
|
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up)
|
|
|
|
|
2018-08-16 11:46:43 +00:00
|
|
|
@callback
|
2018-06-01 14:44:58 +00:00
|
|
|
def shut_down(event):
|
|
|
|
"""Stop Nest update event listener."""
|
2018-08-16 11:46:43 +00:00
|
|
|
nest.update_event.set()
|
2018-06-01 14:44:58 +00:00
|
|
|
|
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down)
|
|
|
|
|
|
|
|
_LOGGER.debug("async_setup_nest is done")
|
2016-11-28 00:18:47 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2020-10-21 08:17:49 +00:00
|
|
|
class NestLegacyDevice:
|
|
|
|
"""Structure Nest functions for hass for legacy API."""
|
2016-11-05 00:22:47 +00:00
|
|
|
|
|
|
|
def __init__(self, hass, conf, nest):
|
|
|
|
"""Init Nest Devices."""
|
|
|
|
self.hass = hass
|
|
|
|
self.nest = nest
|
2018-06-13 15:14:52 +00:00
|
|
|
self.local_structure = conf.get(CONF_STRUCTURE)
|
2016-11-05 00:22:47 +00:00
|
|
|
|
2018-06-13 15:14:52 +00:00
|
|
|
def initialize(self):
|
|
|
|
"""Initialize Nest."""
|
2018-06-25 20:06:00 +00:00
|
|
|
try:
|
|
|
|
# Do not optimize next statement, it is here for initialize
|
|
|
|
# persistence Nest API connection.
|
|
|
|
structure_names = [s.name for s in self.nest.structures]
|
|
|
|
if self.local_structure is None:
|
|
|
|
self.local_structure = structure_names
|
|
|
|
|
2020-04-04 20:09:11 +00:00
|
|
|
except (AuthorizationError, APIError, OSError) as err:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Connection error while access Nest web service: %s", err)
|
2018-06-25 20:06:00 +00:00
|
|
|
return False
|
|
|
|
return True
|
2016-11-05 00:22:47 +00:00
|
|
|
|
2018-05-23 19:40:33 +00:00
|
|
|
def structures(self):
|
|
|
|
"""Generate a list of structures."""
|
|
|
|
try:
|
|
|
|
for structure in self.nest.structures:
|
2018-06-25 20:06:00 +00:00
|
|
|
if structure.name not in self.local_structure:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"Ignoring structure %s, not in %s",
|
|
|
|
structure.name,
|
|
|
|
self.local_structure,
|
|
|
|
)
|
2018-06-25 20:06:00 +00:00
|
|
|
continue
|
|
|
|
yield structure
|
|
|
|
|
2020-04-04 20:09:11 +00:00
|
|
|
except (AuthorizationError, APIError, OSError) as err:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Connection error while access Nest web service: %s", err)
|
2018-05-23 19:40:33 +00:00
|
|
|
|
2016-12-22 19:22:07 +00:00
|
|
|
def thermostats(self):
|
2018-06-25 20:06:00 +00:00
|
|
|
"""Generate a list of thermostats."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return self._devices("thermostats")
|
2016-11-05 00:22:47 +00:00
|
|
|
|
2016-12-22 19:22:07 +00:00
|
|
|
def smoke_co_alarms(self):
|
2018-01-29 22:37:19 +00:00
|
|
|
"""Generate a list of smoke co alarms."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return self._devices("smoke_co_alarms")
|
2016-11-28 00:18:47 +00:00
|
|
|
|
2016-12-22 19:22:07 +00:00
|
|
|
def cameras(self):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Generate a list of cameras."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return self._devices("cameras")
|
2018-06-25 20:06:00 +00:00
|
|
|
|
|
|
|
def _devices(self, device_type):
|
|
|
|
"""Generate a list of Nest devices."""
|
2016-11-28 00:18:47 +00:00
|
|
|
try:
|
|
|
|
for structure in self.nest.structures:
|
2018-06-25 20:06:00 +00:00
|
|
|
if structure.name not in self.local_structure:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"Ignoring structure %s, not in %s",
|
|
|
|
structure.name,
|
|
|
|
self.local_structure,
|
|
|
|
)
|
2018-06-25 20:06:00 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
for device in getattr(structure, device_type, []):
|
|
|
|
try:
|
|
|
|
# Do not optimize next statement,
|
|
|
|
# it is here for verify Nest API permission.
|
|
|
|
device.name_long
|
|
|
|
except KeyError:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning(
|
|
|
|
"Cannot retrieve device name for [%s]"
|
|
|
|
", please check your Nest developer "
|
2020-07-05 21:04:19 +00:00
|
|
|
"account permission settings",
|
2019-07-31 19:25:30 +00:00
|
|
|
device.serial,
|
|
|
|
)
|
2018-06-25 20:06:00 +00:00
|
|
|
continue
|
|
|
|
yield (structure, device)
|
|
|
|
|
2020-04-04 20:09:11 +00:00
|
|
|
except (AuthorizationError, APIError, OSError) as err:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Connection error while access Nest web service: %s", err)
|
2018-06-03 01:54:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
class NestSensorDevice(Entity):
|
|
|
|
"""Representation of a Nest sensor."""
|
|
|
|
|
|
|
|
def __init__(self, structure, device, variable):
|
|
|
|
"""Initialize the sensor."""
|
|
|
|
self.structure = structure
|
|
|
|
self.variable = variable
|
|
|
|
|
|
|
|
if device is not None:
|
|
|
|
# device specific
|
2018-08-26 19:25:39 +00:00
|
|
|
self.device = device
|
2020-04-05 15:48:55 +00:00
|
|
|
self._name = f"{self.device.name_long} {self.variable.replace('_', ' ')}"
|
2018-06-03 01:54:48 +00:00
|
|
|
else:
|
|
|
|
# structure only
|
2018-08-26 19:25:39 +00:00
|
|
|
self.device = structure
|
2020-04-05 15:48:55 +00:00
|
|
|
self._name = f"{self.structure.name} {self.variable.replace('_', ' ')}"
|
2018-06-03 01:54:48 +00:00
|
|
|
|
|
|
|
self._state = None
|
|
|
|
self._unit = None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of the nest, if any."""
|
|
|
|
return self._name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def unit_of_measurement(self):
|
|
|
|
"""Return the unit the value is expressed in."""
|
|
|
|
return self._unit
|
|
|
|
|
|
|
|
@property
|
|
|
|
def should_poll(self):
|
|
|
|
"""Do not need poll thanks using Nest streaming API."""
|
|
|
|
return False
|
|
|
|
|
2018-09-26 07:19:47 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return unique id based on device serial and variable."""
|
2019-09-03 18:35:00 +00:00
|
|
|
return f"{self.device.serial}-{self.variable}"
|
2018-09-26 07:19:47 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def device_info(self):
|
|
|
|
"""Return information about the device."""
|
2019-07-31 19:25:30 +00:00
|
|
|
if not hasattr(self.device, "name_long"):
|
2018-09-26 07:19:47 +00:00
|
|
|
name = self.structure.name
|
|
|
|
model = "Structure"
|
|
|
|
else:
|
|
|
|
name = self.device.name_long
|
|
|
|
if self.device.is_thermostat:
|
2019-07-31 19:25:30 +00:00
|
|
|
model = "Thermostat"
|
2018-09-26 07:19:47 +00:00
|
|
|
elif self.device.is_camera:
|
2019-07-31 19:25:30 +00:00
|
|
|
model = "Camera"
|
2018-09-26 07:19:47 +00:00
|
|
|
elif self.device.is_smoke_co_alarm:
|
2019-07-31 19:25:30 +00:00
|
|
|
model = "Nest Protect"
|
2018-09-26 07:19:47 +00:00
|
|
|
else:
|
|
|
|
model = None
|
|
|
|
|
|
|
|
return {
|
2019-07-31 19:25:30 +00:00
|
|
|
"identifiers": {(DOMAIN, self.device.serial)},
|
|
|
|
"name": name,
|
|
|
|
"manufacturer": "Nest Labs",
|
|
|
|
"model": model,
|
2018-09-26 07:19:47 +00:00
|
|
|
}
|
|
|
|
|
2018-06-03 01:54:48 +00:00
|
|
|
def update(self):
|
|
|
|
"""Do not use NestSensorDevice directly."""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
async def async_added_to_hass(self):
|
|
|
|
"""Register update signal handler."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2018-06-03 01:54:48 +00:00
|
|
|
async def async_update_state():
|
|
|
|
"""Update sensor state."""
|
|
|
|
await self.async_update_ha_state(True)
|
|
|
|
|
2020-04-02 16:25:33 +00:00
|
|
|
self.async_on_remove(
|
|
|
|
async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state)
|
|
|
|
)
|