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:
parent
14a64ea970
commit
8dc06e612f
16 changed files with 721 additions and 86 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
),
|
||||
)
|
||||
|
|
90
homeassistant/components/philips_js/config_flow.py
Normal file
90
homeassistant/components/philips_js/config_flow.py
Normal 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."""
|
4
homeassistant/components/philips_js/const.py
Normal file
4
homeassistant/components/philips_js/const.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
"""The Philips TV constants."""
|
||||
|
||||
DOMAIN = "philips_js"
|
||||
CONF_SYSTEM = "system"
|
65
homeassistant/components/philips_js/device_trigger.py
Normal file
65
homeassistant/components/philips_js/device_trigger.py
Normal 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)
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
|
24
homeassistant/components/philips_js/strings.json
Normal file
24
homeassistant/components/philips_js/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
24
homeassistant/components/philips_js/translations/en.json
Normal file
24
homeassistant/components/philips_js/translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -159,6 +159,7 @@ FLOWS = [
|
|||
"owntracks",
|
||||
"ozw",
|
||||
"panasonic_viera",
|
||||
"philips_js",
|
||||
"pi_hole",
|
||||
"plaato",
|
||||
"plex",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
25
tests/components/philips_js/__init__.py
Normal file
25
tests/components/philips_js/__init__.py
Normal 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"
|
62
tests/components/philips_js/conftest.py
Normal file
62
tests/components/philips_js/conftest.py
Normal 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)},
|
||||
)
|
105
tests/components/philips_js/test_config_flow.py
Normal file
105
tests/components/philips_js/test_config_flow.py
Normal 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"}
|
69
tests/components/philips_js/test_device_trigger.py
Normal file
69
tests/components/philips_js/test_device_trigger.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue