mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +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/ebox/sensor.py
|
||||||
homeassistant/components/ebusd/*
|
homeassistant/components/ebusd/*
|
||||||
homeassistant/components/ecoal_boiler/*
|
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/econet/water_heater.py
|
||||||
homeassistant/components/ecovacs/*
|
homeassistant/components/ecovacs/*
|
||||||
homeassistant/components/eddystone_temperature/sensor.py
|
homeassistant/components/eddystone_temperature/sensor.py
|
||||||
|
@ -73,6 +73,7 @@ homeassistant/components/digital_ocean/* @fabaff
|
|||||||
homeassistant/components/discogs/* @thibmaek
|
homeassistant/components/discogs/* @thibmaek
|
||||||
homeassistant/components/doorbird/* @oblogic7
|
homeassistant/components/doorbird/* @oblogic7
|
||||||
homeassistant/components/dweet/* @fabaff
|
homeassistant/components/dweet/* @fabaff
|
||||||
|
homeassistant/components/ecobee/* @marthoc
|
||||||
homeassistant/components/ecovacs/* @OverloadUT
|
homeassistant/components/ecovacs/* @OverloadUT
|
||||||
homeassistant/components/egardia/* @jeroenterheerdt
|
homeassistant/components/egardia/* @jeroenterheerdt
|
||||||
homeassistant/components/eight_sleep/* @mezz64
|
homeassistant/components/eight_sleep/* @mezz64
|
||||||
|
@ -72,26 +72,6 @@ set_swing_mode:
|
|||||||
swing_mode:
|
swing_mode:
|
||||||
description: New value of 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:
|
mill_set_room_temperature:
|
||||||
description: Set Mill room temperatures.
|
description: Set Mill room temperatures.
|
||||||
fields:
|
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."""
|
"""Support for ecobee."""
|
||||||
import logging
|
import asyncio
|
||||||
import os
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
from pyecobee import Ecobee, ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, ExpiredTokenError
|
||||||
from homeassistant.helpers import discovery
|
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
from homeassistant.util.json import save_json
|
|
||||||
|
|
||||||
_CONFIGURING = {}
|
from .const import (
|
||||||
_LOGGER = logging.getLogger(__name__)
|
CONF_REFRESH_TOKEN,
|
||||||
|
DATA_ECOBEE_CONFIG,
|
||||||
CONF_HOLD_TEMP = "hold_temp"
|
DOMAIN,
|
||||||
|
ECOBEE_PLATFORMS,
|
||||||
DOMAIN = "ecobee"
|
_LOGGER,
|
||||||
|
)
|
||||||
ECOBEE_CONFIG_FILE = "ecobee.conf"
|
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180)
|
||||||
|
|
||||||
NETWORK = None
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{DOMAIN: vol.Schema({vol.Optional(CONF_API_KEY): cv.string})}, extra=vol.ALLOW_EXTRA
|
||||||
DOMAIN: vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Optional(CONF_API_KEY): cv.string,
|
|
||||||
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
extra=vol.ALLOW_EXTRA,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def request_configuration(network, hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Request configuration steps from the user."""
|
"""
|
||||||
configurator = hass.components.configurator
|
Ecobee uses config flow for configuration.
|
||||||
if "ecobee" in _CONFIGURING:
|
|
||||||
configurator.notify_errors(
|
But, an "ecobee:" entry in configuration.yaml will trigger an import flow
|
||||||
_CONFIGURING["ecobee"], "Failed to register, please try again."
|
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
|
return True
|
||||||
|
|
||||||
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.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_ecobee(hass, network, config):
|
async def async_setup_entry(hass, entry):
|
||||||
"""Set up the Ecobee thermostat."""
|
"""Set up ecobee via a config entry."""
|
||||||
# If ecobee has a PIN then it needs to be configured.
|
api_key = entry.data[CONF_API_KEY]
|
||||||
if network.pin is not None:
|
refresh_token = entry.data[CONF_REFRESH_TOKEN]
|
||||||
request_configuration(network, hass, config)
|
|
||||||
return
|
|
||||||
|
|
||||||
if "ecobee" in _CONFIGURING:
|
data = EcobeeData(hass, entry, api_key=api_key, refresh_token=refresh_token)
|
||||||
configurator = hass.components.configurator
|
|
||||||
configurator.request_done(_CONFIGURING.pop("ecobee"))
|
|
||||||
|
|
||||||
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)
|
await data.update()
|
||||||
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
|
||||||
discovery.load_platform(hass, "binary_sensor", DOMAIN, {}, config)
|
if data.ecobee.thermostats is None:
|
||||||
discovery.load_platform(hass, "weather", DOMAIN, {}, config)
|
_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:
|
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):
|
Also handle refreshing tokens and updating config entry with refreshed tokens.
|
||||||
"""Init the Ecobee data object."""
|
"""
|
||||||
from pyecobee import Ecobee
|
|
||||||
|
|
||||||
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)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
def update(self):
|
async def update(self):
|
||||||
"""Get the latest data from pyecobee."""
|
"""Get the latest data from ecobee.com."""
|
||||||
self.ecobee.update()
|
try:
|
||||||
_LOGGER.debug("Ecobee data updated successfully")
|
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):
|
async def async_unload_entry(hass, config_entry):
|
||||||
"""Set up the Ecobee.
|
"""Unload the config entry and platforms."""
|
||||||
|
hass.data.pop(DOMAIN)
|
||||||
|
|
||||||
Will automatically load thermostat and sensor components to support
|
tasks = []
|
||||||
devices discovered on the network.
|
for platform in ECOBEE_PLATFORMS:
|
||||||
"""
|
tasks.append(
|
||||||
global NETWORK
|
hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
||||||
|
)
|
||||||
|
|
||||||
if "ecobee" in _CONFIGURING:
|
return all(await asyncio.gather(*tasks))
|
||||||
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
|
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
"""Support for Ecobee binary sensors."""
|
"""Support for Ecobee binary sensors."""
|
||||||
from homeassistant.components import ecobee
|
from homeassistant.components.binary_sensor import (
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
BinarySensorDevice,
|
||||||
|
DEVICE_CLASS_OCCUPANCY,
|
||||||
|
)
|
||||||
|
|
||||||
ECOBEE_CONFIG_FILE = "ecobee.conf"
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the Ecobee sensors."""
|
"""Old way of setting up ecobee binary sensors."""
|
||||||
if discovery_info is None:
|
pass
|
||||||
return
|
|
||||||
data = ecobee.NETWORK
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up ecobee binary (occupancy) sensors."""
|
||||||
|
data = hass.data[DOMAIN]
|
||||||
dev = list()
|
dev = list()
|
||||||
for index in range(len(data.ecobee.thermostats)):
|
for index in range(len(data.ecobee.thermostats)):
|
||||||
for sensor in data.ecobee.get_remote_sensors(index):
|
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":
|
if item["type"] != "occupancy":
|
||||||
continue
|
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):
|
class EcobeeBinarySensor(BinarySensorDevice):
|
||||||
"""Representation of an Ecobee sensor."""
|
"""Representation of an Ecobee sensor."""
|
||||||
|
|
||||||
def __init__(self, sensor_name, sensor_index):
|
def __init__(self, data, sensor_name, sensor_index):
|
||||||
"""Initialize the Ecobee sensor."""
|
"""Initialize the Ecobee sensor."""
|
||||||
|
self.data = data
|
||||||
self._name = sensor_name + " Occupancy"
|
self._name = sensor_name + " Occupancy"
|
||||||
self.sensor_name = sensor_name
|
self.sensor_name = sensor_name
|
||||||
self.index = sensor_index
|
self.index = sensor_index
|
||||||
self._state = None
|
self._state = None
|
||||||
self._device_class = "occupancy"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -46,13 +51,12 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
|||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
"""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."""
|
"""Get the latest state of the sensor."""
|
||||||
data = ecobee.NETWORK
|
await self.data.update()
|
||||||
data.update()
|
for sensor in self.data.ecobee.get_remote_sensors(self.index):
|
||||||
for sensor in data.ecobee.get_remote_sensors(self.index):
|
|
||||||
for item in sensor["capability"]:
|
for item in sensor["capability"]:
|
||||||
if item["type"] == "occupancy" and self.sensor_name == sensor["name"]:
|
if item["type"] == "occupancy" and self.sensor_name == sensor["name"]:
|
||||||
self._state = item["value"]
|
self._state = item["value"]
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
"""Support for Ecobee Thermostats."""
|
"""Support for Ecobee Thermostats."""
|
||||||
import collections
|
import collections
|
||||||
import logging
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import ecobee
|
|
||||||
from homeassistant.components.climate import ClimateDevice
|
from homeassistant.components.climate import ClimateDevice
|
||||||
from homeassistant.components.climate.const import (
|
from homeassistant.components.climate.const import (
|
||||||
DOMAIN,
|
|
||||||
HVAC_MODE_COOL,
|
HVAC_MODE_COOL,
|
||||||
HVAC_MODE_HEAT,
|
HVAC_MODE_HEAT,
|
||||||
HVAC_MODE_AUTO,
|
HVAC_MODE_AUTO,
|
||||||
@ -38,8 +35,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_CONFIGURING = {}
|
from .const import DOMAIN, _LOGGER
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
ATTR_FAN_MIN_ON_TIME = "fan_min_on_time"
|
ATTR_FAN_MIN_ON_TIME = "fan_min_on_time"
|
||||||
ATTR_RESUME_ALL = "resume_all"
|
ATTR_RESUME_ALL = "resume_all"
|
||||||
@ -88,8 +84,8 @@ PRESET_TO_ECOBEE_HOLD = {
|
|||||||
PRESET_HOLD_INDEFINITE: "indefinite",
|
PRESET_HOLD_INDEFINITE: "indefinite",
|
||||||
}
|
}
|
||||||
|
|
||||||
SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time"
|
SERVICE_SET_FAN_MIN_ON_TIME = "set_fan_min_on_time"
|
||||||
SERVICE_RESUME_PROGRAM = "ecobee_resume_program"
|
SERVICE_RESUME_PROGRAM = "resume_program"
|
||||||
|
|
||||||
SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema(
|
SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -114,20 +110,19 @@ SUPPORT_FLAGS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the Ecobee Thermostat Platform."""
|
"""Old way of setting up ecobee thermostat."""
|
||||||
if discovery_info is None:
|
pass
|
||||||
return
|
|
||||||
data = ecobee.NETWORK
|
|
||||||
hold_temp = discovery_info["hold_temp"]
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
_LOGGER.info(
|
"""Set up the ecobee thermostat."""
|
||||||
"Loading ecobee thermostat component with hold_temp set to %s", hold_temp
|
|
||||||
)
|
data = hass.data[DOMAIN]
|
||||||
devices = [
|
|
||||||
Thermostat(data, index, hold_temp)
|
devices = [Thermostat(data, index) for index in range(len(data.ecobee.thermostats))]
|
||||||
for index in range(len(data.ecobee.thermostats))
|
|
||||||
]
|
async_add_entities(devices, True)
|
||||||
add_entities(devices)
|
|
||||||
|
|
||||||
def fan_min_on_time_set_service(service):
|
def fan_min_on_time_set_service(service):
|
||||||
"""Set the minimum fan on time on the target thermostats."""
|
"""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)
|
thermostat.schedule_update_ha_state(True)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SET_FAN_MIN_ON_TIME,
|
SERVICE_SET_FAN_MIN_ON_TIME,
|
||||||
fan_min_on_time_set_service,
|
fan_min_on_time_set_service,
|
||||||
schema=SET_FAN_MIN_ON_TIME_SCHEMA,
|
schema=SET_FAN_MIN_ON_TIME_SCHEMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_RESUME_PROGRAM,
|
SERVICE_RESUME_PROGRAM,
|
||||||
resume_program_set_service,
|
resume_program_set_service,
|
||||||
@ -181,13 +176,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
class Thermostat(ClimateDevice):
|
class Thermostat(ClimateDevice):
|
||||||
"""A thermostat class for Ecobee."""
|
"""A thermostat class for Ecobee."""
|
||||||
|
|
||||||
def __init__(self, data, thermostat_index, hold_temp):
|
def __init__(self, data, thermostat_index):
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
self.data = data
|
self.data = data
|
||||||
self.thermostat_index = thermostat_index
|
self.thermostat_index = thermostat_index
|
||||||
self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index)
|
self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index)
|
||||||
self._name = self.thermostat["name"]
|
self._name = self.thermostat["name"]
|
||||||
self.hold_temp = hold_temp
|
|
||||||
self.vacation = None
|
self.vacation = None
|
||||||
|
|
||||||
self._operation_list = []
|
self._operation_list = []
|
||||||
@ -206,14 +200,13 @@ class Thermostat(ClimateDevice):
|
|||||||
self._fan_modes = [FAN_AUTO, FAN_ON]
|
self._fan_modes = [FAN_AUTO, FAN_ON]
|
||||||
self.update_without_throttle = False
|
self.update_without_throttle = False
|
||||||
|
|
||||||
def update(self):
|
async def async_update(self):
|
||||||
"""Get the latest state from the thermostat."""
|
"""Get the latest state from the thermostat."""
|
||||||
if self.update_without_throttle:
|
if self.update_without_throttle:
|
||||||
self.data.update(no_throttle=True)
|
await self.data.update(no_throttle=True)
|
||||||
self.update_without_throttle = False
|
self.update_without_throttle = False
|
||||||
else:
|
else:
|
||||||
self.data.update()
|
await self.data.update()
|
||||||
|
|
||||||
self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index)
|
self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index)
|
||||||
|
|
||||||
@property
|
@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",
|
"domain": "ecobee",
|
||||||
"name": "Ecobee",
|
"name": "Ecobee",
|
||||||
"documentation": "https://www.home-assistant.io/components/ecobee",
|
"config_flow": true,
|
||||||
"requirements": [
|
"documentation": "https://www.home-assistant.io/components/ecobee",
|
||||||
"python-ecobee-api==0.0.21"
|
"dependencies": [],
|
||||||
],
|
"requirements": ["python-ecobee-api==0.1.2"],
|
||||||
"dependencies": ["configurator"],
|
"codeowners": ["@marthoc"]
|
||||||
"codeowners": []
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
"""Support for Ecobee Send Message service."""
|
"""Support for Ecobee Send Message service."""
|
||||||
import logging
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components import ecobee
|
|
||||||
from homeassistant.components.notify import BaseNotificationService, PLATFORM_SCHEMA
|
from homeassistant.components.notify import BaseNotificationService, PLATFORM_SCHEMA
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
from .const import CONF_INDEX, DOMAIN
|
||||||
|
|
||||||
CONF_INDEX = "index"
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{vol.Optional(CONF_INDEX, default=0): cv.positive_int}
|
{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):
|
def get_service(hass, config, discovery_info=None):
|
||||||
"""Get the Ecobee notification service."""
|
"""Get the Ecobee notification service."""
|
||||||
|
data = hass.data[DOMAIN]
|
||||||
index = config.get(CONF_INDEX)
|
index = config.get(CONF_INDEX)
|
||||||
return EcobeeNotificationService(index)
|
return EcobeeNotificationService(data, index)
|
||||||
|
|
||||||
|
|
||||||
class EcobeeNotificationService(BaseNotificationService):
|
class EcobeeNotificationService(BaseNotificationService):
|
||||||
"""Implement the notification service for the Ecobee thermostat."""
|
"""Implement the notification service for the Ecobee thermostat."""
|
||||||
|
|
||||||
def __init__(self, thermostat_index):
|
def __init__(self, data, thermostat_index):
|
||||||
"""Initialize the service."""
|
"""Initialize the service."""
|
||||||
|
self.data = data
|
||||||
self.thermostat_index = thermostat_index
|
self.thermostat_index = thermostat_index
|
||||||
|
|
||||||
def send_message(self, message="", **kwargs):
|
def send_message(self, message="", **kwargs):
|
||||||
"""Send a message to a command line."""
|
"""Send a message."""
|
||||||
ecobee.NETWORK.ecobee.send_message(self.thermostat_index, message)
|
self.data.ecobee.send_message(self.thermostat_index, message)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Support for Ecobee sensors."""
|
"""Support for Ecobee sensors."""
|
||||||
from homeassistant.components import ecobee
|
from pyecobee.const import ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
DEVICE_CLASS_HUMIDITY,
|
DEVICE_CLASS_HUMIDITY,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
@ -7,7 +8,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
ECOBEE_CONFIG_FILE = "ecobee.conf"
|
from .const import DOMAIN
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
"temperature": ["Temperature", TEMP_FAHRENHEIT],
|
"temperature": ["Temperature", TEMP_FAHRENHEIT],
|
||||||
@ -15,11 +16,14 @@ SENSOR_TYPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the Ecobee sensors."""
|
"""Old way of setting up ecobee sensors."""
|
||||||
if discovery_info is None:
|
pass
|
||||||
return
|
|
||||||
data = ecobee.NETWORK
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up ecobee (temperature and humidity) sensors."""
|
||||||
|
data = hass.data[DOMAIN]
|
||||||
dev = list()
|
dev = list()
|
||||||
for index in range(len(data.ecobee.thermostats)):
|
for index in range(len(data.ecobee.thermostats)):
|
||||||
for sensor in data.ecobee.get_remote_sensors(index):
|
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"):
|
if item["type"] not in ("temperature", "humidity"):
|
||||||
continue
|
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):
|
class EcobeeSensor(Entity):
|
||||||
"""Representation of an Ecobee sensor."""
|
"""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."""
|
"""Initialize the sensor."""
|
||||||
|
self.data = data
|
||||||
self._name = "{} {}".format(sensor_name, SENSOR_TYPES[sensor_type][0])
|
self._name = "{} {}".format(sensor_name, SENSOR_TYPES[sensor_type][0])
|
||||||
self.sensor_name = sensor_name
|
self.sensor_name = sensor_name
|
||||||
self.type = sensor_type
|
self.type = sensor_type
|
||||||
@ -59,6 +64,12 @@ class EcobeeSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the sensor."""
|
"""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
|
return self._state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -66,14 +77,10 @@ class EcobeeSensor(Entity):
|
|||||||
"""Return the unit of measurement this sensor expresses itself in."""
|
"""Return the unit of measurement this sensor expresses itself in."""
|
||||||
return self._unit_of_measurement
|
return self._unit_of_measurement
|
||||||
|
|
||||||
def update(self):
|
async def async_update(self):
|
||||||
"""Get the latest state of the sensor."""
|
"""Get the latest state of the sensor."""
|
||||||
data = ecobee.NETWORK
|
await self.data.update()
|
||||||
data.update()
|
for sensor in self.data.ecobee.get_remote_sensors(self.index):
|
||||||
for sensor in data.ecobee.get_remote_sensors(self.index):
|
|
||||||
for item in sensor["capability"]:
|
for item in sensor["capability"]:
|
||||||
if item["type"] == self.type and self.sensor_name == sensor["name"]:
|
if item["type"] == self.type and self.sensor_name == sensor["name"]:
|
||||||
if self.type == "temperature" and item["value"] != "unknown":
|
self._state = item["value"]
|
||||||
self._state = float(item["value"]) / 10
|
|
||||||
else:
|
|
||||||
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."""
|
"""Support for displaying weather info from Ecobee API."""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from homeassistant.components import ecobee
|
from pyecobee.const import ECOBEE_STATE_UNKNOWN
|
||||||
|
|
||||||
from homeassistant.components.weather import (
|
from homeassistant.components.weather import (
|
||||||
ATTR_FORECAST_CONDITION,
|
ATTR_FORECAST_CONDITION,
|
||||||
ATTR_FORECAST_TEMP,
|
ATTR_FORECAST_TEMP,
|
||||||
@ -12,33 +13,37 @@ from homeassistant.components.weather import (
|
|||||||
)
|
)
|
||||||
from homeassistant.const import TEMP_FAHRENHEIT
|
from homeassistant.const import TEMP_FAHRENHEIT
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
ATTR_FORECAST_TEMP_HIGH = "temphigh"
|
ATTR_FORECAST_TEMP_HIGH = "temphigh"
|
||||||
ATTR_FORECAST_PRESSURE = "pressure"
|
ATTR_FORECAST_PRESSURE = "pressure"
|
||||||
ATTR_FORECAST_VISIBILITY = "visibility"
|
ATTR_FORECAST_VISIBILITY = "visibility"
|
||||||
ATTR_FORECAST_HUMIDITY = "humidity"
|
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):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up the Ecobee weather platform."""
|
"""Set up the ecobee weather platform."""
|
||||||
if discovery_info is None:
|
data = hass.data[DOMAIN]
|
||||||
return
|
|
||||||
dev = list()
|
dev = list()
|
||||||
data = ecobee.NETWORK
|
|
||||||
for index in range(len(data.ecobee.thermostats)):
|
for index in range(len(data.ecobee.thermostats)):
|
||||||
thermostat = data.ecobee.get_thermostat(index)
|
thermostat = data.ecobee.get_thermostat(index)
|
||||||
if "weather" in thermostat:
|
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):
|
class EcobeeWeather(WeatherEntity):
|
||||||
"""Representation of Ecobee weather data."""
|
"""Representation of Ecobee weather data."""
|
||||||
|
|
||||||
def __init__(self, name, index):
|
def __init__(self, data, name, index):
|
||||||
"""Initialize the Ecobee weather platform."""
|
"""Initialize the Ecobee weather platform."""
|
||||||
|
self.data = data
|
||||||
self._name = name
|
self._name = name
|
||||||
self._index = index
|
self._index = index
|
||||||
self.weather = None
|
self.weather = None
|
||||||
@ -140,26 +145,25 @@ class EcobeeWeather(WeatherEntity):
|
|||||||
ATTR_FORECAST_CONDITION: day["condition"],
|
ATTR_FORECAST_CONDITION: day["condition"],
|
||||||
ATTR_FORECAST_TEMP: float(day["tempHigh"]) / 10,
|
ATTR_FORECAST_TEMP: float(day["tempHigh"]) / 10,
|
||||||
}
|
}
|
||||||
if day["tempHigh"] == MISSING_DATA:
|
if day["tempHigh"] == ECOBEE_STATE_UNKNOWN:
|
||||||
break
|
break
|
||||||
if day["tempLow"] != MISSING_DATA:
|
if day["tempLow"] != ECOBEE_STATE_UNKNOWN:
|
||||||
forecast[ATTR_FORECAST_TEMP_LOW] = float(day["tempLow"]) / 10
|
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"])
|
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"])
|
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"])
|
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"])
|
forecast[ATTR_FORECAST_HUMIDITY] = int(day["relativeHumidity"])
|
||||||
forecasts.append(forecast)
|
forecasts.append(forecast)
|
||||||
return forecasts
|
return forecasts
|
||||||
except (ValueError, IndexError, KeyError):
|
except (ValueError, IndexError, KeyError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update(self):
|
async def async_update(self):
|
||||||
"""Get the latest state of the sensor."""
|
"""Get the latest weather data."""
|
||||||
data = ecobee.NETWORK
|
await self.data.update()
|
||||||
data.update()
|
thermostat = self.data.ecobee.get_thermostat(self._index)
|
||||||
thermostat = data.ecobee.get_thermostat(self._index)
|
|
||||||
self.weather = thermostat.get("weather", None)
|
self.weather = thermostat.get("weather", None)
|
||||||
|
@ -15,6 +15,7 @@ FLOWS = [
|
|||||||
"daikin",
|
"daikin",
|
||||||
"deconz",
|
"deconz",
|
||||||
"dialogflow",
|
"dialogflow",
|
||||||
|
"ecobee",
|
||||||
"emulated_roku",
|
"emulated_roku",
|
||||||
"esphome",
|
"esphome",
|
||||||
"geofency",
|
"geofency",
|
||||||
|
@ -1483,7 +1483,7 @@ python-clementine-remote==1.0.1
|
|||||||
python-digitalocean==1.13.2
|
python-digitalocean==1.13.2
|
||||||
|
|
||||||
# homeassistant.components.ecobee
|
# homeassistant.components.ecobee
|
||||||
python-ecobee-api==0.0.21
|
python-ecobee-api==0.1.2
|
||||||
|
|
||||||
# homeassistant.components.eq3btsmart
|
# homeassistant.components.eq3btsmart
|
||||||
# python-eq3bt==0.1.9
|
# python-eq3bt==0.1.9
|
||||||
|
@ -355,6 +355,9 @@ pysonos==0.0.23
|
|||||||
# homeassistant.components.spc
|
# homeassistant.components.spc
|
||||||
pyspcwebgw==0.4.0
|
pyspcwebgw==0.4.0
|
||||||
|
|
||||||
|
# homeassistant.components.ecobee
|
||||||
|
python-ecobee-api==0.1.2
|
||||||
|
|
||||||
# homeassistant.components.darksky
|
# homeassistant.components.darksky
|
||||||
python-forecastio==1.4.0
|
python-forecastio==1.4.0
|
||||||
|
|
||||||
|
@ -146,6 +146,7 @@ TEST_REQUIREMENTS = (
|
|||||||
"pysonos",
|
"pysonos",
|
||||||
"pyspcwebgw",
|
"pyspcwebgw",
|
||||||
"python_awair",
|
"python_awair",
|
||||||
|
"python-ecobee-api",
|
||||||
"python-forecastio",
|
"python-forecastio",
|
||||||
"python-izone",
|
"python-izone",
|
||||||
"python-nest",
|
"python-nest",
|
||||||
|
@ -54,7 +54,7 @@ class TestEcobee(unittest.TestCase):
|
|||||||
|
|
||||||
self.data = mock.Mock()
|
self.data = mock.Mock()
|
||||||
self.data.ecobee.get_thermostat.return_value = self.ecobee
|
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):
|
def test_name(self):
|
||||||
"""Test name property."""
|
"""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