Update requirement version and add switcher_kis services (#23477)

* Update aioswitcher requirement to 2019.4.26.

* Removed unnecessary legacy function call.

* Fixed log message capital first letter.

* Replaced None argument with empty dict.

* Replaced guard.

* Added ServiceCallType.

* Added set_auto_off and update_device_name services to the component.

* Added test cases for service calls.

* Conditioned the component services registry with the platform discovery.

* Update homeassistant/components/switcher_kis/__init__.py

Co-Authored-By: TomerFi <tomer.figenblat@gmail.com>

* Update homeassistant/components/switcher_kis/__init__.py

Co-Authored-By: TomerFi <tomer.figenblat@gmail.com>

* Resolved change requests.

* Added ContextType.

* Addes permission verification for service calls.

* Added test cases for permision verification and more.

* Replaced POLICY_CONTROL with the more suited POLICY_EDIT.

* More appropriate function name.

* Added domain and entity_id validation for calling services.

* Removed service for setting the vendor's device name.
This commit is contained in:
Tomer Figenblat 2019-06-15 01:48:21 +03:00 committed by Paulus Schoutsen
parent f9b3ba2887
commit fe8a330a45
10 changed files with 202 additions and 16 deletions

View File

@ -7,19 +7,25 @@ from typing import Dict, Optional
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 EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
from homeassistant.const import CONF_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback, split_entity_id
from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.discovery import (async_listen_platform,
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 EventType, HomeAssistantType
from homeassistant.helpers.typing import (ContextType, EventType,
HomeAssistantType, ServiceCallType)
from homeassistant.loader import bind_hass
_LOGGER = getLogger(__name__)
DOMAIN = 'switcher_kis'
CONF_AUTO_OFF = 'auto_off'
CONF_DEVICE_ID = 'device_id'
CONF_DEVICE_PASSWORD = 'device_password'
CONF_PHONE_ID = 'phone_id'
@ -40,6 +46,32 @@ 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(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_AUTO_OFF): cv.time_period_str
})
@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."""
@ -58,13 +90,12 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool:
"""On homeassistant stop, gracefully stop the bridge if running."""
await v2bridge.stop()
hass.async_add_job(hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, async_stop_bridge))
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_bridge)
try:
device_data = await wait_for(v2bridge.queue.get(), timeout=10.0)
except (Asyncio_TimeoutError, RuntimeError):
_LOGGER.exception("failed to get response from device")
_LOGGER.exception("Failed to get response from device")
await v2bridge.stop()
return False
@ -72,8 +103,33 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool:
DATA_DEVICE: device_data
}
async def async_switch_platform_discovered(
platform: str, discovery_info: Optional[Dict]) -> None:
"""Use for registering services after switch platform is discoverd."""
if platform != DOMAIN:
return
async def async_set_auto_off_service(service: ServiceCallType) -> None:
"""Use for handling setting device auto-off service calls."""
from aioswitcher.api import SwitcherV2Api
await _validate_edit_permission(
hass, service.context, service.data[CONF_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])
hass.services.async_register(
DOMAIN, SERVICE_SET_AUTO_OFF_NAME,
async_set_auto_off_service,
schema=SERVICE_SET_AUTO_OFF_SCHEMA)
async_listen_platform(
hass, SWITCH_DOMAIN, async_switch_platform_discovered)
hass.async_create_task(async_load_platform(
hass, SWITCH_DOMAIN, DOMAIN, None, config))
hass, SWITCH_DOMAIN, DOMAIN, {}, config))
@callback
def device_updates(timestamp: Optional[datetime]) -> None:

View File

@ -6,7 +6,7 @@
"@tomerfi"
],
"requirements": [
"aioswitcher==2019.3.21"
"aioswitcher==2019.4.26"
],
"dependencies": []
}

View File

@ -0,0 +1,9 @@
set_auto_off:
description: 'Update Switcher device auto off setting.'
fields:
entity_id:
description: "Name of the entity id associated with the integration, used for permission validation."
example: "switch.switcher_kis_boiler"
auto_off:
description: 'Time period string containing hours and minutes.'
example: '"02:30"'

View File

@ -30,7 +30,8 @@ async def async_setup_platform(hass: HomeAssistantType, config: Dict,
async_add_entities: Callable,
discovery_info: Dict) -> None:
"""Set up the switcher platform for the switch component."""
assert DOMAIN in hass.data
if discovery_info is None:
return
async_add_entities([SwitcherControl(hass.data[DOMAIN][DATA_DEVICE])])

View File

@ -7,8 +7,10 @@ import homeassistant.core
GPSType = Tuple[float, float]
ConfigType = Dict[str, Any]
ContextType = homeassistant.core.Context
EventType = homeassistant.core.Event
HomeAssistantType = homeassistant.core.HomeAssistant
ServiceCallType = homeassistant.core.ServiceCall
ServiceDataType = Dict[str, Any]
TemplateVarsType = Optional[Dict[str, Any]]

View File

@ -160,7 +160,7 @@ aiolifx_effects==0.2.2
aiopvapi==1.6.14
# homeassistant.components.switcher_kis
aioswitcher==2019.3.21
aioswitcher==2019.4.26
# homeassistant.components.unifi
aiounifi==6

View File

@ -58,7 +58,7 @@ aiohttp_cors==0.7.0
aiohue==1.9.1
# homeassistant.components.switcher_kis
aioswitcher==2019.3.21
aioswitcher==2019.4.26
# homeassistant.components.unifi
aiounifi==6

View File

@ -98,7 +98,9 @@ def mock_bridge_fixture() -> Generator[None, Any, None]:
patchers = [
patch('aioswitcher.bridge.SwitcherV2Bridge.start', new=mock_bridge),
patch('aioswitcher.bridge.SwitcherV2Bridge.stop', new=mock_bridge),
patch('aioswitcher.bridge.SwitcherV2Bridge.queue', get=mock_queue)
patch('aioswitcher.bridge.SwitcherV2Bridge.queue', get=mock_queue),
patch('aioswitcher.bridge.SwitcherV2Bridge.running',
return_value=True)
]
for patcher in patchers:
@ -130,3 +132,22 @@ def mock_failed_bridge_fixture() -> Generator[None, Any, None]:
for patcher in patchers:
patcher.stop()
@fixture(name='mock_api')
def mock_api_fixture() -> Generator[CoroutineMock, Any, None]:
"""Fixture for mocking aioswitcher.api.SwitcherV2Api."""
mock_api = CoroutineMock()
patchers = [
patch('aioswitcher.api.SwitcherV2Api.connect', new=mock_api),
patch('aioswitcher.api.SwitcherV2Api.disconnect', new=mock_api)
]
for patcher in patchers:
patcher.start()
yield
for patcher in patchers:
patcher.stop()

View File

@ -17,6 +17,9 @@ DUMMY_PHONE_ID = '1234'
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"
MANDATORY_CONFIGURATION = {
DOMAIN: {
CONF_PHONE_ID: DUMMY_PHONE_ID,

View File

@ -1,16 +1,32 @@
"""Test cases for the switcher_kis component."""
from typing import Any, Generator
from datetime import timedelta
from typing import Any, Generator, TYPE_CHECKING
from homeassistant.components.switcher_kis import (DOMAIN, DATA_DEVICE)
from pytest import raises
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.components.switcher_kis import (
CONF_AUTO_OFF, DOMAIN, DATA_DEVICE, SERVICE_SET_AUTO_OFF_NAME,
SERVICE_SET_AUTO_OFF_SCHEMA, SIGNAL_SWITCHER_DEVICE_UPDATE)
from homeassistant.core import callback, Context
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
from tests.common import async_mock_service, async_fire_time_changed
from .consts import (
DUMMY_AUTO_OFF_SET, DUMMY_DEVICE_ID, DUMMY_DEVICE_NAME,
DUMMY_DEVICE_STATE, DUMMY_ELECTRIC_CURRENT, DUMMY_IP_ADDRESS,
DUMMY_MAC_ADDRESS, DUMMY_PHONE_ID, DUMMY_POWER_CONSUMPTION,
DUMMY_REMAINING_TIME, MANDATORY_CONFIGURATION)
DUMMY_REMAINING_TIME, MANDATORY_CONFIGURATION, SWITCH_ENTITY_ID)
if TYPE_CHECKING:
from tests.common import MockUser
from aioswitcher.devices import SwitcherV2Device
async def test_failed_config(
@ -49,3 +65,81 @@ async def test_discovery_data_bucket(
assert device.power_consumption == DUMMY_POWER_CONSUMPTION
assert device.electric_current == DUMMY_ELECTRIC_CURRENT
assert device.phone_id == DUMMY_PHONE_ID
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') -> None:
"""Test the set_auto_off service."""
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION)
await hass.async_block_till_done()
assert hass.services.has_service(DOMAIN, SERVICE_SET_AUTO_OFF_NAME)
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_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, SERVICE_SET_AUTO_OFF_NAME,
{CONF_ENTITY_ID: SWITCH_ENTITY_ID,
CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET},
blocking=True, context=Context(user_id="not_real_user"))
assert unknown_user_exc.type is UnknownUser
service_calls = async_mock_service(
hass, DOMAIN, SERVICE_SET_AUTO_OFF_NAME, SERVICE_SET_AUTO_OFF_SCHEMA)
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()
assert len(service_calls) == 1
assert str(service_calls[0].data[CONF_AUTO_OFF]) \
== DUMMY_AUTO_OFF_SET.lstrip('0')
async def test_signal_dispatcher(
hass: HomeAssistantType,
mock_bridge: Generator[None, Any, None]) -> None:
"""Test signal dispatcher dispatching device updates every 4 seconds."""
assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION)
await hass.async_block_till_done()
@callback
def verify_update_data(device: 'SwitcherV2Device') -> None:
"""Use as callback for signal dispatcher."""
pass
async_dispatcher_connect(
hass, SIGNAL_SWITCHER_DEVICE_UPDATE, verify_update_data)
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=5))