mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Add config flow to ecobee (#26634)
* Add basic config flow * Fix json files * Update __init__.py * Fix json errors * Move constants to const.py * Add ecobee to generated config flows * Update config_flow for updated API * Update manifest to include new dependencies Bump pyecobee, add aiofiles. * Update constants for ecobee * Modify ecobee setup to use config flow * Bump dependency * Update binary_sensor to use config_entry * Update sensor to use config_entry * Update __init__.py * Update weather to use config_entry * Update notify.py * Update ecobee constants * Update climate to use config_entry * Avoid a breaking change on ecobee services * Store api key from old config entry * Allow unloading of config entry * Show user a form before import * Refine import flow * Update strings.json to remove import step Not needed. * Move third party imports to top of module * Remove periods from end of log messages * Make configuration.yaml config optional * Remove unused strings * Reorganize config flow * Remove unneeded requirement * No need to store API key * Update async_unload_entry * Clean up if/else statements * Update requirements_all.txt * Fix config schema * Update __init__.py * Remove check for DATA_ECOBEE_CONFIG * Remove redundant check * Add check for DATA_ECOBEE_CONFIG * Change setup_platform to async * Fix state unknown and imports * Change init step to user * Have import step raise specific exceptions * Rearrange try/except block in import flow * Convert update() and refresh() to coroutines ...and update platforms to use async_update coroutine. * Finish converting init to async * Preliminary tests * Test full implementation * Update test_config_flow.py * Update test_config_flow.py * Add self to codeowners * Update test_config_flow.py * Use MockConfigEntry * Update test_config_flow.py * Update CODEOWNERS * pylint fixes * Register services under ecobee domain Breaking change! * Pylint fixes * Pylint fixes * Pylint fixes * Move service strings to ecobee domain * Fix log message capitalization * Fix import formatting * Update .coveragerc * Add __init__ to coveragerc * Add option flow test * Update .coveragerc * Act on updated options * Revert "Act on updated options" This reverts commit 56b0a859f2e3e80b6f4c77a8f784a2b29ee2cce9. * Remove hold_temp from climate * Remove hold_temp and options from init * Remove options handler from config flow * Remove options strings * Remove options flow test * Remove hold_temp constants * Fix climate tests * Pass api key to user step in import flow * Update test_config_flow.py Ensure that the import step calls the user step with the user's api key as user input if importing from ecobee.conf/validating imported keys fails.
This commit is contained in:
parent
cff7fd0ef3
commit
f6995b8d17
@ -156,7 +156,12 @@ omit =
|
||||
homeassistant/components/ebox/sensor.py
|
||||
homeassistant/components/ebusd/*
|
||||
homeassistant/components/ecoal_boiler/*
|
||||
homeassistant/components/ecobee/*
|
||||
homeassistant/components/ecobee/__init__.py
|
||||
homeassistant/components/ecobee/binary_sensor.py
|
||||
homeassistant/components/ecobee/climate.py
|
||||
homeassistant/components/ecobee/notify.py
|
||||
homeassistant/components/ecobee/sensor.py
|
||||
homeassistant/components/ecobee/weather.py
|
||||
homeassistant/components/econet/water_heater.py
|
||||
homeassistant/components/ecovacs/*
|
||||
homeassistant/components/eddystone_temperature/sensor.py
|
||||
|
@ -73,6 +73,7 @@ homeassistant/components/digital_ocean/* @fabaff
|
||||
homeassistant/components/discogs/* @thibmaek
|
||||
homeassistant/components/doorbird/* @oblogic7
|
||||
homeassistant/components/dweet/* @fabaff
|
||||
homeassistant/components/ecobee/* @marthoc
|
||||
homeassistant/components/ecovacs/* @OverloadUT
|
||||
homeassistant/components/egardia/* @jeroenterheerdt
|
||||
homeassistant/components/eight_sleep/* @mezz64
|
||||
|
@ -72,26 +72,6 @@ set_swing_mode:
|
||||
swing_mode:
|
||||
description: New value of swing mode.
|
||||
|
||||
ecobee_set_fan_min_on_time:
|
||||
description: Set the minimum fan on time.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
fan_min_on_time:
|
||||
description: New value of fan min on time.
|
||||
example: 5
|
||||
|
||||
ecobee_resume_program:
|
||||
description: Resume the programmed schedule.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
resume_all:
|
||||
description: Resume all events and return to the scheduled program. This default to false which removes only the top event.
|
||||
example: true
|
||||
|
||||
mill_set_room_temperature:
|
||||
description: Set Mill room temperatures.
|
||||
fields:
|
||||
|
23
homeassistant/components/ecobee/.translations/en.json
Normal file
23
homeassistant/components/ecobee/.translations/en.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "ecobee",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "ecobee API key",
|
||||
"description": "Please enter the API key obtained from ecobee.com.",
|
||||
"data": {"api_key": "API Key"}
|
||||
},
|
||||
"authorize": {
|
||||
"title": "Authorize app on ecobee.com",
|
||||
"description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"pin_request_failed": "Error requesting PIN from ecobee; please verify API key is correct.",
|
||||
"token_request_failed": "Error requesting tokens from ecobee; please try again."
|
||||
},
|
||||
"abort": {
|
||||
"one_instance_only": "This integration currently supports only one ecobee instance."
|
||||
}
|
||||
}
|
||||
}
|
@ -1,123 +1,130 @@
|
||||
"""Support for Ecobee devices."""
|
||||
import logging
|
||||
import os
|
||||
"""Support for ecobee."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from pyecobee import Ecobee, ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, ExpiredTokenError
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.util.json import save_json
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_HOLD_TEMP = "hold_temp"
|
||||
|
||||
DOMAIN = "ecobee"
|
||||
|
||||
ECOBEE_CONFIG_FILE = "ecobee.conf"
|
||||
from .const import (
|
||||
CONF_REFRESH_TOKEN,
|
||||
DATA_ECOBEE_CONFIG,
|
||||
DOMAIN,
|
||||
ECOBEE_PLATFORMS,
|
||||
_LOGGER,
|
||||
)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180)
|
||||
|
||||
NETWORK = None
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
{DOMAIN: vol.Schema({vol.Optional(CONF_API_KEY): cv.string})}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
|
||||
def request_configuration(network, hass, config):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
if "ecobee" in _CONFIGURING:
|
||||
configurator.notify_errors(
|
||||
_CONFIGURING["ecobee"], "Failed to register, please try again."
|
||||
async def async_setup(hass, config):
|
||||
"""
|
||||
Ecobee uses config flow for configuration.
|
||||
|
||||
But, an "ecobee:" entry in configuration.yaml will trigger an import flow
|
||||
if a config entry doesn't already exist. If ecobee.conf exists, the import
|
||||
flow will attempt to import it and create a config entry, to assist users
|
||||
migrating from the old ecobee component. Otherwise, the user will have to
|
||||
continue setting up the integration via the config flow.
|
||||
"""
|
||||
hass.data[DATA_ECOBEE_CONFIG] = config.get(DOMAIN, {})
|
||||
|
||||
if not hass.config_entries.async_entries(DOMAIN) and hass.data[DATA_ECOBEE_CONFIG]:
|
||||
# No config entry exists and configuration.yaml config exists, trigger the import flow.
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
def ecobee_configuration_callback(callback_data):
|
||||
"""Handle configuration callbacks."""
|
||||
network.request_tokens()
|
||||
network.update()
|
||||
setup_ecobee(hass, network, config)
|
||||
|
||||
_CONFIGURING["ecobee"] = configurator.request_config(
|
||||
"Ecobee",
|
||||
ecobee_configuration_callback,
|
||||
description=(
|
||||
"Please authorize this app at https://www.ecobee.com/consumer"
|
||||
"portal/index.html with pin code: " + network.pin
|
||||
),
|
||||
description_image="/static/images/config_ecobee_thermostat.png",
|
||||
submit_caption="I have authorized the app.",
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def setup_ecobee(hass, network, config):
|
||||
"""Set up the Ecobee thermostat."""
|
||||
# If ecobee has a PIN then it needs to be configured.
|
||||
if network.pin is not None:
|
||||
request_configuration(network, hass, config)
|
||||
return
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up ecobee via a config entry."""
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
refresh_token = entry.data[CONF_REFRESH_TOKEN]
|
||||
|
||||
if "ecobee" in _CONFIGURING:
|
||||
configurator = hass.components.configurator
|
||||
configurator.request_done(_CONFIGURING.pop("ecobee"))
|
||||
data = EcobeeData(hass, entry, api_key=api_key, refresh_token=refresh_token)
|
||||
|
||||
hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP)
|
||||
if not await data.refresh():
|
||||
return False
|
||||
|
||||
discovery.load_platform(hass, "climate", DOMAIN, {"hold_temp": hold_temp}, config)
|
||||
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, "binary_sensor", DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, "weather", DOMAIN, {}, config)
|
||||
await data.update()
|
||||
|
||||
if data.ecobee.thermostats is None:
|
||||
_LOGGER.error("No ecobee devices found to set up")
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN] = data
|
||||
|
||||
for component in ECOBEE_PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EcobeeData:
|
||||
"""Get the latest data and update the states."""
|
||||
"""
|
||||
Handle getting the latest data from ecobee.com so platforms can use it.
|
||||
|
||||
def __init__(self, config_file):
|
||||
"""Init the Ecobee data object."""
|
||||
from pyecobee import Ecobee
|
||||
Also handle refreshing tokens and updating config entry with refreshed tokens.
|
||||
"""
|
||||
|
||||
self.ecobee = Ecobee(config_file)
|
||||
def __init__(self, hass, entry, api_key, refresh_token):
|
||||
"""Initialize the Ecobee data object."""
|
||||
self._hass = hass
|
||||
self._entry = entry
|
||||
self.ecobee = Ecobee(
|
||||
config={ECOBEE_API_KEY: api_key, ECOBEE_REFRESH_TOKEN: refresh_token}
|
||||
)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from pyecobee."""
|
||||
self.ecobee.update()
|
||||
_LOGGER.debug("Ecobee data updated successfully")
|
||||
async def update(self):
|
||||
"""Get the latest data from ecobee.com."""
|
||||
try:
|
||||
await self._hass.async_add_executor_job(self.ecobee.update)
|
||||
_LOGGER.debug("Updating ecobee")
|
||||
except ExpiredTokenError:
|
||||
_LOGGER.warning(
|
||||
"Ecobee update failed; attempting to refresh expired tokens"
|
||||
)
|
||||
await self.refresh()
|
||||
|
||||
async def refresh(self) -> bool:
|
||||
"""Refresh ecobee tokens and update config entry."""
|
||||
_LOGGER.debug("Refreshing ecobee tokens and updating config entry")
|
||||
if await self._hass.async_add_executor_job(self.ecobee.refresh_tokens):
|
||||
self._hass.config_entries.async_update_entry(
|
||||
self._entry,
|
||||
data={
|
||||
CONF_API_KEY: self.ecobee.config[ECOBEE_API_KEY],
|
||||
CONF_REFRESH_TOKEN: self.ecobee.config[ECOBEE_REFRESH_TOKEN],
|
||||
},
|
||||
)
|
||||
return True
|
||||
_LOGGER.error("Error updating ecobee tokens")
|
||||
return False
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Ecobee.
|
||||
async def async_unload_entry(hass, config_entry):
|
||||
"""Unload the config entry and platforms."""
|
||||
hass.data.pop(DOMAIN)
|
||||
|
||||
Will automatically load thermostat and sensor components to support
|
||||
devices discovered on the network.
|
||||
"""
|
||||
global NETWORK
|
||||
tasks = []
|
||||
for platform in ECOBEE_PLATFORMS:
|
||||
tasks.append(
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
||||
)
|
||||
|
||||
if "ecobee" in _CONFIGURING:
|
||||
return
|
||||
|
||||
# Create ecobee.conf if it doesn't exist
|
||||
if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)):
|
||||
jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)}
|
||||
save_json(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig)
|
||||
|
||||
NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE))
|
||||
|
||||
setup_ecobee(hass, NETWORK.ecobee, config)
|
||||
|
||||
return True
|
||||
return all(await asyncio.gather(*tasks))
|
||||
|
@ -1,15 +1,20 @@
|
||||
"""Support for Ecobee binary sensors."""
|
||||
from homeassistant.components import ecobee
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice,
|
||||
DEVICE_CLASS_OCCUPANCY,
|
||||
)
|
||||
|
||||
ECOBEE_CONFIG_FILE = "ecobee.conf"
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Ecobee sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
data = ecobee.NETWORK
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Old way of setting up ecobee binary sensors."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up ecobee binary (occupancy) sensors."""
|
||||
data = hass.data[DOMAIN]
|
||||
dev = list()
|
||||
for index in range(len(data.ecobee.thermostats)):
|
||||
for sensor in data.ecobee.get_remote_sensors(index):
|
||||
@ -17,21 +22,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
if item["type"] != "occupancy":
|
||||
continue
|
||||
|
||||
dev.append(EcobeeBinarySensor(sensor["name"], index))
|
||||
dev.append(EcobeeBinarySensor(data, sensor["name"], index))
|
||||
|
||||
add_entities(dev, True)
|
||||
async_add_entities(dev, True)
|
||||
|
||||
|
||||
class EcobeeBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an Ecobee sensor."""
|
||||
|
||||
def __init__(self, sensor_name, sensor_index):
|
||||
def __init__(self, data, sensor_name, sensor_index):
|
||||
"""Initialize the Ecobee sensor."""
|
||||
self.data = data
|
||||
self._name = sensor_name + " Occupancy"
|
||||
self.sensor_name = sensor_name
|
||||
self.index = sensor_index
|
||||
self._state = None
|
||||
self._device_class = "occupancy"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -46,13 +51,12 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._device_class
|
||||
return DEVICE_CLASS_OCCUPANCY
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
data = ecobee.NETWORK
|
||||
data.update()
|
||||
for sensor in data.ecobee.get_remote_sensors(self.index):
|
||||
await self.data.update()
|
||||
for sensor in self.data.ecobee.get_remote_sensors(self.index):
|
||||
for item in sensor["capability"]:
|
||||
if item["type"] == "occupancy" and self.sensor_name == sensor["name"]:
|
||||
self._state = item["value"]
|
||||
|
@ -1,14 +1,11 @@
|
||||
"""Support for Ecobee Thermostats."""
|
||||
import collections
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ecobee
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
DOMAIN,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_AUTO,
|
||||
@ -38,8 +35,7 @@ from homeassistant.const import (
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import DOMAIN, _LOGGER
|
||||
|
||||
ATTR_FAN_MIN_ON_TIME = "fan_min_on_time"
|
||||
ATTR_RESUME_ALL = "resume_all"
|
||||
@ -88,8 +84,8 @@ PRESET_TO_ECOBEE_HOLD = {
|
||||
PRESET_HOLD_INDEFINITE: "indefinite",
|
||||
}
|
||||
|
||||
SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time"
|
||||
SERVICE_RESUME_PROGRAM = "ecobee_resume_program"
|
||||
SERVICE_SET_FAN_MIN_ON_TIME = "set_fan_min_on_time"
|
||||
SERVICE_RESUME_PROGRAM = "resume_program"
|
||||
|
||||
SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema(
|
||||
{
|
||||
@ -114,20 +110,19 @@ SUPPORT_FLAGS = (
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Ecobee Thermostat Platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
data = ecobee.NETWORK
|
||||
hold_temp = discovery_info["hold_temp"]
|
||||
_LOGGER.info(
|
||||
"Loading ecobee thermostat component with hold_temp set to %s", hold_temp
|
||||
)
|
||||
devices = [
|
||||
Thermostat(data, index, hold_temp)
|
||||
for index in range(len(data.ecobee.thermostats))
|
||||
]
|
||||
add_entities(devices)
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Old way of setting up ecobee thermostat."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the ecobee thermostat."""
|
||||
|
||||
data = hass.data[DOMAIN]
|
||||
|
||||
devices = [Thermostat(data, index) for index in range(len(data.ecobee.thermostats))]
|
||||
|
||||
async_add_entities(devices, True)
|
||||
|
||||
def fan_min_on_time_set_service(service):
|
||||
"""Set the minimum fan on time on the target thermostats."""
|
||||
@ -163,14 +158,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
thermostat.schedule_update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_FAN_MIN_ON_TIME,
|
||||
fan_min_on_time_set_service,
|
||||
schema=SET_FAN_MIN_ON_TIME_SCHEMA,
|
||||
)
|
||||
|
||||
hass.services.register(
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_RESUME_PROGRAM,
|
||||
resume_program_set_service,
|
||||
@ -181,13 +176,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
class Thermostat(ClimateDevice):
|
||||
"""A thermostat class for Ecobee."""
|
||||
|
||||
def __init__(self, data, thermostat_index, hold_temp):
|
||||
def __init__(self, data, thermostat_index):
|
||||
"""Initialize the thermostat."""
|
||||
self.data = data
|
||||
self.thermostat_index = thermostat_index
|
||||
self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index)
|
||||
self._name = self.thermostat["name"]
|
||||
self.hold_temp = hold_temp
|
||||
self.vacation = None
|
||||
|
||||
self._operation_list = []
|
||||
@ -206,14 +200,13 @@ class Thermostat(ClimateDevice):
|
||||
self._fan_modes = [FAN_AUTO, FAN_ON]
|
||||
self.update_without_throttle = False
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Get the latest state from the thermostat."""
|
||||
if self.update_without_throttle:
|
||||
self.data.update(no_throttle=True)
|
||||
await self.data.update(no_throttle=True)
|
||||
self.update_without_throttle = False
|
||||
else:
|
||||
self.data.update()
|
||||
|
||||
await self.data.update()
|
||||
self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index)
|
||||
|
||||
@property
|
||||
|
120
homeassistant/components/ecobee/config_flow.py
Normal file
120
homeassistant/components/ecobee/config_flow.py
Normal file
@ -0,0 +1,120 @@
|
||||
"""Config flow to configure ecobee."""
|
||||
import voluptuous as vol
|
||||
|
||||
from pyecobee import (
|
||||
Ecobee,
|
||||
ECOBEE_CONFIG_FILENAME,
|
||||
ECOBEE_API_KEY,
|
||||
ECOBEE_REFRESH_TOKEN,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistantError
|
||||
from homeassistant.util.json import load_json
|
||||
|
||||
from .const import CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN, _LOGGER
|
||||
|
||||
|
||||
class EcobeeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle an ecobee config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the ecobee flow."""
|
||||
self._ecobee = None
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initiated by the user."""
|
||||
if self._async_current_entries():
|
||||
# Config entry already exists, only one allowed.
|
||||
return self.async_abort(reason="one_instance_only")
|
||||
|
||||
errors = {}
|
||||
stored_api_key = self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY)
|
||||
|
||||
if user_input is not None:
|
||||
# Use the user-supplied API key to attempt to obtain a PIN from ecobee.
|
||||
self._ecobee = Ecobee(config={ECOBEE_API_KEY: user_input[CONF_API_KEY]})
|
||||
|
||||
if await self.hass.async_add_executor_job(self._ecobee.request_pin):
|
||||
# We have a PIN; move to the next step of the flow.
|
||||
return await self.async_step_authorize()
|
||||
errors["base"] = "pin_request_failed"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_API_KEY, default=stored_api_key): str}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_authorize(self, user_input=None):
|
||||
"""Present the user with the PIN so that the app can be authorized on ecobee.com."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
# Attempt to obtain tokens from ecobee and finish the flow.
|
||||
if await self.hass.async_add_executor_job(self._ecobee.request_tokens):
|
||||
# Refresh token obtained; create the config entry.
|
||||
config = {
|
||||
CONF_API_KEY: self._ecobee.api_key,
|
||||
CONF_REFRESH_TOKEN: self._ecobee.refresh_token,
|
||||
}
|
||||
return self.async_create_entry(title=DOMAIN, data=config)
|
||||
errors["base"] = "token_request_failed"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="authorize",
|
||||
errors=errors,
|
||||
description_placeholders={"pin": self._ecobee.pin},
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data):
|
||||
"""
|
||||
Import ecobee config from configuration.yaml.
|
||||
|
||||
Triggered by async_setup only if a config entry doesn't already exist.
|
||||
If ecobee.conf exists, we will attempt to validate the credentials
|
||||
and create an entry if valid. Otherwise, we will delegate to the user
|
||||
step so that the user can continue the config flow.
|
||||
"""
|
||||
try:
|
||||
legacy_config = await self.hass.async_add_executor_job(
|
||||
load_json, self.hass.config.path(ECOBEE_CONFIG_FILENAME)
|
||||
)
|
||||
config = {
|
||||
ECOBEE_API_KEY: legacy_config[ECOBEE_API_KEY],
|
||||
ECOBEE_REFRESH_TOKEN: legacy_config[ECOBEE_REFRESH_TOKEN],
|
||||
}
|
||||
except (HomeAssistantError, KeyError):
|
||||
_LOGGER.debug(
|
||||
"No valid ecobee.conf configuration found for import, delegating to user step"
|
||||
)
|
||||
return await self.async_step_user(
|
||||
user_input={
|
||||
CONF_API_KEY: self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY)
|
||||
}
|
||||
)
|
||||
|
||||
ecobee = Ecobee(config=config)
|
||||
if await self.hass.async_add_executor_job(ecobee.refresh_tokens):
|
||||
# Credentials found and validated; create the entry.
|
||||
_LOGGER.debug(
|
||||
"Valid ecobee configuration found for import, creating config entry"
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=DOMAIN,
|
||||
data={
|
||||
CONF_API_KEY: ecobee.api_key,
|
||||
CONF_REFRESH_TOKEN: ecobee.refresh_token,
|
||||
},
|
||||
)
|
||||
return await self.async_step_user(
|
||||
user_input={
|
||||
CONF_API_KEY: self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY)
|
||||
}
|
||||
)
|
12
homeassistant/components/ecobee/const.py
Normal file
12
homeassistant/components/ecobee/const.py
Normal file
@ -0,0 +1,12 @@
|
||||
"""Constants for the ecobee integration."""
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "ecobee"
|
||||
DATA_ECOBEE_CONFIG = "ecobee_config"
|
||||
|
||||
CONF_INDEX = "index"
|
||||
CONF_REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"]
|
@ -1,10 +1,9 @@
|
||||
{
|
||||
"domain": "ecobee",
|
||||
"name": "Ecobee",
|
||||
"documentation": "https://www.home-assistant.io/components/ecobee",
|
||||
"requirements": [
|
||||
"python-ecobee-api==0.0.21"
|
||||
],
|
||||
"dependencies": ["configurator"],
|
||||
"codeowners": []
|
||||
"domain": "ecobee",
|
||||
"name": "Ecobee",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/components/ecobee",
|
||||
"dependencies": [],
|
||||
"requirements": ["python-ecobee-api==0.1.2"],
|
||||
"codeowners": ["@marthoc"]
|
||||
}
|
||||
|
@ -1,15 +1,10 @@
|
||||
"""Support for Ecobee Send Message service."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components import ecobee
|
||||
from homeassistant.components.notify import BaseNotificationService, PLATFORM_SCHEMA
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_INDEX = "index"
|
||||
from .const import CONF_INDEX, DOMAIN
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{vol.Optional(CONF_INDEX, default=0): cv.positive_int}
|
||||
@ -18,17 +13,19 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
|
||||
def get_service(hass, config, discovery_info=None):
|
||||
"""Get the Ecobee notification service."""
|
||||
data = hass.data[DOMAIN]
|
||||
index = config.get(CONF_INDEX)
|
||||
return EcobeeNotificationService(index)
|
||||
return EcobeeNotificationService(data, index)
|
||||
|
||||
|
||||
class EcobeeNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for the Ecobee thermostat."""
|
||||
|
||||
def __init__(self, thermostat_index):
|
||||
def __init__(self, data, thermostat_index):
|
||||
"""Initialize the service."""
|
||||
self.data = data
|
||||
self.thermostat_index = thermostat_index
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a command line."""
|
||||
ecobee.NETWORK.ecobee.send_message(self.thermostat_index, message)
|
||||
"""Send a message."""
|
||||
self.data.ecobee.send_message(self.thermostat_index, message)
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Support for Ecobee sensors."""
|
||||
from homeassistant.components import ecobee
|
||||
from pyecobee.const import ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN
|
||||
|
||||
from homeassistant.const import (
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@ -7,7 +8,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
ECOBEE_CONFIG_FILE = "ecobee.conf"
|
||||
from .const import DOMAIN
|
||||
|
||||
SENSOR_TYPES = {
|
||||
"temperature": ["Temperature", TEMP_FAHRENHEIT],
|
||||
@ -15,11 +16,14 @@ SENSOR_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Ecobee sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
data = ecobee.NETWORK
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Old way of setting up ecobee sensors."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up ecobee (temperature and humidity) sensors."""
|
||||
data = hass.data[DOMAIN]
|
||||
dev = list()
|
||||
for index in range(len(data.ecobee.thermostats)):
|
||||
for sensor in data.ecobee.get_remote_sensors(index):
|
||||
@ -27,16 +31,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
if item["type"] not in ("temperature", "humidity"):
|
||||
continue
|
||||
|
||||
dev.append(EcobeeSensor(sensor["name"], item["type"], index))
|
||||
dev.append(EcobeeSensor(data, sensor["name"], item["type"], index))
|
||||
|
||||
add_entities(dev, True)
|
||||
async_add_entities(dev, True)
|
||||
|
||||
|
||||
class EcobeeSensor(Entity):
|
||||
"""Representation of an Ecobee sensor."""
|
||||
|
||||
def __init__(self, sensor_name, sensor_type, sensor_index):
|
||||
def __init__(self, data, sensor_name, sensor_type, sensor_index):
|
||||
"""Initialize the sensor."""
|
||||
self.data = data
|
||||
self._name = "{} {}".format(sensor_name, SENSOR_TYPES[sensor_type][0])
|
||||
self.sensor_name = sensor_name
|
||||
self.type = sensor_type
|
||||
@ -59,6 +64,12 @@ class EcobeeSensor(Entity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._state in [ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN]:
|
||||
return None
|
||||
|
||||
if self.type == "temperature":
|
||||
return float(self._state) / 10
|
||||
|
||||
return self._state
|
||||
|
||||
@property
|
||||
@ -66,14 +77,10 @@ class EcobeeSensor(Entity):
|
||||
"""Return the unit of measurement this sensor expresses itself in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
data = ecobee.NETWORK
|
||||
data.update()
|
||||
for sensor in data.ecobee.get_remote_sensors(self.index):
|
||||
await self.data.update()
|
||||
for sensor in self.data.ecobee.get_remote_sensors(self.index):
|
||||
for item in sensor["capability"]:
|
||||
if item["type"] == self.type and self.sensor_name == sensor["name"]:
|
||||
if self.type == "temperature" and item["value"] != "unknown":
|
||||
self._state = float(item["value"]) / 10
|
||||
else:
|
||||
self._state = item["value"]
|
||||
self._state = item["value"]
|
||||
|
@ -0,0 +1,19 @@
|
||||
resume_program:
|
||||
description: Resume the programmed schedule.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
resume_all:
|
||||
description: Resume all events and return to the scheduled program. This default to false which removes only the top event.
|
||||
example: true
|
||||
|
||||
set_fan_min_on_time:
|
||||
description: Set the minimum fan on time.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
fan_min_on_time:
|
||||
description: New value of fan min on time.
|
||||
example: 5
|
23
homeassistant/components/ecobee/strings.json
Normal file
23
homeassistant/components/ecobee/strings.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "ecobee",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "ecobee API key",
|
||||
"description": "Please enter the API key obtained from ecobee.com.",
|
||||
"data": {"api_key": "API Key"}
|
||||
},
|
||||
"authorize": {
|
||||
"title": "Authorize app on ecobee.com",
|
||||
"description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"pin_request_failed": "Error requesting PIN from ecobee; please verify API key is correct.",
|
||||
"token_request_failed": "Error requesting tokens from ecobee; please try again."
|
||||
},
|
||||
"abort": {
|
||||
"one_instance_only": "This integration currently supports only one ecobee instance."
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
"""Support for displaying weather info from Ecobee API."""
|
||||
from datetime import datetime
|
||||
|
||||
from homeassistant.components import ecobee
|
||||
from pyecobee.const import ECOBEE_STATE_UNKNOWN
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_TEMP,
|
||||
@ -12,33 +13,37 @@ from homeassistant.components.weather import (
|
||||
)
|
||||
from homeassistant.const import TEMP_FAHRENHEIT
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
ATTR_FORECAST_TEMP_HIGH = "temphigh"
|
||||
ATTR_FORECAST_PRESSURE = "pressure"
|
||||
ATTR_FORECAST_VISIBILITY = "visibility"
|
||||
ATTR_FORECAST_HUMIDITY = "humidity"
|
||||
|
||||
MISSING_DATA = -5002
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Old way of setting up the ecobee weather platform."""
|
||||
pass
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Ecobee weather platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the ecobee weather platform."""
|
||||
data = hass.data[DOMAIN]
|
||||
dev = list()
|
||||
data = ecobee.NETWORK
|
||||
for index in range(len(data.ecobee.thermostats)):
|
||||
thermostat = data.ecobee.get_thermostat(index)
|
||||
if "weather" in thermostat:
|
||||
dev.append(EcobeeWeather(thermostat["name"], index))
|
||||
dev.append(EcobeeWeather(data, thermostat["name"], index))
|
||||
|
||||
add_entities(dev, True)
|
||||
async_add_entities(dev, True)
|
||||
|
||||
|
||||
class EcobeeWeather(WeatherEntity):
|
||||
"""Representation of Ecobee weather data."""
|
||||
|
||||
def __init__(self, name, index):
|
||||
def __init__(self, data, name, index):
|
||||
"""Initialize the Ecobee weather platform."""
|
||||
self.data = data
|
||||
self._name = name
|
||||
self._index = index
|
||||
self.weather = None
|
||||
@ -140,26 +145,25 @@ class EcobeeWeather(WeatherEntity):
|
||||
ATTR_FORECAST_CONDITION: day["condition"],
|
||||
ATTR_FORECAST_TEMP: float(day["tempHigh"]) / 10,
|
||||
}
|
||||
if day["tempHigh"] == MISSING_DATA:
|
||||
if day["tempHigh"] == ECOBEE_STATE_UNKNOWN:
|
||||
break
|
||||
if day["tempLow"] != MISSING_DATA:
|
||||
if day["tempLow"] != ECOBEE_STATE_UNKNOWN:
|
||||
forecast[ATTR_FORECAST_TEMP_LOW] = float(day["tempLow"]) / 10
|
||||
if day["pressure"] != MISSING_DATA:
|
||||
if day["pressure"] != ECOBEE_STATE_UNKNOWN:
|
||||
forecast[ATTR_FORECAST_PRESSURE] = int(day["pressure"])
|
||||
if day["windSpeed"] != MISSING_DATA:
|
||||
if day["windSpeed"] != ECOBEE_STATE_UNKNOWN:
|
||||
forecast[ATTR_FORECAST_WIND_SPEED] = int(day["windSpeed"])
|
||||
if day["visibility"] != MISSING_DATA:
|
||||
if day["visibility"] != ECOBEE_STATE_UNKNOWN:
|
||||
forecast[ATTR_FORECAST_WIND_SPEED] = int(day["visibility"])
|
||||
if day["relativeHumidity"] != MISSING_DATA:
|
||||
if day["relativeHumidity"] != ECOBEE_STATE_UNKNOWN:
|
||||
forecast[ATTR_FORECAST_HUMIDITY] = int(day["relativeHumidity"])
|
||||
forecasts.append(forecast)
|
||||
return forecasts
|
||||
except (ValueError, IndexError, KeyError):
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
data = ecobee.NETWORK
|
||||
data.update()
|
||||
thermostat = data.ecobee.get_thermostat(self._index)
|
||||
async def async_update(self):
|
||||
"""Get the latest weather data."""
|
||||
await self.data.update()
|
||||
thermostat = self.data.ecobee.get_thermostat(self._index)
|
||||
self.weather = thermostat.get("weather", None)
|
||||
|
@ -15,6 +15,7 @@ FLOWS = [
|
||||
"daikin",
|
||||
"deconz",
|
||||
"dialogflow",
|
||||
"ecobee",
|
||||
"emulated_roku",
|
||||
"esphome",
|
||||
"geofency",
|
||||
|
@ -1483,7 +1483,7 @@ python-clementine-remote==1.0.1
|
||||
python-digitalocean==1.13.2
|
||||
|
||||
# homeassistant.components.ecobee
|
||||
python-ecobee-api==0.0.21
|
||||
python-ecobee-api==0.1.2
|
||||
|
||||
# homeassistant.components.eq3btsmart
|
||||
# python-eq3bt==0.1.9
|
||||
|
@ -355,6 +355,9 @@ pysonos==0.0.23
|
||||
# homeassistant.components.spc
|
||||
pyspcwebgw==0.4.0
|
||||
|
||||
# homeassistant.components.ecobee
|
||||
python-ecobee-api==0.1.2
|
||||
|
||||
# homeassistant.components.darksky
|
||||
python-forecastio==1.4.0
|
||||
|
||||
|
@ -146,6 +146,7 @@ TEST_REQUIREMENTS = (
|
||||
"pysonos",
|
||||
"pyspcwebgw",
|
||||
"python_awair",
|
||||
"python-ecobee-api",
|
||||
"python-forecastio",
|
||||
"python-izone",
|
||||
"python-nest",
|
||||
|
@ -54,7 +54,7 @@ class TestEcobee(unittest.TestCase):
|
||||
|
||||
self.data = mock.Mock()
|
||||
self.data.ecobee.get_thermostat.return_value = self.ecobee
|
||||
self.thermostat = ecobee.Thermostat(self.data, 1, False)
|
||||
self.thermostat = ecobee.Thermostat(self.data, 1)
|
||||
|
||||
def test_name(self):
|
||||
"""Test name property."""
|
||||
|
206
tests/components/ecobee/test_config_flow.py
Normal file
206
tests/components/ecobee/test_config_flow.py
Normal file
@ -0,0 +1,206 @@
|
||||
"""Tests for the ecobee config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.ecobee import config_flow
|
||||
from homeassistant.components.ecobee.const import (
|
||||
CONF_REFRESH_TOKEN,
|
||||
DATA_ECOBEE_CONFIG,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from tests.common import MockConfigEntry, mock_coro
|
||||
|
||||
|
||||
async def test_abort_if_already_setup(hass):
|
||||
"""Test we abort if ecobee is already setup."""
|
||||
flow = config_flow.EcobeeFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
|
||||
|
||||
result = await flow.async_step_user()
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "one_instance_only"
|
||||
|
||||
|
||||
async def test_user_step_without_user_input(hass):
|
||||
"""Test expected result if user step is called."""
|
||||
flow = config_flow.EcobeeFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.hass.data[DATA_ECOBEE_CONFIG] = {}
|
||||
|
||||
result = await flow.async_step_user()
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
|
||||
async def test_pin_request_succeeds(hass):
|
||||
"""Test expected result if pin request succeeds."""
|
||||
flow = config_flow.EcobeeFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.hass.data[DATA_ECOBEE_CONFIG] = {}
|
||||
|
||||
with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee:
|
||||
mock_ecobee = MockEcobee.return_value
|
||||
mock_ecobee.request_pin.return_value = True
|
||||
mock_ecobee.pin = "test-pin"
|
||||
|
||||
result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"})
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "authorize"
|
||||
assert result["description_placeholders"] == {"pin": "test-pin"}
|
||||
|
||||
|
||||
async def test_pin_request_fails(hass):
|
||||
"""Test expected result if pin request fails."""
|
||||
flow = config_flow.EcobeeFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.hass.data[DATA_ECOBEE_CONFIG] = {}
|
||||
|
||||
with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee:
|
||||
mock_ecobee = MockEcobee.return_value
|
||||
mock_ecobee.request_pin.return_value = False
|
||||
|
||||
result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"})
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"]["base"] == "pin_request_failed"
|
||||
|
||||
|
||||
async def test_token_request_succeeds(hass):
|
||||
"""Test expected result if token request succeeds."""
|
||||
flow = config_flow.EcobeeFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.hass.data[DATA_ECOBEE_CONFIG] = {}
|
||||
|
||||
with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee:
|
||||
mock_ecobee = MockEcobee.return_value
|
||||
mock_ecobee.request_tokens.return_value = True
|
||||
mock_ecobee.api_key = "test-api-key"
|
||||
mock_ecobee.refresh_token = "test-token"
|
||||
flow._ecobee = mock_ecobee
|
||||
|
||||
result = await flow.async_step_authorize(user_input={})
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == DOMAIN
|
||||
assert result["data"] == {
|
||||
CONF_API_KEY: "test-api-key",
|
||||
CONF_REFRESH_TOKEN: "test-token",
|
||||
}
|
||||
|
||||
|
||||
async def test_token_request_fails(hass):
|
||||
"""Test expected result if token request fails."""
|
||||
flow = config_flow.EcobeeFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.hass.data[DATA_ECOBEE_CONFIG] = {}
|
||||
|
||||
with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee:
|
||||
mock_ecobee = MockEcobee.return_value
|
||||
mock_ecobee.request_tokens.return_value = False
|
||||
mock_ecobee.pin = "test-pin"
|
||||
flow._ecobee = mock_ecobee
|
||||
|
||||
result = await flow.async_step_authorize(user_input={})
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "authorize"
|
||||
assert result["errors"]["base"] == "token_request_failed"
|
||||
assert result["description_placeholders"] == {"pin": "test-pin"}
|
||||
|
||||
|
||||
async def test_import_flow_triggered_but_no_ecobee_conf(hass):
|
||||
"""Test expected result if import flow triggers but ecobee.conf doesn't exist."""
|
||||
flow = config_flow.EcobeeFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.hass.data[DATA_ECOBEE_CONFIG] = {}
|
||||
|
||||
result = await flow.async_step_import(import_data=None)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
|
||||
async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_tokens(
|
||||
hass
|
||||
):
|
||||
"""Test expected result if import flow triggers and ecobee.conf exists with valid tokens."""
|
||||
flow = config_flow.EcobeeFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
MOCK_ECOBEE_CONF = {ECOBEE_API_KEY: None, ECOBEE_REFRESH_TOKEN: None}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.ecobee.config_flow.load_json",
|
||||
return_value=MOCK_ECOBEE_CONF,
|
||||
), patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee:
|
||||
mock_ecobee = MockEcobee.return_value
|
||||
mock_ecobee.refresh_tokens.return_value = True
|
||||
mock_ecobee.api_key = "test-api-key"
|
||||
mock_ecobee.refresh_token = "test-token"
|
||||
|
||||
result = await flow.async_step_import(import_data=None)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == DOMAIN
|
||||
assert result["data"] == {
|
||||
CONF_API_KEY: "test-api-key",
|
||||
CONF_REFRESH_TOKEN: "test-token",
|
||||
}
|
||||
|
||||
|
||||
async def test_import_flow_triggered_with_ecobee_conf_and_invalid_data(hass):
|
||||
"""Test expected result if import flow triggers and ecobee.conf exists with invalid data."""
|
||||
flow = config_flow.EcobeeFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.hass.data[DATA_ECOBEE_CONFIG] = {CONF_API_KEY: "test-api-key"}
|
||||
|
||||
MOCK_ECOBEE_CONF = {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.ecobee.config_flow.load_json",
|
||||
return_value=MOCK_ECOBEE_CONF,
|
||||
), patch.object(
|
||||
flow, "async_step_user", return_value=mock_coro()
|
||||
) as mock_async_step_user:
|
||||
|
||||
await flow.async_step_import(import_data=None)
|
||||
|
||||
mock_async_step_user.assert_called_once_with(
|
||||
user_input={CONF_API_KEY: "test-api-key"}
|
||||
)
|
||||
|
||||
|
||||
async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_stale_tokens(
|
||||
hass
|
||||
):
|
||||
"""Test expected result if import flow triggers and ecobee.conf exists with stale tokens."""
|
||||
flow = config_flow.EcobeeFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.hass.data[DATA_ECOBEE_CONFIG] = {CONF_API_KEY: "test-api-key"}
|
||||
|
||||
MOCK_ECOBEE_CONF = {ECOBEE_API_KEY: None, ECOBEE_REFRESH_TOKEN: None}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.ecobee.config_flow.load_json",
|
||||
return_value=MOCK_ECOBEE_CONF,
|
||||
), patch(
|
||||
"homeassistant.components.ecobee.config_flow.Ecobee"
|
||||
) as MockEcobee, patch.object(
|
||||
flow, "async_step_user", return_value=mock_coro()
|
||||
) as mock_async_step_user:
|
||||
mock_ecobee = MockEcobee.return_value
|
||||
mock_ecobee.refresh_tokens.return_value = False
|
||||
|
||||
await flow.async_step_import(import_data=None)
|
||||
|
||||
mock_async_step_user.assert_called_once_with(
|
||||
user_input={CONF_API_KEY: "test-api-key"}
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user