Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Panasonic Viera integration."""
2 
3 from collections.abc import Callable
4 from functools import partial
5 import logging
6 from typing import Any
7 from urllib.error import HTTPError, URLError
8 
9 from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError
10 import voluptuous as vol
11 
12 from homeassistant.components.media_player import MediaPlayerState, MediaType
13 from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
14 from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform
15 from homeassistant.core import Context, HomeAssistant
17 from homeassistant.helpers.script import Script
18 from homeassistant.helpers.typing import ConfigType
19 
20 from .const import (
21  ATTR_DEVICE_INFO,
22  ATTR_REMOTE,
23  ATTR_UDN,
24  CONF_APP_ID,
25  CONF_ENCRYPTION_KEY,
26  CONF_ON_ACTION,
27  DEFAULT_NAME,
28  DEFAULT_PORT,
29  DOMAIN,
30 )
31 
32 _LOGGER = logging.getLogger(__name__)
33 
34 CONFIG_SCHEMA = vol.Schema(
35  {
36  DOMAIN: vol.All(
37  cv.ensure_list,
38  [
39  vol.Schema(
40  {
41  vol.Required(CONF_HOST): cv.string,
42  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
43  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
44  vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
45  }
46  )
47  ],
48  )
49  },
50  extra=vol.ALLOW_EXTRA,
51 )
52 
53 PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]
54 
55 
56 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
57  """Set up Panasonic Viera from configuration.yaml."""
58  if DOMAIN not in config:
59  return True
60 
61  for conf in config[DOMAIN]:
62  hass.async_create_task(
63  hass.config_entries.flow.async_init(
64  DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
65  )
66  )
67 
68  return True
69 
70 
71 async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
72  """Set up Panasonic Viera from a config entry."""
73  panasonic_viera_data = hass.data.setdefault(DOMAIN, {})
74 
75  config = config_entry.data
76 
77  host = config[CONF_HOST]
78  port = config[CONF_PORT]
79 
80  if (on_action := config[CONF_ON_ACTION]) is not None:
81  on_action = Script(hass, on_action, config[CONF_NAME], DOMAIN)
82 
83  params = {}
84  if CONF_APP_ID in config and CONF_ENCRYPTION_KEY in config:
85  params["app_id"] = config[CONF_APP_ID]
86  params["encryption_key"] = config[CONF_ENCRYPTION_KEY]
87 
88  remote = Remote(hass, host, port, on_action, **params)
89  await remote.async_create_remote_control(during_setup=True)
90 
91  panasonic_viera_data[config_entry.entry_id] = {ATTR_REMOTE: remote}
92 
93  # Add device_info to older config entries
94  if ATTR_DEVICE_INFO not in config or config[ATTR_DEVICE_INFO] is None:
95  device_info = await remote.async_get_device_info()
96  unique_id = config_entry.unique_id
97  if device_info is None:
98  _LOGGER.error(
99  "Couldn't gather device info; Please restart Home Assistant with your"
100  " TV turned on and connected to your network"
101  )
102  else:
103  unique_id = device_info[ATTR_UDN]
104  hass.config_entries.async_update_entry(
105  config_entry,
106  unique_id=unique_id,
107  data={**config, ATTR_DEVICE_INFO: device_info},
108  )
109 
110  await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
111 
112  return True
113 
114 
115 async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
116  """Unload a config entry."""
117  unload_ok = await hass.config_entries.async_unload_platforms(
118  config_entry, PLATFORMS
119  )
120  if unload_ok:
121  hass.data[DOMAIN].pop(config_entry.entry_id)
122 
123  return unload_ok
124 
125 
126 class Remote:
127  """The Remote class. It stores the TV properties and the remote control connection itself."""
128 
129  def __init__(
130  self,
131  hass: HomeAssistant,
132  host: str,
133  port: int,
134  on_action: Script | None = None,
135  app_id: str | None = None,
136  encryption_key: str | None = None,
137  ) -> None:
138  """Initialize the Remote class."""
139  self._hass_hass = hass
140 
141  self._host_host = host
142  self._port_port = port
143 
144  self._on_action_on_action = on_action
145 
146  self._app_id_app_id = app_id
147  self._encryption_key_encryption_key = encryption_key
148 
149  self._control_control: RemoteControl | None = None
150  self.statestate: MediaPlayerState | None = None
151  self.availableavailable: bool = False
152  self.volumevolume: float = 0
153  self.mutedmuted: bool = False
154  self.playing: bool = True
155 
156  async def async_create_remote_control(self, during_setup: bool = False) -> None:
157  """Create remote control."""
158  try:
159  params = {}
160  if self._app_id_app_id and self._encryption_key_encryption_key:
161  params["app_id"] = self._app_id_app_id
162  params["encryption_key"] = self._encryption_key_encryption_key
163 
164  self._control_control = await self._hass_hass.async_add_executor_job(
165  partial(RemoteControl, self._host_host, self._port_port, **params)
166  )
167 
168  if during_setup:
169  await self.async_updateasync_update()
170  except (URLError, SOAPError, OSError) as err:
171  _LOGGER.debug("Could not establish remote connection: %s", err)
172  self._control_control = None
173  self.statestate = MediaPlayerState.OFF
174  self.availableavailable = self._on_action_on_action is not None
175  except Exception:
176  _LOGGER.exception("An unknown error occurred")
177  self._control_control = None
178  self.statestate = MediaPlayerState.OFF
179  self.availableavailable = self._on_action_on_action is not None
180 
181  async def async_update(self) -> None:
182  """Update device data."""
183  if self._control_control is None:
184  await self.async_create_remote_controlasync_create_remote_control()
185  return
186 
187  await self._handle_errors(self._update_update)
188 
189  def _update(self) -> None:
190  """Retrieve the latest data."""
191  assert self._control_control is not None
192  self.mutedmuted = self._control_control.get_mute()
193  self.volumevolume = self._control_control.get_volume() / 100
194 
195  async def async_send_key(self, key: Keys | str) -> None:
196  """Send a key to the TV and handle exceptions."""
197  try:
198  key = getattr(Keys, key.upper())
199  except (AttributeError, TypeError):
200  key = getattr(key, "value", key)
201 
202  assert self._control_control is not None
203  await self._handle_errors(self._control_control.send_key, key)
204 
205  async def async_turn_on(self, context: Context | None) -> None:
206  """Turn on the TV."""
207  if self._on_action_on_action is not None:
208  await self._on_action_on_action.async_run(context=context)
209  await self.async_updateasync_update()
210  elif self.statestate is not MediaPlayerState.ON:
211  await self.async_send_keyasync_send_key(Keys.POWER)
212  await self.async_updateasync_update()
213 
214  async def async_turn_off(self) -> None:
215  """Turn off the TV."""
216  if self.statestate is not MediaPlayerState.OFF:
217  await self.async_send_keyasync_send_key(Keys.POWER)
218  self.statestate = MediaPlayerState.OFF
219  await self.async_updateasync_update()
220 
221  async def async_set_mute(self, enable: bool) -> None:
222  """Set mute based on 'enable'."""
223  assert self._control_control is not None
224  await self._handle_errors(self._control_control.set_mute, enable)
225 
226  async def async_set_volume(self, volume: float) -> None:
227  """Set volume level, range 0..1."""
228  assert self._control_control is not None
229  volume = int(volume * 100)
230  await self._handle_errors(self._control_control.set_volume, volume)
231 
232  async def async_play_media(self, media_type: MediaType, media_id: str) -> None:
233  """Play media."""
234  assert self._control_control is not None
235  _LOGGER.debug("Play media: %s (%s)", media_id, media_type)
236  await self._handle_errors(self._control_control.open_webpage, media_id)
237 
238  async def async_get_device_info(self) -> dict[str, Any] | None:
239  """Return device info."""
240  if self._control_control is None:
241  return None
242  device_info = await self._handle_errors(self._control_control.get_device_info)
243  _LOGGER.debug("Fetched device info: %s", str(device_info))
244  return device_info
245 
246  async def _handle_errors[_R, *_Ts](
247  self, func: Callable[[*_Ts], _R], *args: *_Ts
248  ) -> _R | None:
249  """Handle errors from func, set available and reconnect if needed."""
250  try:
251  result = await self._hass_hass.async_add_executor_job(func, *args)
252  except EncryptionRequired:
253  _LOGGER.error(
254  "The connection couldn't be encrypted. Please reconfigure your TV"
255  )
256  self.availableavailable = False
257  return None
258  except (SOAPError, HTTPError) as err:
259  _LOGGER.debug("An error occurred: %s", err)
260  self.statestate = MediaPlayerState.OFF
261  self.availableavailable = True
262  await self.async_create_remote_controlasync_create_remote_control()
263  return None
264  except (URLError, OSError) as err:
265  _LOGGER.debug("An error occurred: %s", err)
266  self.statestate = MediaPlayerState.OFF
267  self.availableavailable = self._on_action_on_action is not None
268  await self.async_create_remote_controlasync_create_remote_control()
269  return None
270  except Exception:
271  _LOGGER.exception("An unknown error occurred")
272  self.statestate = MediaPlayerState.OFF
273  self.availableavailable = self._on_action_on_action is not None
274  return None
275  self.statestate = MediaPlayerState.ON
276  self.availableavailable = True
277  return result
None async_play_media(self, MediaType media_type, str media_id)
Definition: __init__.py:232
None __init__(self, HomeAssistant hass, str host, int port, Script|None on_action=None, str|None app_id=None, str|None encryption_key=None)
Definition: __init__.py:137
None async_set_volume(self, float volume)
Definition: __init__.py:226
dict[str, Any]|None async_get_device_info(self)
Definition: __init__.py:238
None async_turn_on(self, Context|None context)
Definition: __init__.py:205
None async_create_remote_control(self, bool during_setup=False)
Definition: __init__.py:156
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:56
bool async_setup_entry(HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:71
bool async_unload_entry(HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:115