Improve Atag integration and bump version to 0.3.5.3 (#47778)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
MatsNl 2021-03-12 07:15:45 +01:00 committed by GitHub
parent f4b775b125
commit fa0c544bf5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 160 additions and 182 deletions

View file

@ -31,17 +31,34 @@ async def async_setup(hass: HomeAssistant, config):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Atag integration from a config entry.""" """Set up Atag integration from a config entry."""
session = async_get_clientsession(hass)
coordinator = AtagDataUpdateCoordinator(hass, session, entry) async def _async_update_data():
"""Update data via library."""
with async_timeout.timeout(20):
try:
await atag.update()
except AtagException as err:
raise UpdateFailed(err) from err
return atag
atag = AtagOne(
session=async_get_clientsession(hass), **entry.data, device=entry.unique_id
)
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=DOMAIN.title(),
update_method=_async_update_data,
update_interval=timedelta(seconds=60),
)
await coordinator.async_refresh() await coordinator.async_refresh()
if not coordinator.last_update_success: if not coordinator.last_update_success:
raise ConfigEntryNotReady raise ConfigEntryNotReady
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
hass.data[DOMAIN][entry.entry_id] = coordinator
if entry.unique_id is None: if entry.unique_id is None:
hass.config_entries.async_update_entry(entry, unique_id=coordinator.atag.id) hass.config_entries.async_update_entry(entry, unique_id=atag.id)
for platform in PLATFORMS: for platform in PLATFORMS:
hass.async_create_task( hass.async_create_task(
@ -51,28 +68,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
return True return True
class AtagDataUpdateCoordinator(DataUpdateCoordinator):
"""Define an object to hold Atag data."""
def __init__(self, hass, session, entry):
"""Initialize."""
self.atag = AtagOne(session=session, **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:
if not await self.atag.update():
raise UpdateFailed("No data received")
except AtagException as error:
raise UpdateFailed(error) from error
return self.atag.report
async def async_unload_entry(hass, entry): async def async_unload_entry(hass, entry):
"""Unload Atag config entry.""" """Unload Atag config entry."""
unload_ok = all( unload_ok = all(
@ -91,7 +86,7 @@ async def async_unload_entry(hass, entry):
class AtagEntity(CoordinatorEntity): class AtagEntity(CoordinatorEntity):
"""Defines a base Atag entity.""" """Defines a base Atag entity."""
def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_id: str) -> None: def __init__(self, coordinator: DataUpdateCoordinator, atag_id: str) -> None:
"""Initialize the Atag entity.""" """Initialize the Atag entity."""
super().__init__(coordinator) super().__init__(coordinator)
@ -101,8 +96,8 @@ class AtagEntity(CoordinatorEntity):
@property @property
def device_info(self) -> dict: def device_info(self) -> dict:
"""Return info for device registry.""" """Return info for device registry."""
device = self.coordinator.atag.id device = self.coordinator.data.id
version = self.coordinator.atag.apiversion version = self.coordinator.data.apiversion
return { return {
"identifiers": {(DOMAIN, device)}, "identifiers": {(DOMAIN, device)},
"name": "Atag Thermostat", "name": "Atag Thermostat",
@ -119,4 +114,4 @@ class AtagEntity(CoordinatorEntity):
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique ID to use for this entity.""" """Return a unique ID to use for this entity."""
return f"{self.coordinator.atag.id}-{self._id}" return f"{self.coordinator.data.id}-{self._id}"

View file

@ -16,16 +16,14 @@ from homeassistant.const import ATTR_TEMPERATURE
from . import CLIMATE, DOMAIN, AtagEntity from . import CLIMATE, DOMAIN, AtagEntity
PRESET_SCHEDULE = "Auto" PRESET_MAP = {
PRESET_MANUAL = "Manual" "Manual": "manual",
PRESET_EXTEND = "Extend" "Auto": "automatic",
SUPPORT_PRESET = [ "Extend": "extend",
PRESET_MANUAL, PRESET_AWAY: "vacation",
PRESET_SCHEDULE, PRESET_BOOST: "fireplace",
PRESET_EXTEND, }
PRESET_AWAY, PRESET_INVERTED = {v: k for k, v in PRESET_MAP.items()}
PRESET_BOOST,
]
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT]
@ -47,8 +45,8 @@ class AtagThermostat(AtagEntity, ClimateEntity):
@property @property
def hvac_mode(self) -> Optional[str]: def hvac_mode(self) -> Optional[str]:
"""Return hvac operation ie. heat, cool mode.""" """Return hvac operation ie. heat, cool mode."""
if self.coordinator.atag.climate.hvac_mode in HVAC_MODES: if self.coordinator.data.climate.hvac_mode in HVAC_MODES:
return self.coordinator.atag.climate.hvac_mode return self.coordinator.data.climate.hvac_mode
return None return None
@property @property
@ -59,46 +57,46 @@ class AtagThermostat(AtagEntity, ClimateEntity):
@property @property
def hvac_action(self) -> Optional[str]: def hvac_action(self) -> Optional[str]:
"""Return the current running hvac operation.""" """Return the current running hvac operation."""
if self.coordinator.atag.climate.status: is_active = self.coordinator.data.climate.status
return CURRENT_HVAC_HEAT return CURRENT_HVAC_HEAT if is_active else CURRENT_HVAC_IDLE
return CURRENT_HVAC_IDLE
@property @property
def temperature_unit(self): def temperature_unit(self) -> Optional[str]:
"""Return the unit of measurement.""" """Return the unit of measurement."""
return self.coordinator.atag.climate.temp_unit return self.coordinator.data.climate.temp_unit
@property @property
def current_temperature(self) -> Optional[float]: def current_temperature(self) -> Optional[float]:
"""Return the current temperature.""" """Return the current temperature."""
return self.coordinator.atag.climate.temperature return self.coordinator.data.climate.temperature
@property @property
def target_temperature(self) -> Optional[float]: def target_temperature(self) -> Optional[float]:
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self.coordinator.atag.climate.target_temperature return self.coordinator.data.climate.target_temperature
@property @property
def preset_mode(self) -> Optional[str]: def preset_mode(self) -> Optional[str]:
"""Return the current preset mode, e.g., auto, manual, fireplace, extend, etc.""" """Return the current preset mode, e.g., auto, manual, fireplace, extend, etc."""
return self.coordinator.atag.climate.preset_mode preset = self.coordinator.data.climate.preset_mode
return PRESET_INVERTED.get(preset)
@property @property
def preset_modes(self) -> Optional[List[str]]: def preset_modes(self) -> Optional[List[str]]:
"""Return a list of available preset modes.""" """Return a list of available preset modes."""
return SUPPORT_PRESET return list(PRESET_MAP.keys())
async def async_set_temperature(self, **kwargs) -> None: async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature.""" """Set new target temperature."""
await self.coordinator.atag.climate.set_temp(kwargs.get(ATTR_TEMPERATURE)) await self.coordinator.data.climate.set_temp(kwargs.get(ATTR_TEMPERATURE))
self.async_write_ha_state() self.async_write_ha_state()
async def async_set_hvac_mode(self, hvac_mode: str) -> None: async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target hvac mode.""" """Set new target hvac mode."""
await self.coordinator.atag.climate.set_hvac_mode(hvac_mode) await self.coordinator.data.climate.set_hvac_mode(hvac_mode)
self.async_write_ha_state() self.async_write_ha_state()
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
await self.coordinator.atag.climate.set_preset_mode(preset_mode) await self.coordinator.data.climate.set_preset_mode(PRESET_MAP[preset_mode])
self.async_write_ha_state() self.async_write_ha_state()

