From 973b19d733ab3db6350db0157929de883e2efe8a Mon Sep 17 00:00:00 2001 From: matsnl Date: Fri, 28 Feb 2020 23:29:11 +0000 Subject: [PATCH 01/24] add atag integration --- .gitignore | 2 +- .../components/atag/.translations/en.json | 21 ++ homeassistant/components/atag/__init__.py | 251 ++++++++++++++++++ homeassistant/components/atag/climate.py | 106 ++++++++ homeassistant/components/atag/config_flow.py | 59 ++++ homeassistant/components/atag/manifest.json | 9 + homeassistant/components/atag/sensor.py | 34 +++ homeassistant/components/atag/strings.json | 21 ++ homeassistant/components/atag/water_heater.py | 79 ++++++ tests/components/atag/__init__.py | 1 + tests/components/atag/test_atag_flow.py | 82 ++++++ 11 files changed, 664 insertions(+), 1 deletion(-) 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_atag_flow.py diff --git a/.gitignore b/.gitignore index 2473aeb4bf650d..2f3e09d27270dd 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ data/ .AppleDouble .LSOverride Icon - +__pycache__ # Thumbnails ._* diff --git a/homeassistant/components/atag/.translations/en.json b/homeassistant/components/atag/.translations/en.json new file mode 100644 index 00000000000000..b6f41db8f113b4 --- /dev/null +++ b/homeassistant/components/atag/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Atag", + "step": { + "user": { + "title": "Connect to the device", + "data": { + "host": "Host", + "email": "Email (optional)", + "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 00000000000000..0f334ae1386f06 --- /dev/null +++ b/homeassistant/components/atag/__init__.py @@ -0,0 +1,251 @@ +"""The ATAG Integration.""" +from datetime import timedelta + +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_STATE, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + EVENT_HOMEASSISTANT_STOP, + PRESSURE_BAR, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant, asyncio, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import dispatcher +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval + +DOMAIN = "atag" +DATA_LISTENER = "listener" +SIGNAL_UPDATE_ATAG = f"{DOMAIN}_update" +PLATFORMS = [CLIMATE, WATER_HEATER, SENSOR] +SCAN_INTERVAL = timedelta(seconds=30) +HOUR = "h" +FIRE = "fire" +PERCENTAGE = "%" + +ICONS = { + TEMP_CELSIUS: "mdi:thermometer", + PRESSURE_BAR: "mdi:gauge", + FIRE: "mdi:fire", + ATTR_MODE: "mdi:settings", +} + +ENTITY_TYPES = { + SENSOR: { + 1: { + 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], + }, + 2: { + 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], + }, + 3: { + ATTR_NAME: "Weather Status", + ATTR_ID: "weather_status", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: None, + }, + 4: { + ATTR_NAME: "Operation Mode", + ATTR_ID: "ch_mode", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: ICONS[ATTR_MODE], + }, + 5: { + 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], + }, + 6: { + 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], + }, + 7: { + 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], + }, + 8: { + ATTR_NAME: "Burning Hours", + ATTR_ID: "burning_hours", + ATTR_UNIT_OF_MEASUREMENT: HOUR, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: ICONS[FIRE], + }, + 9: { + ATTR_NAME: "Flame", + ATTR_ID: "rel_mod_level", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: ICONS[FIRE], + }, + }, + CLIMATE: {ATTR_NAME: CLIMATE.title()}, + WATER_HEATER: {ATTR_NAME: "Domestic Hot Water"}, +} + + +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) + atag = AtagDataStore(session, paired=True, **entry.data) + + try: + await atag.async_update() + if not atag.sensordata: + raise ConfigEntryNotReady + except AtagException: + raise ConfigEntryNotReady + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = atag + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + async def refresh(event_time): + """Poll Atag for latest data.""" + await atag.async_update() + dispatcher.async_dispatcher_send(hass, SIGNAL_UPDATE_ATAG) + + async def async_close_session(event): + """Close the session on shutdown.""" + await atag.async_close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_session) + + hass.data.setdefault(DATA_LISTENER, {})[entry.entry_id] = async_track_time_interval( + hass, refresh, SCAN_INTERVAL + ) + + return True + + +async def async_unload_entry(hass, entry): + """Unload Atag config entry.""" + remove_listener = hass.data[DATA_LISTENER].pop(entry.entry_id) + remove_listener() + 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, atag: AtagDataStore, atag_type: dict) -> None: + """Initialize the Atag entity.""" + self.atag = atag + + self._id = atag_type.get(ATTR_ID) + self._name = f"{atag_type[ATTR_NAME]}" + self._icon = atag_type.get(ATTR_ICON) + self._unit = atag_type.get(ATTR_UNIT_OF_MEASUREMENT) + self._class = atag_type.get(ATTR_DEVICE_CLASS) + self._sensor_value = atag.sensordata.get(self._id, {}).get(ATTR_STATE) + self._unsub_dispatcher = None + + @property + def device_info(self) -> dict: + """Return info for device registry.""" + device = self.atag.device + host = self.atag.config.host + version = self.atag.apiversion + return { + "identifiers": {(DOMAIN, device, host)}, + 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.""" + if hasattr(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.""" + if hasattr(self, "_unit"): + return self._unit + + @property + def device_class(self): + """Return the device class.""" + if hasattr(self, "_class"): + return self._class + + async def async_added_to_hass(self) -> None: + """Connect to dispatcher listening for entity data notifications.""" + self._unsub_dispatcher = dispatcher.async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ATAG, self._update_callback + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect from update signal.""" + self._unsub_dispatcher() + + @callback + def _update_callback(self) -> None: + """Schedule an immediate update of the entity.""" + self.async_schedule_update_ha_state(True) + + @property + def unique_id(self): + """Return a unique ID to use for this entity.""" + return f"{self.atag.device}-{self._name}" diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py new file mode 100644 index 00000000000000..86cc73e0c1e23d --- /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, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.helpers.restore_state import RestoreEntity + +from . import CLIMATE, DOMAIN, ENTITY_TYPES, AtagEntity + + +async def async_setup_platform(hass, _config, async_add_devices, _discovery_info=None): + """Atag updated to use config entry.""" + pass + + +async def async_setup_entry(hass, entry, async_add_devices): + """Load a config entry.""" + atag = hass.data[DOMAIN][entry.entry_id] + async_add_devices([AtagThermostat(atag, ENTITY_TYPES[CLIMATE])]) + + +class AtagThermostat(AtagEntity, ClimateDevice, RestoreEntity): + """Atag climate device.""" + + def __init__(self, atag, atagtype): + """Initialize with fake on/off state.""" + super().__init__(atag, atagtype) + self._on = None + + async def async_added_to_hass(self): + """Register callbacks & state restore for fake "Off" mode.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + if last_state: + self._on = last_state.state != HVAC_MODE_OFF + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def hvac_mode(self) -> Optional[str]: + """Return hvac operation ie. heat, cool mode.""" + if not self._on: + return HVAC_MODE_OFF + return self.atag.hvac_mode + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes.""" + return [HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF] + + @property + def hvac_action(self) -> Optional[str]: + """Return the current running hvac operation.""" + if self.atag.cv_status: + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self.atag.temperature + + @property + def target_temperature(self) -> Optional[float]: + """Return the temperature we try to reach.""" + return self.atag.target_temperature + + @property + def max_temp(self): + """Return the maximum temperature.""" + return DEFAULT_MAX_TEMP + + @property + def min_temp(self): + """Return the minimum temperature.""" + return DEFAULT_MIN_TEMP + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + if self._on and await self.atag.set_temp(kwargs.get(ATTR_TEMPERATURE)): + self.async_schedule_update_ha_state(True) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + self._on = hvac_mode != HVAC_MODE_OFF + if self._on: + await self.atag.set_hvac_mode(hvac_mode) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/atag/config_flow.py b/homeassistant/components/atag/config_flow.py new file mode 100644 index 00000000000000..e79fe5da5fd077 --- /dev/null +++ b/homeassistant/components/atag/config_flow.py @@ -0,0 +1,59 @@ +"""Config flow for the Atag component.""" +from aiohttp import ClientSession +from pyatag import DEFAULT_PORT, AtagDataStore, AtagException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_DEVICE, CONF_EMAIL, CONF_HOST, CONF_PORT +from homeassistant.core import callback + +from . import DOMAIN # pylint: disable=unused-import + +DATA_SCHEMA = { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_EMAIL): 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() + + try: + async with ClientSession() as session: + 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 {}, + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + return await self.async_step_user(import_config) diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json new file mode 100644 index 00000000000000..74d526111dcc9e --- /dev/null +++ b/homeassistant/components/atag/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "atag", + "name": "Atag", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/atag/", + "requirements": ["pyatag==0.2.15"], + "dependencies": [], + "codeowners": ["@MatsNL"] +} diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py new file mode 100644 index 00000000000000..734c8e3557b28b --- /dev/null +++ b/homeassistant/components/atag/sensor.py @@ -0,0 +1,34 @@ +"""Initialization of ATAG One sensor platform.""" +from homeassistant.const import ATTR_ICON, ATTR_STATE + +from . import DOMAIN, ENTITY_TYPES, SENSOR, AtagEntity + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Atag updated to use config entry.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Initialize sensor platform from config entry.""" + atag = hass.data[DOMAIN][config_entry.entry_id] + entities = [] + for sensor in ENTITY_TYPES[SENSOR]: + entities.append(AtagSensor(atag, ENTITY_TYPES[SENSOR][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._sensor_value + + async def async_update(self): + """Get latest data from datastore.""" + data = self.atag.sensordata.get(self._id) + if data: + self._sensor_value = data.get(ATTR_STATE) + self._icon = data.get(ATTR_ICON) or self._icon diff --git a/homeassistant/components/atag/strings.json b/homeassistant/components/atag/strings.json new file mode 100644 index 00000000000000..b6f41db8f113b4 --- /dev/null +++ b/homeassistant/components/atag/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Atag", + "step": { + "user": { + "title": "Connect to the device", + "data": { + "host": "Host", + "email": "Email (optional)", + "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 00000000000000..a822a2b52d66fe --- /dev/null +++ b/homeassistant/components/atag/water_heater.py @@ -0,0 +1,79 @@ +"""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_platform(hass, config, async_add_devices, _discovery_info=None): + """Atag updated to use config entry.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Initialize DHW device from config entry.""" + atag = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([AtagWaterHeater(atag, 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.atag.dhw_temperature + + @property + def current_operation(self): + """Return current operation.""" + if self.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.atag.dhw_set_temp(kwargs.get(ATTR_TEMPERATURE)): + self.async_schedule_update_ha_state(True) + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + pass + + @property + def target_temperature(self): + """Return the setpoint if water demand, otherwise return base temp (comfort level).""" + return self.atag.dhw_target_temperature + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self.atag.dhw_max_temp + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self.atag.dhw_min_temp diff --git a/tests/components/atag/__init__.py b/tests/components/atag/__init__.py new file mode 100644 index 00000000000000..b975a8de929635 --- /dev/null +++ b/tests/components/atag/__init__.py @@ -0,0 +1 @@ +"""Tests for the Atag component.""" diff --git a/tests/components/atag/test_atag_flow.py b/tests/components/atag/test_atag_flow.py new file mode 100644 index 00000000000000..75e14f1ad78f94 --- /dev/null +++ b/tests/components/atag/test_atag_flow.py @@ -0,0 +1,82 @@ +"""Tests for the Atag config flow.""" +from unittest.mock import PropertyMock, patch + +from asynctest import CoroutineMock + +from homeassistant import data_entry_flow +from homeassistant.components.atag import config_flow +from homeassistant.const import CONF_DEVICE, CONF_EMAIL, CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + +FIXTURE_USER_INPUT = { + CONF_HOST: "127.0.0.1", + CONF_PORT: 10000, + CONF_EMAIL: "user@email.com", +} +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.""" + flow = config_flow.AtagConfigFlow() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + 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.""" + flow = config_flow.AtagConfigFlow() + flow.hass = hass + + MockConfigEntry(domain="atag", data=FIXTURE_USER_INPUT).add_to_hass(hass) + + step_user_result = await flow.async_step_user() + + assert step_user_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step_user_result["reason"] == "already_configured" + + conf = {CONF_HOST: "atag.local", CONF_EMAIL: "user@email.com", CONF_PORT: 10000} + + import_config_result = await flow.async_step_import(conf) + + assert import_config_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert import_config_result["reason"] == "already_configured" + + +async def test_connection_error(hass): + """Test we show user form on Atag connection error.""" + + flow = config_flow.AtagConfigFlow() + flow.hass = hass + result = await flow.async_step_user(user_input=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", + new=CoroutineMock(), + ), patch( + "homeassistant.components.atag.AtagDataStore.device", + new_callable=PropertyMock(return_value="device_identifier"), + ): + flow = config_flow.AtagConfigFlow() + flow.hass = hass + result = await flow.async_step_import(import_config=None) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await flow.async_step_user(user_input=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 From 1780ccc5b0403bc2ac5f0152f4a7240225bf3f11 Mon Sep 17 00:00:00 2001 From: matsnl Date: Fri, 28 Feb 2020 23:34:39 +0000 Subject: [PATCH 02/24] ignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2f3e09d27270dd..9a3c3517f71557 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ data/ .AppleDouble .LSOverride Icon -__pycache__ # Thumbnails ._* From abe8c493ae45ee1cb1ea1e99d4e691f9f0a19aa8 Mon Sep 17 00:00:00 2001 From: matsnl Date: Sat, 29 Feb 2020 14:00:16 +0000 Subject: [PATCH 03/24] generated --- CODEOWNERS | 1 + homeassistant/generated/config_flows.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index bdf3bbbbe52388..564765a1ba6e77 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/generated/config_flows.py b/homeassistant/generated/config_flows.py index 975b62b3e99a28..1a939fc11b8d0d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -13,6 +13,7 @@ "almond", "ambiclimate", "ambient_station", + "atag", "august", "axis", "braviatv", From 03fb398daaf831075d9b6fd6c30bd9bd149b5f80 Mon Sep 17 00:00:00 2001 From: MatsNl <37705266+MatsNl@users.noreply.github.com> Date: Sun, 1 Mar 2020 00:03:49 +0100 Subject: [PATCH 04/24] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9a3c3517f71557..2473aeb4bf650d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ data/ .AppleDouble .LSOverride Icon + # Thumbnails ._* From 5c677cae4b7cbd53aa15fb1d46bbae5479620f76 Mon Sep 17 00:00:00 2001 From: matsnl Date: Sat, 29 Feb 2020 14:11:52 +0000 Subject: [PATCH 05/24] requirements update --- requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 2 files changed, 6 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index c39d1760efbcb4..59b0e23daee5c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1177,6 +1177,9 @@ pyalmond==0.0.2 # homeassistant.components.arlo pyarlo==0.2.3 +# homeassistant.components.atag +pyatag==0.2.15 + # homeassistant.components.netatmo pyatmo==3.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f013a52da050b..85a883914e43a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -468,6 +468,9 @@ pyalmond==0.0.2 # homeassistant.components.arlo pyarlo==0.2.3 +# homeassistant.components.atag +pyatag==0.2.15 + # homeassistant.components.netatmo pyatmo==3.3.0 From b2de593bed636e5941397e7cd74d1b65377d5667 Mon Sep 17 00:00:00 2001 From: matsnl Date: Mon, 2 Mar 2020 03:12:40 +0000 Subject: [PATCH 06/24] update coveragerc --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 3e0e523f95f6e1..84d9253cd9bda4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -50,6 +50,9 @@ omit = homeassistant/components/arwn/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* + 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 From 466e3f4c16e075dcba282c4c0b73cf0f1a7a54d0 Mon Sep 17 00:00:00 2001 From: matsnl Date: Mon, 2 Mar 2020 04:14:06 +0000 Subject: [PATCH 07/24] Revert "update coveragerc" --- .coveragerc | 3 --- 1 file changed, 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index 84d9253cd9bda4..3e0e523f95f6e1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -50,9 +50,6 @@ omit = homeassistant/components/arwn/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* - 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 From 509fc360477d54bcc96691fb6f6ac8639abb6055 Mon Sep 17 00:00:00 2001 From: matsnl Date: Mon, 2 Mar 2020 21:56:18 +0000 Subject: [PATCH 08/24] make entity_types more readable --- homeassistant/components/atag/__init__.py | 22 +++++++++++----------- homeassistant/components/atag/sensor.py | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 0f334ae1386f06..d360e76c222eeb 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -45,71 +45,71 @@ } ENTITY_TYPES = { - SENSOR: { - 1: { + 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], }, - 2: { + { 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], }, - 3: { + { ATTR_NAME: "Weather Status", ATTR_ID: "weather_status", ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, ATTR_ICON: None, }, - 4: { + { ATTR_NAME: "Operation Mode", ATTR_ID: "ch_mode", ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, ATTR_ICON: ICONS[ATTR_MODE], }, - 5: { + { 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], }, - 6: { + { 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], }, - 7: { + { 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], }, - 8: { + { ATTR_NAME: "Burning Hours", ATTR_ID: "burning_hours", ATTR_UNIT_OF_MEASUREMENT: HOUR, ATTR_DEVICE_CLASS: None, ATTR_ICON: ICONS[FIRE], }, - 9: { + { ATTR_NAME: "Flame", ATTR_ID: "rel_mod_level", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: None, ATTR_ICON: ICONS[FIRE], }, - }, + ], CLIMATE: {ATTR_NAME: CLIMATE.title()}, WATER_HEATER: {ATTR_NAME: "Domestic Hot Water"}, } diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py index 734c8e3557b28b..b4cdbbd4638b47 100644 --- a/homeassistant/components/atag/sensor.py +++ b/homeassistant/components/atag/sensor.py @@ -14,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): atag = hass.data[DOMAIN][config_entry.entry_id] entities = [] for sensor in ENTITY_TYPES[SENSOR]: - entities.append(AtagSensor(atag, ENTITY_TYPES[SENSOR][sensor])) + entities.append(AtagSensor(atag, sensor)) async_add_entities(entities) From 4ba63a3a7d759ea64939c5cb7df904b890324e60 Mon Sep 17 00:00:00 2001 From: matsnl Date: Mon, 2 Mar 2020 22:00:13 +0000 Subject: [PATCH 09/24] add DOMAIN to listener --- homeassistant/components/atag/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index d360e76c222eeb..a8bdbe58309d43 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -29,7 +29,7 @@ from homeassistant.helpers.event import async_track_time_interval DOMAIN = "atag" -DATA_LISTENER = "listener" +DATA_LISTENER = f"{DOMAIN}_listener" SIGNAL_UPDATE_ATAG = f"{DOMAIN}_update" PLATFORMS = [CLIMATE, WATER_HEATER, SENSOR] SCAN_INTERVAL = timedelta(seconds=30) From 2b613f5fba4cf2b62097af4ccf5b530047eeb889 Mon Sep 17 00:00:00 2001 From: matsnl Date: Thu, 16 Apr 2020 16:40:59 +0000 Subject: [PATCH 10/24] entity name --- homeassistant/components/atag/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index a8bdbe58309d43..98b041f77fa8ec 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -181,7 +181,7 @@ def __init__(self, atag: AtagDataStore, atag_type: dict) -> None: self.atag = atag self._id = atag_type.get(ATTR_ID) - self._name = f"{atag_type[ATTR_NAME]}" + self._name = atag_type[ATTR_NAME] self._icon = atag_type.get(ATTR_ICON) self._unit = atag_type.get(ATTR_UNIT_OF_MEASUREMENT) self._class = atag_type.get(ATTR_DEVICE_CLASS) From a707d82e115c192d10b42c1e37e2fe6355da9da0 Mon Sep 17 00:00:00 2001 From: matsnl Date: Thu, 16 Apr 2020 21:53:38 +0000 Subject: [PATCH 11/24] Use DataUpdateCoordinator --- .../components/atag/.translations/en.json | 1 - homeassistant/components/atag/__init__.py | 138 ++++++++++-------- homeassistant/components/atag/climate.py | 26 ++-- homeassistant/components/atag/config_flow.py | 3 +- homeassistant/components/atag/sensor.py | 15 +- homeassistant/components/atag/strings.json | 3 +- homeassistant/components/atag/water_heater.py | 18 +-- tests/components/atag/test_atag_flow.py | 5 +- 8 files changed, 108 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/atag/.translations/en.json b/homeassistant/components/atag/.translations/en.json index b6f41db8f113b4..1a5d15a25a8530 100644 --- a/homeassistant/components/atag/.translations/en.json +++ b/homeassistant/components/atag/.translations/en.json @@ -6,7 +6,6 @@ "title": "Connect to the device", "data": { "host": "Host", - "email": "Email (optional)", "port": "Port (10000)" } } diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 98b041f77fa8ec..b5d60086665032 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -1,6 +1,8 @@ """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 @@ -13,20 +15,19 @@ ATTR_ID, ATTR_MODE, ATTR_NAME, - ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, - EVENT_HOMEASSISTANT_STOP, PRESSURE_BAR, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant, asyncio, callback +from homeassistant.core import HomeAssistant, asyncio from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import dispatcher from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) DOMAIN = "atag" DATA_LISTENER = f"{DOMAIN}_listener" @@ -110,8 +111,20 @@ ATTR_ICON: ICONS[FIRE], }, ], - CLIMATE: {ATTR_NAME: CLIMATE.title()}, - WATER_HEATER: {ATTR_NAME: "Domestic Hot Water"}, + 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, + }, } @@ -123,43 +136,49 @@ async def async_setup(hass: HomeAssistant, config): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Atag integration from a config entry.""" session = async_get_clientsession(hass) - atag = AtagDataStore(session, paired=True, **entry.data) - try: - await atag.async_update() - if not atag.sensordata: - raise ConfigEntryNotReady - except AtagException: + coordinator = AtagDataUpdateCoordinator(hass, session, entry) + + await coordinator.async_refresh() + + if not coordinator.last_update_success: raise ConfigEntryNotReady - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = atag + + 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) ) - async def refresh(event_time): - """Poll Atag for latest data.""" - await atag.async_update() - dispatcher.async_dispatcher_send(hass, SIGNAL_UPDATE_ATAG) + return True - async def async_close_session(event): - """Close the session on shutdown.""" - await atag.async_close() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_session) +class AtagDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Atag data.""" - hass.data.setdefault(DATA_LISTENER, {})[entry.entry_id] = async_track_time_interval( - hass, refresh, SCAN_INTERVAL - ) + def __init__(self, hass, session, entry): + """Initialize.""" + self.atag = AtagDataStore(session, paired=True, **entry.data) - return True + 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.""" - remove_listener = hass.data[DATA_LISTENER].pop(entry.entry_id) - remove_listener() unload_ok = all( await asyncio.gather( *[ @@ -176,26 +195,23 @@ async def async_unload_entry(hass, entry): class AtagEntity(Entity): """Defines a base Atag entity.""" - def __init__(self, atag: AtagDataStore, atag_type: dict) -> None: + def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_type: dict) -> None: """Initialize the Atag entity.""" - self.atag = atag + self.coordinator = coordinator - self._id = atag_type.get(ATTR_ID) + self._id = atag_type[ATTR_ID] self._name = atag_type[ATTR_NAME] - self._icon = atag_type.get(ATTR_ICON) - self._unit = atag_type.get(ATTR_UNIT_OF_MEASUREMENT) - self._class = atag_type.get(ATTR_DEVICE_CLASS) - self._sensor_value = atag.sensordata.get(self._id, {}).get(ATTR_STATE) - self._unsub_dispatcher = None + 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.atag.device - host = self.atag.config.host - version = self.atag.apiversion + device = self.coordinator.atag.device + version = self.coordinator.atag.apiversion return { - "identifiers": {(DOMAIN, device, host)}, + "identifiers": {(DOMAIN, device)}, ATTR_NAME: "Atag Thermostat", "model": "Atag One", "sw_version": version, @@ -210,8 +226,7 @@ def name(self) -> str: @property def icon(self) -> str: """Return the mdi icon of the entity.""" - if hasattr(self, "_icon"): - return self._icon + return self._icon @property def should_poll(self) -> bool: @@ -221,31 +236,32 @@ def should_poll(self) -> bool: @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - if hasattr(self, "_unit"): - return self._unit + return self._unit @property def device_class(self): """Return the device class.""" - if hasattr(self, "_class"): - return self._class + # if hasattr(self, "_class"): + return self._class - async def async_added_to_hass(self) -> None: - """Connect to dispatcher listening for entity data notifications.""" - self._unsub_dispatcher = dispatcher.async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_ATAG, self._update_callback - ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect from update signal.""" - self._unsub_dispatcher() - - @callback - def _update_callback(self) -> None: - """Schedule an immediate update of the entity.""" - self.async_schedule_update_ha_state(True) + @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.atag.device}-{self._name}" + return f"{self.coordinator.atag.device}-{self._id}" + + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications.""" + self.coordinator.async_add_listener(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Disconnect from update signal.""" + self.coordinator.async_remove_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 index 86cc73e0c1e23d..d7c5e05d0e2b7c 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -25,17 +25,17 @@ async def async_setup_platform(hass, _config, async_add_devices, _discovery_info async def async_setup_entry(hass, entry, async_add_devices): """Load a config entry.""" - atag = hass.data[DOMAIN][entry.entry_id] - async_add_devices([AtagThermostat(atag, ENTITY_TYPES[CLIMATE])]) + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_devices([AtagThermostat(coordinator, ENTITY_TYPES[CLIMATE])]) class AtagThermostat(AtagEntity, ClimateDevice, RestoreEntity): """Atag climate device.""" - def __init__(self, atag, atagtype): + def __init__(self, coordinator, atagtype): """Initialize with fake on/off state.""" - super().__init__(atag, atagtype) self._on = None + super().__init__(coordinator, atagtype) async def async_added_to_hass(self): """Register callbacks & state restore for fake "Off" mode.""" @@ -54,7 +54,7 @@ def hvac_mode(self) -> Optional[str]: """Return hvac operation ie. heat, cool mode.""" if not self._on: return HVAC_MODE_OFF - return self.atag.hvac_mode + return self.coordinator.atag.hvac_mode @property def hvac_modes(self) -> List[str]: @@ -64,7 +64,7 @@ def hvac_modes(self) -> List[str]: @property def hvac_action(self) -> Optional[str]: """Return the current running hvac operation.""" - if self.atag.cv_status: + if self.coordinator.atag.cv_status: return CURRENT_HVAC_HEAT return CURRENT_HVAC_IDLE @@ -76,12 +76,12 @@ def temperature_unit(self): @property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" - return self.atag.temperature + return self.coordinator.atag.temperature @property def target_temperature(self) -> Optional[float]: """Return the temperature we try to reach.""" - return self.atag.target_temperature + return self.coordinator.atag.target_temperature @property def max_temp(self): @@ -95,12 +95,14 @@ def min_temp(self): async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" - if self._on and await self.atag.set_temp(kwargs.get(ATTR_TEMPERATURE)): - self.async_schedule_update_ha_state(True) + if self._on and await self.coordinator.atag.set_temp( + kwargs.get(ATTR_TEMPERATURE) + ): + await self.coordinator.async_refresh() async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" self._on = hvac_mode != HVAC_MODE_OFF if self._on: - await self.atag.set_hvac_mode(hvac_mode) - self.async_schedule_update_ha_state() + await self.coordinator.atag.set_hvac_mode(hvac_mode) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/atag/config_flow.py b/homeassistant/components/atag/config_flow.py index e79fe5da5fd077..01bcf1529808a4 100644 --- a/homeassistant/components/atag/config_flow.py +++ b/homeassistant/components/atag/config_flow.py @@ -4,14 +4,13 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_DEVICE, CONF_EMAIL, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT from homeassistant.core import callback from . import DOMAIN # pylint: disable=unused-import DATA_SCHEMA = { vol.Required(CONF_HOST): str, - vol.Optional(CONF_EMAIL): str, vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int), } diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py index b4cdbbd4638b47..b4d44516e69ee4 100644 --- a/homeassistant/components/atag/sensor.py +++ b/homeassistant/components/atag/sensor.py @@ -1,5 +1,5 @@ """Initialization of ATAG One sensor platform.""" -from homeassistant.const import ATTR_ICON, ATTR_STATE +from homeassistant.const import ATTR_STATE from . import DOMAIN, ENTITY_TYPES, SENSOR, AtagEntity @@ -11,10 +11,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Initialize sensor platform from config entry.""" - atag = hass.data[DOMAIN][config_entry.entry_id] + coordinator = hass.data[DOMAIN][config_entry.entry_id] entities = [] for sensor in ENTITY_TYPES[SENSOR]: - entities.append(AtagSensor(atag, sensor)) + entities.append(AtagSensor(coordinator, sensor)) async_add_entities(entities) @@ -24,11 +24,4 @@ class AtagSensor(AtagEntity): @property def state(self): """Return the state of the sensor.""" - return self._sensor_value - - async def async_update(self): - """Get latest data from datastore.""" - data = self.atag.sensordata.get(self._id) - if data: - self._sensor_value = data.get(ATTR_STATE) - self._icon = data.get(ATTR_ICON) or self._icon + return self.coordinator.data[self._id][ATTR_STATE] diff --git a/homeassistant/components/atag/strings.json b/homeassistant/components/atag/strings.json index b6f41db8f113b4..094fde70dc95f6 100644 --- a/homeassistant/components/atag/strings.json +++ b/homeassistant/components/atag/strings.json @@ -1,12 +1,11 @@ { + "title": "Atag", "config": { - "title": "Atag", "step": { "user": { "title": "Connect to the device", "data": { "host": "Host", - "email": "Email (optional)", "port": "Port (10000)" } } diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index a822a2b52d66fe..ed82552e8b1c5d 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -20,8 +20,8 @@ async def async_setup_platform(hass, config, async_add_devices, _discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Initialize DHW device from config entry.""" - atag = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([AtagWaterHeater(atag, ENTITY_TYPES[WATER_HEATER])]) + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([AtagWaterHeater(coordinator, ENTITY_TYPES[WATER_HEATER])]) class AtagWaterHeater(AtagEntity, WaterHeaterDevice): @@ -40,12 +40,12 @@ def temperature_unit(self): @property def current_temperature(self): """Return the current temperature.""" - return self.atag.dhw_temperature + return self.coordinator.atag.dhw_temperature @property def current_operation(self): """Return current operation.""" - if self.atag.dhw_status: + if self.coordinator.atag.dhw_status: return STATE_PERFORMANCE return STATE_OFF @@ -56,8 +56,8 @@ def operation_list(self): async def set_temperature(self, **kwargs): """Set new target temperature.""" - if await self.atag.dhw_set_temp(kwargs.get(ATTR_TEMPERATURE)): - self.async_schedule_update_ha_state(True) + if await self.coordinator.atag.dhw_set_temp(kwargs.get(ATTR_TEMPERATURE)): + await self.coordinator.async_refresh() def set_operation_mode(self, operation_mode): """Set operation mode.""" @@ -66,14 +66,14 @@ def set_operation_mode(self, operation_mode): @property def target_temperature(self): """Return the setpoint if water demand, otherwise return base temp (comfort level).""" - return self.atag.dhw_target_temperature + return self.coordinator.atag.dhw_target_temperature @property def max_temp(self): """Return the maximum temperature.""" - return self.atag.dhw_max_temp + return self.coordinator.atag.dhw_max_temp @property def min_temp(self): """Return the minimum temperature.""" - return self.atag.dhw_min_temp + return self.coordinator.atag.dhw_min_temp diff --git a/tests/components/atag/test_atag_flow.py b/tests/components/atag/test_atag_flow.py index 75e14f1ad78f94..23dce313ffeb1e 100644 --- a/tests/components/atag/test_atag_flow.py +++ b/tests/components/atag/test_atag_flow.py @@ -5,14 +5,13 @@ from homeassistant import data_entry_flow from homeassistant.components.atag import config_flow -from homeassistant.const import CONF_DEVICE, CONF_EMAIL, CONF_HOST, CONF_PORT +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, - CONF_EMAIL: "user@email.com", } FIXTURE_COMPLETE_ENTRY = FIXTURE_USER_INPUT.copy() FIXTURE_COMPLETE_ENTRY[CONF_DEVICE] = "device_identifier" @@ -41,7 +40,7 @@ async def test_one_config_allowed(hass): assert step_user_result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert step_user_result["reason"] == "already_configured" - conf = {CONF_HOST: "atag.local", CONF_EMAIL: "user@email.com", CONF_PORT: 10000} + conf = {CONF_HOST: "atag.local", CONF_PORT: 10000} import_config_result = await flow.async_step_import(conf) From 13227529b1d3819e77df39d8dbb0ef0a40bfad28 Mon Sep 17 00:00:00 2001 From: matsnl Date: Thu, 16 Apr 2020 22:04:09 +0000 Subject: [PATCH 12/24] fix translations --- homeassistant/components/atag/.translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/atag/.translations/en.json b/homeassistant/components/atag/.translations/en.json index 1a5d15a25a8530..094fde70dc95f6 100644 --- a/homeassistant/components/atag/.translations/en.json +++ b/homeassistant/components/atag/.translations/en.json @@ -1,6 +1,6 @@ { + "title": "Atag", "config": { - "title": "Atag", "step": { "user": { "title": "Connect to the device", From 14d20000bbe365ec94aca8b32d2799fabe1ade6c Mon Sep 17 00:00:00 2001 From: matsnl Date: Fri, 17 Apr 2020 10:46:40 +0000 Subject: [PATCH 13/24] enable preset_modes --- homeassistant/components/atag/__init__.py | 19 ++++------- homeassistant/components/atag/climate.py | 34 ++++++++++++++++--- homeassistant/components/atag/manifest.json | 2 +- homeassistant/components/atag/sensor.py | 5 --- homeassistant/components/atag/water_heater.py | 5 --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 38 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index b5d60086665032..82d2bed59ca5f5 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -68,13 +68,6 @@ ATTR_DEVICE_CLASS: None, ATTR_ICON: None, }, - { - ATTR_NAME: "Operation Mode", - ATTR_ID: "ch_mode", - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: ICONS[ATTR_MODE], - }, { ATTR_NAME: "CH Water Pressure", ATTR_ID: "ch_water_pres", @@ -226,6 +219,9 @@ def name(self) -> str: @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 @@ -241,7 +237,6 @@ def unit_of_measurement(self): @property def device_class(self): """Return the device class.""" - # if hasattr(self, "_class"): return self._class @property @@ -256,11 +251,9 @@ def unique_id(self): async def async_added_to_hass(self): """Connect to dispatcher listening for entity data notifications.""" - self.coordinator.async_add_listener(self.async_write_ha_state) - - async def async_will_remove_from_hass(self): - """Disconnect from update signal.""" - self.coordinator.async_remove_listener(self.async_write_ha_state) + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) async def async_update(self): """Update Atag entity.""" diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index d7c5e05d0e2b7c..723497534ca71b 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -10,6 +10,8 @@ HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_BOOST, + SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -17,10 +19,16 @@ from . import CLIMATE, DOMAIN, ENTITY_TYPES, AtagEntity - -async def async_setup_platform(hass, _config, async_add_devices, _discovery_info=None): - """Atag updated to use config entry.""" - pass +PRESET_SCHEDULE = "Auto" +PRESET_MANUAL = "Manual" +PRESET_EXTEND = "Extend" +SUPPORT_PRESET = [ + PRESET_MANUAL, + PRESET_SCHEDULE, + PRESET_EXTEND, + PRESET_BOOST, +] +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE async def async_setup_entry(hass, entry, async_add_devices): @@ -47,7 +55,7 @@ async def async_added_to_hass(self): @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_TARGET_TEMPERATURE + return SUPPORT_FLAGS @property def hvac_mode(self) -> Optional[str]: @@ -93,6 +101,16 @@ def min_temp(self): """Return the minimum temperature.""" return DEFAULT_MIN_TEMP + @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.""" if self._on and await self.coordinator.atag.set_temp( @@ -106,3 +124,9 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: if self._on: await self.coordinator.atag.set_hvac_mode(hvac_mode) await self.coordinator.async_refresh() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if self._on: + await self.coordinator.atag.set_hold_mode(preset_mode) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json index 74d526111dcc9e..4ae01a2d74dbba 100644 --- a/homeassistant/components/atag/manifest.json +++ b/homeassistant/components/atag/manifest.json @@ -3,7 +3,7 @@ "name": "Atag", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/atag/", - "requirements": ["pyatag==0.2.15"], + "requirements": ["pyatag==0.2.16"], "dependencies": [], "codeowners": ["@MatsNL"] } diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py index b4d44516e69ee4..743b50ef40d4dd 100644 --- a/homeassistant/components/atag/sensor.py +++ b/homeassistant/components/atag/sensor.py @@ -4,11 +4,6 @@ from . import DOMAIN, ENTITY_TYPES, SENSOR, AtagEntity -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Atag updated to use config entry.""" - pass - - 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] diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index ed82552e8b1c5d..478fd1c07ce6bc 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -13,11 +13,6 @@ OPERATION_LIST = [STATE_OFF, STATE_ECO, STATE_PERFORMANCE] -async def async_setup_platform(hass, config, async_add_devices, _discovery_info=None): - """Atag updated to use config entry.""" - pass - - 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] diff --git a/requirements_all.txt b/requirements_all.txt index 59b0e23daee5c5..f2bd9f7be995b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1178,7 +1178,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.atag -pyatag==0.2.15 +pyatag==0.2.16 # homeassistant.components.netatmo pyatmo==3.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85a883914e43a5..e044cac1989b57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -469,7 +469,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.atag -pyatag==0.2.15 +pyatag==0.2.16 # homeassistant.components.netatmo pyatmo==3.3.0 From f40c6be67755d7c2e5e710a2efd46c8eecc44242 Mon Sep 17 00:00:00 2001 From: matsnl Date: Fri, 17 Apr 2020 12:06:28 +0000 Subject: [PATCH 14/24] fix water_heater --- homeassistant/components/atag/water_heater.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index 478fd1c07ce6bc..c37bb7c0c526c8 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -54,10 +54,6 @@ async def set_temperature(self, **kwargs): if await self.coordinator.atag.dhw_set_temp(kwargs.get(ATTR_TEMPERATURE)): await self.coordinator.async_refresh() - def set_operation_mode(self, operation_mode): - """Set operation mode.""" - pass - @property def target_temperature(self): """Return the setpoint if water demand, otherwise return base temp (comfort level).""" From 9fcc108664954063ac01fc69d318c5e038f9b081 Mon Sep 17 00:00:00 2001 From: matsnl Date: Fri, 17 Apr 2020 16:03:23 +0000 Subject: [PATCH 15/24] update coveragerc --- .coveragerc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.coveragerc b/.coveragerc index 3e0e523f95f6e1..e7d620f86e40bd 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 From 761c392bd209e1bcc641e78b35d72ad46ccf7f21 Mon Sep 17 00:00:00 2001 From: MatsNl <37705266+MatsNl@users.noreply.github.com> Date: Mon, 20 Apr 2020 09:57:12 +0200 Subject: [PATCH 16/24] remove scan_interval Co-Authored-By: J. Nick Koston --- homeassistant/components/atag/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 82d2bed59ca5f5..cb90df1650cdbe 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -33,7 +33,6 @@ DATA_LISTENER = f"{DOMAIN}_listener" SIGNAL_UPDATE_ATAG = f"{DOMAIN}_update" PLATFORMS = [CLIMATE, WATER_HEATER, SENSOR] -SCAN_INTERVAL = timedelta(seconds=30) HOUR = "h" FIRE = "fire" PERCENTAGE = "%" From 181b843b59c092530d5cc36f2153679cf3271c7b Mon Sep 17 00:00:00 2001 From: MatsNl <37705266+MatsNl@users.noreply.github.com> Date: Mon, 20 Apr 2020 18:31:18 +0200 Subject: [PATCH 17/24] Apply suggestions from code review Co-Authored-By: Martin Hjelmare --- homeassistant/components/atag/climate.py | 14 +----- homeassistant/components/atag/config_flow.py | 7 --- homeassistant/components/atag/manifest.json | 1 - tests/components/atag/test_atag_flow.py | 50 ++++++++------------ 4 files changed, 22 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index 723497534ca71b..979dd62c360de7 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -31,10 +31,10 @@ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE -async def async_setup_entry(hass, entry, async_add_devices): +async def async_setup_entry(hass, entry, async_add_entities): """Load a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_devices([AtagThermostat(coordinator, ENTITY_TYPES[CLIMATE])]) + async_add_entities([AtagThermostat(coordinator, ENTITY_TYPES[CLIMATE])]) class AtagThermostat(AtagEntity, ClimateDevice, RestoreEntity): @@ -91,16 +91,6 @@ def target_temperature(self) -> Optional[float]: """Return the temperature we try to reach.""" return self.coordinator.atag.target_temperature - @property - def max_temp(self): - """Return the maximum temperature.""" - return DEFAULT_MAX_TEMP - - @property - def min_temp(self): - """Return the minimum temperature.""" - return DEFAULT_MIN_TEMP - @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., auto, manual, fireplace, extend, etc.""" diff --git a/homeassistant/components/atag/config_flow.py b/homeassistant/components/atag/config_flow.py index 01bcf1529808a4..b9dcfd2c0fa24b 100644 --- a/homeassistant/components/atag/config_flow.py +++ b/homeassistant/components/atag/config_flow.py @@ -49,10 +49,3 @@ async def _show_form(self, errors=None): data_schema=vol.Schema(DATA_SCHEMA), errors=errors if errors else {}, ) - - async def async_step_import(self, import_config): - """Import a config entry from configuration.yaml.""" - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - - return await self.async_step_user(import_config) diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json index 4ae01a2d74dbba..d9b0204176cebb 100644 --- a/homeassistant/components/atag/manifest.json +++ b/homeassistant/components/atag/manifest.json @@ -4,6 +4,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/atag/", "requirements": ["pyatag==0.2.16"], - "dependencies": [], "codeowners": ["@MatsNL"] } diff --git a/tests/components/atag/test_atag_flow.py b/tests/components/atag/test_atag_flow.py index 23dce313ffeb1e..af4e6f3c258c6c 100644 --- a/tests/components/atag/test_atag_flow.py +++ b/tests/components/atag/test_atag_flow.py @@ -1,7 +1,7 @@ """Tests for the Atag config flow.""" -from unittest.mock import PropertyMock, patch +from unittest.mock import PropertyMock -from asynctest import CoroutineMock +from asynctest import patch from homeassistant import data_entry_flow from homeassistant.components.atag import config_flow @@ -19,10 +19,9 @@ async def test_show_form(hass): """Test that the form is served with no input.""" - flow = config_flow.AtagConfigFlow() - flow.hass = hass - - result = await flow.async_step_user(user_input=None) + 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" @@ -30,30 +29,24 @@ async def test_show_form(hass): async def test_one_config_allowed(hass): """Test that only one Atag configuration is allowed.""" - flow = config_flow.AtagConfigFlow() - flow.hass = hass - MockConfigEntry(domain="atag", data=FIXTURE_USER_INPUT).add_to_hass(hass) - step_user_result = await flow.async_step_user() - - assert step_user_result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert step_user_result["reason"] == "already_configured" - - conf = {CONF_HOST: "atag.local", CONF_PORT: 10000} - - import_config_result = await flow.async_step_import(conf) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - assert import_config_result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert import_config_result["reason"] == "already_configured" + 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.""" - flow = config_flow.AtagConfigFlow() - flow.hass = hass - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + with patch( + "homeassistant.components.atag.config_flow.AtagDataStore.async_check_pair_status", + side_effect=AtagException() + ): + result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -64,18 +57,15 @@ 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", - new=CoroutineMock(), ), patch( "homeassistant.components.atag.AtagDataStore.device", new_callable=PropertyMock(return_value="device_identifier"), ): - flow = config_flow.AtagConfigFlow() - flow.hass = hass - result = await flow.async_step_import(import_config=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + 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 From f82474d605f9cc05535204f25bef1f2de2ef3128 Mon Sep 17 00:00:00 2001 From: matsnl Date: Mon, 20 Apr 2020 18:34:14 +0000 Subject: [PATCH 18/24] fixes review remarks --- homeassistant/components/atag/config_flow.py | 9 ++++----- tests/components/atag/test_atag_flow.py | 13 +++++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/atag/config_flow.py b/homeassistant/components/atag/config_flow.py index b9dcfd2c0fa24b..27b2b7a42f62d7 100644 --- a/homeassistant/components/atag/config_flow.py +++ b/homeassistant/components/atag/config_flow.py @@ -1,11 +1,11 @@ """Config flow for the Atag component.""" -from aiohttp import ClientSession 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 @@ -29,11 +29,10 @@ async def async_step_user(self, user_input=None): if not user_input: return await self._show_form() - + session = async_get_clientsession(self.hass) try: - async with ClientSession() as session: - atag = AtagDataStore(session, **user_input) - await atag.async_check_pair_status() + atag = AtagDataStore(session, **user_input) + await atag.async_check_pair_status() except AtagException: return await self._show_form({"base": "connection_error"}) diff --git a/tests/components/atag/test_atag_flow.py b/tests/components/atag/test_atag_flow.py index af4e6f3c258c6c..bda4ccc9023f59 100644 --- a/tests/components/atag/test_atag_flow.py +++ b/tests/components/atag/test_atag_flow.py @@ -2,9 +2,10 @@ from unittest.mock import PropertyMock from asynctest import patch +from pyatag import AtagException -from homeassistant import data_entry_flow -from homeassistant.components.atag import config_flow +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 @@ -44,9 +45,13 @@ async def test_connection_error(hass): with patch( "homeassistant.components.atag.config_flow.AtagDataStore.async_check_pair_status", - side_effect=AtagException() + side_effect=AtagException(), ): - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + 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" From d2a30116cc1ebf95c026f5096163cd221de03b5f Mon Sep 17 00:00:00 2001 From: matsnl Date: Mon, 20 Apr 2020 18:46:32 +0000 Subject: [PATCH 19/24] fix flake8 errors --- homeassistant/components/atag/climate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index 979dd62c360de7..6ed948caac5042 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -5,8 +5,6 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, - DEFAULT_MAX_TEMP, - DEFAULT_MIN_TEMP, HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, From a64c5133173d5e313cc06f9da61dca45d6ae1f66 Mon Sep 17 00:00:00 2001 From: matsnl Date: Tue, 21 Apr 2020 08:41:48 +0000 Subject: [PATCH 20/24] ensure correct HVACmode --- homeassistant/components/atag/climate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index 6ed948caac5042..9a8ddb488e49ea 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -27,6 +27,7 @@ PRESET_BOOST, ] SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE +HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] async def async_setup_entry(hass, entry, async_add_entities): @@ -60,12 +61,13 @@ def hvac_mode(self) -> Optional[str]: """Return hvac operation ie. heat, cool mode.""" if not self._on: return HVAC_MODE_OFF - return self.coordinator.atag.hvac_mode + if self.coordinator.atag.hvac_mode in HVAC_MODES: + return self.coordinator.atag.hvac_mode @property def hvac_modes(self) -> List[str]: """Return the list of available hvac operation modes.""" - return [HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF] + return HVAC_MODES @property def hvac_action(self) -> Optional[str]: From b6e936bcee3416f0cce3127ac09e67bb4817f7d6 Mon Sep 17 00:00:00 2001 From: matsnl Date: Tue, 21 Apr 2020 21:12:04 +0000 Subject: [PATCH 21/24] add away mode --- homeassistant/components/atag/climate.py | 2 ++ homeassistant/components/atag/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index 9a8ddb488e49ea..876f96fd2b99b4 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -8,6 +8,7 @@ HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_AWAY, PRESET_BOOST, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, @@ -24,6 +25,7 @@ PRESET_MANUAL, PRESET_SCHEDULE, PRESET_EXTEND, + PRESET_AWAY, PRESET_BOOST, ] SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json index d9b0204176cebb..4bd80e8f1e73de 100644 --- a/homeassistant/components/atag/manifest.json +++ b/homeassistant/components/atag/manifest.json @@ -3,6 +3,6 @@ "name": "Atag", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/atag/", - "requirements": ["pyatag==0.2.16"], + "requirements": ["pyatag==0.2.17"], "codeowners": ["@MatsNL"] } diff --git a/requirements_all.txt b/requirements_all.txt index f2bd9f7be995b6..a7012ba106d878 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1178,7 +1178,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.atag -pyatag==0.2.16 +pyatag==0.2.17 # homeassistant.components.netatmo pyatmo==3.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e044cac1989b57..4ff6b95e696d6c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -469,7 +469,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.atag -pyatag==0.2.16 +pyatag==0.2.17 # homeassistant.components.netatmo pyatmo==3.3.0 From 96031c541b15797e6c28cc1fdde34faee6aeeedb Mon Sep 17 00:00:00 2001 From: matsnl Date: Tue, 21 Apr 2020 21:42:46 +0000 Subject: [PATCH 22/24] use write_ha_state instead of refresh --- homeassistant/components/atag/climate.py | 6 +++--- homeassistant/components/atag/water_heater.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index 876f96fd2b99b4..b7f84fdefbaf84 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -108,17 +108,17 @@ async def async_set_temperature(self, **kwargs) -> None: if self._on and await self.coordinator.atag.set_temp( kwargs.get(ATTR_TEMPERATURE) ): - await self.coordinator.async_refresh() + self.async_write_ha_state() async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" self._on = hvac_mode != HVAC_MODE_OFF if self._on: await self.coordinator.atag.set_hvac_mode(hvac_mode) - await self.coordinator.async_refresh() + self.async_write_ha_state() async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if self._on: await self.coordinator.atag.set_hold_mode(preset_mode) - await self.coordinator.async_refresh() + self.async_write_ha_state() diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index c37bb7c0c526c8..bb1f72d6a8e096 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -52,7 +52,7 @@ def operation_list(self): async def set_temperature(self, **kwargs): """Set new target temperature.""" if await self.coordinator.atag.dhw_set_temp(kwargs.get(ATTR_TEMPERATURE)): - await self.coordinator.async_refresh() + self.async_write_ha_state() @property def target_temperature(self): From ad72450b78132b4f7af0162a36a43362da75c78e Mon Sep 17 00:00:00 2001 From: matsnl Date: Wed, 22 Apr 2020 08:04:41 +0000 Subject: [PATCH 23/24] remove OFF support, add Fahrenheit --- homeassistant/components/atag/climate.py | 40 ++++++--------------- homeassistant/components/atag/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index b7f84fdefbaf84..40bd8cd4cc762c 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -7,14 +7,12 @@ CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, HVAC_MODE_HEAT, - HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from . import CLIMATE, DOMAIN, ENTITY_TYPES, AtagEntity @@ -29,7 +27,7 @@ PRESET_BOOST, ] SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE -HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] +HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] async def async_setup_entry(hass, entry, async_add_entities): @@ -38,21 +36,9 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities([AtagThermostat(coordinator, ENTITY_TYPES[CLIMATE])]) -class AtagThermostat(AtagEntity, ClimateDevice, RestoreEntity): +class AtagThermostat(AtagEntity, ClimateDevice): """Atag climate device.""" - def __init__(self, coordinator, atagtype): - """Initialize with fake on/off state.""" - self._on = None - super().__init__(coordinator, atagtype) - - async def async_added_to_hass(self): - """Register callbacks & state restore for fake "Off" mode.""" - await super().async_added_to_hass() - last_state = await self.async_get_last_state() - if last_state: - self._on = last_state.state != HVAC_MODE_OFF - @property def supported_features(self): """Return the list of supported features.""" @@ -61,10 +47,9 @@ def supported_features(self): @property def hvac_mode(self) -> Optional[str]: """Return hvac operation ie. heat, cool mode.""" - if not self._on: - return HVAC_MODE_OFF if self.coordinator.atag.hvac_mode in HVAC_MODES: return self.coordinator.atag.hvac_mode + return None @property def hvac_modes(self) -> List[str]: @@ -81,7 +66,9 @@ def hvac_action(self) -> Optional[str]: @property def temperature_unit(self): """Return the unit of measurement.""" - return TEMP_CELSIUS + 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]: @@ -105,20 +92,15 @@ def preset_modes(self) -> Optional[List[str]]: async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" - if self._on and await self.coordinator.atag.set_temp( - kwargs.get(ATTR_TEMPERATURE) - ): - self.async_write_ha_state() + 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.""" - self._on = hvac_mode != HVAC_MODE_OFF - if self._on: - await self.coordinator.atag.set_hvac_mode(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.""" - if self._on: - await self.coordinator.atag.set_hold_mode(preset_mode) + await self.coordinator.atag.set_hold_mode(preset_mode) self.async_write_ha_state() diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json index 4bd80e8f1e73de..9534bad6df8f24 100644 --- a/homeassistant/components/atag/manifest.json +++ b/homeassistant/components/atag/manifest.json @@ -3,6 +3,6 @@ "name": "Atag", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/atag/", - "requirements": ["pyatag==0.2.17"], + "requirements": ["pyatag==0.2.18"], "codeowners": ["@MatsNL"] } diff --git a/requirements_all.txt b/requirements_all.txt index a7012ba106d878..00b50bbf73dddd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1178,7 +1178,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.atag -pyatag==0.2.17 +pyatag==0.2.18 # homeassistant.components.netatmo pyatmo==3.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ff6b95e696d6c..9c21bfe97d38d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -469,7 +469,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.atag -pyatag==0.2.17 +pyatag==0.2.18 # homeassistant.components.netatmo pyatmo==3.3.0 From bdc8bfe6fe6a8b3796157a2a070a90f92092f27c Mon Sep 17 00:00:00 2001 From: matsnl Date: Wed, 22 Apr 2020 10:56:16 +0000 Subject: [PATCH 24/24] rename test_config_flow.py --- tests/components/atag/{test_atag_flow.py => test_config_flow.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/components/atag/{test_atag_flow.py => test_config_flow.py} (100%) diff --git a/tests/components/atag/test_atag_flow.py b/tests/components/atag/test_config_flow.py similarity index 100% rename from tests/components/atag/test_atag_flow.py rename to tests/components/atag/test_config_flow.py