mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 17:27:10 +00:00
Add typing to Panasonic Viera (#120772)
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
This commit is contained in:
parent
921430d497
commit
f08638eead
@ -1,22 +1,18 @@
|
|||||||
"""The Panasonic Viera integration."""
|
"""The Panasonic Viera integration."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
from urllib.error import HTTPError, URLError
|
from urllib.error import HTTPError, URLError
|
||||||
|
|
||||||
from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError
|
from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import MediaPlayerState, MediaType
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform
|
||||||
CONF_HOST,
|
from homeassistant.core import Context, HomeAssistant
|
||||||
CONF_NAME,
|
|
||||||
CONF_PORT,
|
|
||||||
STATE_OFF,
|
|
||||||
STATE_ON,
|
|
||||||
Platform,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.script import Script
|
from homeassistant.helpers.script import Script
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
@ -132,13 +128,13 @@ class Remote:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass,
|
hass: HomeAssistant,
|
||||||
host,
|
host: str,
|
||||||
port,
|
port: int,
|
||||||
on_action=None,
|
on_action: Script | None = None,
|
||||||
app_id=None,
|
app_id: str | None = None,
|
||||||
encryption_key=None,
|
encryption_key: str | None = None,
|
||||||
):
|
) -> None:
|
||||||
"""Initialize the Remote class."""
|
"""Initialize the Remote class."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
|
||||||
@ -150,15 +146,14 @@ class Remote:
|
|||||||
self._app_id = app_id
|
self._app_id = app_id
|
||||||
self._encryption_key = encryption_key
|
self._encryption_key = encryption_key
|
||||||
|
|
||||||
self.state = None
|
self._control: RemoteControl | None = None
|
||||||
self.available = False
|
self.state: MediaPlayerState | None = None
|
||||||
self.volume = 0
|
self.available: bool = False
|
||||||
self.muted = False
|
self.volume: float = 0
|
||||||
self.playing = True
|
self.muted: bool = False
|
||||||
|
self.playing: bool = True
|
||||||
|
|
||||||
self._control = None
|
async def async_create_remote_control(self, during_setup: bool = False) -> None:
|
||||||
|
|
||||||
async def async_create_remote_control(self, during_setup=False):
|
|
||||||
"""Create remote control."""
|
"""Create remote control."""
|
||||||
try:
|
try:
|
||||||
params = {}
|
params = {}
|
||||||
@ -175,15 +170,15 @@ class Remote:
|
|||||||
except (URLError, SOAPError, OSError) as err:
|
except (URLError, SOAPError, OSError) as err:
|
||||||
_LOGGER.debug("Could not establish remote connection: %s", err)
|
_LOGGER.debug("Could not establish remote connection: %s", err)
|
||||||
self._control = None
|
self._control = None
|
||||||
self.state = STATE_OFF
|
self.state = MediaPlayerState.OFF
|
||||||
self.available = self._on_action is not None
|
self.available = self._on_action is not None
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("An unknown error occurred")
|
_LOGGER.exception("An unknown error occurred")
|
||||||
self._control = None
|
self._control = None
|
||||||
self.state = STATE_OFF
|
self.state = MediaPlayerState.OFF
|
||||||
self.available = self._on_action is not None
|
self.available = self._on_action is not None
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self) -> None:
|
||||||
"""Update device data."""
|
"""Update device data."""
|
||||||
if self._control is None:
|
if self._control is None:
|
||||||
await self.async_create_remote_control()
|
await self.async_create_remote_control()
|
||||||
@ -191,8 +186,9 @@ class Remote:
|
|||||||
|
|
||||||
await self._handle_errors(self._update)
|
await self._handle_errors(self._update)
|
||||||
|
|
||||||
def _update(self):
|
def _update(self) -> None:
|
||||||
"""Retrieve the latest data."""
|
"""Retrieve the latest data."""
|
||||||
|
assert self._control is not None
|
||||||
self.muted = self._control.get_mute()
|
self.muted = self._control.get_mute()
|
||||||
self.volume = self._control.get_volume() / 100
|
self.volume = self._control.get_volume() / 100
|
||||||
|
|
||||||
@ -203,39 +199,43 @@ class Remote:
|
|||||||
except (AttributeError, TypeError):
|
except (AttributeError, TypeError):
|
||||||
key = getattr(key, "value", key)
|
key = getattr(key, "value", key)
|
||||||
|
|
||||||
|
assert self._control is not None
|
||||||
await self._handle_errors(self._control.send_key, key)
|
await self._handle_errors(self._control.send_key, key)
|
||||||
|
|
||||||
async def async_turn_on(self, context):
|
async def async_turn_on(self, context: Context | None) -> None:
|
||||||
"""Turn on the TV."""
|
"""Turn on the TV."""
|
||||||
if self._on_action is not None:
|
if self._on_action is not None:
|
||||||
await self._on_action.async_run(context=context)
|
await self._on_action.async_run(context=context)
|
||||||
await self.async_update()
|
await self.async_update()
|
||||||
elif self.state != STATE_ON:
|
elif self.state is not MediaPlayerState.ON:
|
||||||
await self.async_send_key(Keys.POWER)
|
await self.async_send_key(Keys.POWER)
|
||||||
await self.async_update()
|
await self.async_update()
|
||||||
|
|
||||||
async def async_turn_off(self):
|
async def async_turn_off(self) -> None:
|
||||||
"""Turn off the TV."""
|
"""Turn off the TV."""
|
||||||
if self.state != STATE_OFF:
|
if self.state is not MediaPlayerState.OFF:
|
||||||
await self.async_send_key(Keys.POWER)
|
await self.async_send_key(Keys.POWER)
|
||||||
self.state = STATE_OFF
|
self.state = MediaPlayerState.OFF
|
||||||
await self.async_update()
|
await self.async_update()
|
||||||
|
|
||||||
async def async_set_mute(self, enable):
|
async def async_set_mute(self, enable: bool) -> None:
|
||||||
"""Set mute based on 'enable'."""
|
"""Set mute based on 'enable'."""
|
||||||
|
assert self._control is not None
|
||||||
await self._handle_errors(self._control.set_mute, enable)
|
await self._handle_errors(self._control.set_mute, enable)
|
||||||
|
|
||||||
async def async_set_volume(self, volume):
|
async def async_set_volume(self, volume: float) -> None:
|
||||||
"""Set volume level, range 0..1."""
|
"""Set volume level, range 0..1."""
|
||||||
|
assert self._control is not None
|
||||||
volume = int(volume * 100)
|
volume = int(volume * 100)
|
||||||
await self._handle_errors(self._control.set_volume, volume)
|
await self._handle_errors(self._control.set_volume, volume)
|
||||||
|
|
||||||
async def async_play_media(self, media_type, media_id):
|
async def async_play_media(self, media_type: MediaType, media_id: str) -> None:
|
||||||
"""Play media."""
|
"""Play media."""
|
||||||
|
assert self._control is not None
|
||||||
_LOGGER.debug("Play media: %s (%s)", media_id, media_type)
|
_LOGGER.debug("Play media: %s (%s)", media_id, media_type)
|
||||||
await self._handle_errors(self._control.open_webpage, media_id)
|
await self._handle_errors(self._control.open_webpage, media_id)
|
||||||
|
|
||||||
async def async_get_device_info(self):
|
async def async_get_device_info(self) -> dict[str, Any] | None:
|
||||||
"""Return device info."""
|
"""Return device info."""
|
||||||
if self._control is None:
|
if self._control is None:
|
||||||
return None
|
return None
|
||||||
@ -243,7 +243,9 @@ class Remote:
|
|||||||
_LOGGER.debug("Fetched device info: %s", str(device_info))
|
_LOGGER.debug("Fetched device info: %s", str(device_info))
|
||||||
return device_info
|
return device_info
|
||||||
|
|
||||||
async def _handle_errors(self, func, *args):
|
async def _handle_errors[_R, *_Ts](
|
||||||
|
self, func: Callable[[*_Ts], _R], *args: *_Ts
|
||||||
|
) -> _R | None:
|
||||||
"""Handle errors from func, set available and reconnect if needed."""
|
"""Handle errors from func, set available and reconnect if needed."""
|
||||||
try:
|
try:
|
||||||
result = await self._hass.async_add_executor_job(func, *args)
|
result = await self._hass.async_add_executor_job(func, *args)
|
||||||
@ -252,23 +254,24 @@ class Remote:
|
|||||||
"The connection couldn't be encrypted. Please reconfigure your TV"
|
"The connection couldn't be encrypted. Please reconfigure your TV"
|
||||||
)
|
)
|
||||||
self.available = False
|
self.available = False
|
||||||
|
return None
|
||||||
except (SOAPError, HTTPError) as err:
|
except (SOAPError, HTTPError) as err:
|
||||||
_LOGGER.debug("An error occurred: %s", err)
|
_LOGGER.debug("An error occurred: %s", err)
|
||||||
self.state = STATE_OFF
|
self.state = MediaPlayerState.OFF
|
||||||
self.available = True
|
self.available = True
|
||||||
await self.async_create_remote_control()
|
await self.async_create_remote_control()
|
||||||
return None
|
return None
|
||||||
except (URLError, OSError) as err:
|
except (URLError, OSError) as err:
|
||||||
_LOGGER.debug("An error occurred: %s", err)
|
_LOGGER.debug("An error occurred: %s", err)
|
||||||
self.state = STATE_OFF
|
self.state = MediaPlayerState.OFF
|
||||||
self.available = self._on_action is not None
|
self.available = self._on_action is not None
|
||||||
await self.async_create_remote_control()
|
await self.async_create_remote_control()
|
||||||
return None
|
return None
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("An unknown error occurred")
|
_LOGGER.exception("An unknown error occurred")
|
||||||
self.state = STATE_OFF
|
self.state = MediaPlayerState.OFF
|
||||||
self.available = self._on_action is not None
|
self.available = self._on_action is not None
|
||||||
return None
|
return None
|
||||||
self.state = STATE_ON
|
self.state = MediaPlayerState.ON
|
||||||
self.available = True
|
self.available = True
|
||||||
return result
|
return result
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
from urllib.error import URLError
|
from urllib.error import URLError
|
||||||
|
|
||||||
from panasonic_viera import TV_TYPE_ENCRYPTED, RemoteControl, SOAPError
|
from panasonic_viera import TV_TYPE_ENCRYPTED, RemoteControl, SOAPError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -33,7 +34,7 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize the Panasonic Viera config flow."""
|
"""Initialize the Panasonic Viera config flow."""
|
||||||
self._data = {
|
self._data: dict[str, Any] = {
|
||||||
CONF_HOST: None,
|
CONF_HOST: None,
|
||||||
CONF_NAME: None,
|
CONF_NAME: None,
|
||||||
CONF_PORT: None,
|
CONF_PORT: None,
|
||||||
@ -41,11 +42,13 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
ATTR_DEVICE_INFO: None,
|
ATTR_DEVICE_INFO: None,
|
||||||
}
|
}
|
||||||
|
|
||||||
self._remote = None
|
self._remote: RemoteControl | None = None
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
errors = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
await self.async_load_data(user_input)
|
await self.async_load_data(user_input)
|
||||||
@ -53,7 +56,7 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self._remote = await self.hass.async_add_executor_job(
|
self._remote = await self.hass.async_add_executor_job(
|
||||||
partial(RemoteControl, self._data[CONF_HOST], self._data[CONF_PORT])
|
partial(RemoteControl, self._data[CONF_HOST], self._data[CONF_PORT])
|
||||||
)
|
)
|
||||||
|
assert self._remote is not None
|
||||||
self._data[ATTR_DEVICE_INFO] = await self.hass.async_add_executor_job(
|
self._data[ATTR_DEVICE_INFO] = await self.hass.async_add_executor_job(
|
||||||
self._remote.get_device_info
|
self._remote.get_device_info
|
||||||
)
|
)
|
||||||
@ -63,8 +66,7 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("An unknown error occurred")
|
_LOGGER.exception("An unknown error occurred")
|
||||||
return self.async_abort(reason="unknown")
|
return self.async_abort(reason="unknown")
|
||||||
|
else:
|
||||||
if "base" not in errors:
|
|
||||||
await self.async_set_unique_id(self._data[ATTR_DEVICE_INFO][ATTR_UDN])
|
await self.async_set_unique_id(self._data[ATTR_DEVICE_INFO][ATTR_UDN])
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
@ -102,9 +104,12 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_pairing(self, user_input=None):
|
async def async_step_pairing(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
"""Handle the pairing step."""
|
"""Handle the pairing step."""
|
||||||
errors = {}
|
errors: dict[str, str] = {}
|
||||||
|
assert self._remote is not None
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
pin = user_input[CONF_PIN]
|
pin = user_input[CONF_PIN]
|
||||||
@ -152,11 +157,13 @@ class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_import(self, import_config):
|
async def async_step_import(
|
||||||
|
self, import_config: dict[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
"""Import a config entry from configuration.yaml."""
|
"""Import a config entry from configuration.yaml."""
|
||||||
return await self.async_step_user(user_input=import_config)
|
return await self.async_step_user(user_input=import_config)
|
||||||
|
|
||||||
async def async_load_data(self, config):
|
async def async_load_data(self, config: dict[str, Any]) -> None:
|
||||||
"""Load the data."""
|
"""Load the data."""
|
||||||
self._data = config
|
self._data = config
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ from homeassistant.components.media_player import (
|
|||||||
MediaPlayerDeviceClass,
|
MediaPlayerDeviceClass,
|
||||||
MediaPlayerEntity,
|
MediaPlayerEntity,
|
||||||
MediaPlayerEntityFeature,
|
MediaPlayerEntityFeature,
|
||||||
|
MediaPlayerState,
|
||||||
MediaType,
|
MediaType,
|
||||||
async_process_play_media_url,
|
async_process_play_media_url,
|
||||||
)
|
)
|
||||||
@ -72,6 +73,7 @@ class PanasonicVieraTVEntity(MediaPlayerEntity):
|
|||||||
)
|
)
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
|
_attr_device_class = MediaPlayerDeviceClass.TV
|
||||||
|
|
||||||
def __init__(self, remote, name, device_info):
|
def __init__(self, remote, name, device_info):
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
@ -88,12 +90,7 @@ class PanasonicVieraTVEntity(MediaPlayerEntity):
|
|||||||
self._attr_name = name
|
self._attr_name = name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def state(self) -> MediaPlayerState | None:
|
||||||
"""Return the device class of the device."""
|
|
||||||
return MediaPlayerDeviceClass.TV
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self):
|
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
return self._remote.state
|
return self._remote.state
|
||||||
|
|
||||||
@ -103,12 +100,12 @@ class PanasonicVieraTVEntity(MediaPlayerEntity):
|
|||||||
return self._remote.available
|
return self._remote.available
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self) -> float | None:
|
||||||
"""Volume level of the media player (0..1)."""
|
"""Volume level of the media player (0..1)."""
|
||||||
return self._remote.volume
|
return self._remote.volume
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_volume_muted(self):
|
def is_volume_muted(self) -> bool | None:
|
||||||
"""Boolean if volume is currently muted."""
|
"""Boolean if volume is currently muted."""
|
||||||
return self._remote.muted
|
return self._remote.muted
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import Remote
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_DEVICE_INFO,
|
ATTR_DEVICE_INFO,
|
||||||
ATTR_MANUFACTURER,
|
ATTR_MANUFACTURER,
|
||||||
@ -43,7 +44,9 @@ async def async_setup_entry(
|
|||||||
class PanasonicVieraRemoteEntity(RemoteEntity):
|
class PanasonicVieraRemoteEntity(RemoteEntity):
|
||||||
"""Representation of a Panasonic Viera TV Remote."""
|
"""Representation of a Panasonic Viera TV Remote."""
|
||||||
|
|
||||||
def __init__(self, remote, name, device_info):
|
def __init__(
|
||||||
|
self, remote: Remote, name: str, device_info: dict[str, Any] | None = None
|
||||||
|
) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
# Save a reference to the imported class
|
# Save a reference to the imported class
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
@ -51,7 +54,7 @@ class PanasonicVieraRemoteEntity(RemoteEntity):
|
|||||||
self._device_info = device_info
|
self._device_info = device_info
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self) -> str | None:
|
||||||
"""Return the unique ID of the device."""
|
"""Return the unique ID of the device."""
|
||||||
if self._device_info is None:
|
if self._device_info is None:
|
||||||
return None
|
return None
|
||||||
@ -70,7 +73,7 @@ class PanasonicVieraRemoteEntity(RemoteEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@ -80,7 +83,7 @@ class PanasonicVieraRemoteEntity(RemoteEntity):
|
|||||||
return self._remote.available
|
return self._remote.available
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self) -> bool:
|
||||||
"""Return true if device is on."""
|
"""Return true if device is on."""
|
||||||
return self._remote.state == STATE_ON
|
return self._remote.state == STATE_ON
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user