View file

@ -3,14 +3,13 @@ import pyatag
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_HOST, CONF_PORT from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import DOMAIN # pylint: disable=unused-import from . import DOMAIN # pylint: disable=unused-import
DATA_SCHEMA = { DATA_SCHEMA = {
vol.Required(CONF_HOST): str, vol.Required(CONF_HOST): str,
vol.Optional(CONF_EMAIL): str,
vol.Required(CONF_PORT, default=pyatag.const.DEFAULT_PORT): vol.Coerce(int), vol.Required(CONF_PORT, default=pyatag.const.DEFAULT_PORT): vol.Coerce(int),
} }
@ -26,15 +25,14 @@ class AtagConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if not user_input: if not user_input:
return await self._show_form() return await self._show_form()
session = async_get_clientsession(self.hass)
try:
atag = pyatag.AtagOne(session=session, **user_input)
await atag.authorize()
await atag.update(force=True)
except pyatag.errors.Unauthorized: atag = pyatag.AtagOne(session=async_get_clientsession(self.hass), **user_input)
try:
await atag.update()
except pyatag.Unauthorized:
return await self._show_form({"base": "unauthorized"}) return await self._show_form({"base": "unauthorized"})
except pyatag.errors.AtagException: except pyatag.AtagException:
return await self._show_form({"base": "cannot_connect"}) return await self._show_form({"base": "cannot_connect"})
await self.async_set_unique_id(atag.id) await self.async_set_unique_id(atag.id)

