Add config flow to philips_js (#45784)

* Add config flow to philips_js

* Adjust name of entry to contain serial

* Use device id in event rather than entity id

* Adjust turn on text

* Deprecate all fields

* Be somewhat more explicit in typing

* Switch to direct coordinator access

* Refactor the pluggable action

* Adjust tests a bit

* Minor adjustment

* More adjustments

* Add missing await in update coordinator

* Be more lenient to lack of system info

* Use constant for trigger type and simplify

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Joakim Plate 2021-02-11 21:37:53 +01:00 committed by GitHub
parent 14a64ea970
commit 8dc06e612f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 721 additions and 86 deletions

View file

@ -704,6 +704,7 @@ omit =
homeassistant/components/pandora/media_player.py
homeassistant/components/pcal9535a/*
homeassistant/components/pencom/switch.py
homeassistant/components/philips_js/__init__.py
homeassistant/components/philips_js/media_player.py
homeassistant/components/pi_hole/sensor.py
homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py

View file

@ -1 +1,131 @@
"""The philips_js component."""
"""The Philips TV integration."""
import asyncio
from datetime import timedelta
import logging
from typing import Any, Callable, Dict, Optional
from haphilipsjs import ConnectionFailure, PhilipsTV
from homeassistant.components.automation import AutomationActionType
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_VERSION, CONF_HOST
from homeassistant.core import Context, HassJob, HomeAssistant, callback
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
PLATFORMS = ["media_player"]
LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Philips TV component."""
hass.data[DOMAIN] = {}
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Philips TV from a config entry."""
tvapi = PhilipsTV(entry.data[CONF_HOST], entry.data[CONF_API_VERSION])
coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi)
await coordinator.async_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class PluggableAction:
"""A pluggable action handler."""
_actions: Dict[Any, AutomationActionType] = {}
def __init__(self, update: Callable[[], None]):
"""Initialize."""
self._update = update
def __bool__(self):
"""Return if we have something attached."""
return bool(self._actions)
@callback
def async_attach(self, action: AutomationActionType, variables: Dict[str, Any]):
"""Attach a device trigger for turn on."""
@callback
def _remove():
del self._actions[_remove]
self._update()
job = HassJob(action)
self._actions[_remove] = (job, variables)
self._update()
return _remove
async def async_run(
self, hass: HomeAssistantType, context: Optional[Context] = None
):
"""Run all turn on triggers."""
for job, variables in self._actions.values():
hass.async_run_hass_job(job, variables, context)
class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""Coordinator to update data."""
api: PhilipsTV
def __init__(self, hass, api: PhilipsTV) -> None:
"""Set up the coordinator."""
self.api = api
def _update_listeners():
for update_callback in self._listeners:
update_callback()
self.turn_on = PluggableAction(_update_listeners)
async def _async_update():
try:
await self.hass.async_add_executor_job(self.api.update)
except ConnectionFailure:
pass
super().__init__(
hass,
LOGGER,
name=DOMAIN,
update_method=_async_update,
update_interval=timedelta(seconds=30),
request_refresh_debouncer=Debouncer(
hass, LOGGER, cooldown=2.0, immediate=False
),
)

View file

@ -0,0 +1,90 @@
"""Config flow for Philips TV integration."""
import logging
from typing import Any, Dict, Optional, TypedDict
from haphilipsjs import ConnectionFailure, PhilipsTV
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_API_VERSION, CONF_HOST
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
class FlowUserDict(TypedDict):
"""Data for user step."""
host: str
api_version: int
async def validate_input(hass: core.HomeAssistant, data: FlowUserDict):
"""Validate the user input allows us to connect."""
hub = PhilipsTV(data[CONF_HOST], data[CONF_API_VERSION])
await hass.async_add_executor_job(hub.getSystem)
if hub.system is None:
raise ConnectionFailure
return hub.system
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Philips TV."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
_default = {}
async def async_step_import(self, conf: Dict[str, Any]):
"""Import a configuration from config.yaml."""
for entry in self._async_current_entries():
if entry.data[CONF_HOST] == conf[CONF_HOST]:
return self.async_abort(reason="already_configured")
return await self.async_step_user(
{
CONF_HOST: conf[CONF_HOST],
CONF_API_VERSION: conf[CONF_API_VERSION],
}
)
async def async_step_user(self, user_input: Optional[FlowUserDict] = None):
"""Handle the initial step."""
errors = {}
if user_input:
self._default = user_input
try:
system = await validate_input(self.hass, user_input)
except ConnectionFailure:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(system["serialnumber"])
self._abort_if_unique_id_configured(updates=user_input)
data = {**user_input, "system": system}
return self.async_create_entry(
title=f"{system['name']} ({system['serialnumber']})", data=data
)
schema = vol.Schema(
{
vol.Required(CONF_HOST, default=self._default.get(CONF_HOST)): str,
vol.Required(
CONF_API_VERSION, default=self._default.get(CONF_API_VERSION)
): vol.In([1, 6]),
}
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""

View file

@ -0,0 +1,4 @@
"""The Philips TV constants."""
DOMAIN = "philips_js"
CONF_SYSTEM = "system"

View file

@ -0,0 +1,65 @@
"""Provides device automations for control of device."""
from typing import List
import voluptuous as vol
from homeassistant.components.automation import AutomationActionType
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry
from homeassistant.helpers.typing import ConfigType
from . import PhilipsTVDataUpdateCoordinator
from .const import DOMAIN
TRIGGER_TYPE_TURN_ON = "turn_on"
TRIGGER_TYPES = {TRIGGER_TYPE_TURN_ON}
TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
}
)
async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
"""List device triggers for device."""
triggers = []
triggers.append(
{
CONF_PLATFORM: "device",
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_TYPE: TRIGGER_TYPE_TURN_ON,
}
)
return triggers
async def async_attach_trigger(
hass: HomeAssistant,
config: ConfigType,
action: AutomationActionType,
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
registry: DeviceRegistry = await async_get_registry(hass)
if config[CONF_TYPE] == TRIGGER_TYPE_TURN_ON:
variables = {
"trigger": {
"platform": "device",
"domain": DOMAIN,
"device_id": config[CONF_DEVICE_ID],
"description": f"philips_js '{config[CONF_TYPE]}' event",
}
}
device = registry.async_get(config[CONF_DEVICE_ID])
for config_entry_id in device.config_entries:
coordinator: PhilipsTVDataUpdateCoordinator = hass.data[DOMAIN].get(
config_entry_id
)
if coordinator:
return coordinator.turn_on.async_attach(action, variables)

View file

@ -2,6 +2,11 @@
"domain": "philips_js",
"name": "Philips TV",
"documentation": "https://www.home-assistant.io/integrations/philips_js",
"requirements": ["ha-philipsjs==0.0.8"],
"codeowners": ["@elupus"]
}
"requirements": [
"ha-philipsjs==0.1.0"
],
"codeowners": [
"@elupus"
],
"config_flow": true
}

View file

@ -1,11 +1,11 @@
"""Media Player component to integrate TVs exposing the Joint Space API."""
from datetime import timedelta
import logging
from typing import Any, Dict
from haphilipsjs import PhilipsTV
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.media_player import (
DEVICE_CLASS_TV,
PLATFORM_SCHEMA,
BrowseMedia,
MediaPlayerEntity,
@ -27,6 +27,7 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_STEP,
)
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.philips_js import PhilipsTVDataUpdateCoordinator
from homeassistant.const import (
CONF_API_VERSION,
CONF_HOST,
@ -34,11 +35,13 @@ from homeassistant.const import (
STATE_OFF,
STATE_ON,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import call_later, track_time_interval
from homeassistant.helpers.script import Script
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
_LOGGER = logging.getLogger(__name__)
from . import LOGGER as _LOGGER
from .const import CONF_SYSTEM, DOMAIN
SUPPORT_PHILIPS_JS = (
SUPPORT_TURN_OFF
@ -54,24 +57,25 @@ SUPPORT_PHILIPS_JS = (
CONF_ON_ACTION = "turn_on_action"
DEFAULT_NAME = "Philips TV"
DEFAULT_API_VERSION = "1"
DEFAULT_SCAN_INTERVAL = 30
DELAY_ACTION_DEFAULT = 2.0
DELAY_ACTION_ON = 10.0
PREFIX_SEPARATOR = ": "
PREFIX_SOURCE = "Input"
PREFIX_CHANNEL = "Channel"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): cv.string,
vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
}
PLATFORM_SCHEMA = vol.All(
cv.deprecated(CONF_HOST),
cv.deprecated(CONF_NAME),
cv.deprecated(CONF_API_VERSION),
cv.deprecated(CONF_ON_ACTION),
PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Remove(CONF_NAME): cv.string,
vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): cv.string,
vol.Remove(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
}
),
)
@ -81,70 +85,69 @@ def _inverted(data):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Philips TV platform."""
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
api_version = config.get(CONF_API_VERSION)
turn_on_action = config.get(CONF_ON_ACTION)
tvapi = PhilipsTV(host, api_version)
domain = __name__.split(".")[-2]
on_script = Script(hass, turn_on_action, name, domain) if turn_on_action else None
add_entities([PhilipsTVMediaPlayer(tvapi, name, on_script)])
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=config,
)
)
class PhilipsTVMediaPlayer(MediaPlayerEntity):
async def async_setup_entry(
hass: HomeAssistantType,
config_entry: config_entries.ConfigEntry,
async_add_entities,
):
"""Set up the configuration entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
[
PhilipsTVMediaPlayer(
coordinator,
config_entry.data[CONF_SYSTEM],
config_entry.unique_id or config_entry.entry_id,
)
]
)
class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
"""Representation of a Philips TV exposing the JointSpace API."""
def __init__(self, tv: PhilipsTV, name: str, on_script: Script):
def __init__(
self,
coordinator: PhilipsTVDataUpdateCoordinator,
system: Dict[str, Any],
unique_id: str,
):
"""Initialize the Philips TV."""
self._tv = tv
self._name = name
self._tv = coordinator.api
self._coordinator = coordinator
self._sources = {}
self._channels = {}
self._on_script = on_script
self._supports = SUPPORT_PHILIPS_JS
if self._on_script:
self._supports |= SUPPORT_TURN_ON
self._update_task = None
self._system = system
self._unique_id = unique_id
super().__init__(coordinator)
self._update_from_coordinator()
def _update_soon(self, delay):
def _update_soon(self):
"""Reschedule update task."""
if self._update_task:
self._update_task()
self._update_task = None
self.schedule_update_ha_state(force_refresh=False)
def update_forced(event_time):
self.schedule_update_ha_state(force_refresh=True)
def update_and_restart(event_time):
update_forced(event_time)
self._update_task = track_time_interval(
self.hass, update_forced, timedelta(seconds=DEFAULT_SCAN_INTERVAL)
)
call_later(self.hass, delay, update_and_restart)
async def async_added_to_hass(self):
"""Start running updates once we are added to hass."""
await self.hass.async_add_executor_job(self._update_soon, 0)
self.hass.add_job(self.coordinator.async_request_refresh)
@property
def name(self):
"""Return the device name."""
return self._name
@property
def should_poll(self):
"""Device should be polled."""
return False
return self._system["name"]
@property
def supported_features(self):
"""Flag media player features that are supported."""
return self._supports
supports = self._supports
if self._coordinator.turn_on:
supports |= SUPPORT_TURN_ON
return supports
@property
def state(self):
@ -178,7 +181,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
source_id = _inverted(self._sources).get(source)
if source_id:
self._tv.setSource(source_id)
self._update_soon(DELAY_ACTION_DEFAULT)
self._update_soon()
@property
def volume_level(self):
@ -190,47 +193,45 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
"""Boolean if volume is currently muted."""
return self._tv.muted
def turn_on(self):
async def async_turn_on(self):
"""Turn on the device."""
if self._on_script:
self._on_script.run(context=self._context)
self._update_soon(DELAY_ACTION_ON)
await self._coordinator.turn_on.async_run(self.hass, self._context)
def turn_off(self):
"""Turn off the device."""
self._tv.sendKey("Standby")
self._tv.on = False
self._update_soon(DELAY_ACTION_DEFAULT)
self._update_soon()
def volume_up(self):
"""Send volume up command."""
self._tv.sendKey("VolumeUp")
self._update_soon(DELAY_ACTION_DEFAULT)
self._update_soon()
def volume_down(self):
"""Send volume down command."""
self._tv.sendKey("VolumeDown")
self._update_soon(DELAY_ACTION_DEFAULT)
self._update_soon()
def mute_volume(self, mute):
"""Send mute command."""
self._tv.setVolume(None, mute)
self._update_soon(DELAY_ACTION_DEFAULT)
self._update_soon()
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self._tv.setVolume(volume, self._tv.muted)
self._update_soon(DELAY_ACTION_DEFAULT)
self._update_soon()
def media_previous_track(self):
"""Send rewind command."""
self._tv.sendKey("Previous")
self._update_soon(DELAY_ACTION_DEFAULT)
self._update_soon()
def media_next_track(self):
"""Send fast forward command."""
self._tv.sendKey("Next")
self._update_soon(DELAY_ACTION_DEFAULT)
self._update_soon()
@property
def media_channel(self):
@ -267,6 +268,29 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
"""Return the state attributes."""
return {"channel_list": list(self._channels.values())}
@property
def device_class(self):
"""Return the device class."""
return DEVICE_CLASS_TV
@property
def unique_id(self):
"""Return unique identifier if known."""
return self._unique_id
@property
def device_info(self):
"""Return a device description for device registry."""
return {
"name": self._system["name"],
"identifiers": {
(DOMAIN, self._unique_id),
},
"model": self._system.get("model"),
"manufacturer": "Philips",
"sw_version": self._system.get("softwareversion"),
}
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
_LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id)
@ -275,7 +299,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
channel_id = _inverted(self._channels).get(media_id)
if channel_id:
self._tv.setChannel(channel_id)
self._update_soon(DELAY_ACTION_DEFAULT)
self._update_soon()
else:
_LOGGER.error("Unable to find channel <%s>", media_id)
else:
@ -308,10 +332,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
],
)
def update(self):
"""Get the latest data and update device state."""
self._tv.update()
def _update_from_coordinator(self):
self._sources = {
srcid: source.get("name") or f"Source {srcid}"
for srcid, source in (self._tv.sources or {}).items()
@ -321,3 +342,9 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
chid: channel.get("name") or f"Channel {chid}"
for chid, channel in (self._tv.channels or {}).items()
}
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_from_coordinator()
super()._handle_coordinator_update()

View file

@ -0,0 +1,24 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"api_version": "API Version"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"device_automation": {
"trigger_type": {
"turn_on": "Device is requested to turn on"
}
}
}

View file

@ -0,0 +1,24 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"host": "Host",
"api_version": "API Version"
}
}
}
},
"device_automation": {
"trigger_type": {
"turn_on": "Device is requested to turn on"
}
}
}

View file

@ -159,6 +159,7 @@ FLOWS = [
"owntracks",
"ozw",
"panasonic_viera",
"philips_js",
"pi_hole",
"plaato",
"plex",

View file

@ -720,7 +720,7 @@ guppy3==3.1.0
ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
ha-philipsjs==0.0.8
ha-philipsjs==0.1.0
# homeassistant.components.habitica
habitipy==0.2.0

View file

@ -380,6 +380,9 @@ guppy3==3.1.0
# homeassistant.components.ffmpeg
ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
ha-philipsjs==0.1.0
# homeassistant.components.hangouts
hangups==0.4.11

View file

@ -0,0 +1,25 @@
"""Tests for the Philips TV integration."""
MOCK_SERIAL_NO = "1234567890"
MOCK_NAME = "Philips TV"
MOCK_SYSTEM = {
"menulanguage": "English",
"name": MOCK_NAME,
"country": "Sweden",
"serialnumber": MOCK_SERIAL_NO,
"softwareversion": "abcd",
"model": "modelname",
}
MOCK_USERINPUT = {
"host": "1.1.1.1",
"api_version": 1,
}
MOCK_CONFIG = {
**MOCK_USERINPUT,
"system": MOCK_SYSTEM,
}
MOCK_ENTITY_ID = "media_player.philips_tv"

View file

@ -0,0 +1,62 @@
"""Standard setup for tests."""
from unittest.mock import Mock, patch
from pytest import fixture
from homeassistant import setup
from homeassistant.components.philips_js.const import DOMAIN
from . import MOCK_CONFIG, MOCK_ENTITY_ID, MOCK_NAME, MOCK_SERIAL_NO, MOCK_SYSTEM
from tests.common import MockConfigEntry, mock_device_registry
@fixture(autouse=True)
async def setup_notification(hass):
"""Configure notification system."""
await setup.async_setup_component(hass, "persistent_notification", {})
@fixture(autouse=True)
def mock_tv():
"""Disable component actual use."""
tv = Mock(autospec="philips_js.PhilipsTV")
tv.sources = {}
tv.channels = {}
tv.system = MOCK_SYSTEM
with patch(
"homeassistant.components.philips_js.config_flow.PhilipsTV", return_value=tv
), patch("homeassistant.components.philips_js.PhilipsTV", return_value=tv):
yield tv
@fixture
async def mock_config_entry(hass):
"""Get standard player."""
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME)
config_entry.add_to_hass(hass)
return config_entry
@fixture
def mock_device_reg(hass):
"""Get standard device."""
return mock_device_registry(hass)
@fixture
async def mock_entity(hass, mock_device_reg, mock_config_entry):
"""Get standard player."""
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
yield MOCK_ENTITY_ID
@fixture
def mock_device(hass, mock_device_reg, mock_entity, mock_config_entry):
"""Get standard device."""
return mock_device_reg.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={(DOMAIN, MOCK_SERIAL_NO)},
)

View file

@ -0,0 +1,105 @@
"""Test the Philips TV config flow."""
from unittest.mock import patch
from pytest import fixture
from homeassistant import config_entries
from homeassistant.components.philips_js.const import DOMAIN
from . import MOCK_CONFIG, MOCK_USERINPUT
@fixture(autouse=True)
def mock_setup():
"""Disable component setup."""
with patch(
"homeassistant.components.philips_js.async_setup", return_value=True
) as mock_setup:
yield mock_setup
@fixture(autouse=True)
def mock_setup_entry():
"""Disable component setup."""
with patch(
"homeassistant.components.philips_js.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
async def test_import(hass, mock_setup, mock_setup_entry):
"""Test we get an item on import."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=MOCK_USERINPUT,
)
assert result["type"] == "create_entry"
assert result["title"] == "Philips TV (1234567890)"
assert result["data"] == MOCK_CONFIG
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_import_exist(hass, mock_config_entry):
"""Test we get an item on import."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=MOCK_USERINPUT,
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
async def test_form(hass, mock_setup, mock_setup_entry):
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
MOCK_USERINPUT,
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "Philips TV (1234567890)"
assert result2["data"] == MOCK_CONFIG
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_cannot_connect(hass, mock_tv):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
mock_tv.system = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], MOCK_USERINPUT
)
assert result["type"] == "form"
assert result["errors"] == {"base": "cannot_connect"}
async def test_form_unexpected_error(hass, mock_tv):
"""Test we handle unexpected exceptions."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
mock_tv.getSystem.side_effect = Exception("Unexpected exception")
result = await hass.config_entries.flow.async_configure(
result["flow_id"], MOCK_USERINPUT
)
assert result["type"] == "form"
assert result["errors"] == {"base": "unknown"}

View file

@ -0,0 +1,69 @@
"""The tests for Philips TV device triggers."""
import pytest
import homeassistant.components.automation as automation
from homeassistant.components.philips_js.const import DOMAIN
from homeassistant.setup import async_setup_component
from tests.common import (
assert_lists_same,
async_get_device_automations,
async_mock_service,
)
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
def calls(hass):
"""Track calls to a mock service."""
return async_mock_service(hass, "test", "automation")
async def test_get_triggers(hass, mock_device):
"""Test we get the expected triggers."""
expected_triggers = [
{
"platform": "device",
"domain": DOMAIN,
"type": "turn_on",
"device_id": mock_device.id,
},
]
triggers = await async_get_device_automations(hass, "trigger", mock_device.id)
assert_lists_same(triggers, expected_triggers)
async def test_if_fires_on_turn_on_request(hass, calls, mock_entity, mock_device):
"""Test for turn_on and turn_off triggers firing."""
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": "device",
"domain": DOMAIN,
"device_id": mock_device.id,
"type": "turn_on",
},
"action": {
"service": "test.automation",
"data_template": {"some": "{{ trigger.device_id }}"},
},
}
]
},
)
await hass.services.async_call(
"media_player",
"turn_on",
{"entity_id": mock_entity},
blocking=True,
)
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].data["some"] == mock_device.id