diff --git a/.coveragerc b/.coveragerc
index de778888097..28cadab252f 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -22,6 +22,7 @@ omit =
homeassistant/components/alarmdotcom/alarm_control_panel.py
homeassistant/components/alpha_vantage/sensor.py
homeassistant/components/amazon_polly/tts.py
+ homeassistant/components/ambiclimate/climate.py
homeassistant/components/ambient_station/*
homeassistant/components/amcrest/*
homeassistant/components/ampio/*
diff --git a/CODEOWNERS b/CODEOWNERS
index 6d27c8563fc..804426ce779 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -21,6 +21,7 @@ homeassistant/components/airvisual/* @bachya
homeassistant/components/alarm_control_panel/* @colinodell
homeassistant/components/alpha_vantage/* @fabaff
homeassistant/components/amazon_polly/* @robbiet480
+homeassistant/components/ambiclimate/* @danielhiversen
homeassistant/components/ambient_station/* @bachya
homeassistant/components/api/* @home-assistant/core
homeassistant/components/arduino/* @fabaff
diff --git a/homeassistant/components/ambiclimate/.translations/en.json b/homeassistant/components/ambiclimate/.translations/en.json
new file mode 100644
index 00000000000..78386077af2
--- /dev/null
+++ b/homeassistant/components/ambiclimate/.translations/en.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "title": "Ambiclimate",
+ "step": {
+ "auth": {
+ "title": "Authenticate Ambiclimate",
+ "description": "Please follow this [link]({authorization_url}) and Allow access to your Ambiclimate account, then come back and press Submit below.\n(Make sure the specified callback url is {cb_url})"
+ }
+ },
+ "create_entry": {
+ "default": "Successfully authenticated with Ambiclimate"
+ },
+ "error": {
+ "no_token": "Not authenticated with Ambiclimate",
+ "follow_link": "Please follow the link and authenticate before pressing Submit"
+ },
+ "abort": {
+ "already_setup": "The Ambiclimate account is configured.",
+ "no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/).",
+ "access_token": "Unknown error generating an access token."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambiclimate/__init__.py b/homeassistant/components/ambiclimate/__init__.py
new file mode 100644
index 00000000000..07494ce6cf7
--- /dev/null
+++ b/homeassistant/components/ambiclimate/__init__.py
@@ -0,0 +1,44 @@
+"""Support for Ambiclimate devices."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.helpers import config_validation as cv
+from . import config_flow
+from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, DOMAIN
+
+
+_LOGGER = logging.getLogger(__name__)
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN:
+ vol.Schema({
+ vol.Required(CONF_CLIENT_ID): cv.string,
+ vol.Required(CONF_CLIENT_SECRET): cv.string,
+ })
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup(hass, config):
+ """Set up Ambiclimate components."""
+ if DOMAIN not in config:
+ return True
+
+ conf = config[DOMAIN]
+
+ config_flow.register_flow_implementation(
+ hass, conf[CONF_CLIENT_ID],
+ conf[CONF_CLIENT_SECRET])
+
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up Ambiclimate from a config entry."""
+ hass.async_create_task(hass.config_entries.async_forward_entry_setup(
+ entry, 'climate'))
+
+ return True
diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py
new file mode 100644
index 00000000000..d326a943761
--- /dev/null
+++ b/homeassistant/components/ambiclimate/climate.py
@@ -0,0 +1,230 @@
+"""Support for Ambiclimate ac."""
+import asyncio
+import logging
+
+import ambiclimate
+import voluptuous as vol
+
+from homeassistant.components.climate import ClimateDevice
+from homeassistant.components.climate.const import (
+ SUPPORT_TARGET_TEMPERATURE,
+ SUPPORT_ON_OFF, STATE_HEAT)
+from homeassistant.const import ATTR_NAME
+from homeassistant.const import (ATTR_TEMPERATURE,
+ STATE_OFF, TEMP_CELSIUS)
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from .const import (ATTR_VALUE, CONF_CLIENT_ID, CONF_CLIENT_SECRET,
+ DOMAIN, SERVICE_COMFORT_FEEDBACK, SERVICE_COMFORT_MODE,
+ SERVICE_TEMPERATURE_MODE, STORAGE_KEY, STORAGE_VERSION)
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
+ SUPPORT_ON_OFF)
+
+SEND_COMFORT_FEEDBACK_SCHEMA = vol.Schema({
+ vol.Required(ATTR_NAME): cv.string,
+ vol.Required(ATTR_VALUE): cv.string,
+})
+
+SET_COMFORT_MODE_SCHEMA = vol.Schema({
+ vol.Required(ATTR_NAME): cv.string,
+})
+
+SET_TEMPERATURE_MODE_SCHEMA = vol.Schema({
+ vol.Required(ATTR_NAME): cv.string,
+ vol.Required(ATTR_VALUE): cv.string,
+})
+
+
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Set up the Ambicliamte device."""
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up the Ambicliamte device from config entry."""
+ config = entry.data
+ websession = async_get_clientsession(hass)
+ store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
+ token_info = await store.async_load()
+
+ oauth = ambiclimate.AmbiclimateOAuth(config[CONF_CLIENT_ID],
+ config[CONF_CLIENT_SECRET],
+ config['callback_url'],
+ websession)
+
+ try:
+ _token_info = await oauth.refresh_access_token(token_info)
+ except ambiclimate.AmbiclimateOauthError:
+ _LOGGER.error("Failed to refresh access token")
+ return
+
+ if _token_info:
+ await store.async_save(token_info)
+ token_info = _token_info
+
+ data_connection = ambiclimate.AmbiclimateConnection(oauth,
+ token_info=token_info,
+ websession=websession)
+
+ if not await data_connection.find_devices():
+ _LOGGER.error("No devices found")
+ return
+
+ tasks = []
+ for heater in data_connection.get_devices():
+ tasks.append(heater.update_device_info())
+ await asyncio.wait(tasks)
+
+ devs = []
+ for heater in data_connection.get_devices():
+ devs.append(AmbiclimateEntity(heater, store))
+
+ async_add_entities(devs, True)
+
+ async def send_comfort_feedback(service):
+ """Send comfort feedback."""
+ device_name = service.data[ATTR_NAME]
+ device = data_connection.find_device_by_room_name(device_name)
+ if device:
+ await device.set_comfort_feedback(service.data[ATTR_VALUE])
+
+ hass.services.async_register(DOMAIN,
+ SERVICE_COMFORT_FEEDBACK,
+ send_comfort_feedback,
+ schema=SEND_COMFORT_FEEDBACK_SCHEMA)
+
+ async def set_comfort_mode(service):
+ """Set comfort mode."""
+ device_name = service.data[ATTR_NAME]
+ device = data_connection.find_device_by_room_name(device_name)
+ if device:
+ await device.set_comfort_mode()
+
+ hass.services.async_register(DOMAIN,
+ SERVICE_COMFORT_MODE,
+ set_comfort_mode,
+ schema=SET_COMFORT_MODE_SCHEMA)
+
+ async def set_temperature_mode(service):
+ """Set temperature mode."""
+ device_name = service.data[ATTR_NAME]
+ device = data_connection.find_device_by_room_name(device_name)
+ if device:
+ await device.set_temperature_mode(service.data[ATTR_VALUE])
+
+ hass.services.async_register(DOMAIN,
+ SERVICE_TEMPERATURE_MODE,
+ set_temperature_mode,
+ schema=SET_TEMPERATURE_MODE_SCHEMA)
+
+
+class AmbiclimateEntity(ClimateDevice):
+ """Representation of a Ambiclimate Thermostat device."""
+
+ def __init__(self, heater, store):
+ """Initialize the thermostat."""
+ self._heater = heater
+ self._store = store
+ self._data = {}
+
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._heater.device_id
+
+ @property
+ def name(self):
+ """Return the name of the entity."""
+ return self._heater.name
+
+ @property
+ def device_info(self):
+ """Return the device info."""
+ return {
+ 'identifiers': {
+ (DOMAIN, self.unique_id)
+ },
+ 'name': self.name,
+ 'manufacturer': 'Ambiclimate',
+ }
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement which this thermostat uses."""
+ return TEMP_CELSIUS
+
+ @property
+ def target_temperature(self):
+ """Return the target temperature."""
+ return self._data.get('target_temperature')
+
+ @property
+ def target_temperature_step(self):
+ """Return the supported step of target temperature."""
+ return 1
+
+ @property
+ def current_temperature(self):
+ """Return the current temperature."""
+ return self._data.get('temperature')
+
+ @property
+ def current_humidity(self):
+ """Return the current humidity."""
+ return self._data.get('humidity')
+
+ @property
+ def is_on(self):
+ """Return true if heater is on."""
+ return self._data.get('power', '').lower() == 'on'
+
+ @property
+ def min_temp(self):
+ """Return the minimum temperature."""
+ return self._heater.get_min_temp()
+
+ @property
+ def max_temp(self):
+ """Return the maximum temperature."""
+ return self._heater.get_max_temp()
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ return SUPPORT_FLAGS
+
+ @property
+ def current_operation(self):
+ """Return current operation."""
+ return STATE_HEAT if self.is_on else STATE_OFF
+
+ async def async_set_temperature(self, **kwargs):
+ """Set new target temperature."""
+ temperature = kwargs.get(ATTR_TEMPERATURE)
+ if temperature is None:
+ return
+ await self._heater.set_target_temperature(temperature)
+
+ async def async_turn_on(self):
+ """Turn device on."""
+ await self._heater.turn_on()
+
+ async def async_turn_off(self):
+ """Turn device off."""
+ await self._heater.turn_off()
+
+ async def async_update(self):
+ """Retrieve latest state."""
+ try:
+ token_info = await self._heater.control.refresh_access_token()
+ except ambiclimate.AmbiclimateOauthError:
+ _LOGGER.error("Failed to refresh access token")
+ return
+
+ if token_info:
+ await self._store.async_save(token_info)
+
+ self._data = await self._heater.update_device()
diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py
new file mode 100644
index 00000000000..9bbdfceb7b0
--- /dev/null
+++ b/homeassistant/components/ambiclimate/config_flow.py
@@ -0,0 +1,153 @@
+"""Config flow for Ambiclimate."""
+import logging
+
+import ambiclimate
+
+from homeassistant import config_entries
+from homeassistant.components.http import HomeAssistantView
+from homeassistant.core import callback
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from .const import (AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, CONF_CLIENT_ID,
+ CONF_CLIENT_SECRET, DOMAIN, STORAGE_VERSION, STORAGE_KEY)
+
+DATA_AMBICLIMATE_IMPL = 'ambiclimate_flow_implementation'
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@callback
+def register_flow_implementation(hass, client_id, client_secret):
+ """Register a ambiclimate implementation.
+
+ client_id: Client id.
+ client_secret: Client secret.
+ """
+ hass.data.setdefault(DATA_AMBICLIMATE_IMPL, {})
+
+ hass.data[DATA_AMBICLIMATE_IMPL] = {
+ CONF_CLIENT_ID: client_id,
+ CONF_CLIENT_SECRET: client_secret,
+ }
+
+
+@config_entries.HANDLERS.register('ambiclimate')
+class AmbiclimateFlowHandler(config_entries.ConfigFlow):
+ """Handle a config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ def __init__(self):
+ """Initialize flow."""
+ self._registered_view = False
+ self._oauth = None
+
+ async def async_step_user(self, user_input=None):
+ """Handle external yaml configuration."""
+ if self.hass.config_entries.async_entries(DOMAIN):
+ return self.async_abort(reason='already_setup')
+
+ config = self.hass.data.get(DATA_AMBICLIMATE_IMPL, {})
+
+ if not config:
+ _LOGGER.debug("No config")
+ return self.async_abort(reason='no_config')
+
+ return await self.async_step_auth()
+
+ async def async_step_auth(self, user_input=None):
+ """Handle a flow start."""
+ if self.hass.config_entries.async_entries(DOMAIN):
+ return self.async_abort(reason='already_setup')
+
+ errors = {}
+
+ if user_input is not None:
+ errors['base'] = 'follow_link'
+
+ if not self._registered_view:
+ self._generate_view()
+
+ return self.async_show_form(
+ step_id='auth',
+ description_placeholders={'authorization_url':
+ await self._get_authorize_url(),
+ 'cb_url': self._cb_url()},
+ errors=errors,
+ )
+
+ async def async_step_code(self, code=None):
+ """Received code for authentication."""
+ if self.hass.config_entries.async_entries(DOMAIN):
+ return self.async_abort(reason='already_setup')
+
+ token_info = await self._get_token_info(code)
+
+ if token_info is None:
+ return self.async_abort(reason='access_token')
+
+ config = self.hass.data[DATA_AMBICLIMATE_IMPL].copy()
+ config['callback_url'] = self._cb_url()
+
+ return self.async_create_entry(
+ title="Ambiclimate",
+ data=config,
+ )
+
+ async def _get_token_info(self, code):
+ oauth = self._generate_oauth()
+ try:
+ token_info = await oauth.get_access_token(code)
+ except ambiclimate.AmbiclimateOauthError:
+ _LOGGER.error("Failed to get access token", exc_info=True)
+ return None
+
+ store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
+ await store.async_save(token_info)
+
+ return token_info
+
+ def _generate_view(self):
+ self.hass.http.register_view(AmbiclimateAuthCallbackView())
+ self._registered_view = True
+
+ def _generate_oauth(self):
+ config = self.hass.data[DATA_AMBICLIMATE_IMPL]
+ clientsession = async_get_clientsession(self.hass)
+ callback_url = self._cb_url()
+
+ oauth = ambiclimate.AmbiclimateOAuth(config.get(CONF_CLIENT_ID),
+ config.get(CONF_CLIENT_SECRET),
+ callback_url,
+ clientsession)
+ return oauth
+
+ def _cb_url(self):
+ return '{}{}'.format(self.hass.config.api.base_url,
+ AUTH_CALLBACK_PATH)
+
+ async def _get_authorize_url(self):
+ oauth = self._generate_oauth()
+ return oauth.get_authorize_url()
+
+
+class AmbiclimateAuthCallbackView(HomeAssistantView):
+ """Ambiclimate Authorization Callback View."""
+
+ requires_auth = False
+ url = AUTH_CALLBACK_PATH
+ name = AUTH_CALLBACK_NAME
+
+ async def get(self, request):
+ """Receive authorization token."""
+ code = request.query.get('code')
+ if code is None:
+ return "No code"
+ hass = request.app['hass']
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={'source': 'code'},
+ data=code,
+ ))
+ return "OK!"
diff --git a/homeassistant/components/ambiclimate/const.py b/homeassistant/components/ambiclimate/const.py
new file mode 100644
index 00000000000..b1b9f4c2767
--- /dev/null
+++ b/homeassistant/components/ambiclimate/const.py
@@ -0,0 +1,14 @@
+"""Constants used by the Ambiclimate component."""
+
+ATTR_VALUE = 'value'
+CONF_CLIENT_ID = 'client_id'
+CONF_CLIENT_SECRET = 'client_secret'
+DOMAIN = 'ambiclimate'
+SERVICE_COMFORT_FEEDBACK = 'send_comfort_feedback'
+SERVICE_COMFORT_MODE = 'set_comfort_mode'
+SERVICE_TEMPERATURE_MODE = 'set_temperature_mode'
+STORAGE_KEY = 'ambiclimate_auth'
+STORAGE_VERSION = 1
+
+AUTH_CALLBACK_NAME = 'api:ambiclimate'
+AUTH_CALLBACK_PATH = '/api/ambiclimate'
diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json
new file mode 100644
index 00000000000..f3b3450f163
--- /dev/null
+++ b/homeassistant/components/ambiclimate/manifest.json
@@ -0,0 +1,12 @@
+{
+ "domain": "ambiclimate",
+ "name": "Ambiclimate",
+ "documentation": "https://www.home-assistant.io/components/ambiclimate",
+ "requirements": [
+ "ambiclimate==0.1.1"
+ ],
+ "dependencies": [],
+ "codeowners": [
+ "@danielhiversen"
+ ]
+}
diff --git a/homeassistant/components/ambiclimate/services.yaml b/homeassistant/components/ambiclimate/services.yaml
new file mode 100644
index 00000000000..19f47c6c35f
--- /dev/null
+++ b/homeassistant/components/ambiclimate/services.yaml
@@ -0,0 +1,36 @@
+# Describes the format for available services for ambiclimate
+
+set_comfort_mode:
+ description: >
+ Enable comfort mode on your AC
+ fields:
+ Name:
+ description: >
+ String with device name.
+ example: Bedroom
+
+send_comfort_feedback:
+ description: >
+ Send feedback for comfort mode
+ fields:
+ Name:
+ description: >
+ String with device name.
+ example: Bedroom
+ Value:
+ description: >
+ Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing
+ example: bit_warm
+
+set_temperature_mode:
+ description: >
+ Enable temperature mode on your AC
+ fields:
+ Name:
+ description: >
+ String with device name.
+ example: Bedroom
+ Value:
+ description: >
+ Target value in celsius
+ example: 22
diff --git a/homeassistant/components/ambiclimate/strings.json b/homeassistant/components/ambiclimate/strings.json
new file mode 100644
index 00000000000..78386077af2
--- /dev/null
+++ b/homeassistant/components/ambiclimate/strings.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "title": "Ambiclimate",
+ "step": {
+ "auth": {
+ "title": "Authenticate Ambiclimate",
+ "description": "Please follow this [link]({authorization_url}) and Allow access to your Ambiclimate account, then come back and press Submit below.\n(Make sure the specified callback url is {cb_url})"
+ }
+ },
+ "create_entry": {
+ "default": "Successfully authenticated with Ambiclimate"
+ },
+ "error": {
+ "no_token": "Not authenticated with Ambiclimate",
+ "follow_link": "Please follow the link and authenticate before pressing Submit"
+ },
+ "abort": {
+ "already_setup": "The Ambiclimate account is configured.",
+ "no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/).",
+ "access_token": "Unknown error generating an access token."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 393a046b5a2..a2b34a00efd 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -142,6 +142,7 @@ SOURCE_IMPORT = 'import'
HANDLERS = Registry()
# Components that have config flows. In future we will auto-generate this list.
FLOWS = [
+ 'ambiclimate',
'ambient_station',
'axis',
'cast',
diff --git a/requirements_all.txt b/requirements_all.txt
index 1f09fd64542..4201c26ffe3 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -163,6 +163,9 @@ alarmdecoder==1.13.2
# homeassistant.components.alpha_vantage
alpha_vantage==2.1.0
+# homeassistant.components.ambiclimate
+ambiclimate==0.1.1
+
# homeassistant.components.amcrest
amcrest==1.4.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index b22a3be31b0..552e9cbc7ab 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -57,6 +57,9 @@ aioswitcher==2019.3.21
# homeassistant.components.unifi
aiounifi==4
+# homeassistant.components.ambiclimate
+ambiclimate==0.1.1
+
# homeassistant.components.apns
apns2==0.3.0
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index ad0a833436e..14303bd6d65 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -42,6 +42,7 @@ COMMENT_REQUIREMENTS = (
)
TEST_REQUIREMENTS = (
+ 'ambiclimate',
'aioambient',
'aioautomatic',
'aiobotocore',
diff --git a/tests/components/ambiclimate/__init__.py b/tests/components/ambiclimate/__init__.py
new file mode 100644
index 00000000000..b3f9a5ad3a6
--- /dev/null
+++ b/tests/components/ambiclimate/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Ambiclimate component."""
diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py
new file mode 100644
index 00000000000..f61665b9e5b
--- /dev/null
+++ b/tests/components/ambiclimate/test_config_flow.py
@@ -0,0 +1,123 @@
+"""Tests for the Ambiclimate config flow."""
+import ambiclimate
+from unittest.mock import Mock, patch
+
+from homeassistant.components.ambiclimate import config_flow
+from homeassistant.setup import async_setup_component
+from homeassistant.util import aiohttp
+from homeassistant import data_entry_flow
+from tests.common import mock_coro
+
+
+async def init_config_flow(hass):
+ """Init a configuration flow."""
+ await async_setup_component(hass, 'http', {
+ 'http': {
+ 'base_url': 'https://hass.com'
+ }
+ })
+
+ config_flow.register_flow_implementation(hass, 'id', 'secret')
+ flow = config_flow.AmbiclimateFlowHandler()
+
+ flow.hass = hass
+ return flow
+
+
+async def test_abort_if_no_implementation_registered(hass):
+ """Test we abort if no implementation is registered."""
+ flow = config_flow.AmbiclimateFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_user()
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'no_config'
+
+
+async def test_abort_if_already_setup(hass):
+ """Test we abort if Ambiclimate is already setup."""
+ flow = await init_config_flow(hass)
+
+ with patch.object(hass.config_entries, 'async_entries', return_value=[{}]):
+ result = await flow.async_step_user()
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'already_setup'
+
+ with patch.object(hass.config_entries, 'async_entries', return_value=[{}]):
+ result = await flow.async_step_code()
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'already_setup'
+
+
+async def test_full_flow_implementation(hass):
+ """Test registering an implementation and finishing flow works."""
+ config_flow.register_flow_implementation(hass, None, None)
+ flow = await init_config_flow(hass)
+
+ result = await flow.async_step_user()
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'auth'
+ assert result['description_placeholders']['cb_url']\
+ == 'https://hass.com/api/ambiclimate'
+
+ url = result['description_placeholders']['authorization_url']
+ assert 'https://api.ambiclimate.com/oauth2/authorize' in url
+ assert 'client_id=id' in url
+ assert 'response_type=code' in url
+ assert 'redirect_uri=https%3A%2F%2Fhass.com%2Fapi%2Fambiclimate' in url
+
+ with patch('ambiclimate.AmbiclimateOAuth.get_access_token',
+ return_value=mock_coro('test')):
+ result = await flow.async_step_code('123ABC')
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['title'] == 'Ambiclimate'
+ assert result['data']['callback_url'] == 'https://hass.com/api/ambiclimate'
+ assert result['data']['client_secret'] == 'secret'
+ assert result['data']['client_id'] == 'id'
+
+ with patch('ambiclimate.AmbiclimateOAuth.get_access_token',
+ return_value=mock_coro(None)):
+ result = await flow.async_step_code('123ABC')
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+
+ with patch('ambiclimate.AmbiclimateOAuth.get_access_token',
+ side_effect=ambiclimate.AmbiclimateOauthError()):
+ result = await flow.async_step_code('123ABC')
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+
+
+async def test_abort_no_code(hass):
+ """Test if no code is given to step_code."""
+ config_flow.register_flow_implementation(hass, None, None)
+ flow = await init_config_flow(hass)
+
+ result = await flow.async_step_code('invalid')
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'access_token'
+
+
+async def test_already_setup(hass):
+ """Test when already setup."""
+ config_flow.register_flow_implementation(hass, None, None)
+ flow = await init_config_flow(hass)
+
+ with patch.object(hass.config_entries, 'async_entries', return_value=True):
+ result = await flow.async_step_user()
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'already_setup'
+
+
+async def test_view(hass):
+ """Test view."""
+ hass.config_entries.flow.async_init = Mock()
+
+ request = aiohttp.MockRequest(b'', query_string='code=test_code')
+ request.app = {'hass': hass}
+ view = config_flow.AmbiclimateAuthCallbackView()
+ assert await view.get(request) == 'OK!'
+
+ request = aiohttp.MockRequest(b'', query_string='')
+ request.app = {'hass': hass}
+ view = config_flow.AmbiclimateAuthCallbackView()
+ assert await view.get(request) == 'No code'