diff --git a/CODEOWNERS b/CODEOWNERS index 0a5450662a3..33bc045e971 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,6 +17,7 @@ homeassistant/components/abode/* @shred86 homeassistant/components/accuweather/* @bieniu homeassistant/components/acmeda/* @atmurray homeassistant/components/adguard/* @frenck +homeassistant/components/advantage_air/* @Bre77 homeassistant/components/agent_dvr/* @ispysoftware homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py new file mode 100644 index 00000000000..ae87a674533 --- /dev/null +++ b/homeassistant/components/advantage_air/__init__.py @@ -0,0 +1,74 @@ +"""Advantage Air climate integration.""" + +from datetime import timedelta +import logging + +from advantage_air import ApiError, advantage_air + +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ADVANTAGE_AIR_RETRY, DOMAIN + +ADVANTAGE_AIR_SYNC_INTERVAL = 15 +ADVANTAGE_AIR_PLATFORMS = ["climate"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up AdvantageAir.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass, config_entry): + """Set up AdvantageAir Config.""" + ip_address = config_entry.data[CONF_IP_ADDRESS] + port = config_entry.data[CONF_PORT] + api = advantage_air( + ip_address, + port=port, + session=async_get_clientsession(hass), + retry=ADVANTAGE_AIR_RETRY, + ) + + async def async_get(): + try: + return await api.async_get() + except ApiError as err: + raise UpdateFailed(err) from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="Advantage Air", + update_method=async_get, + update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL), + ) + + async def async_change(change): + try: + if await api.async_change(change): + await coordinator.async_refresh() + except ApiError as err: + _LOGGER.warning(err) + + await coordinator.async_refresh() + + if not coordinator.data: + raise ConfigEntryNotReady + + hass.data[DOMAIN][config_entry.entry_id] = { + "coordinator": coordinator, + "async_change": async_change, + } + + for platform in ADVANTAGE_AIR_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) + + return True diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py new file mode 100644 index 00000000000..dfb30b2b782 --- /dev/null +++ b/homeassistant/components/advantage_air/climate.py @@ -0,0 +1,297 @@ +"""Climate platform for Advantage Air integration.""" +import logging + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + ADVANTAGE_AIR_STATE_CLOSE, + ADVANTAGE_AIR_STATE_OFF, + ADVANTAGE_AIR_STATE_ON, + ADVANTAGE_AIR_STATE_OPEN, + DOMAIN, +) + +ADVANTAGE_AIR_HVAC_MODES = { + "heat": HVAC_MODE_HEAT, + "cool": HVAC_MODE_COOL, + "vent": HVAC_MODE_FAN_ONLY, + "dry": HVAC_MODE_DRY, +} +HASS_HVAC_MODES = {v: k for k, v in ADVANTAGE_AIR_HVAC_MODES.items()} + +ADVANTAGE_AIR_FAN_MODES = { + "auto": FAN_AUTO, + "low": FAN_LOW, + "medium": FAN_MEDIUM, + "high": FAN_HIGH, +} +HASS_FAN_MODES = {v: k for k, v in ADVANTAGE_AIR_FAN_MODES.items()} +FAN_SPEEDS = {FAN_LOW: 30, FAN_MEDIUM: 60, FAN_HIGH: 100} + +AC_HVAC_MODES = [ + HVAC_MODE_OFF, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_DRY, +] +ZONE_HVAC_MODES = [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AdvantageAir climate platform.""" + + instance = hass.data[DOMAIN][config_entry.entry_id] + + entities = [] + for ac_key in instance["coordinator"].data["aircons"]: + entities.append(AdvantageAirAC(instance, ac_key)) + for zone_key in instance["coordinator"].data["aircons"][ac_key]["zones"]: + # Only add zone climate control when zone is in temperature control + if ( + instance["coordinator"].data["aircons"][ac_key]["zones"][zone_key][ + "type" + ] + != 0 + ): + entities.append(AdvantageAirZone(instance, ac_key, zone_key)) + async_add_entities(entities) + + +class AdvantageAirClimateEntity(CoordinatorEntity, ClimateEntity): + """AdvantageAir Climate class.""" + + def __init__(self, instance): + """Initialize the base Advantage Air climate entity.""" + super().__init__(instance["coordinator"]) + self.async_change = instance["async_change"] + + @property + def temperature_unit(self): + """Return the temperature unit.""" + return TEMP_CELSIUS + + @property + def target_temperature_step(self): + """Return the supported temperature step.""" + return PRECISION_WHOLE + + @property + def max_temp(self): + """Return the maximum supported temperature.""" + return 32 + + @property + def min_temp(self): + """Return the minimum supported temperature.""" + return 16 + + @property + def device_info(self): + """Return parent device information.""" + return { + "identifiers": {(DOMAIN, self.coordinator.data["system"]["rid"])}, + "name": self.coordinator.data["system"]["name"], + "manufacturer": "Advantage Air", + "model": self.coordinator.data["system"]["sysType"], + "sw_version": self.coordinator.data["system"]["myAppRev"], + } + + +class AdvantageAirAC(AdvantageAirClimateEntity): + """AdvantageAir AC unit.""" + + def __init__(self, instance, ac_key): + """Initialize the Advantage Air AC climate entity.""" + super().__init__(instance) + self.ac_key = ac_key + + @property + def name(self): + """Return the name.""" + return self.coordinator.data["aircons"][self.ac_key]["info"]["name"] + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}' + + @property + def target_temperature(self): + """Return the current target temperature.""" + return self.coordinator.data["aircons"][self.ac_key]["info"]["setTemp"] + + @property + def hvac_mode(self): + """Return the current HVAC modes.""" + if ( + self.coordinator.data["aircons"][self.ac_key]["info"]["state"] + == ADVANTAGE_AIR_STATE_ON + ): + return ADVANTAGE_AIR_HVAC_MODES.get( + self.coordinator.data["aircons"][self.ac_key]["info"]["mode"] + ) + return HVAC_MODE_OFF + + @property + def hvac_modes(self): + """Return the supported HVAC modes.""" + return AC_HVAC_MODES + + @property + def fan_mode(self): + """Return the current fan modes.""" + return ADVANTAGE_AIR_FAN_MODES.get( + self.coordinator.data["aircons"][self.ac_key]["info"]["fan"], FAN_OFF + ) + + @property + def fan_modes(self): + """Return the supported fan modes.""" + return [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH] + + @property + def supported_features(self): + """Return the supported features.""" + return SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE + + @property + def device_state_attributes(self): + """Return additional attributes about AC unit.""" + return self.coordinator.data["aircons"][self.ac_key]["info"] + + async def async_set_hvac_mode(self, hvac_mode): + """Set the HVAC Mode and State.""" + if hvac_mode == HVAC_MODE_OFF: + await self.async_change( + {self.ac_key: {"info": {"state": ADVANTAGE_AIR_STATE_OFF}}} + ) + else: + await self.async_change( + { + self.ac_key: { + "info": { + "state": ADVANTAGE_AIR_STATE_ON, + "mode": HASS_HVAC_MODES.get(hvac_mode), + } + } + } + ) + + async def async_set_fan_mode(self, fan_mode): + """Set the Fan Mode.""" + await self.async_change( + {self.ac_key: {"info": {"fan": HASS_FAN_MODES.get(fan_mode)}}} + ) + + async def async_set_temperature(self, **kwargs): + """Set the Temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + await self.async_change({self.ac_key: {"info": {"setTemp": temp}}}) + + +class AdvantageAirZone(AdvantageAirClimateEntity): + """AdvantageAir Zone control.""" + + def __init__(self, instance, ac_key, zone_key): + """Initialize the Advantage Air Zone climate entity.""" + super().__init__(instance) + self.ac_key = ac_key + self.zone_key = zone_key + + @property + def name(self): + """Return the name.""" + return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key][ + "name" + ] + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}' + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key][ + "measuredTemp" + ] + + @property + def target_temperature(self): + """Return the target temperature.""" + return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key][ + "setTemp" + ] + + @property + def hvac_mode(self): + """Return the current HVAC modes.""" + if ( + self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key][ + "state" + ] + == ADVANTAGE_AIR_STATE_OPEN + ): + return HVAC_MODE_FAN_ONLY + return HVAC_MODE_OFF + + @property + def hvac_modes(self): + """Return supported HVAC modes.""" + return ZONE_HVAC_MODES + + @property + def device_state_attributes(self): + """Return additional attributes about Zone.""" + return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key] + + @property + def supported_features(self): + """Return the supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + async def async_set_hvac_mode(self, hvac_mode): + """Set the HVAC Mode and State.""" + if hvac_mode == HVAC_MODE_OFF: + await self.async_change( + { + self.ac_key: { + "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_CLOSE}} + } + } + ) + else: + await self.async_change( + { + self.ac_key: { + "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_OPEN}} + } + } + ) + + async def async_set_temperature(self, **kwargs): + """Set the Temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + await self.async_change( + {self.ac_key: {"zones": {self.zone_key: {"setTemp": temp}}}} + ) diff --git a/homeassistant/components/advantage_air/config_flow.py b/homeassistant/components/advantage_air/config_flow.py new file mode 100644 index 00000000000..7c2f5e11f53 --- /dev/null +++ b/homeassistant/components/advantage_air/config_flow.py @@ -0,0 +1,61 @@ +"""Config Flow for Advantage Air integration.""" +import logging + +from advantage_air import ApiError, advantage_air +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ADVANTAGE_AIR_RETRY, DOMAIN + +ADVANTAGE_AIR_DEFAULT_PORT = 2025 + +ADVANTAGE_AIR_SCHEMA = vol.Schema( + { + vol.Required(CONF_IP_ADDRESS): str, + vol.Optional(CONF_PORT, default=ADVANTAGE_AIR_DEFAULT_PORT): int, + } +) + +_LOGGER = logging.getLogger(__name__) + + +class AdvantageAirConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config Advantage Air API connection.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + DOMAIN = DOMAIN + + async def async_step_user(self, user_input=None): + """Get configuration from the user.""" + errors = {} + if user_input: + ip_address = user_input[CONF_IP_ADDRESS] + port = user_input[CONF_PORT] + + try: + data = await advantage_air( + ip_address, + port=port, + session=async_get_clientsession(self.hass), + retry=ADVANTAGE_AIR_RETRY, + ).async_get(1) + except ApiError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(data["system"]["rid"]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=data["system"]["name"], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=ADVANTAGE_AIR_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/advantage_air/const.py b/homeassistant/components/advantage_air/const.py new file mode 100644 index 00000000000..078c266bfb0 --- /dev/null +++ b/homeassistant/components/advantage_air/const.py @@ -0,0 +1,7 @@ +"""Constants used by Advantage Air integration.""" +DOMAIN = "advantage_air" +ADVANTAGE_AIR_RETRY = 5 +ADVANTAGE_AIR_STATE_OPEN = "open" +ADVANTAGE_AIR_STATE_CLOSE = "close" +ADVANTAGE_AIR_STATE_ON = "on" +ADVANTAGE_AIR_STATE_OFF = "off" diff --git a/homeassistant/components/advantage_air/manifest.json b/homeassistant/components/advantage_air/manifest.json new file mode 100644 index 00000000000..eab487cd7da --- /dev/null +++ b/homeassistant/components/advantage_air/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "advantage_air", + "name": "Advantage Air", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/advantage_air", + "codeowners": [ + "@Bre77" + ], + "requirements": [ + "advantage_air==0.2.1" + ] +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/strings.json b/homeassistant/components/advantage_air/strings.json new file mode 100644 index 00000000000..c8b9cd1dc5f --- /dev/null +++ b/homeassistant/components/advantage_air/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "Advantage Air Setup", + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "step": { + "user": { + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "description": "Connect to the API of your Advantage Air wall mounted tablet.", + "title": "Connect" + } + } + }, + "title": "Advantage Air" +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/en.json b/homeassistant/components/advantage_air/translations/en.json new file mode 100644 index 00000000000..46de1393435 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "flow_title": "Advantage Air Setup", + "step": { + "user": { + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "description": "Connect to the API of your Advantage Air wall mounted tablet.", + "title": "Connect" + } + } + }, + "title": "Advantage Air" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6266660546f..de84c037488 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -10,6 +10,7 @@ FLOWS = [ "accuweather", "acmeda", "adguard", + "advantage_air", "agent_dvr", "airly", "airvisual", diff --git a/requirements_all.txt b/requirements_all.txt index 45e7aacfd38..3eb104592ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -122,6 +122,9 @@ adext==0.3 # homeassistant.components.adguard adguardhome==0.4.2 +# homeassistant.components.advantage_air +advantage_air==0.2.1 + # homeassistant.components.frontier_silicon afsapi==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6194b229394..41c8af1e873 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -56,6 +56,9 @@ adext==0.3 # homeassistant.components.adguard adguardhome==0.4.2 +# homeassistant.components.advantage_air +advantage_air==0.2.1 + # homeassistant.components.agent_dvr agent-py==0.0.23 diff --git a/tests/components/advantage_air/__init__.py b/tests/components/advantage_air/__init__.py new file mode 100644 index 00000000000..92faaff6359 --- /dev/null +++ b/tests/components/advantage_air/__init__.py @@ -0,0 +1,33 @@ +"""Tests for the Advantage Air component.""" + +from homeassistant.components.advantage_air.const import DOMAIN +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT + +from tests.common import MockConfigEntry, load_fixture + +TEST_SYSTEM_DATA = load_fixture("advantage_air/getSystemData.json") +TEST_SET_RESPONSE = load_fixture("advantage_air/setAircon.json") + +USER_INPUT = { + CONF_IP_ADDRESS: "1.2.3.4", + CONF_PORT: 2025, +} + +TEST_SYSTEM_URL = ( + f"http://{USER_INPUT[CONF_IP_ADDRESS]}:{USER_INPUT[CONF_PORT]}/getSystemData" +) +TEST_SET_URL = f"http://{USER_INPUT[CONF_IP_ADDRESS]}:{USER_INPUT[CONF_PORT]}/setAircon" + + +async def add_mock_config(hass): + """Create a fake Advantage Air Config Entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="test entry", + unique_id="0123456", + data=USER_INPUT, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return entry diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py new file mode 100644 index 00000000000..517750aa98b --- /dev/null +++ b/tests/components/advantage_air/test_climate.py @@ -0,0 +1,176 @@ +"""Test the Advantage Air Climate Platform.""" + +from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, + ATTR_HVAC_MODE, + DOMAIN as CLIMATE_DOMAIN, + FAN_OFF, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE + +from tests.components.advantage_air import ( + TEST_SET_RESPONSE, + TEST_SET_URL, + TEST_SYSTEM_DATA, + TEST_SYSTEM_URL, + add_mock_config, +) + + +async def test_climate_async_setup_entry(hass, aioclient_mock): + """Test climate setup.""" + + aioclient_mock.get( + TEST_SYSTEM_URL, + text=TEST_SYSTEM_DATA, + ) + aioclient_mock.get( + TEST_SET_URL, + text=TEST_SET_RESPONSE, + ) + await add_mock_config(hass) + + registry = await hass.helpers.entity_registry.async_get_registry() + + assert len(aioclient_mock.mock_calls) == 1 + + # Test Main Climate Entity + entity_id = "climate.ac_one" + state = hass.states.get(entity_id) + assert state + assert state.state == HVAC_MODE_FAN_ONLY + assert state.attributes.get("min_temp") == 16 + assert state.attributes.get("max_temp") == 32 + assert state.attributes.get("current_temperature") is None + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == "uniqueid-ac1" + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: [entity_id], ATTR_HVAC_MODE: HVAC_MODE_FAN_ONLY}, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 3 + assert aioclient_mock.mock_calls[-2][0] == "GET" + assert aioclient_mock.mock_calls[-2][1].path == "/setAircon" + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: [entity_id], ATTR_HVAC_MODE: HVAC_MODE_OFF}, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 5 + assert aioclient_mock.mock_calls[-2][0] == "GET" + assert aioclient_mock.mock_calls[-2][1].path == "/setAircon" + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: [entity_id], ATTR_FAN_MODE: FAN_OFF}, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 7 + assert aioclient_mock.mock_calls[-2][0] == "GET" + assert aioclient_mock.mock_calls[-2][1].path == "/setAircon" + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 25}, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 9 + assert aioclient_mock.mock_calls[-2][0] == "GET" + assert aioclient_mock.mock_calls[-2][1].path == "/setAircon" + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + # Test Climate Zone Entity + entity_id = "climate.zone_open_with_sensor" + state = hass.states.get(entity_id) + assert state + assert state.attributes.get("min_temp") == 16 + assert state.attributes.get("max_temp") == 32 + assert state.attributes.get("measuredTemp") == 25 + assert state.attributes.get("setTemp") == 24 + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == "uniqueid-ac1-z01" + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: [entity_id], ATTR_HVAC_MODE: HVAC_MODE_FAN_ONLY}, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 11 + assert aioclient_mock.mock_calls[-2][0] == "GET" + assert aioclient_mock.mock_calls[-2][1].path == "/setAircon" + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: [entity_id], ATTR_HVAC_MODE: HVAC_MODE_OFF}, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 13 + assert aioclient_mock.mock_calls[-2][0] == "GET" + assert aioclient_mock.mock_calls[-2][1].path == "/setAircon" + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 25}, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 15 + assert aioclient_mock.mock_calls[-2][0] == "GET" + assert aioclient_mock.mock_calls[-2][1].path == "/setAircon" + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + +async def test_climate_async_failed_update(hass, aioclient_mock): + """Test climate change failure.""" + + aioclient_mock.get( + TEST_SYSTEM_URL, + text=TEST_SYSTEM_DATA, + ) + aioclient_mock.get( + TEST_SET_URL, + exc=SyntaxError, + ) + await add_mock_config(hass) + + assert len(aioclient_mock.mock_calls) == 1 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ["climate.ac_one"], ATTR_TEMPERATURE: 25}, + blocking=True, + ) + assert len(aioclient_mock.mock_calls) == 2 + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/setAircon" diff --git a/tests/components/advantage_air/test_config_flow.py b/tests/components/advantage_air/test_config_flow.py new file mode 100644 index 00000000000..248ee25858b --- /dev/null +++ b/tests/components/advantage_air/test_config_flow.py @@ -0,0 +1,71 @@ +"""Test the Advantage Air config flow.""" + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.advantage_air.const import DOMAIN + +from tests.async_mock import patch +from tests.components.advantage_air import TEST_SYSTEM_DATA, TEST_SYSTEM_URL, USER_INPUT + + +async def test_form(hass, aioclient_mock): + """Test that form shows up.""" + + aioclient_mock.get( + TEST_SYSTEM_URL, + text=TEST_SYSTEM_DATA, + ) + + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["step_id"] == "user" + assert result1["errors"] == {} + + with patch( + "homeassistant.components.advantage_air.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + USER_INPUT, + ) + + assert len(aioclient_mock.mock_calls) == 1 + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "testname" + assert result2["data"] == USER_INPUT + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + + # Test Duplicate Config Flow + result3 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + USER_INPUT, + ) + assert result4["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_form_cannot_connect(hass, aioclient_mock): + """Test we handle cannot connect error.""" + + aioclient_mock.get( + TEST_SYSTEM_URL, + exc=SyntaxError, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/components/advantage_air/test_init.py b/tests/components/advantage_air/test_init.py new file mode 100644 index 00000000000..22be288e452 --- /dev/null +++ b/tests/components/advantage_air/test_init.py @@ -0,0 +1,33 @@ +"""Test the Advantage Air Initialization.""" + +from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_SETUP_RETRY + +from tests.components.advantage_air import ( + TEST_SYSTEM_DATA, + TEST_SYSTEM_URL, + add_mock_config, +) + + +async def test_async_setup_entry(hass, aioclient_mock): + """Test a successful setup entry.""" + + aioclient_mock.get( + TEST_SYSTEM_URL, + text=TEST_SYSTEM_DATA, + ) + + entry = await add_mock_config(hass) + assert entry.state == ENTRY_STATE_LOADED + + +async def test_async_setup_entry_failure(hass, aioclient_mock): + """Test a unsuccessful setup entry.""" + + aioclient_mock.get( + TEST_SYSTEM_URL, + exc=SyntaxError, + ) + + entry = await add_mock_config(hass) + assert entry.state == ENTRY_STATE_SETUP_RETRY diff --git a/tests/fixtures/advantage_air/getSystemData.json b/tests/fixtures/advantage_air/getSystemData.json new file mode 100644 index 00000000000..ebeadaa84b3 --- /dev/null +++ b/tests/fixtures/advantage_air/getSystemData.json @@ -0,0 +1,110 @@ +{ + "aircons": { + "ac1": { + "info": { + "climateControlModeIsRunning": false, + "countDownToOff": 0, + "countDownToOn": 0, + "fan": "high", + "filterCleanStatus": 0, + "freshAirStatus": "none", + "mode": "vent", + "myZone": 0, + "name": "AC One", + "setTemp": 24, + "state": "on" + }, + "zones": { + "z01": { + "error": 0, + "maxDamper": 100, + "measuredTemp": 25, + "minDamper": 0, + "motion": 1, + "motionConfig": 1, + "name": "Zone open with Sensor", + "number": 1, + "rssi": -50, + "setTemp": 24, + "state": "open", + "type": 1, + "value": 100 + }, + "z02": { + "error": 0, + "maxDamper": 100, + "measuredTemp": 25, + "minDamper": 0, + "motion": 1, + "motionConfig": 1, + "name": "Zone closed with Sensor", + "number": 1, + "rssi": -50, + "setTemp": 24, + "state": "close", + "type": 1, + "value": 0 + } + } + }, + "ac2": { + "info": { + "climateControlModeIsRunning": false, + "countDownToOff": 0, + "countDownToOn": 0, + "fan": "low", + "filterCleanStatus": 0, + "freshAirStatus": "none", + "mode": "cool", + "myZone": 1, + "name": "AC Two", + "setTemp": 24, + "state": "off" + }, + "zones": { + "z01": { + "error": 0, + "maxDamper": 100, + "measuredTemp": 0, + "minDamper": 0, + "motion": 0, + "motionConfig": 0, + "name": "Zone open without sensor", + "number": 1, + "rssi": 0, + "setTemp": 24, + "state": "open", + "type": 0, + "value": 100 + }, + "z02": { + "error": 0, + "maxDamper": 100, + "measuredTemp": 0, + "minDamper": 0, + "motion": 0, + "motionConfig": 0, + "name": "Zone closed without sensor", + "number": 1, + "rssi": 0, + "setTemp": 24, + "state": "close", + "type": 0, + "value": 0 + } + } + } + }, + "system": { + "hasAircons": true, + "hasLights": false, + "hasSensors": false, + "hasThings": false, + "hasThingsBOG": false, + "hasThingsLight": false, + "name": "testname", + "rid": "uniqueid", + "sysType": "e-zone", + "myAppRev": "testversion" + } +} \ No newline at end of file diff --git a/tests/fixtures/advantage_air/setAircon.json b/tests/fixtures/advantage_air/setAircon.json new file mode 100644 index 00000000000..ca439c142ae --- /dev/null +++ b/tests/fixtures/advantage_air/setAircon.json @@ -0,0 +1,4 @@ +{ + "ack": true, + "request": "setAircon" +} \ No newline at end of file