From 2857739958dde9615e7759c7952ebeaadb1f520e Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 29 Aug 2022 10:44:08 +1000 Subject: [PATCH] Add light platform to Advantage Air (#75425) --- .../components/advantage_air/__init__.py | 21 ++-- .../components/advantage_air/climate.py | 2 +- .../components/advantage_air/light.py | 90 +++++++++++++++ .../components/advantage_air/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/advantage_air/__init__.py | 3 + .../advantage_air/fixtures/getSystemData.json | 19 +++- tests/components/advantage_air/test_light.py | 105 ++++++++++++++++++ 9 files changed, 234 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/advantage_air/light.py create mode 100644 tests/components/advantage_air/test_light.py diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index d50224698b8..b5e6e0be024 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -20,6 +20,7 @@ PLATFORMS = [ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.LIGHT, ] _LOGGER = logging.getLogger(__name__) @@ -50,19 +51,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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) + def error_handle_factory(func): + """Return the provided API function wrapped in an error handler and coordinator refresh.""" + + async def error_handle(param): + try: + if await func(param): + await coordinator.async_refresh() + except ApiError as err: + _LOGGER.warning(err) + + return error_handle await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { "coordinator": coordinator, - "async_change": async_change, + "async_change": error_handle_factory(api.aircon.async_set), + "async_set_light": error_handle_factory(api.lights.async_set), } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index d889bf35642..c11b01f3ace 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -45,7 +45,7 @@ AC_HVAC_MODES = [ ] ADVANTAGE_AIR_FAN_MODES = { - "auto": FAN_AUTO, + "autoAA": FAN_AUTO, "low": FAN_LOW, "medium": FAN_MEDIUM, "high": FAN_HIGH, diff --git a/homeassistant/components/advantage_air/light.py b/homeassistant/components/advantage_air/light.py new file mode 100644 index 00000000000..b1c8495edf8 --- /dev/null +++ b/homeassistant/components/advantage_air/light.py @@ -0,0 +1,90 @@ +"""Light platform for Advantage Air integration.""" +from typing import Any + +from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + ADVANTAGE_AIR_STATE_OFF, + ADVANTAGE_AIR_STATE_ON, + DOMAIN as ADVANTAGE_AIR_DOMAIN, +) +from .entity import AdvantageAirEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up AdvantageAir light platform.""" + + instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] + + entities = [] + if "myLights" in instance["coordinator"].data: + for light in instance["coordinator"].data["myLights"]["lights"].values(): + if light.get("relay"): + entities.append(AdvantageAirLight(instance, light)) + else: + entities.append(AdvantageAirLightDimmable(instance, light)) + async_add_entities(entities) + + +class AdvantageAirLight(AdvantageAirEntity, LightEntity): + """Representation of Advantage Air Light.""" + + _attr_supported_color_modes = {ColorMode.ONOFF} + + def __init__(self, instance, light): + """Initialize an Advantage Air Light.""" + super().__init__(instance) + self.async_set_light = instance["async_set_light"] + self._id = light["id"] + self._attr_unique_id += f"-{self._id}" + self._attr_device_info = DeviceInfo( + identifiers={(ADVANTAGE_AIR_DOMAIN, self._attr_unique_id)}, + via_device=(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"]), + manufacturer="Advantage Air", + model=light.get("moduleType"), + name=light["name"], + ) + + @property + def _light(self): + """Return the light object.""" + return self.coordinator.data["myLights"]["lights"][self._id] + + @property + def is_on(self) -> bool: + """Return if the light is on.""" + return self._light["state"] == ADVANTAGE_AIR_STATE_ON + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + await self.async_set_light({"id": self._id, "state": ADVANTAGE_AIR_STATE_ON}) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self.async_set_light({"id": self._id, "state": ADVANTAGE_AIR_STATE_OFF}) + + +class AdvantageAirLightDimmable(AdvantageAirLight): + """Representation of Advantage Air Dimmable Light.""" + + _attr_supported_color_modes = {ColorMode.ONOFF, ColorMode.BRIGHTNESS} + + @property + def brightness(self) -> int: + """Return the brightness of this light between 0..255.""" + return round(self._light["value"] * 255 / 100) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on and optionally set the brightness.""" + data = {"id": self._id, "state": ADVANTAGE_AIR_STATE_ON} + if ATTR_BRIGHTNESS in kwargs: + data["value"] = round(kwargs[ATTR_BRIGHTNESS] * 100 / 255) + await self.async_set_light(data) diff --git a/homeassistant/components/advantage_air/manifest.json b/homeassistant/components/advantage_air/manifest.json index f95a32e186b..51b6158954e 100644 --- a/homeassistant/components/advantage_air/manifest.json +++ b/homeassistant/components/advantage_air/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/advantage_air", "codeowners": ["@Bre77"], - "requirements": ["advantage_air==0.3.1"], + "requirements": ["advantage_air==0.4.1"], "quality_scale": "platinum", "iot_class": "local_polling", "loggers": ["advantage_air"] diff --git a/requirements_all.txt b/requirements_all.txt index b56d75d7b66..d3246a5aa83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -86,7 +86,7 @@ adext==0.4.2 adguardhome==0.5.1 # homeassistant.components.advantage_air -advantage_air==0.3.1 +advantage_air==0.4.1 # homeassistant.components.frontier_silicon afsapi==0.2.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82674d34ab5..3fa0fe132f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -76,7 +76,7 @@ adext==0.4.2 adguardhome==0.5.1 # homeassistant.components.advantage_air -advantage_air==0.3.1 +advantage_air==0.4.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 index 92faaff6359..e415485821f 100644 --- a/tests/components/advantage_air/__init__.py +++ b/tests/components/advantage_air/__init__.py @@ -17,6 +17,9 @@ 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" +TEST_SET_LIGHT_URL = ( + f"http://{USER_INPUT[CONF_IP_ADDRESS]}:{USER_INPUT[CONF_PORT]}/setLight" +) async def add_mock_config(hass): diff --git a/tests/components/advantage_air/fixtures/getSystemData.json b/tests/components/advantage_air/fixtures/getSystemData.json index 35a06c2d468..00ce2b1f095 100644 --- a/tests/components/advantage_air/fixtures/getSystemData.json +++ b/tests/components/advantage_air/fixtures/getSystemData.json @@ -143,9 +143,26 @@ } } }, + "myLights": { + "lights": { + "100": { + "id": "100", + "moduleType": "RM2", + "name": "Light A", + "relay": true, + "state": "off" + }, + "101": { + "id": "101", + "name": "Light B", + "value": 50, + "state": "on" + } + } + }, "system": { "hasAircons": true, - "hasLights": false, + "hasLights": true, "hasSensors": false, "hasThings": false, "hasThingsBOG": false, diff --git a/tests/components/advantage_air/test_light.py b/tests/components/advantage_air/test_light.py new file mode 100644 index 00000000000..85223700dbf --- /dev/null +++ b/tests/components/advantage_air/test_light.py @@ -0,0 +1,105 @@ +"""Test the Advantage Air Switch Platform.""" +from json import loads + +from homeassistant.components.advantage_air.const import ( + ADVANTAGE_AIR_STATE_OFF, + ADVANTAGE_AIR_STATE_ON, +) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + DOMAIN as LIGHT_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF +from homeassistant.helpers import entity_registry as er + +from tests.components.advantage_air import ( + TEST_SET_LIGHT_URL, + TEST_SET_RESPONSE, + TEST_SYSTEM_DATA, + TEST_SYSTEM_URL, + add_mock_config, +) + + +async def test_light_async_setup_entry(hass, aioclient_mock): + """Test light setup.""" + + aioclient_mock.get( + TEST_SYSTEM_URL, + text=TEST_SYSTEM_DATA, + ) + aioclient_mock.get( + TEST_SET_LIGHT_URL, + text=TEST_SET_RESPONSE, + ) + + await add_mock_config(hass) + + registry = er.async_get(hass) + + assert len(aioclient_mock.mock_calls) == 1 + + # Test Light Entity + entity_id = "light.light_a" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == "uniqueid-100" + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {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 == "/setLight" + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["id"] == "100" + assert data["state"] == ADVANTAGE_AIR_STATE_ON + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {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 == "/setLight" + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["id"] == "100" + assert data["state"] == ADVANTAGE_AIR_STATE_OFF + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" + + # Test Dimmable Light Entity + entity_id = "light.light_b" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == "uniqueid-101" + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id], ATTR_BRIGHTNESS: 128}, + 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 == "/setLight" + data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) + assert data["id"] == "101" + assert data["value"] == 50 + assert data["state"] == ADVANTAGE_AIR_STATE_ON + assert aioclient_mock.mock_calls[-1][0] == "GET" + assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData"