From a2574a4ed5448550751b42e1c5af060dce5b8528 Mon Sep 17 00:00:00 2001 From: Brett Date: Thu, 15 Oct 2020 20:45:05 +1000 Subject: [PATCH] Add Cover Platform to Advantage Air (#41757) Co-authored-by: Martin Hjelmare --- .../components/advantage_air/__init__.py | 2 +- .../components/advantage_air/cover.py | 132 ++++++++++++++++++ tests/components/advantage_air/test_cover.py | 113 +++++++++++++++ 3 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/advantage_air/cover.py create mode 100644 tests/components/advantage_air/test_cover.py diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index ae87a674533..a6efc479715 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -13,7 +13,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ADVANTAGE_AIR_RETRY, DOMAIN ADVANTAGE_AIR_SYNC_INTERVAL = 15 -ADVANTAGE_AIR_PLATFORMS = ["climate"] +ADVANTAGE_AIR_PLATFORMS = ["climate", "cover"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py new file mode 100644 index 00000000000..3c9d7540212 --- /dev/null +++ b/homeassistant/components/advantage_air/cover.py @@ -0,0 +1,132 @@ +"""Cover platform for Advantage Air integration.""" + +from homeassistant.components.cover import ( + ATTR_POSITION, + DEVICE_CLASS_DAMPER, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverEntity, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ADVANTAGE_AIR_STATE_CLOSE, ADVANTAGE_AIR_STATE_OPEN, DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AdvantageAir cover platform.""" + + instance = hass.data[DOMAIN][config_entry.entry_id] + + entities = [] + for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): + for zone_key, zone in ac_device["zones"].items(): + # Only add zone vent controls when zone in vent control mode. + if zone["type"] == 0: + entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) + async_add_entities(entities) + + +class AdvantageAirZoneVent(CoordinatorEntity, CoverEntity): + """Advantage Air Cover Class.""" + + def __init__(self, instance, ac_key, zone_key): + """Initialize the Advantage Air Zone Vent cover entity.""" + super().__init__(instance["coordinator"]) + self.async_change = instance["async_change"] + self.ac_key = ac_key + self.zone_key = zone_key + + @property + def _zone(self): + return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key] + + @property + def name(self): + """Return the name.""" + return f'{self._zone["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 device_class(self): + """Return the device class of the vent.""" + return DEVICE_CLASS_DAMPER + + @property + def supported_features(self): + """Return the supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + @property + def is_closed(self): + """Return if vent is fully closed.""" + return self._zone["state"] == ADVANTAGE_AIR_STATE_CLOSE + + @property + def current_cover_position(self): + """Return vents current position as a percentage.""" + if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: + return self._zone["value"] + return 0 + + @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"], + } + + async def async_open_cover(self, **kwargs): + """Fully open zone vent.""" + await self.async_change( + { + self.ac_key: { + "zones": { + self.zone_key: {"state": ADVANTAGE_AIR_STATE_OPEN, "value": 100} + } + } + } + ) + + async def async_close_cover(self, **kwargs): + """Fully close zone vent.""" + await self.async_change( + { + self.ac_key: { + "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_CLOSE}} + } + } + ) + + async def async_set_cover_position(self, **kwargs): + """Change vent position.""" + position = round(kwargs[ATTR_POSITION] / 5) * 5 + if position == 0: + 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, + "value": position, + } + } + } + } + ) diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py new file mode 100644 index 00000000000..18fd4f05b5a --- /dev/null +++ b/tests/components/advantage_air/test_cover.py @@ -0,0 +1,113 @@ +"""Test the Advantage Air Cover Platform.""" + +from json import loads + +from homeassistant.components.advantage_air.const import ( + ADVANTAGE_AIR_STATE_CLOSE, + ADVANTAGE_AIR_STATE_OPEN, +) +from homeassistant.components.cover import ( + ATTR_POSITION, + DEVICE_CLASS_DAMPER, + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OPEN + +from tests.components.advantage_air import ( + TEST_SET_RESPONSE, + TEST_SET_URL, + TEST_SYSTEM_DATA, + TEST_SYSTEM_URL, + add_mock_config, +) + + +async def test_cover_async_setup_entry(hass, aioclient_mock): + """Test climate setup without sensors.""" + + 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 Cover Zone Entity + entity_id = "cover.zone_open_without_sensor" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OPEN + assert state.attributes.get("device_class") == DEVICE_CLASS_DAMPER + assert state.attributes.get("current_position") == 100 + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == "uniqueid-ac2-z01" + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: [entity_id]}, + 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" + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["ac2"]["zones"]["z01"]["state"] == ADVANTAGE_AIR_STATE_CLOSE + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: [entity_id]}, + 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" + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["ac2"]["zones"]["z01"]["state"] == ADVANTAGE_AIR_STATE_OPEN + assert data["ac2"]["zones"]["z01"]["value"] == 100 + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: [entity_id], ATTR_POSITION: 50}, + 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" + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["ac2"]["zones"]["z01"]["value"] == 50 + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: [entity_id], ATTR_POSITION: 0}, + 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" + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["ac2"]["zones"]["z01"]["state"] == ADVANTAGE_AIR_STATE_CLOSE + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData"