mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 00:07:10 +00:00
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:
parent
f9b3ba2887
commit
fe8a330a45
@ -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:
|
||||
|
@ -6,7 +6,7 @@
|
||||
"@tomerfi"
|
||||
],
|
||||
"requirements": [
|
||||
"aioswitcher==2019.3.21"
|
||||
"aioswitcher==2019.4.26"
|
||||
],
|
||||
"dependencies": []
|
||||
}
|
||||
|
9
homeassistant/components/switcher_kis/services.yaml
Normal file
9
homeassistant/components/switcher_kis/services.yaml
Normal 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"'
|
@ -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])])
|
||||
|
||||
|
||||
|
@ -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]]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
|
Loading…
x
Reference in New Issue
Block a user