View file

@ -3,6 +3,6 @@
"name": "Atag", "name": "Atag",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/atag/", "documentation": "https://www.home-assistant.io/integrations/atag/",
"requirements": ["pyatag==0.3.4.4"], "requirements": ["pyatag==0.3.5.3"],
"codeowners": ["@MatsNL"] "codeowners": ["@MatsNL"]
} }

View file

@ -26,10 +26,7 @@ SENSORS = {
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Initialize sensor platform from config entry.""" """Initialize sensor platform from config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = hass.data[DOMAIN][config_entry.entry_id]
entities = [] async_add_entities([AtagSensor(coordinator, sensor) for sensor in SENSORS])
for sensor in SENSORS:
entities.append(AtagSensor(coordinator, sensor))
async_add_entities(entities)
class AtagSensor(AtagEntity): class AtagSensor(AtagEntity):
@ -43,32 +40,32 @@ class AtagSensor(AtagEntity):
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.coordinator.data[self._id].state return self.coordinator.data.report[self._id].state
@property @property
def icon(self): def icon(self):
"""Return icon.""" """Return icon."""
return self.coordinator.data[self._id].icon return self.coordinator.data.report[self._id].icon
@property @property
def device_class(self): def device_class(self):
"""Return deviceclass.""" """Return deviceclass."""
if self.coordinator.data[self._id].sensorclass in [ if self.coordinator.data.report[self._id].sensorclass in [
DEVICE_CLASS_PRESSURE, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
]: ]:
return self.coordinator.data[self._id].sensorclass return self.coordinator.data.report[self._id].sensorclass
return None return None
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return measure.""" """Return measure."""
if self.coordinator.data[self._id].measure in [ if self.coordinator.data.report[self._id].measure in [
PRESSURE_BAR, PRESSURE_BAR,
TEMP_CELSIUS, TEMP_CELSIUS,
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
PERCENTAGE, PERCENTAGE,
TIME_HOURS, TIME_HOURS,
]: ]:
return self.coordinator.data[self._id].measure return self.coordinator.data.report[self._id].measure
return None return None

View file

@ -5,7 +5,6 @@
"title": "Connect to the device", "title": "Connect to the device",
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"email": "[%key:common::config_flow::data::email%]",
"port": "[%key:common::config_flow::data::port%]" "port": "[%key:common::config_flow::data::port%]"
} }
} }

View file

@ -35,12 +35,12 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
return self.coordinator.atag.dhw.temperature return self.coordinator.data.dhw.temperature
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation.""" """Return current operation."""
operation = self.coordinator.atag.dhw.current_operation operation = self.coordinator.data.dhw.current_operation
return operation if operation in self.operation_list else STATE_OFF return operation if operation in self.operation_list else STATE_OFF
@property @property
@ -50,20 +50,20 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
if await self.coordinator.atag.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)): if await self.coordinator.data.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)):
self.async_write_ha_state() self.async_write_ha_state()
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the setpoint if water demand, otherwise return base temp (comfort level).""" """Return the setpoint if water demand, otherwise return base temp (comfort level)."""
return self.coordinator.atag.dhw.target_temperature return self.coordinator.data.dhw.target_temperature
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return self.coordinator.atag.dhw.max_temp return self.coordinator.data.dhw.max_temp
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return self.coordinator.atag.dhw.min_temp return self.coordinator.data.dhw.min_temp

View file

@ -1269,7 +1269,7 @@ pyalmond==0.0.2
pyarlo==0.2.4 pyarlo==0.2.4
# homeassistant.components.atag # homeassistant.components.atag
pyatag==0.3.4.4 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==4.2.2 pyatmo==4.2.2

View file

@ -667,7 +667,7 @@ pyalmond==0.0.2
pyarlo==0.2.4 pyarlo==0.2.4
# homeassistant.components.atag # homeassistant.components.atag
pyatag==0.3.4.4 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==4.2.2 pyatmo==4.2.2

View file

@ -1,7 +1,7 @@
"""Tests for the Atag integration.""" """Tests for the Atag integration."""
from homeassistant.components.atag import DOMAIN from homeassistant.components.atag import DOMAIN, AtagException
from homeassistant.const import CONF_EMAIL, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -9,12 +9,15 @@ from tests.test_util.aiohttp import AiohttpClientMocker
USER_INPUT = { USER_INPUT = {
CONF_HOST: "127.0.0.1", CONF_HOST: "127.0.0.1",
CONF_EMAIL: "atag@domain.com",
CONF_PORT: 10000, CONF_PORT: 10000,
} }
UID = "xxxx-xxxx-xxxx_xx-xx-xxx-xxx" UID = "xxxx-xxxx-xxxx_xx-xx-xxx-xxx"
PAIR_REPLY = {"pair_reply": {"status": {"device_id": UID}, "acc_status": 2}} AUTHORIZED = 2
UPDATE_REPLY = {"update_reply": {"status": {"device_id": UID}, "acc_status": 2}} UNAUTHORIZED = 3
PAIR_REPLY = {"pair_reply": {"status": {"device_id": UID}, "acc_status": AUTHORIZED}}
UPDATE_REPLY = {
"update_reply": {"status": {"device_id": UID}, "acc_status": AUTHORIZED}
}
RECEIVE_REPLY = { RECEIVE_REPLY = {
"retrieve_reply": { "retrieve_reply": {
"status": {"device_id": UID}, "status": {"device_id": UID},
@ -46,35 +49,52 @@ RECEIVE_REPLY = {
"dhw_max_set": 65, "dhw_max_set": 65,
"dhw_min_set": 40, "dhw_min_set": 40,
}, },
"acc_status": 2, "acc_status": AUTHORIZED,
} }
} }
def mock_connection(
aioclient_mock: AiohttpClientMocker, authorized=True, conn_error=False
) -> None:
"""Mock the requests to Atag endpoint."""
if conn_error:
aioclient_mock.post(
"http://127.0.0.1:10000/pair",
exc=AtagException,
)
aioclient_mock.post(
"http://127.0.0.1:10000/retrieve",
exc=AtagException,
)
return
PAIR_REPLY["pair_reply"].update(
{"acc_status": AUTHORIZED if authorized else UNAUTHORIZED}
)
RECEIVE_REPLY["retrieve_reply"].update(
{"acc_status": AUTHORIZED if authorized else UNAUTHORIZED}
)
aioclient_mock.post(
"http://127.0.0.1:10000/retrieve",
json=RECEIVE_REPLY,
)
aioclient_mock.post(
"http://127.0.0.1:10000/update",
json=UPDATE_REPLY,
)
aioclient_mock.post(
"http://127.0.0.1:10000/pair",
json=PAIR_REPLY,
)
async def init_integration( async def init_integration(
hass: HomeAssistant, hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker, aioclient_mock: AiohttpClientMocker,
rgbw: bool = False,
skip_setup: bool = False, skip_setup: bool = False,
) -> MockConfigEntry: ) -> MockConfigEntry:
"""Set up the Atag integration in Home Assistant.""" """Set up the Atag integration in Home Assistant."""
mock_connection(aioclient_mock)
aioclient_mock.post(
"http://127.0.0.1:10000/retrieve",
json=RECEIVE_REPLY,
headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.post(
"http://127.0.0.1:10000/update",
json=UPDATE_REPLY,
headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.post(
"http://127.0.0.1:10000/pair",
json=PAIR_REPLY,
headers={"Content-Type": CONTENT_TYPE_JSON},
)
entry = MockConfigEntry(domain=DOMAIN, data=USER_INPUT) entry = MockConfigEntry(domain=DOMAIN, data=USER_INPUT)
entry.add_to_hass(hass) entry.add_to_hass(hass)

View file

@ -1,7 +1,7 @@
"""Tests for the Atag climate platform.""" """Tests for the Atag climate platform."""
from unittest.mock import PropertyMock, patch from unittest.mock import PropertyMock, patch
from homeassistant.components.atag import CLIMATE, DOMAIN from homeassistant.components.atag.climate import CLIMATE, DOMAIN, PRESET_MAP
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_HVAC_ACTION, ATTR_HVAC_ACTION,
ATTR_HVAC_MODE, ATTR_HVAC_MODE,
@ -11,11 +11,8 @@ from homeassistant.components.climate import (
SERVICE_SET_PRESET_MODE, SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
) )
from homeassistant.components.climate.const import CURRENT_HVAC_HEAT, PRESET_AWAY from homeassistant.components.climate.const import CURRENT_HVAC_IDLE, PRESET_AWAY
from homeassistant.components.homeassistant import ( from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN
DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -31,17 +28,13 @@ async def test_climate(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None: ) -> None:
"""Test the creation and values of Atag climate device.""" """Test the creation and values of Atag climate device."""
with patch("pyatag.entities.Climate.status"): await init_integration(hass, aioclient_mock)
entry = await init_integration(hass, aioclient_mock) entity_registry = er.async_get(hass)
registry = er.async_get(hass)
assert registry.async_is_registered(CLIMATE_ID) assert entity_registry.async_is_registered(CLIMATE_ID)
entry = registry.async_get(CLIMATE_ID) entity = entity_registry.async_get(CLIMATE_ID)
assert entry.unique_id == f"{UID}-{CLIMATE}" assert entity.unique_id == f"{UID}-{CLIMATE}"
assert ( assert hass.states.get(CLIMATE_ID).attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
hass.states.get(CLIMATE_ID).attributes[ATTR_HVAC_ACTION]
== CURRENT_HVAC_HEAT
)
async def test_setting_climate( async def test_setting_climate(
@ -67,7 +60,7 @@ async def test_setting_climate(
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
mock_set_preset.assert_called_once_with(PRESET_AWAY) mock_set_preset.assert_called_once_with(PRESET_MAP[PRESET_AWAY])
with patch("pyatag.entities.Climate.set_hvac_mode") as mock_set_hvac: with patch("pyatag.entities.Climate.set_hvac_mode") as mock_set_hvac:
await hass.services.async_call( await hass.services.async_call(
@ -93,18 +86,18 @@ async def test_incorrect_modes(
assert hass.states.get(CLIMATE_ID).state == STATE_UNKNOWN assert hass.states.get(CLIMATE_ID).state == STATE_UNKNOWN
async def test_update_service( async def test_update_failed(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
) -> None: ) -> None:
"""Test the updater service is called.""" """Test data is not destroyed on update failure."""
await init_integration(hass, aioclient_mock) entry = await init_integration(hass, aioclient_mock)
await async_setup_component(hass, HA_DOMAIN, {}) await async_setup_component(hass, HA_DOMAIN, {})
with patch("pyatag.AtagOne.update") as updater: assert hass.states.get(CLIMATE_ID).state == HVAC_MODE_HEAT
await hass.services.async_call( coordinator = hass.data[DOMAIN][entry.entry_id]
HA_DOMAIN, with patch("pyatag.AtagOne.update", side_effect=TimeoutError) as updater:
SERVICE_UPDATE_ENTITY, await coordinator.async_refresh()
{ATTR_ENTITY_ID: CLIMATE_ID},
blocking=True,
)
await hass.async_block_till_done() await hass.async_block_till_done()
updater.assert_called_once() updater.assert_called_once()
assert not coordinator.last_update_success
assert coordinator.data.id == UID

View file

@ -1,24 +1,18 @@
"""Tests for the Atag config flow.""" """Tests for the Atag config flow."""
from unittest.mock import PropertyMock, patch from unittest.mock import PropertyMock, patch
from pyatag import errors
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.components.atag import DOMAIN from homeassistant.components.atag import DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.components.atag import ( from . import UID, USER_INPUT, init_integration, mock_connection
PAIR_REPLY,
RECEIVE_REPLY,
UID,
USER_INPUT,
init_integration,
)
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
async def test_show_form(hass): async def test_show_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker):
"""Test that the form is served with no input.""" """Test that the form is served with no input."""
mock_connection(aioclient_mock)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
@ -48,28 +42,30 @@ async def test_adding_second_device(
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
async def test_connection_error(hass): async def test_connection_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
):
"""Test we show user form on Atag connection error.""" """Test we show user form on Atag connection error."""
with patch("pyatag.AtagOne.authorize", side_effect=errors.AtagException()): mock_connection(aioclient_mock, conn_error=True)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data=USER_INPUT, data=USER_INPUT,
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"} assert result["errors"] == {"base": "cannot_connect"}
async def test_unauthorized(hass): async def test_unauthorized(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker):
"""Test we show correct form when Unauthorized error is raised.""" """Test we show correct form when Unauthorized error is raised."""
with patch("pyatag.AtagOne.authorize", side_effect=errors.Unauthorized()): mock_connection(aioclient_mock, authorized=False)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data=USER_INPUT, data=USER_INPUT,
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
assert result["errors"] == {"base": "unauthorized"} assert result["errors"] == {"base": "unauthorized"}
@ -79,14 +75,7 @@ async def test_full_flow_implementation(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None: ) -> None:
"""Test registering an integration and finishing flow works.""" """Test registering an integration and finishing flow works."""
aioclient_mock.post( mock_connection(aioclient_mock)
"http://127.0.0.1:10000/pair",
json=PAIR_REPLY,
)
aioclient_mock.post(
"http://127.0.0.1:10000/retrieve",
json=RECEIVE_REPLY,
)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},

View file

@ -1,13 +1,11 @@
"""Tests for the ATAG integration.""" """Tests for the ATAG integration."""
from unittest.mock import patch
import aiohttp
from homeassistant.components.atag import DOMAIN from homeassistant.components.atag import DOMAIN
from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.components.atag import init_integration from . import init_integration, mock_connection
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
@ -15,20 +13,11 @@ async def test_config_entry_not_ready(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None: ) -> None:
"""Test configuration entry not ready on library error.""" """Test configuration entry not ready on library error."""
aioclient_mock.post("http://127.0.0.1:10000/retrieve", exc=aiohttp.ClientError) mock_connection(aioclient_mock, conn_error=True)
entry = await init_integration(hass, aioclient_mock) entry = await init_integration(hass, aioclient_mock)
assert entry.state == ENTRY_STATE_SETUP_RETRY assert entry.state == ENTRY_STATE_SETUP_RETRY
async def test_config_entry_empty_reply(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test configuration entry not ready when library returns False."""
with patch("pyatag.AtagOne.update", return_value=False):
entry = await init_integration(hass, aioclient_mock)
assert entry.state == ENTRY_STATE_SETUP_RETRY
async def test_unload_config_entry( async def test_unload_config_entry(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None: ) -> None: