From ccc3ce81f9cb7902c1f432074b4b1745ec4878f1 Mon Sep 17 00:00:00 2001 From: MatsNl <37705266+MatsNl@users.noreply.github.com> Date: Wed, 22 Apr 2020 18:09:51 +0200 Subject: [PATCH] Add Atag One thermostat integration (#32361) * add atag integration * ignore * generated * Update .gitignore * requirements update * update coveragerc * Revert "update coveragerc" * make entity_types more readable * add DOMAIN to listener * entity name * Use DataUpdateCoordinator * fix translations * enable preset_modes * fix water_heater * update coveragerc * remove scan_interval Co-Authored-By: J. Nick Koston * Apply suggestions from code review Co-Authored-By: Martin Hjelmare * fixes review remarks * fix flake8 errors * ensure correct HVACmode * add away mode * use write_ha_state instead of refresh * remove OFF support, add Fahrenheit * rename test_config_flow.py Co-authored-by: J. Nick Koston Co-authored-by: Martin Hjelmare --- .coveragerc | 4 + CODEOWNERS | 1 + .../components/atag/.translations/en.json | 20 ++ homeassistant/components/atag/__init__.py | 259 ++++++++++++++++++ homeassistant/components/atag/climate.py | 106 +++++++ homeassistant/components/atag/config_flow.py | 50 ++++ homeassistant/components/atag/manifest.json | 8 + homeassistant/components/atag/sensor.py | 22 ++ homeassistant/components/atag/strings.json | 20 ++ homeassistant/components/atag/water_heater.py | 70 +++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/atag/__init__.py | 1 + tests/components/atag/test_config_flow.py | 76 +++++ 15 files changed, 644 insertions(+) create mode 100644 homeassistant/components/atag/.translations/en.json create mode 100644 homeassistant/components/atag/__init__.py create mode 100644 homeassistant/components/atag/climate.py create mode 100644 homeassistant/components/atag/config_flow.py create mode 100644 homeassistant/components/atag/manifest.json create mode 100644 homeassistant/components/atag/sensor.py create mode 100644 homeassistant/components/atag/strings.json create mode 100644 homeassistant/components/atag/water_heater.py create mode 100644 tests/components/atag/__init__.py create mode 100644 tests/components/atag/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 00ff568aa25..a63e1ae33a5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -50,6 +50,10 @@ omit = homeassistant/components/arwn/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* + homeassistant/components/atag/__init__.py + homeassistant/components/atag/climate.py + homeassistant/components/atag/sensor.py + homeassistant/components/atag/water_heater.py homeassistant/components/aten_pe/* homeassistant/components/atome/* homeassistant/components/aurora_abb_powerone/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 2c5749d9799..815f1b6b85a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -35,6 +35,7 @@ homeassistant/components/arduino/* @fabaff homeassistant/components/arest/* @fabaff homeassistant/components/arris_tg2492lg/* @vanbalken homeassistant/components/asuswrt/* @kennedyshead +homeassistant/components/atag/* @MatsNL homeassistant/components/aten_pe/* @mtdcr homeassistant/components/atome/* @baqs homeassistant/components/august/* @bdraco diff --git a/homeassistant/components/atag/.translations/en.json b/homeassistant/components/atag/.translations/en.json new file mode 100644 index 00000000000..094fde70dc9 --- /dev/null +++ b/homeassistant/components/atag/.translations/en.json @@ -0,0 +1,20 @@ +{ + "title": "Atag", + "config": { + "step": { + "user": { + "title": "Connect to the device", + "data": { + "host": "Host", + "port": "Port (10000)" + } + } + }, + "error": { + "connection_error": "Failed to connect, please try again" + }, + "abort": { + "already_configured": "Only one Atag device can be added to Home Assistant" + } + } +} diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py new file mode 100644 index 00000000000..cb90df1650c --- /dev/null +++ b/homeassistant/components/atag/__init__.py @@ -0,0 +1,259 @@ +"""The ATAG Integration.""" +from datetime import timedelta +import logging + +import async_timeout +from pyatag import AtagDataStore, AtagException + +from homeassistant.components.climate import DOMAIN as CLIMATE +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components.water_heater import DOMAIN as WATER_HEATER +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_ID, + ATTR_MODE, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PRESSURE_BAR, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant, asyncio +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "atag" +DATA_LISTENER = f"{DOMAIN}_listener" +SIGNAL_UPDATE_ATAG = f"{DOMAIN}_update" +PLATFORMS = [CLIMATE, WATER_HEATER, SENSOR] +HOUR = "h" +FIRE = "fire" +PERCENTAGE = "%" + +ICONS = { + TEMP_CELSIUS: "mdi:thermometer", + PRESSURE_BAR: "mdi:gauge", + FIRE: "mdi:fire", + ATTR_MODE: "mdi:settings", +} + +ENTITY_TYPES = { + SENSOR: [ + { + ATTR_NAME: "Outside Temperature", + ATTR_ID: "outside_temp", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: ICONS[TEMP_CELSIUS], + }, + { + ATTR_NAME: "Average Outside Temperature", + ATTR_ID: "tout_avg", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: ICONS[TEMP_CELSIUS], + }, + { + ATTR_NAME: "Weather Status", + ATTR_ID: "weather_status", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: None, + }, + { + ATTR_NAME: "CH Water Pressure", + ATTR_ID: "ch_water_pres", + ATTR_UNIT_OF_MEASUREMENT: PRESSURE_BAR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: ICONS[PRESSURE_BAR], + }, + { + ATTR_NAME: "CH Water Temperature", + ATTR_ID: "ch_water_temp", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: ICONS[TEMP_CELSIUS], + }, + { + ATTR_NAME: "CH Return Temperature", + ATTR_ID: "ch_return_temp", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: ICONS[TEMP_CELSIUS], + }, + { + ATTR_NAME: "Burning Hours", + ATTR_ID: "burning_hours", + ATTR_UNIT_OF_MEASUREMENT: HOUR, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: ICONS[FIRE], + }, + { + ATTR_NAME: "Flame", + ATTR_ID: "rel_mod_level", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: ICONS[FIRE], + }, + ], + CLIMATE: { + ATTR_NAME: DOMAIN.title(), + ATTR_ID: CLIMATE, + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: None, + }, + WATER_HEATER: { + ATTR_NAME: DOMAIN.title(), + ATTR_ID: WATER_HEATER, + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: None, + }, +} + + +async def async_setup(hass: HomeAssistant, config): + """Set up the Atag component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Atag integration from a config entry.""" + session = async_get_clientsession(hass) + + coordinator = AtagDataUpdateCoordinator(hass, session, entry) + + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +class AtagDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Atag data.""" + + def __init__(self, hass, session, entry): + """Initialize.""" + self.atag = AtagDataStore(session, paired=True, **entry.data) + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30) + ) + + async def _async_update_data(self): + """Update data via library.""" + with async_timeout.timeout(20): + try: + await self.atag.async_update() + except (AtagException) as error: + raise UpdateFailed(error) + + return self.atag.sensordata + + +async def async_unload_entry(hass, entry): + """Unload Atag config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +class AtagEntity(Entity): + """Defines a base Atag entity.""" + + def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_type: dict) -> None: + """Initialize the Atag entity.""" + self.coordinator = coordinator + + self._id = atag_type[ATTR_ID] + self._name = atag_type[ATTR_NAME] + self._icon = atag_type[ATTR_ICON] + self._unit = atag_type[ATTR_UNIT_OF_MEASUREMENT] + self._class = atag_type[ATTR_DEVICE_CLASS] + + @property + def device_info(self) -> dict: + """Return info for device registry.""" + device = self.coordinator.atag.device + version = self.coordinator.atag.apiversion + return { + "identifiers": {(DOMAIN, device)}, + ATTR_NAME: "Atag Thermostat", + "model": "Atag One", + "sw_version": version, + "manufacturer": "Atag", + } + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + self._icon = ( + self.coordinator.data.get(self._id, {}).get(ATTR_ICON) or self._icon + ) + return self._icon + + @property + def should_poll(self) -> bool: + """Return the polling requirement of the entity.""" + return False + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit + + @property + def device_class(self): + """Return the device class.""" + return self._class + + @property + def available(self): + """Return True if entity is available.""" + return self.coordinator.last_update_success + + @property + def unique_id(self): + """Return a unique ID to use for this entity.""" + return f"{self.coordinator.atag.device}-{self._id}" + + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_update(self): + """Update Atag entity.""" + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py new file mode 100644 index 00000000000..40bd8cd4cc7 --- /dev/null +++ b/homeassistant/components/atag/climate.py @@ -0,0 +1,106 @@ +"""Initialization of ATAG One climate platform.""" +from typing import List, Optional + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + PRESET_AWAY, + PRESET_BOOST, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT + +from . import CLIMATE, DOMAIN, ENTITY_TYPES, AtagEntity + +PRESET_SCHEDULE = "Auto" +PRESET_MANUAL = "Manual" +PRESET_EXTEND = "Extend" +SUPPORT_PRESET = [ + PRESET_MANUAL, + PRESET_SCHEDULE, + PRESET_EXTEND, + PRESET_AWAY, + PRESET_BOOST, +] +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE +HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] + + +async def async_setup_entry(hass, entry, async_add_entities): + """Load a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([AtagThermostat(coordinator, ENTITY_TYPES[CLIMATE])]) + + +class AtagThermostat(AtagEntity, ClimateDevice): + """Atag climate device.""" + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def hvac_mode(self) -> Optional[str]: + """Return hvac operation ie. heat, cool mode.""" + if self.coordinator.atag.hvac_mode in HVAC_MODES: + return self.coordinator.atag.hvac_mode + return None + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes.""" + return HVAC_MODES + + @property + def hvac_action(self) -> Optional[str]: + """Return the current running hvac operation.""" + if self.coordinator.atag.cv_status: + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + if self.coordinator.atag.temp_unit in [TEMP_CELSIUS, TEMP_FAHRENHEIT]: + return self.coordinator.atag.temp_unit + return None + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self.coordinator.atag.temperature + + @property + def target_temperature(self) -> Optional[float]: + """Return the temperature we try to reach.""" + return self.coordinator.atag.target_temperature + + @property + def preset_mode(self) -> Optional[str]: + """Return the current preset mode, e.g., auto, manual, fireplace, extend, etc.""" + return self.coordinator.atag.hold_mode + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return a list of available preset modes.""" + return SUPPORT_PRESET + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + await self.coordinator.atag.set_temp(kwargs.get(ATTR_TEMPERATURE)) + self.async_write_ha_state() + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + await self.coordinator.atag.set_hvac_mode(hvac_mode) + self.async_write_ha_state() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + await self.coordinator.atag.set_hold_mode(preset_mode) + self.async_write_ha_state() diff --git a/homeassistant/components/atag/config_flow.py b/homeassistant/components/atag/config_flow.py new file mode 100644 index 00000000000..27b2b7a42f6 --- /dev/null +++ b/homeassistant/components/atag/config_flow.py @@ -0,0 +1,50 @@ +"""Config flow for the Atag component.""" +from pyatag import DEFAULT_PORT, AtagDataStore, AtagException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from . import DOMAIN # pylint: disable=unused-import + +DATA_SCHEMA = { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int), +} + + +class AtagConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Atag.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + if not user_input: + return await self._show_form() + session = async_get_clientsession(self.hass) + try: + atag = AtagDataStore(session, **user_input) + await atag.async_check_pair_status() + + except AtagException: + return await self._show_form({"base": "connection_error"}) + + user_input.update({CONF_DEVICE: atag.device}) + return self.async_create_entry(title=atag.device, data=user_input) + + @callback + async def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(DATA_SCHEMA), + errors=errors if errors else {}, + ) diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json new file mode 100644 index 00000000000..9534bad6df8 --- /dev/null +++ b/homeassistant/components/atag/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "atag", + "name": "Atag", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/atag/", + "requirements": ["pyatag==0.2.18"], + "codeowners": ["@MatsNL"] +} diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py new file mode 100644 index 00000000000..743b50ef40d --- /dev/null +++ b/homeassistant/components/atag/sensor.py @@ -0,0 +1,22 @@ +"""Initialization of ATAG One sensor platform.""" +from homeassistant.const import ATTR_STATE + +from . import DOMAIN, ENTITY_TYPES, SENSOR, AtagEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Initialize sensor platform from config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + entities = [] + for sensor in ENTITY_TYPES[SENSOR]: + entities.append(AtagSensor(coordinator, sensor)) + async_add_entities(entities) + + +class AtagSensor(AtagEntity): + """Representation of a AtagOne Sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self.coordinator.data[self._id][ATTR_STATE] diff --git a/homeassistant/components/atag/strings.json b/homeassistant/components/atag/strings.json new file mode 100644 index 00000000000..094fde70dc9 --- /dev/null +++ b/homeassistant/components/atag/strings.json @@ -0,0 +1,20 @@ +{ + "title": "Atag", + "config": { + "step": { + "user": { + "title": "Connect to the device", + "data": { + "host": "Host", + "port": "Port (10000)" + } + } + }, + "error": { + "connection_error": "Failed to connect, please try again" + }, + "abort": { + "already_configured": "Only one Atag device can be added to Home Assistant" + } + } +} diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py new file mode 100644 index 00000000000..bb1f72d6a8e --- /dev/null +++ b/homeassistant/components/atag/water_heater.py @@ -0,0 +1,70 @@ +"""ATAG water heater component.""" +from homeassistant.components.water_heater import ( + ATTR_TEMPERATURE, + STATE_ECO, + STATE_PERFORMANCE, + WaterHeaterDevice, +) +from homeassistant.const import STATE_OFF, TEMP_CELSIUS + +from . import DOMAIN, ENTITY_TYPES, WATER_HEATER, AtagEntity + +SUPPORT_FLAGS_HEATER = 0 +OPERATION_LIST = [STATE_OFF, STATE_ECO, STATE_PERFORMANCE] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Initialize DHW device from config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([AtagWaterHeater(coordinator, ENTITY_TYPES[WATER_HEATER])]) + + +class AtagWaterHeater(AtagEntity, WaterHeaterDevice): + """Representation of an ATAG water heater.""" + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS_HEATER + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.coordinator.atag.dhw_temperature + + @property + def current_operation(self): + """Return current operation.""" + if self.coordinator.atag.dhw_status: + return STATE_PERFORMANCE + return STATE_OFF + + @property + def operation_list(self): + """List of available operation modes.""" + return OPERATION_LIST + + async def set_temperature(self, **kwargs): + """Set new target temperature.""" + if await self.coordinator.atag.dhw_set_temp(kwargs.get(ATTR_TEMPERATURE)): + self.async_write_ha_state() + + @property + def target_temperature(self): + """Return the setpoint if water demand, otherwise return base temp (comfort level).""" + return self.coordinator.atag.dhw_target_temperature + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self.coordinator.atag.dhw_max_temp + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self.coordinator.atag.dhw_min_temp diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 520c1658e89..e17aefac636 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -13,6 +13,7 @@ FLOWS = [ "almond", "ambiclimate", "ambient_station", + "atag", "august", "axis", "braviatv", diff --git a/requirements_all.txt b/requirements_all.txt index b6579b45bfd..9b847514197 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1181,6 +1181,9 @@ pyalmond==0.0.2 # homeassistant.components.arlo pyarlo==0.2.3 +# homeassistant.components.atag +pyatag==0.2.18 + # homeassistant.components.netatmo pyatmo==3.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0159d5244cd..32f0bf37920 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -478,6 +478,9 @@ pyalmond==0.0.2 # homeassistant.components.arlo pyarlo==0.2.3 +# homeassistant.components.atag +pyatag==0.2.18 + # homeassistant.components.netatmo pyatmo==3.3.0 diff --git a/tests/components/atag/__init__.py b/tests/components/atag/__init__.py new file mode 100644 index 00000000000..b975a8de929 --- /dev/null +++ b/tests/components/atag/__init__.py @@ -0,0 +1 @@ +"""Tests for the Atag component.""" diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py new file mode 100644 index 00000000000..bda4ccc9023 --- /dev/null +++ b/tests/components/atag/test_config_flow.py @@ -0,0 +1,76 @@ +"""Tests for the Atag config flow.""" +from unittest.mock import PropertyMock + +from asynctest import patch +from pyatag import AtagException + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.atag import DOMAIN +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + +FIXTURE_USER_INPUT = { + CONF_HOST: "127.0.0.1", + CONF_PORT: 10000, +} +FIXTURE_COMPLETE_ENTRY = FIXTURE_USER_INPUT.copy() +FIXTURE_COMPLETE_ENTRY[CONF_DEVICE] = "device_identifier" + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_one_config_allowed(hass): + """Test that only one Atag configuration is allowed.""" + MockConfigEntry(domain="atag", data=FIXTURE_USER_INPUT).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_connection_error(hass): + """Test we show user form on Atag connection error.""" + + with patch( + "homeassistant.components.atag.config_flow.AtagDataStore.async_check_pair_status", + side_effect=AtagException(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=FIXTURE_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "connection_error"} + + +async def test_full_flow_implementation(hass): + """Test registering an integration and finishing flow works.""" + with patch( + "homeassistant.components.atag.AtagDataStore.async_check_pair_status", + ), patch( + "homeassistant.components.atag.AtagDataStore.device", + new_callable=PropertyMock(return_value="device_identifier"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=FIXTURE_USER_INPUT, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == FIXTURE_COMPLETE_ENTRY[CONF_DEVICE] + assert result["data"] == FIXTURE_COMPLETE_ENTRY