Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update requirement version and add switcher_kis services #23477

Merged
merged 19 commits into from Jun 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 64 additions & 8 deletions homeassistant/components/switcher_kis/__init__.py
Expand Up @@ -7,19 +7,25 @@

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'
Expand All @@ -40,6 +46,32 @@
})
}, 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."""
Expand All @@ -58,22 +90,46 @@ async def async_stop_bridge(event: EventType) -> None:
"""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

hass.data[DOMAIN] = {
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:
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
"""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:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/switcher_kis/manifest.json
Expand Up @@ -6,7 +6,7 @@
"@tomerfi"
],
"requirements": [
"aioswitcher==2019.3.21"
"aioswitcher==2019.4.26"
],
"dependencies": []
}
9 changes: 9 additions & 0 deletions homeassistant/components/switcher_kis/services.yaml
@@ -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"'
3 changes: 2 additions & 1 deletion homeassistant/components/switcher_kis/switch.py
Expand Up @@ -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])])


Expand Down
2 changes: 2 additions & 0 deletions homeassistant/helpers/typing.py
Expand Up @@ -7,8 +7,10 @@

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]]

Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Expand Up @@ -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
Expand Down
23 changes: 22 additions & 1 deletion tests/components/switcher_kis/conftest.py
Expand Up @@ -98,7 +98,9 @@ async def mock_queue():
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:
Expand Down Expand Up @@ -130,3 +132,22 @@ async def mock_queue():

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()
3 changes: 3 additions & 0 deletions tests/components/switcher_kis/consts.py
Expand Up @@ -17,6 +17,9 @@
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,
Expand Down
100 changes: 97 additions & 3 deletions tests/components/switcher_kis/test_init.py
@@ -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(
Expand Down Expand Up @@ -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))