From 8151721fbc8f0022b5eee80c32adb095c6d8ea10 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 16 Jan 2021 19:18:40 +0200 Subject: [PATCH] Move switcher_kis services to entity services (#45204) --- .../components/switcher_kis/__init__.py | 101 +------------- .../components/switcher_kis/services.yaml | 2 +- .../components/switcher_kis/switch.py | 77 +++++++++-- tests/components/switcher_kis/conftest.py | 11 +- tests/components/switcher_kis/consts.py | 3 +- tests/components/switcher_kis/test_init.py | 130 +++++++++++------- 6 files changed, 163 insertions(+), 161 deletions(-) diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 23b69eefb74..244ed708cc7 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -4,35 +4,22 @@ from datetime import datetime, timedelta import logging from typing import Dict, Optional -from aioswitcher.api import SwitcherV2Api from aioswitcher.bridge import SwitcherV2Bridge -from aioswitcher.consts import COMMAND_ON import voluptuous as vol -from homeassistant.auth.permissions.const import POLICY_EDIT from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback, split_entity_id -from homeassistant.exceptions import Unauthorized, UnknownUser +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.discovery import async_listen_platform, async_load_platform +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ( - ContextType, - DiscoveryInfoType, - EventType, - HomeAssistantType, - ServiceCallType, -) -from homeassistant.loader import bind_hass +from homeassistant.helpers.typing import EventType, HomeAssistantType _LOGGER = logging.getLogger(__name__) DOMAIN = "switcher_kis" -CONF_AUTO_OFF = "auto_off" -CONF_TIMER_MINUTES = "timer_minutes" CONF_DEVICE_ID = "device_id" CONF_DEVICE_PASSWORD = "device_password" CONF_PHONE_ID = "phone_id" @@ -58,39 +45,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SERVICE_SET_AUTO_OFF_NAME = "set_auto_off" -SERVICE_SET_AUTO_OFF_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(CONF_AUTO_OFF): cv.time_period_str, - } -) - -SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer" -SERVICE_TURN_ON_WITH_TIMER_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TIMER_MINUTES): vol.All( - cv.positive_int, vol.Range(min=1, max=90) - ), - } -) - - -@bind_hass -async def _validate_edit_permission( - hass: HomeAssistantType, context: ContextType, entity_id: str -) -> None: - """Use for validating user control permissions.""" - splited = split_entity_id(entity_id) - if splited[0] != SWITCH_DOMAIN or not splited[1].startswith(DOMAIN): - raise Unauthorized(context=context, entity_id=entity_id, permission=POLICY_EDIT) - user = await hass.auth.async_get_user(context.user_id) - if user is None: - raise UnknownUser(context=context, entity_id=entity_id, permission=POLICY_EDIT) - if not user.permissions.check_entity(entity_id, POLICY_EDIT): - raise Unauthorized(context=context, entity_id=entity_id, permission=POLICY_EDIT) - async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: """Set up the switcher component.""" @@ -117,53 +71,6 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: return False hass.data[DOMAIN] = {DATA_DEVICE: device_data} - async def async_switch_platform_discovered( - platform: str, discovery_info: DiscoveryInfoType - ) -> None: - """Use for registering services after switch platform is discovered.""" - if platform != DOMAIN: - return - - async def async_set_auto_off_service(service: ServiceCallType) -> None: - """Use for handling setting device auto-off service calls.""" - - await _validate_edit_permission( - hass, service.context, service.data[ATTR_ENTITY_ID] - ) - - async with SwitcherV2Api( - hass.loop, device_data.ip_addr, phone_id, device_id, device_password - ) as swapi: - await swapi.set_auto_shutdown(service.data[CONF_AUTO_OFF]) - - async def async_turn_on_with_timer_service(service: ServiceCallType) -> None: - """Use for handling turning device on with a timer service calls.""" - - await _validate_edit_permission( - hass, service.context, service.data[ATTR_ENTITY_ID] - ) - - async with SwitcherV2Api( - hass.loop, device_data.ip_addr, phone_id, device_id, device_password - ) as swapi: - await swapi.control_device(COMMAND_ON, service.data[CONF_TIMER_MINUTES]) - - hass.services.async_register( - DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - async_set_auto_off_service, - schema=SERVICE_SET_AUTO_OFF_SCHEMA, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_TURN_ON_WITH_TIMER_NAME, - async_turn_on_with_timer_service, - schema=SERVICE_TURN_ON_WITH_TIMER_SCHEMA, - ) - - async_listen_platform(hass, SWITCH_DOMAIN, async_switch_platform_discovered) - hass.async_create_task(async_load_platform(hass, SWITCH_DOMAIN, DOMAIN, {}, config)) @callback diff --git a/homeassistant/components/switcher_kis/services.yaml b/homeassistant/components/switcher_kis/services.yaml index 07e0cfe1198..be812f8d7e1 100644 --- a/homeassistant/components/switcher_kis/services.yaml +++ b/homeassistant/components/switcher_kis/services.yaml @@ -15,5 +15,5 @@ turn_on_with_timer: description: "Name of the entity id associated with the integration, used for permission validation." example: "switch.switcher_kis_boiler" timer_minutes: - description: 'Minutes to turn on (valid range from 1 to 90)' + description: 'Minutes to turn on (valid range from 1 to 150)' example: '30' diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index af922c3ab8b..5e75a0e6090 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -1,7 +1,8 @@ """Home Assistant Switcher Component Switch platform.""" -from typing import TYPE_CHECKING, Callable, Dict +from typing import Callable, Dict from aioswitcher.api import SwitcherV2Api +from aioswitcher.api.messages import SwitcherV2ControlResponseMSG from aioswitcher.consts import ( COMMAND_OFF, COMMAND_ON, @@ -9,10 +10,13 @@ from aioswitcher.consts import ( STATE_ON as SWITCHER_STATE_ON, WAITING_TEXT, ) +from aioswitcher.devices import SwitcherV2Device +import voluptuous as vol from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchEntity +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.typing import HomeAssistantType, ServiceCallType from . import ( ATTR_AUTO_OFF_SET, @@ -23,11 +27,8 @@ from . import ( SIGNAL_SWITCHER_DEVICE_UPDATE, ) -# pylint: disable=ungrouped-imports -if TYPE_CHECKING: - from aioswitcher.api.messages import SwitcherV2ControlResponseMSG - from aioswitcher.devices import SwitcherV2Device - +CONF_AUTO_OFF = "auto_off" +CONF_TIMER_MINUTES = "timer_minutes" DEVICE_PROPERTIES_TO_HA_ATTRIBUTES = { "power_consumption": ATTR_CURRENT_POWER_W, @@ -36,6 +37,18 @@ DEVICE_PROPERTIES_TO_HA_ATTRIBUTES = { "auto_off_set": ATTR_AUTO_OFF_SET, } +SERVICE_SET_AUTO_OFF_NAME = "set_auto_off" +SERVICE_SET_AUTO_OFF_SCHEMA = { + vol.Required(CONF_AUTO_OFF): cv.time_period_str, +} + +SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer" +SERVICE_TURN_ON_WITH_TIMER_SCHEMA = { + vol.Required(CONF_TIMER_MINUTES): vol.All( + cv.positive_int, vol.Range(min=1, max=150) + ), +} + async def async_setup_platform( hass: HomeAssistantType, @@ -46,13 +59,57 @@ async def async_setup_platform( """Set up the switcher platform for the switch component.""" if discovery_info is None: return + + async def async_set_auto_off_service(entity, service_call: ServiceCallType) -> None: + """Use for handling setting device auto-off service calls.""" + + async with SwitcherV2Api( + hass.loop, + device_data.ip_addr, + device_data.phone_id, + device_data.device_id, + device_data.device_password, + ) as swapi: + await swapi.set_auto_shutdown(service_call.data[CONF_AUTO_OFF]) + + async def async_turn_on_with_timer_service( + entity, service_call: ServiceCallType + ) -> None: + """Use for handling turning device on with a timer service calls.""" + + async with SwitcherV2Api( + hass.loop, + device_data.ip_addr, + device_data.phone_id, + device_data.device_id, + device_data.device_password, + ) as swapi: + await swapi.control_device( + COMMAND_ON, service_call.data[CONF_TIMER_MINUTES] + ) + + device_data = hass.data[DOMAIN][DATA_DEVICE] async_add_entities([SwitcherControl(hass.data[DOMAIN][DATA_DEVICE])]) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_SET_AUTO_OFF_NAME, + SERVICE_SET_AUTO_OFF_SCHEMA, + async_set_auto_off_service, + ) + + platform.async_register_entity_service( + SERVICE_TURN_ON_WITH_TIMER_NAME, + SERVICE_TURN_ON_WITH_TIMER_SCHEMA, + async_turn_on_with_timer_service, + ) + class SwitcherControl(SwitchEntity): """Home Assistant switch entity.""" - def __init__(self, device_data: "SwitcherV2Device") -> None: + def __init__(self, device_data: SwitcherV2Device) -> None: """Initialize the entity.""" self._self_initiated = False self._device_data = device_data @@ -111,7 +168,7 @@ class SwitcherControl(SwitchEntity): ) ) - async def async_update_data(self, device_data: "SwitcherV2Device") -> None: + async def async_update_data(self, device_data: SwitcherV2Device) -> None: """Update the entity data.""" if device_data: if self._self_initiated: @@ -132,7 +189,7 @@ class SwitcherControl(SwitchEntity): async def _control_device(self, send_on: bool) -> None: """Turn the entity on or off.""" - response: "SwitcherV2ControlResponseMSG" = None + response: SwitcherV2ControlResponseMSG = None async with SwitcherV2Api( self.hass.loop, self._device_data.ip_addr, diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index a95dd2fd6d1..e7303a20ea5 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -11,6 +11,7 @@ from .consts import ( DUMMY_AUTO_OFF_SET, DUMMY_DEVICE_ID, DUMMY_DEVICE_NAME, + DUMMY_DEVICE_PASSWORD, DUMMY_DEVICE_STATE, DUMMY_ELECTRIC_CURRENT, DUMMY_IP_ADDRESS, @@ -79,6 +80,11 @@ class MockSwitcherV2Device: """Return the phone id.""" return DUMMY_PHONE_ID + @property + def device_password(self) -> str: + """Return the device password.""" + return DUMMY_DEVICE_PASSWORD + @property def last_data_update(self) -> datetime: """Return the timestamp of the last update.""" @@ -169,10 +175,11 @@ def mock_api_fixture() -> Generator[AsyncMock, Any, None]: patchers = [ patch( - "homeassistant.components.switcher_kis.SwitcherV2Api.connect", new=mock_api + "homeassistant.components.switcher_kis.switch.SwitcherV2Api.connect", + new=mock_api, ), patch( - "homeassistant.components.switcher_kis.SwitcherV2Api.disconnect", + "homeassistant.components.switcher_kis.switch.SwitcherV2Api.disconnect", new=mock_api, ), ] diff --git a/tests/components/switcher_kis/consts.py b/tests/components/switcher_kis/consts.py index b2e312183a7..ab5951710f4 100644 --- a/tests/components/switcher_kis/consts.py +++ b/tests/components/switcher_kis/consts.py @@ -8,6 +8,7 @@ from homeassistant.components.switcher_kis import ( ) DUMMY_AUTO_OFF_SET = "01:30:00" +DUMMY_TIMER_MINUTES_SET = "90" DUMMY_DEVICE_ID = "a123bc" DUMMY_DEVICE_NAME = "Device Name" DUMMY_DEVICE_PASSWORD = "12345678" @@ -22,7 +23,7 @@ DUMMY_POWER_CONSUMPTION = 2780 DUMMY_REMAINING_TIME = "01:29:32" # Adjust if any modification were made to DUMMY_DEVICE_NAME -SWITCH_ENTITY_ID = "switch.switcher_kis_device_name" +SWITCH_ENTITY_ID = "switch.device_name" MANDATORY_CONFIGURATION = { DOMAIN: { diff --git a/tests/components/switcher_kis/test_init.py b/tests/components/switcher_kis/test_init.py index aa187b80d8f..394f48d001a 100644 --- a/tests/components/switcher_kis/test_init.py +++ b/tests/components/switcher_kis/test_init.py @@ -1,21 +1,27 @@ """Test cases for the switcher_kis component.""" - from datetime import timedelta -from typing import TYPE_CHECKING, Any, Generator +from typing import Any, Generator +from unittest.mock import patch +from aioswitcher.consts import COMMAND_ON +from aioswitcher.devices import SwitcherV2Device from pytest import raises from homeassistant.components.switcher_kis import ( - CONF_AUTO_OFF, DATA_DEVICE, DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - SERVICE_SET_AUTO_OFF_SCHEMA, SIGNAL_SWITCHER_DEVICE_UPDATE, ) +from homeassistant.components.switcher_kis.switch import ( + CONF_AUTO_OFF, + CONF_TIMER_MINUTES, + SERVICE_SET_AUTO_OFF_NAME, + SERVICE_TURN_ON_WITH_TIMER_NAME, +) from homeassistant.const import CONF_ENTITY_ID from homeassistant.core import Context, callback -from homeassistant.exceptions import Unauthorized, UnknownUser +from homeassistant.exceptions import UnknownUser +from homeassistant.helpers.config_validation import time_period_str from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component @@ -32,16 +38,12 @@ from .consts import ( DUMMY_PHONE_ID, DUMMY_POWER_CONSUMPTION, DUMMY_REMAINING_TIME, + DUMMY_TIMER_MINUTES_SET, MANDATORY_CONFIGURATION, SWITCH_ENTITY_ID, ) -from tests.common import async_fire_time_changed, async_mock_service - -if TYPE_CHECKING: - from aioswitcher.devices import SwitcherV2Device - - from tests.common import MockUser +from tests.common import MockUser, async_fire_time_changed async def test_failed_config( @@ -83,8 +85,7 @@ async def test_set_auto_off_service( hass: HomeAssistantType, mock_bridge: Generator[None, Any, None], mock_api: Generator[None, Any, None], - hass_owner_user: "MockUser", - hass_read_only_user: "MockUser", + hass_owner_user: MockUser, ) -> None: """Test the set_auto_off service.""" assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) @@ -101,31 +102,6 @@ async def test_set_auto_off_service( context=Context(user_id=hass_owner_user.id), ) - with raises(Unauthorized) as unauthorized_read_only_exc: - await hass.services.async_call( - DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - {CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET}, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) - - assert unauthorized_read_only_exc.type is Unauthorized - - with raises(Unauthorized) as unauthorized_wrong_entity_exc: - await hass.services.async_call( - DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - { - CONF_ENTITY_ID: "light.not_related_entity", - CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET, - }, - blocking=True, - context=Context(user_id=hass_owner_user.id), - ) - - assert unauthorized_wrong_entity_exc.type is Unauthorized - with raises(UnknownUser) as unknown_user_exc: await hass.services.async_call( DOMAIN, @@ -137,20 +113,74 @@ async def test_set_auto_off_service( assert unknown_user_exc.type is UnknownUser - service_calls = async_mock_service( - hass, DOMAIN, SERVICE_SET_AUTO_OFF_NAME, SERVICE_SET_AUTO_OFF_SCHEMA - ) + with patch( + "homeassistant.components.switcher_kis.switch.SwitcherV2Api.set_auto_shutdown" + ) as mock_set_auto_shutdown: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_AUTO_OFF_NAME, + {CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET}, + ) - await hass.services.async_call( - DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - {CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET}, - ) + await hass.async_block_till_done() + + mock_set_auto_shutdown.assert_called_once_with( + time_period_str(DUMMY_AUTO_OFF_SET) + ) + + +async def test_turn_on_with_timer_service( + hass: HomeAssistantType, + mock_bridge: Generator[None, Any, None], + mock_api: Generator[None, Any, None], + hass_owner_user: MockUser, +) -> None: + """Test the set_auto_off service.""" + assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) await hass.async_block_till_done() - assert len(service_calls) == 1 - assert str(service_calls[0].data[CONF_AUTO_OFF]) == DUMMY_AUTO_OFF_SET.lstrip("0") + assert hass.services.has_service(DOMAIN, SERVICE_TURN_ON_WITH_TIMER_NAME) + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON_WITH_TIMER_NAME, + {CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET}, + blocking=True, + context=Context(user_id=hass_owner_user.id), + ) + + with raises(UnknownUser) as unknown_user_exc: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON_WITH_TIMER_NAME, + { + CONF_ENTITY_ID: SWITCH_ENTITY_ID, + CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET, + }, + blocking=True, + context=Context(user_id="not_real_user"), + ) + + assert unknown_user_exc.type is UnknownUser + + with patch( + "homeassistant.components.switcher_kis.switch.SwitcherV2Api.control_device" + ) as mock_control_device: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON_WITH_TIMER_NAME, + { + CONF_ENTITY_ID: SWITCH_ENTITY_ID, + CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET, + }, + ) + + await hass.async_block_till_done() + + mock_control_device.assert_called_once_with( + COMMAND_ON, int(DUMMY_TIMER_MINUTES_SET) + ) async def test_signal_dispatcher( @@ -162,7 +192,7 @@ async def test_signal_dispatcher( await hass.async_block_till_done() @callback - def verify_update_data(device: "SwitcherV2Device") -> None: + def verify_update_data(device: SwitcherV2Device) -> None: """Use as callback for signal dispatcher.""" pass