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'