mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 08:17:08 +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
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.auth.permissions.const import POLICY_EDIT
|
||||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import CONF_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import callback
|
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 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.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
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__)
|
_LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'switcher_kis'
|
DOMAIN = 'switcher_kis'
|
||||||
|
|
||||||
|
CONF_AUTO_OFF = 'auto_off'
|
||||||
CONF_DEVICE_ID = 'device_id'
|
CONF_DEVICE_ID = 'device_id'
|
||||||
CONF_DEVICE_PASSWORD = 'device_password'
|
CONF_DEVICE_PASSWORD = 'device_password'
|
||||||
CONF_PHONE_ID = 'phone_id'
|
CONF_PHONE_ID = 'phone_id'
|
||||||
@ -40,6 +46,32 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, 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:
|
async def async_setup(hass: HomeAssistantType, config: Dict) -> bool:
|
||||||
"""Set up the switcher component."""
|
"""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."""
|
"""On homeassistant stop, gracefully stop the bridge if running."""
|
||||||
await v2bridge.stop()
|
await v2bridge.stop()
|
||||||
|
|
||||||
hass.async_add_job(hass.bus.async_listen_once(
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_bridge)
|
||||||
EVENT_HOMEASSISTANT_STOP, async_stop_bridge))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
device_data = await wait_for(v2bridge.queue.get(), timeout=10.0)
|
device_data = await wait_for(v2bridge.queue.get(), timeout=10.0)
|
||||||
except (Asyncio_TimeoutError, RuntimeError):
|
except (Asyncio_TimeoutError, RuntimeError):
|
||||||
_LOGGER.exception("failed to get response from device")
|
_LOGGER.exception("Failed to get response from device")
|
||||||
await v2bridge.stop()
|
await v2bridge.stop()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -72,8 +103,33 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool:
|
|||||||
DATA_DEVICE: device_data
|
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.async_create_task(async_load_platform(
|
||||||
hass, SWITCH_DOMAIN, DOMAIN, None, config))
|
hass, SWITCH_DOMAIN, DOMAIN, {}, config))
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def device_updates(timestamp: Optional[datetime]) -> None:
|
def device_updates(timestamp: Optional[datetime]) -> None:
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"@tomerfi"
|
"@tomerfi"
|
||||||
],
|
],
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aioswitcher==2019.3.21"
|
"aioswitcher==2019.4.26"
|
||||||
],
|
],
|
||||||
"dependencies": []
|
"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,
|
async_add_entities: Callable,
|
||||||
discovery_info: Dict) -> None:
|
discovery_info: Dict) -> None:
|
||||||
"""Set up the switcher platform for the switch component."""
|
"""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])])
|
async_add_entities([SwitcherControl(hass.data[DOMAIN][DATA_DEVICE])])
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,8 +7,10 @@ import homeassistant.core
|
|||||||
|
|
||||||
GPSType = Tuple[float, float]
|
GPSType = Tuple[float, float]
|
||||||
ConfigType = Dict[str, Any]
|
ConfigType = Dict[str, Any]
|
||||||
|
ContextType = homeassistant.core.Context
|
||||||
EventType = homeassistant.core.Event
|
EventType = homeassistant.core.Event
|
||||||
HomeAssistantType = homeassistant.core.HomeAssistant
|
HomeAssistantType = homeassistant.core.HomeAssistant
|
||||||
|
ServiceCallType = homeassistant.core.ServiceCall
|
||||||
ServiceDataType = Dict[str, Any]
|
ServiceDataType = Dict[str, Any]
|
||||||
TemplateVarsType = Optional[Dict[str, Any]]
|
TemplateVarsType = Optional[Dict[str, Any]]
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ aiolifx_effects==0.2.2
|
|||||||
aiopvapi==1.6.14
|
aiopvapi==1.6.14
|
||||||
|
|
||||||
# homeassistant.components.switcher_kis
|
# homeassistant.components.switcher_kis
|
||||||
aioswitcher==2019.3.21
|
aioswitcher==2019.4.26
|
||||||
|
|
||||||
# homeassistant.components.unifi
|
# homeassistant.components.unifi
|
||||||
aiounifi==6
|
aiounifi==6
|
||||||
|
@ -58,7 +58,7 @@ aiohttp_cors==0.7.0
|
|||||||
aiohue==1.9.1
|
aiohue==1.9.1
|
||||||
|
|
||||||
# homeassistant.components.switcher_kis
|
# homeassistant.components.switcher_kis
|
||||||
aioswitcher==2019.3.21
|
aioswitcher==2019.4.26
|
||||||
|
|
||||||
# homeassistant.components.unifi
|
# homeassistant.components.unifi
|
||||||
aiounifi==6
|
aiounifi==6
|
||||||
|
@ -98,7 +98,9 @@ def mock_bridge_fixture() -> Generator[None, Any, None]:
|
|||||||
patchers = [
|
patchers = [
|
||||||
patch('aioswitcher.bridge.SwitcherV2Bridge.start', new=mock_bridge),
|
patch('aioswitcher.bridge.SwitcherV2Bridge.start', new=mock_bridge),
|
||||||
patch('aioswitcher.bridge.SwitcherV2Bridge.stop', 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:
|
for patcher in patchers:
|
||||||
@ -130,3 +132,22 @@ def mock_failed_bridge_fixture() -> Generator[None, Any, None]:
|
|||||||
|
|
||||||
for patcher in patchers:
|
for patcher in patchers:
|
||||||
patcher.stop()
|
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_POWER_CONSUMPTION = 2780
|
||||||
DUMMY_REMAINING_TIME = '01:29:32'
|
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 = {
|
MANDATORY_CONFIGURATION = {
|
||||||
DOMAIN: {
|
DOMAIN: {
|
||||||
CONF_PHONE_ID: DUMMY_PHONE_ID,
|
CONF_PHONE_ID: DUMMY_PHONE_ID,
|
||||||
|
@ -1,16 +1,32 @@
|
|||||||
"""Test cases for the switcher_kis component."""
|
"""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.helpers.typing import HomeAssistantType
|
||||||
|
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||||
from homeassistant.setup import async_setup_component
|
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 (
|
from .consts import (
|
||||||
DUMMY_AUTO_OFF_SET, DUMMY_DEVICE_ID, DUMMY_DEVICE_NAME,
|
DUMMY_AUTO_OFF_SET, DUMMY_DEVICE_ID, DUMMY_DEVICE_NAME,
|
||||||
DUMMY_DEVICE_STATE, DUMMY_ELECTRIC_CURRENT, DUMMY_IP_ADDRESS,
|
DUMMY_DEVICE_STATE, DUMMY_ELECTRIC_CURRENT, DUMMY_IP_ADDRESS,
|
||||||
DUMMY_MAC_ADDRESS, DUMMY_PHONE_ID, DUMMY_POWER_CONSUMPTION,
|
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(
|
async def test_failed_config(
|
||||||
@ -49,3 +65,81 @@ async def test_discovery_data_bucket(
|
|||||||
assert device.power_consumption == DUMMY_POWER_CONSUMPTION
|
assert device.power_consumption == DUMMY_POWER_CONSUMPTION
|
||||||
assert device.electric_current == DUMMY_ELECTRIC_CURRENT
|
assert device.electric_current == DUMMY_ELECTRIC_CURRENT
|
||||||
assert device.phone_id == DUMMY_PHONE_ID
|
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