Move Legacy Works With Nest integration to subdirectory (#44368)

* Move Legacy Works With Nest integration to subdirectory

Motivation is to streamline the actively developed integration e.g. make code coverage easier to reason about and simplify __init__.py
This commit is contained in:
Allen Porter 2020-12-22 12:42:37 -08:00 committed by GitHub
parent 9c5f608ffd
commit 24ccdb55bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 625 additions and 591 deletions

View file

@ -579,12 +579,9 @@ omit =
homeassistant/components/nest/api.py
homeassistant/components/nest/binary_sensor.py
homeassistant/components/nest/camera.py
homeassistant/components/nest/camera_legacy.py
homeassistant/components/nest/climate.py
homeassistant/components/nest/climate_legacy.py
homeassistant/components/nest/local_auth.py
homeassistant/components/nest/legacy/*
homeassistant/components/nest/sensor.py
homeassistant/components/nest/sensor_legacy.py
homeassistant/components/netatmo/__init__.py
homeassistant/components/netatmo/api.py
homeassistant/components/netatmo/camera.py

View file

@ -1,45 +1,32 @@
"""Support for Nest devices."""
import asyncio
from datetime import datetime, timedelta
import logging
import threading
from google_nest_sdm.event import AsyncEventCallback, EventMessage
from google_nest_sdm.exceptions import AuthException, GoogleNestException
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
from nest import Nest
from nest.nest import APIError, AuthorizationError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.const import (
CONF_BINARY_SENSORS,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_FILENAME,
CONF_MONITORED_CONDITIONS,
CONF_SENSORS,
CONF_STRUCTURE,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
config_validation as cv,
)
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.dispatcher import async_dispatcher_send
from . import api, config_flow, local_auth
from . import api, config_flow
from .const import (
API_URL,
DATA_SDM,
@ -50,34 +37,15 @@ from .const import (
SIGNAL_NEST_UPDATE,
)
from .events import EVENT_NAME_MAP, NEST_EVENT
from .legacy import async_setup_legacy, async_setup_legacy_entry
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
CONF_PROJECT_ID = "project_id"
CONF_SUBSCRIBER_ID = "subscriber_id"
# Configuration for the legacy nest API
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)}
)
@ -104,31 +72,6 @@ CONFIG_SCHEMA = vol.Schema(
# Platforms for SDM API
PLATFORMS = ["sensor", "camera", "climate"]
# Services for the legacy API
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]),
}
)
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up Nest components with dispatch between old/new flows."""
@ -283,348 +226,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data[DOMAIN].pop(DATA_SUBSCRIBER)
return unload_ok
def nest_update_event_broker(hass, nest):
"""
Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data.
Used for the legacy nest API.
Runs in its own thread.
"""
_LOGGER.debug("Listening for nest.update_event")
while hass.is_running:
nest.update_event.wait()
if not hass.is_running:
break
nest.update_event.clear()
_LOGGER.debug("Dispatching nest data update")
dispatcher_send(hass, SIGNAL_NEST_UPDATE)
_LOGGER.debug("Stop listening for nest.update_event")
async def async_setup_legacy(hass, config):
"""Set up Nest components using the legacy nest API."""
if DOMAIN not in config:
return True
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)
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},
)
)
# Store config to be used during entry setup
hass.data[DATA_NEST_CONFIG] = conf
return True
async def async_setup_legacy_entry(hass, entry):
"""Set up Nest from legacy config entry."""
nest = Nest(access_token=entry.data["tokens"]["access_token"])
_LOGGER.debug("proceeding with setup")
conf = hass.data.get(DATA_NEST_CONFIG, {})
hass.data[DATA_NEST] = NestLegacyDevice(hass, conf, nest)
if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize):
return False
for component in "climate", "camera", "sensor", "binary_sensor":
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
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:
_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
now = datetime.utcnow()
trip_id = service.data.get(
ATTR_TRIP_ID, f"trip_{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
_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.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, 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):
"""Start Nest update event listener."""
threading.Thread(
name="Nest update listener",
target=nest_update_event_broker,
args=(hass, nest),
).start()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up)
@callback
def shut_down(event):
"""Stop Nest update event listener."""
nest.update_event.set()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down)
_LOGGER.debug("async_setup_nest is done")
return True
class NestLegacyDevice:
"""Structure Nest functions for hass for legacy API."""
def __init__(self, hass, conf, nest):
"""Init Nest Devices."""
self.hass = hass
self.nest = nest
self.local_structure = conf.get(CONF_STRUCTURE)
def initialize(self):
"""Initialize Nest."""
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
except (AuthorizationError, APIError, OSError) as err:
_LOGGER.error("Connection error while access Nest web service: %s", err)
return False
return True
def structures(self):
"""Generate a list of structures."""
try:
for structure in self.nest.structures:
if structure.name not in self.local_structure:
_LOGGER.debug(
"Ignoring structure %s, not in %s",
structure.name,
self.local_structure,
)
continue
yield structure
except (AuthorizationError, APIError, OSError) as err:
_LOGGER.error("Connection error while access Nest web service: %s", err)
def thermostats(self):
"""Generate a list of thermostats."""
return self._devices("thermostats")
def smoke_co_alarms(self):
"""Generate a list of smoke co alarms."""
return self._devices("smoke_co_alarms")
def cameras(self):
"""Generate a list of cameras."""
return self._devices("cameras")
def _devices(self, device_type):
"""Generate a list of Nest devices."""
try:
for structure in self.nest.structures:
if structure.name not in self.local_structure:
_LOGGER.debug(
"Ignoring structure %s, not in %s",
structure.name,
self.local_structure,
)
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:
_LOGGER.warning(
"Cannot retrieve device name for [%s]"
", please check your Nest developer "
"account permission settings",
device.serial,
)
continue
yield (structure, device)
except (AuthorizationError, APIError, OSError) as err:
_LOGGER.error("Connection error while access Nest web service: %s", err)
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
self.device = device
self._name = f"{self.device.name_long} {self.variable.replace('_', ' ')}"
else:
# structure only
self.device = structure
self._name = f"{self.structure.name} {self.variable.replace('_', ' ')}"
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
@property
def unique_id(self):
"""Return unique id based on device serial and variable."""
return f"{self.device.serial}-{self.variable}"
@property
def device_info(self):
"""Return information about the device."""
if not hasattr(self.device, "name_long"):
name = self.structure.name
model = "Structure"
else:
name = self.device.name_long
if self.device.is_thermostat:
model = "Thermostat"
elif self.device.is_camera:
model = "Camera"
elif self.device.is_smoke_co_alarm:
model = "Nest Protect"
else:
model = None
return {
"identifiers": {(DOMAIN, self.device.serial)},
"name": name,
"manufacturer": "Nest Labs",
"model": model,
}
def update(self):
"""Do not use NestSensorDevice directly."""
raise NotImplementedError
async def async_added_to_hass(self):
"""Register update signal handler."""
async def async_update_state():
"""Update sensor state."""
await self.async_update_ha_state(True)
self.async_on_remove(
async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state)
)

View file

@ -1,166 +1,15 @@
"""Support for Nest Thermostat binary sensors."""
from itertools import chain
import logging
"""Support for Nest binary sensors that dispatches between API versions."""
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
DEVICE_CLASS_SOUND,
BinarySensorEntity,
)
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from . import CONF_BINARY_SENSORS, DATA_NEST, DATA_NEST_CONFIG, NestSensorDevice
_LOGGER = logging.getLogger(__name__)
BINARY_TYPES = {"online": DEVICE_CLASS_CONNECTIVITY}
CLIMATE_BINARY_TYPES = {
"fan": None,
"is_using_emergency_heat": "heat",
"is_locked": None,
"has_leaf": None,
}
CAMERA_BINARY_TYPES = {
"motion_detected": DEVICE_CLASS_MOTION,
"sound_detected": DEVICE_CLASS_SOUND,
"person_detected": DEVICE_CLASS_OCCUPANCY,
}
STRUCTURE_BINARY_TYPES = {"away": None}
STRUCTURE_BINARY_STATE_MAP = {"away": {"away": True, "home": False}}
_BINARY_TYPES_DEPRECATED = [
"hvac_ac_state",
"hvac_aux_heater_state",
"hvac_heater_state",
"hvac_heat_x2_state",
"hvac_heat_x3_state",
"hvac_alt_heat_state",
"hvac_alt_heat_x2_state",
"hvac_emer_heat_state",
]
_VALID_BINARY_SENSOR_TYPES = {
**BINARY_TYPES,
**CLIMATE_BINARY_TYPES,
**CAMERA_BINARY_TYPES,
**STRUCTURE_BINARY_TYPES,
}
from .const import DATA_SDM
from .legacy.sensor import async_setup_legacy_entry
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Nest binary sensors.
No longer used.
"""
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up a Nest binary sensor based on a config entry."""
nest = hass.data[DATA_NEST]
discovery_info = hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {})
# Add all available binary sensors if no Nest binary sensor config is set
if discovery_info == {}:
conditions = _VALID_BINARY_SENSOR_TYPES
else:
conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {})
for variable in conditions:
if variable in _BINARY_TYPES_DEPRECATED:
wstr = (
f"{variable} is no a longer supported "
"monitored_conditions. See "
"https://www.home-assistant.io/integrations/binary_sensor.nest/ "
"for valid options."
)
_LOGGER.error(wstr)
def get_binary_sensors():
"""Get the Nest binary sensors."""
sensors = []
for structure in nest.structures():
sensors += [
NestBinarySensor(structure, None, variable)
for variable in conditions
if variable in STRUCTURE_BINARY_TYPES
]
device_chain = chain(nest.thermostats(), nest.smoke_co_alarms(), nest.cameras())
for structure, device in device_chain:
sensors += [
NestBinarySensor(structure, device, variable)
for variable in conditions
if variable in BINARY_TYPES
]
sensors += [
NestBinarySensor(structure, device, variable)
for variable in conditions
if variable in CLIMATE_BINARY_TYPES and device.is_thermostat
]
if device.is_camera:
sensors += [
NestBinarySensor(structure, device, variable)
for variable in conditions
if variable in CAMERA_BINARY_TYPES
]
for activity_zone in device.activity_zones:
sensors += [
NestActivityZoneSensor(structure, device, activity_zone)
]
return sensors
async_add_entities(await hass.async_add_executor_job(get_binary_sensors), True)
class NestBinarySensor(NestSensorDevice, BinarySensorEntity):
"""Represents a Nest binary sensor."""
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
@property
def device_class(self):
"""Return the device class of the binary sensor."""
return _VALID_BINARY_SENSOR_TYPES.get(self.variable)
def update(self):
"""Retrieve latest state."""
value = getattr(self.device, self.variable)
if self.variable in STRUCTURE_BINARY_TYPES:
self._state = bool(STRUCTURE_BINARY_STATE_MAP[self.variable].get(value))
else:
self._state = bool(value)
class NestActivityZoneSensor(NestBinarySensor):
"""Represents a Nest binary sensor for activity in a zone."""
def __init__(self, structure, device, zone):
"""Initialize the sensor."""
super().__init__(structure, device, "")
self.zone = zone
self._name = f"{self._name} {self.zone.name} activity"
@property
def unique_id(self):
"""Return unique id based on camera serial and zone id."""
return f"{self.device.serial}-{self.zone.zone_id}"
@property
def device_class(self):
"""Return the device class of the binary sensor."""
return DEVICE_CLASS_MOTION
def update(self):
"""Retrieve latest state."""
self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id)
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the binary sensors."""
assert DATA_SDM not in entry.data
await async_setup_legacy_entry(hass, entry, async_add_entities)

View file

@ -3,9 +3,9 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from .camera_legacy import async_setup_legacy_entry
from .camera_sdm import async_setup_sdm_entry
from .const import DATA_SDM
from .legacy.camera import async_setup_legacy_entry
async def async_setup_entry(

View file

@ -3,9 +3,9 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from .climate_legacy import async_setup_legacy_entry
from .climate_sdm import async_setup_sdm_entry
from .const import DATA_SDM
from .legacy.climate import async_setup_legacy_entry
async def async_setup_entry(

View file

@ -0,0 +1,416 @@
"""Support for Nest devices."""
from datetime import datetime, timedelta
import logging
import threading
from nest import Nest
from nest.nest import APIError, AuthorizationError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_FILENAME,
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 async_dispatcher_connect, dispatcher_send
from homeassistant.helpers.entity import Entity
from . import local_auth
from .const import DATA_NEST, DATA_NEST_CONFIG, DOMAIN, SIGNAL_NEST_UPDATE
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
# Configuration for the legacy nest API
SERVICE_CANCEL_ETA = "cancel_eta"
SERVICE_SET_ETA = "set_eta"
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"
# Services for the legacy API
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):
"""
Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data.
Used for the legacy nest API.
Runs in its own thread.
"""
_LOGGER.debug("Listening for nest.update_event")
while hass.is_running:
nest.update_event.wait()
if not hass.is_running:
break
nest.update_event.clear()
_LOGGER.debug("Dispatching nest data update")
dispatcher_send(hass, SIGNAL_NEST_UPDATE)
_LOGGER.debug("Stop listening for nest.update_event")
async def async_setup_legacy(hass, config):
"""Set up Nest components using the legacy nest API."""
if DOMAIN not in config:
return True
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)
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},
)
)
# Store config to be used during entry setup
hass.data[DATA_NEST_CONFIG] = conf
return True
async def async_setup_legacy_entry(hass, entry):
"""Set up Nest from legacy config entry."""
nest = Nest(access_token=entry.data["tokens"]["access_token"])
_LOGGER.debug("proceeding with setup")
conf = hass.data.get(DATA_NEST_CONFIG, {})
hass.data[DATA_NEST] = NestLegacyDevice(hass, conf, nest)
if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize):
return False
for component in "climate", "camera", "sensor", "binary_sensor":
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
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:
_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
now = datetime.utcnow()
trip_id = service.data.get(
ATTR_TRIP_ID, f"trip_{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
_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.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, 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):
"""Start Nest update event listener."""
threading.Thread(
name="Nest update listener",
target=nest_update_event_broker,
args=(hass, nest),
).start()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up)
@callback
def shut_down(event):
"""Stop Nest update event listener."""
nest.update_event.set()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down)
_LOGGER.debug("async_setup_nest is done")
return True
class NestLegacyDevice:
"""Structure Nest functions for hass for legacy API."""
def __init__(self, hass, conf, nest):
"""Init Nest Devices."""
self.hass = hass
self.nest = nest
self.local_structure = conf.get(CONF_STRUCTURE)
def initialize(self):
"""Initialize Nest."""
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
except (AuthorizationError, APIError, OSError) as err:
_LOGGER.error("Connection error while access Nest web service: %s", err)
return False
return True
def structures(self):
"""Generate a list of structures."""
try:
for structure in self.nest.structures:
if structure.name not in self.local_structure:
_LOGGER.debug(
"Ignoring structure %s, not in %s",
structure.name,
self.local_structure,
)
continue
yield structure
except (AuthorizationError, APIError, OSError) as err:
_LOGGER.error("Connection error while access Nest web service: %s", err)
def thermostats(self):
"""Generate a list of thermostats."""
return self._devices("thermostats")
def smoke_co_alarms(self):
"""Generate a list of smoke co alarms."""
return self._devices("smoke_co_alarms")
def cameras(self):
"""Generate a list of cameras."""
return self._devices("cameras")
def _devices(self, device_type):
"""Generate a list of Nest devices."""
try:
for structure in self.nest.structures:
if structure.name not in self.local_structure:
_LOGGER.debug(
"Ignoring structure %s, not in %s",
structure.name,
self.local_structure,
)
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:
_LOGGER.warning(
"Cannot retrieve device name for [%s]"
", please check your Nest developer "
"account permission settings",
device.serial,
)
continue
yield (structure, device)
except (AuthorizationError, APIError, OSError) as err:
_LOGGER.error("Connection error while access Nest web service: %s", err)
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
self.device = device
self._name = f"{self.device.name_long} {self.variable.replace('_', ' ')}"
else:
# structure only
self.device = structure
self._name = f"{self.structure.name} {self.variable.replace('_', ' ')}"
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
@property
def unique_id(self):
"""Return unique id based on device serial and variable."""
return f"{self.device.serial}-{self.variable}"
@property
def device_info(self):
"""Return information about the device."""
if not hasattr(self.device, "name_long"):
name = self.structure.name
model = "Structure"
else:
name = self.device.name_long
if self.device.is_thermostat:
model = "Thermostat"
elif self.device.is_camera:
model = "Camera"
elif self.device.is_smoke_co_alarm:
model = "Nest Protect"
else:
model = None
return {
"identifiers": {(DOMAIN, self.device.serial)},
"name": name,
"manufacturer": "Nest Labs",
"model": model,
}
def update(self):
"""Do not use NestSensorDevice directly."""
raise NotImplementedError
async def async_added_to_hass(self):
"""Register update signal handler."""
async def async_update_state():
"""Update sensor state."""
await self.async_update_ha_state(True)
self.async_on_remove(
async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state)
)

View file

@ -0,0 +1,167 @@
"""Support for Nest Thermostat binary sensors."""
from itertools import chain
import logging
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
DEVICE_CLASS_SOUND,
BinarySensorEntity,
)
from homeassistant.const import CONF_BINARY_SENSORS, CONF_MONITORED_CONDITIONS
from . import NestSensorDevice
from .const import DATA_NEST, DATA_NEST_CONFIG
_LOGGER = logging.getLogger(__name__)
BINARY_TYPES = {"online": DEVICE_CLASS_CONNECTIVITY}
CLIMATE_BINARY_TYPES = {
"fan": None,
"is_using_emergency_heat": "heat",
"is_locked": None,
"has_leaf": None,
}
CAMERA_BINARY_TYPES = {
"motion_detected": DEVICE_CLASS_MOTION,
"sound_detected": DEVICE_CLASS_SOUND,
"person_detected": DEVICE_CLASS_OCCUPANCY,
}
STRUCTURE_BINARY_TYPES = {"away": None}
STRUCTURE_BINARY_STATE_MAP = {"away": {"away": True, "home": False}}
_BINARY_TYPES_DEPRECATED = [
"hvac_ac_state",
"hvac_aux_heater_state",
"hvac_heater_state",
"hvac_heat_x2_state",
"hvac_heat_x3_state",
"hvac_alt_heat_state",
"hvac_alt_heat_x2_state",
"hvac_emer_heat_state",
]
_VALID_BINARY_SENSOR_TYPES = {
**BINARY_TYPES,
**CLIMATE_BINARY_TYPES,
**CAMERA_BINARY_TYPES,
**STRUCTURE_BINARY_TYPES,
}
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Nest binary sensors.
No longer used.
"""
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up a Nest binary sensor based on a config entry."""
nest = hass.data[DATA_NEST]
discovery_info = hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {})
# Add all available binary sensors if no Nest binary sensor config is set
if discovery_info == {}:
conditions = _VALID_BINARY_SENSOR_TYPES
else:
conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {})
for variable in conditions:
if variable in _BINARY_TYPES_DEPRECATED:
wstr = (
f"{variable} is no a longer supported "
"monitored_conditions. See "
"https://www.home-assistant.io/integrations/binary_sensor.nest/ "
"for valid options."
)
_LOGGER.error(wstr)
def get_binary_sensors():
"""Get the Nest binary sensors."""
sensors = []
for structure in nest.structures():
sensors += [
NestBinarySensor(structure, None, variable)
for variable in conditions
if variable in STRUCTURE_BINARY_TYPES
]
device_chain = chain(nest.thermostats(), nest.smoke_co_alarms(), nest.cameras())
for structure, device in device_chain:
sensors += [
NestBinarySensor(structure, device, variable)
for variable in conditions
if variable in BINARY_TYPES
]
sensors += [
NestBinarySensor(structure, device, variable)
for variable in conditions
if variable in CLIMATE_BINARY_TYPES and device.is_thermostat
]
if device.is_camera:
sensors += [
NestBinarySensor(structure, device, variable)
for variable in conditions
if variable in CAMERA_BINARY_TYPES
]
for activity_zone in device.activity_zones:
sensors += [
NestActivityZoneSensor(structure, device, activity_zone)
]
return sensors
async_add_entities(await hass.async_add_executor_job(get_binary_sensors), True)
class NestBinarySensor(NestSensorDevice, BinarySensorEntity):
"""Represents a Nest binary sensor."""
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
@property
def device_class(self):
"""Return the device class of the binary sensor."""
return _VALID_BINARY_SENSOR_TYPES.get(self.variable)
def update(self):
"""Retrieve latest state."""
value = getattr(self.device, self.variable)
if self.variable in STRUCTURE_BINARY_TYPES:
self._state = bool(STRUCTURE_BINARY_STATE_MAP[self.variable].get(value))
else:
self._state = bool(value)
class NestActivityZoneSensor(NestBinarySensor):
"""Represents a Nest binary sensor for activity in a zone."""
def __init__(self, structure, device, zone):
"""Initialize the sensor."""
super().__init__(structure, device, "")
self.zone = zone
self._name = f"{self._name} {self.zone.name} activity"
@property
def unique_id(self):
"""Return unique id based on camera serial and zone id."""
return f"{self.device.serial}-{self.zone.zone_id}"
@property
def device_class(self):
"""Return the device class of the binary sensor."""
return DEVICE_CLASS_MOTION
def update(self):
"""Retrieve latest state."""
self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id)

View file

@ -4,10 +4,11 @@ import logging
import requests
from homeassistant.components import nest
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_ON_OFF, Camera
from homeassistant.util.dt import utcnow
from .const import DATA_NEST, DOMAIN
_LOGGER = logging.getLogger(__name__)
NEST_BRAND = "Nest"
@ -24,9 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_legacy_entry(hass, entry, async_add_entities):
"""Set up a Nest sensor based on a config entry."""
camera_devices = await hass.async_add_executor_job(
hass.data[nest.DATA_NEST].cameras
)
camera_devices = await hass.async_add_executor_job(hass.data[DATA_NEST].cameras)
cameras = [NestCamera(structure, device) for structure, device in camera_devices]
async_add_entities(cameras, True)
@ -63,7 +62,7 @@ class NestCamera(Camera):
def device_info(self):
"""Return information about the device."""
return {
"identifiers": {(nest.DOMAIN, self.device.device_id)},
"identifiers": {(DOMAIN, self.device.device_id)},
"name": self.device.name_long,
"manufacturer": "Nest Labs",
"model": "Camera",

View file

@ -1,4 +1,4 @@
"""Support for Nest thermostats."""
"""Legacy Works with Nest climate implementation."""
import logging
from nest.nest import APIError
@ -33,8 +33,7 @@ from homeassistant.const import (
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import DATA_NEST, DOMAIN as NEST_DOMAIN
from .const import SIGNAL_NEST_UPDATE
from .const import DATA_NEST, DOMAIN, SIGNAL_NEST_UPDATE
_LOGGER = logging.getLogger(__name__)
@ -170,7 +169,7 @@ class NestThermostat(ClimateEntity):
def device_info(self):
"""Return information about the device."""
return {
"identifiers": {(NEST_DOMAIN, self.device.device_id)},
"identifiers": {(DOMAIN, self.device.device_id)},
"name": self.device.name_long,
"manufacturer": "Nest Labs",
"model": "Thermostat",

View file

@ -0,0 +1,6 @@
"""Constants used by the legacy Nest component."""
DOMAIN = "nest"
DATA_NEST = "nest"
DATA_NEST_CONFIG = "nest_config"
SIGNAL_NEST_UPDATE = "nest_update"

View file

@ -7,14 +7,14 @@ from nest.nest import AUTHORIZE_URL, AuthorizationError, NestAuth
from homeassistant.const import HTTP_UNAUTHORIZED
from homeassistant.core import callback
from . import config_flow
from ..config_flow import CodeInvalid, NestAuthError, register_flow_implementation
from .const import DOMAIN
@callback
def initialize(hass, client_id, client_secret):
"""Initialize a local auth provider."""
config_flow.register_flow_implementation(
register_flow_implementation(
hass,
DOMAIN,
"configuration.yaml",
@ -44,7 +44,7 @@ async def resolve_auth_code(hass, client_id, client_secret, code):
return await result
except AuthorizationError as err:
if err.response.status_code == HTTP_UNAUTHORIZED:
raise config_flow.CodeInvalid()
raise config_flow.NestAuthError(
raise CodeInvalid() from err
raise NestAuthError(
f"Unknown error: {err} ({err.response.status_code})"
)
) from err

View file

@ -3,6 +3,7 @@ import logging
from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
CONF_SENSORS,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
PERCENTAGE,
@ -11,7 +12,8 @@ from homeassistant.const import (
TEMP_FAHRENHEIT,
)
from . import CONF_SENSORS, DATA_NEST, DATA_NEST_CONFIG, NestSensorDevice
from . import NestSensorDevice
from .const import DATA_NEST, DATA_NEST_CONFIG
SENSOR_TYPES = ["humidity", "operation_mode", "hvac_state"]

View file

@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from .const import DATA_SDM
from .sensor_legacy import async_setup_legacy_entry
from .legacy.sensor import async_setup_legacy_entry
from .sensor_sdm import async_setup_sdm_entry

View file

@ -4,7 +4,8 @@ from urllib.parse import parse_qsl
import pytest
import requests_mock as rmock
from homeassistant.components.nest import config_flow, const, local_auth
from homeassistant.components.nest import config_flow, const
from homeassistant.components.nest.legacy import local_auth
@pytest.fixture