Refactor Remote class in panasonic_viera (#34911)

This commit is contained in:
João Gabriel 2020-04-30 21:35:02 -03:00 committed by GitHub
parent bd72ddda3c
commit 4e55fa6c5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 311 additions and 155 deletions

View File

@ -531,7 +531,6 @@ omit =
homeassistant/components/osramlightify/light.py
homeassistant/components/otp/sensor.py
homeassistant/components/panasonic_bluray/media_player.py
homeassistant/components/panasonic_viera/__init__.py
homeassistant/components/panasonic_viera/media_player.py
homeassistant/components/pandora/media_player.py
homeassistant/components/pcal9535a/*

View File

@ -1,13 +1,29 @@
"""The Panasonic Viera integration."""
import asyncio
from functools import partial
import logging
from urllib.request import URLError
from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError
import voluptuous as vol
from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.script import Script
from .const import CONF_ON_ACTION, DEFAULT_NAME, DEFAULT_PORT, DOMAIN
from .const import (
ATTR_REMOTE,
CONF_APP_ID,
CONF_ENCRYPTION_KEY,
CONF_ON_ACTION,
DEFAULT_NAME,
DEFAULT_PORT,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
@ -28,7 +44,7 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
PLATFORMS = ["media_player"]
PLATFORMS = [MEDIA_PLAYER_DOMAIN]
async def async_setup(hass, config):
@ -49,6 +65,27 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, config_entry):
"""Set up Panasonic Viera from a config entry."""
panasonic_viera_data = hass.data.setdefault(DOMAIN, {})
config = config_entry.data
host = config[CONF_HOST]
port = config[CONF_PORT]
on_action = config[CONF_ON_ACTION]
if on_action is not None:
on_action = Script(hass, on_action)
params = {}
if CONF_APP_ID in config and CONF_ENCRYPTION_KEY in config:
params["app_id"] = config[CONF_APP_ID]
params["encryption_key"] = config[CONF_ENCRYPTION_KEY]
remote = Remote(hass, host, port, on_action, **params)
await remote.async_create_remote_control(during_setup=True)
panasonic_viera_data[config_entry.entry_id] = {ATTR_REMOTE: remote}
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component)
@ -59,7 +96,7 @@ async def async_setup_entry(hass, config_entry):
async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
return all(
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(config_entry, component)
@ -67,3 +104,135 @@ async def async_unload_entry(hass, config_entry):
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
class Remote:
"""The Remote class. It stores the TV properties and the remote control connection itself."""
def __init__(
self, hass, host, port, on_action=None, app_id=None, encryption_key=None,
):
"""Initialize the Remote class."""
self._hass = hass
self._host = host
self._port = port
self._on_action = on_action
self._app_id = app_id
self._encryption_key = encryption_key
self.state = None
self.available = False
self.volume = 0
self.muted = False
self.playing = True
self._control = None
async def async_create_remote_control(self, during_setup=False):
"""Create remote control."""
control_existed = self._control is not None
try:
params = {}
if self._app_id and self._encryption_key:
params["app_id"] = self._app_id
params["encryption_key"] = self._encryption_key
self._control = await self._hass.async_add_executor_job(
partial(RemoteControl, self._host, self._port, **params)
)
self.state = STATE_ON
self.available = True
except (TimeoutError, URLError, SOAPError, OSError) as err:
if control_existed or during_setup:
_LOGGER.debug("Could not establish remote connection: %s", err)
self._control = None
self.state = STATE_OFF
self.available = self._on_action is not None
except Exception as err: # pylint: disable=broad-except
if control_existed or during_setup:
_LOGGER.exception("An unknown error occurred: %s", err)
self._control = None
self.state = STATE_OFF
self.available = self._on_action is not None
async def async_update(self):
"""Update device data."""
if self._control is None:
await self.async_create_remote_control()
return
await self._handle_errors(self._update)
def _update(self):
"""Retrieve the latest data."""
self.muted = self._control.get_mute()
self.volume = self._control.get_volume() / 100
self.state = STATE_ON
self.available = True
async def async_send_key(self, key):
"""Send a key to the TV and handle exceptions."""
try:
key = getattr(Keys, key)
except (AttributeError, TypeError):
key = getattr(key, "value", key)
await self._handle_errors(self._control.send_key, key)
async def async_turn_on(self):
"""Turn on the TV."""
if self._on_action is not None:
await self._on_action.async_run()
self.state = STATE_ON
elif self.state != STATE_ON:
await self.async_send_key(Keys.power)
self.state = STATE_ON
async def async_turn_off(self):
"""Turn off the TV."""
if self.state != STATE_OFF:
await self.async_send_key(Keys.power)
self.state = STATE_OFF
await self.async_update()
async def async_set_mute(self, enable):
"""Set mute based on 'enable'."""
await self._handle_errors(self._control.set_mute, enable)
async def async_set_volume(self, volume):
"""Set volume level, range 0..1."""
volume = int(volume * 100)
await self._handle_errors(self._control.set_volume, volume)
async def async_play_media(self, media_type, media_id):
"""Play media."""
_LOGGER.debug("Play media: %s (%s)", media_id, media_type)
await self._handle_errors(self._control.open_webpage, media_id)
async def _handle_errors(self, func, *args):
"""Handle errors from func, set available and reconnect if needed."""
try:
return await self._hass.async_add_executor_job(func, *args)
except EncryptionRequired:
_LOGGER.error(
"The connection couldn't be encrypted. Please reconfigure your TV"
)
except (TimeoutError, URLError, SOAPError, OSError):
self.state = STATE_OFF
self.available = self._on_action is not None
await self.async_create_remote_control()
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("An unknown error occurred: %s", err)
self.state = STATE_OFF
self.available = self._on_action is not None

View File

@ -10,6 +10,8 @@ CONF_ENCRYPTION_KEY = "encryption_key"
DEFAULT_NAME = "Panasonic Viera TV"
DEFAULT_PORT = 55000
ATTR_REMOTE = "remote"
ERROR_NOT_CONNECTED = "not_connected"
ERROR_INVALID_PIN_CODE = "invalid_pin_code"

View File

@ -1,9 +1,7 @@
"""Support for interface with a Panasonic Viera TV."""
from functools import partial
"""Media player support for Panasonic Viera TV."""
import logging
from urllib.request import URLError
from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError
from panasonic_viera import Keys
from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player.const import (
@ -20,10 +18,9 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON
from homeassistant.helpers.script import Script
from homeassistant.const import CONF_NAME
from .const import CONF_APP_ID, CONF_ENCRYPTION_KEY, CONF_ON_ACTION
from .const import ATTR_REMOTE, DOMAIN
SUPPORT_VIERATV = (
SUPPORT_PAUSE
@ -47,42 +44,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
config = config_entry.data
host = config[CONF_HOST]
port = config[CONF_PORT]
remote = hass.data[DOMAIN][config_entry.entry_id][ATTR_REMOTE]
name = config[CONF_NAME]
on_action = config[CONF_ON_ACTION]
if on_action is not None:
on_action = Script(hass, on_action)
params = {}
if CONF_APP_ID in config and CONF_ENCRYPTION_KEY in config:
params["app_id"] = config[CONF_APP_ID]
params["encryption_key"] = config[CONF_ENCRYPTION_KEY]
remote = Remote(hass, host, port, on_action, **params)
await remote.async_create_remote_control(during_setup=True)
tv_device = PanasonicVieraTVDevice(remote, name)
tv_device = PanasonicVieraTVEntity(remote, name)
async_add_entities([tv_device])
class PanasonicVieraTVDevice(MediaPlayerEntity):
class PanasonicVieraTVEntity(MediaPlayerEntity):
"""Representation of a Panasonic Viera TV."""
def __init__(
self, remote, name, uuid=None,
):
"""Initialize the Panasonic device."""
# Save a reference to the imported class
def __init__(self, remote, name, uuid=None):
"""Initialize the entity."""
self._remote = remote
self._name = name
self._uuid = uuid
@property
def unique_id(self) -> str:
"""Return the unique ID of this Viera TV."""
def unique_id(self):
"""Return the unique ID of the device."""
return self._uuid
@property
@ -97,7 +77,7 @@ class PanasonicVieraTVDevice(MediaPlayerEntity):
@property
def available(self):
"""Return if True the device is available."""
"""Return True if the device is available."""
return self._remote.available
@property
@ -176,125 +156,8 @@ class PanasonicVieraTVDevice(MediaPlayerEntity):
async def async_play_media(self, media_type, media_id, **kwargs):
"""Play media."""
await self._remote.async_play_media(media_type, media_id)
class Remote:
"""The Remote class. It stores the TV properties and the remote control connection itself."""
def __init__(
self, hass, host, port, on_action=None, app_id=None, encryption_key=None,
):
"""Initialize the Remote class."""
self._hass = hass
self._host = host
self._port = port
self._on_action = on_action
self._app_id = app_id
self._encryption_key = encryption_key
self.state = None
self.available = False
self.volume = 0
self.muted = False
self.playing = True
self._control = None
async def async_create_remote_control(self, during_setup=False):
"""Create remote control."""
control_existed = self._control is not None
try:
params = {}
if self._app_id and self._encryption_key:
params["app_id"] = self._app_id
params["encryption_key"] = self._encryption_key
self._control = await self._hass.async_add_executor_job(
partial(RemoteControl, self._host, self._port, **params)
)
self.state = STATE_ON
self.available = True
except (TimeoutError, URLError, SOAPError, OSError) as err:
if control_existed or during_setup:
_LOGGER.error("Could not establish remote connection: %s", err)
self._control = None
self.state = STATE_OFF
self.available = self._on_action is not None
except Exception as err: # pylint: disable=broad-except
if control_existed or during_setup:
_LOGGER.exception("An unknown error occurred: %s", err)
self._control = None
self.state = STATE_OFF
self.available = self._on_action is not None
async def async_update(self):
"""Update device data."""
if self._control is None:
await self.async_create_remote_control()
return
await self._handle_errors(self._update)
async def _update(self):
"""Retrieve the latest data."""
self.muted = self._control.get_mute()
self.volume = self._control.get_volume() / 100
self.state = STATE_ON
self.available = True
async def async_send_key(self, key):
"""Send a key to the TV and handle exceptions."""
await self._handle_errors(self._control.send_key, key)
async def async_turn_on(self):
"""Turn on the TV."""
if self._on_action is not None:
await self._on_action.async_run()
self.state = STATE_ON
elif self.state != STATE_ON:
await self.async_send_key(Keys.power)
self.state = STATE_ON
async def async_turn_off(self):
"""Turn off the TV."""
if self.state != STATE_OFF:
await self.async_send_key(Keys.power)
self.state = STATE_OFF
await self.async_update()
async def async_set_mute(self, enable):
"""Set mute based on 'enable'."""
await self._handle_errors(self._control.set_mute, enable)
async def async_set_volume(self, volume):
"""Set volume level, range 0..1."""
volume = int(volume * 100)
await self._handle_errors(self._control.set_volume, volume)
async def async_play_media(self, media_type, media_id):
"""Play media."""
_LOGGER.debug("Play media: %s (%s)", media_id, media_type)
if media_type != MEDIA_TYPE_URL:
_LOGGER.warning("Unsupported media_type: %s", media_type)
return
await self._handle_errors(self._control.open_webpage, media_id)
async def _handle_errors(self, func, *args):
"""Handle errors from func, set available and reconnect if needed."""
try:
await self._hass.async_add_executor_job(func, *args)
except EncryptionRequired:
_LOGGER.error("The connection couldn't be encrypted")
except (TimeoutError, URLError, SOAPError, OSError):
self.state = STATE_OFF
self.available = self._on_action is not None
await self.async_create_remote_control()
await self._remote.async_play_media(media_type, media_id)

View File

@ -0,0 +1,123 @@
"""Test the Panasonic Viera setup process."""
from unittest.mock import Mock
from asynctest import patch
from homeassistant.components.panasonic_viera.const import (
CONF_APP_ID,
CONF_ENCRYPTION_KEY,
CONF_ON_ACTION,
DEFAULT_NAME,
DEFAULT_PORT,
DOMAIN,
)
from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
MOCK_CONFIG_DATA = {
CONF_HOST: "0.0.0.0",
CONF_NAME: DEFAULT_NAME,
CONF_PORT: DEFAULT_PORT,
CONF_ON_ACTION: None,
}
MOCK_ENCRYPTION_DATA = {
CONF_APP_ID: "mock-app-id",
CONF_ENCRYPTION_KEY: "mock-encryption-key",
}
def get_mock_remote():
"""Return a mock remote."""
mock_remote = Mock()
async def async_create_remote_control(during_setup=False):
return
mock_remote.async_create_remote_control = async_create_remote_control
return mock_remote
async def test_setup_entry_encrypted(hass):
"""Test setup with encrypted config entry."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
unique_id=MOCK_CONFIG_DATA[CONF_HOST],
data={**MOCK_CONFIG_DATA, **MOCK_ENCRYPTION_DATA},
)
mock_entry.add_to_hass(hass)
mock_remote = get_mock_remote()
with patch(
"homeassistant.components.panasonic_viera.Remote", return_value=mock_remote,
):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("media_player.panasonic_viera_tv")
assert state
assert state.name == DEFAULT_NAME
async def test_setup_entry_unencrypted(hass):
"""Test setup with unencrypted config entry."""
mock_entry = MockConfigEntry(
domain=DOMAIN, unique_id=MOCK_CONFIG_DATA[CONF_HOST], data=MOCK_CONFIG_DATA,
)
mock_entry.add_to_hass(hass)
mock_remote = get_mock_remote()
with patch(
"homeassistant.components.panasonic_viera.Remote", return_value=mock_remote,
):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("media_player.panasonic_viera_tv")
assert state
assert state.name == DEFAULT_NAME
async def test_setup_config_flow_initiated(hass):
"""Test if config flow is initiated in setup."""
assert (
await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_HOST: "0.0.0.0"}},)
is True
)
assert len(hass.config_entries.flow.async_progress()) == 1
async def test_setup_unload_entry(hass):
"""Test if config entry is unloaded."""
mock_entry = MockConfigEntry(
domain=DOMAIN, unique_id=MOCK_CONFIG_DATA[CONF_HOST], data=MOCK_CONFIG_DATA
)
mock_entry.add_to_hass(hass)
mock_remote = get_mock_remote()
with patch(
"homeassistant.components.panasonic_viera.Remote", return_value=mock_remote,
):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
await hass.config_entries.async_unload(mock_entry.entry_id)
assert mock_entry.state == ENTRY_STATE_NOT_LOADED
state = hass.states.get("media_player.panasonic_viera_tv")
assert state is None