diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index 7eee8ea9ad8..ca5e5388aaa 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -13,6 +13,7 @@ from aiomodernforms.models import Device as ModernFormsDeviceState from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, CONF_HOST from homeassistant.core import HomeAssistant @@ -30,6 +31,7 @@ SCAN_INTERVAL = timedelta(seconds=5) PLATFORMS = [ LIGHT_DOMAIN, FAN_DOMAIN, + SWITCH_DOMAIN, ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/modern_forms/manifest.json b/homeassistant/components/modern_forms/manifest.json index c2da0239fbe..1466537259b 100644 --- a/homeassistant/components/modern_forms/manifest.json +++ b/homeassistant/components/modern_forms/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/modern_forms", "requirements": [ - "aiomodernforms==0.1.5" + "aiomodernforms==0.1.8" ], "zeroconf": [ {"type":"_easylink._tcp.local.", "name":"wac*"} diff --git a/homeassistant/components/modern_forms/switch.py b/homeassistant/components/modern_forms/switch.py new file mode 100644 index 00000000000..90d5d13d649 --- /dev/null +++ b/homeassistant/components/modern_forms/switch.py @@ -0,0 +1,113 @@ +"""Support for Modern Forms switches.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ( + ModernFormsDataUpdateCoordinator, + ModernFormsDeviceEntity, + modernforms_exception_handler, +) +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Modern Forms switch based on a config entry.""" + coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + switches = [ + ModernFormsAwaySwitch(entry.entry_id, coordinator), + ModernFormsAdaptiveLearningSwitch(entry.entry_id, coordinator), + ] + async_add_entities(switches) + + +class ModernFormsSwitch(ModernFormsDeviceEntity, SwitchEntity): + """Defines a Modern Forms switch.""" + + def __init__( + self, + *, + entry_id: str, + coordinator: ModernFormsDataUpdateCoordinator, + name: str, + icon: str, + key: str, + ) -> None: + """Initialize Modern Forms switch.""" + self._key = key + super().__init__( + entry_id=entry_id, coordinator=coordinator, name=name, icon=icon + ) + self._attr_unique_id = f"{self.coordinator.data.info.mac_address}_{self._key}" + + +class ModernFormsAwaySwitch(ModernFormsSwitch): + """Defines a Modern Forms Away mode switch.""" + + def __init__( + self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator + ) -> None: + """Initialize Modern Forms Away mode switch.""" + super().__init__( + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:airplane-takeoff", + key="away_mode", + name=f"{coordinator.data.info.device_name} Away Mode", + ) + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return bool(self.coordinator.data.state.away_mode_enabled) + + @modernforms_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the Modern Forms Away mode switch.""" + await self.coordinator.modern_forms.away(away=False) + + @modernforms_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the Modern Forms Away mode switch.""" + await self.coordinator.modern_forms.away(away=True) + + +class ModernFormsAdaptiveLearningSwitch(ModernFormsSwitch): + """Defines a Modern Forms Adaptive Learning switch.""" + + def __init__( + self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator + ) -> None: + """Initialize Modern Forms Adaptive Learning switch.""" + super().__init__( + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:school-outline", + key="adaptive_learning", + name=f"{coordinator.data.info.device_name} Adaptive Learning", + ) + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return bool(self.coordinator.data.state.adaptive_learning_enabled) + + @modernforms_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the Modern Forms Adaptive Learning switch.""" + await self.coordinator.modern_forms.adaptive_learning(adaptive_learning=False) + + @modernforms_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the Modern Forms Adaptive Learning switch.""" + await self.coordinator.modern_forms.adaptive_learning(adaptive_learning=True) diff --git a/requirements_all.txt b/requirements_all.txt index 92e2151b737..746adfc31ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,7 +206,7 @@ aiolip==1.1.4 aiolyric==1.0.7 # homeassistant.components.modern_forms -aiomodernforms==0.1.5 +aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast aiomusiccast==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5128a8ab41d..8661bd0f93e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiolip==1.1.4 aiolyric==1.0.7 # homeassistant.components.modern_forms -aiomodernforms==0.1.5 +aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast aiomusiccast==0.6 diff --git a/tests/components/modern_forms/test_switch.py b/tests/components/modern_forms/test_switch.py new file mode 100644 index 00000000000..963264e3ca1 --- /dev/null +++ b/tests/components/modern_forms/test_switch.py @@ -0,0 +1,144 @@ +"""Tests for the Modern Forms switch platform.""" +from unittest.mock import patch + +from aiomodernforms import ModernFormsConnectionError + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.components.modern_forms import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_switch_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the Modern Forms switches.""" + await init_integration(hass, aioclient_mock) + + entity_registry = er.async_get(hass) + + state = hass.states.get("switch.modernformsfan_away_mode") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:airplane-takeoff" + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.modernformsfan_away_mode") + assert entry + assert entry.unique_id == "AA:BB:CC:DD:EE:FF_away_mode" + + state = hass.states.get("switch.modernformsfan_adaptive_learning") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:school-outline" + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.modernformsfan_adaptive_learning") + assert entry + assert entry.unique_id == "AA:BB:CC:DD:EE:FF_adaptive_learning" + + +async def test_switch_change_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the change of state of the Modern Forms switches.""" + await init_integration(hass, aioclient_mock) + + # Away Mode + with patch("aiomodernforms.ModernFormsDevice.away") as away_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + away_mock.assert_called_once_with(away=True) + + with patch("aiomodernforms.ModernFormsDevice.away") as away_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + away_mock.assert_called_once_with(away=False) + + # Adaptive Learning + with patch( + "aiomodernforms.ModernFormsDevice.adaptive_learning" + ) as adaptive_learning_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.modernformsfan_adaptive_learning"}, + blocking=True, + ) + await hass.async_block_till_done() + adaptive_learning_mock.assert_called_once_with(adaptive_learning=True) + + with patch( + "aiomodernforms.ModernFormsDevice.adaptive_learning" + ) as adaptive_learning_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.modernformsfan_adaptive_learning"}, + blocking=True, + ) + await hass.async_block_till_done() + adaptive_learning_mock.assert_called_once_with(adaptive_learning=False) + + +async def test_switch_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test error handling of the Modern Forms switches.""" + await init_integration(hass, aioclient_mock) + + aioclient_mock.clear_requests() + aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=400) + + with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.modernformsfan_away_mode") + assert state.state == STATE_OFF + assert "Invalid response from API" in caplog.text + + +async def test_switch_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error handling of the Modern Forms switches.""" + await init_integration(hass, aioclient_mock) + + with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), patch( + "homeassistant.components.modern_forms.ModernFormsDevice.away", + side_effect=ModernFormsConnectionError, + ): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.modernformsfan_away_mode") + assert state.state == STATE_UNAVAILABLE