media_player.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. """PJLink2 media_player platform."""
  2. from __future__ import annotations
  3. from collections.abc import Callable
  4. from datetime import timedelta
  5. import logging
  6. from typing import Any
  7. from aiopjlink import (
  8. PJLink,
  9. PJLinkException,
  10. PJLinkProjectorError,
  11. Power,
  12. Sources,
  13. Lamp,
  14. Information,
  15. )
  16. from homeassistant import config_entries, core
  17. from homeassistant.components.media_player import (
  18. MediaPlayerEntity,
  19. MediaPlayerEntityFeature,
  20. MediaPlayerState,
  21. PLATFORM_SCHEMA,
  22. )
  23. from homeassistant.const import (
  24. CONF_HOST,
  25. CONF_PORT,
  26. CONF_NAME,
  27. CONF_PASSWORD,
  28. CONF_TIMEOUT,
  29. )
  30. from homeassistant.core import HomeAssistant as HomeAssistantType
  31. import homeassistant.helpers.config_validation as cv
  32. from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
  33. import voluptuous as vol
  34. from .const import (
  35. DOMAIN,
  36. CONF_ENCODING,
  37. DEFAULT_ENCODING,
  38. DEFAULT_PORT,
  39. DEFAULT_TIMEOUT,
  40. ATTR_PRODUCT_NAME,
  41. ATTR_MANUFACTURER_NAME,
  42. ATTR_PROJECTOR_NAME,
  43. ATTR_RESOLUTION_X,
  44. ATTR_RESOLUTION_Y,
  45. ATTR_LAMP_HOURS,
  46. ProjectorState,
  47. )
  48. _LOGGER = logging.getLogger(__name__)
  49. # Time between updating data from projector
  50. SCAN_INTERVAL = timedelta(seconds=3)
  51. PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
  52. {
  53. vol.Required(CONF_HOST): cv.string,
  54. vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
  55. vol.Optional(CONF_NAME): cv.string,
  56. vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
  57. vol.Optional(CONF_PASSWORD): cv.string,
  58. vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_float,
  59. }
  60. )
  61. async def async_setup_platform(
  62. hass: HomeAssistantType,
  63. config: ConfigType,
  64. async_add_entities: Callable,
  65. discovery_info: DiscoveryInfoType | None = None,
  66. ) -> None:
  67. """Set up the media_player platform."""
  68. host = config.get(CONF_HOST)
  69. port = config.get(CONF_PORT)
  70. password = config.get(CONF_PASSWORD)
  71. timeout = config.get(CONF_TIMEOUT)
  72. name = config.get(CONF_NAME)
  73. pjl = PJLink(host, port, password, timeout)
  74. devices = [PJLink2MediaPlayer(pjl, name)]
  75. async_add_entities(devices, update_before_add=False)
  76. class PJLink2MediaPlayer(MediaPlayerEntity):
  77. """Representation of a PJLink2 media player."""
  78. _attr_supported_features = (
  79. MediaPlayerEntityFeature.TURN_ON
  80. | MediaPlayerEntityFeature.TURN_OFF
  81. | MediaPlayerEntityFeature.SELECT_SOURCE
  82. )
  83. def __init__(self, pjl, name):
  84. super().__init__()
  85. self._projector = pjl
  86. self.attrs: dict[str, Any] = {}
  87. self._name = name
  88. self._state = MediaPlayerState.OFF
  89. self._available = False
  90. self._connectionErrorLogged = False
  91. self._current_source = None
  92. # PJLink standard inputs. You can modify these to friendly names later.
  93. # Format is typically "31" for HDMI1, "32" for HDMI2, etc.
  94. self._source_list = ["11", "12", "21", "22", "31", "32", "33"]
  95. async def async_will_remove_from_hass(self) -> None:
  96. """Close connection."""
  97. await super().async_will_remove_from_hass()
  98. if self._available:
  99. try:
  100. await self._projector.__aexit__(0, 0, 0)
  101. except (PJLinkException, OSError) as err:
  102. _LOGGER.error(
  103. "PJLink2 ERROR when closing connection: %s", repr(err)
  104. )
  105. @property
  106. def name(self) -> str:
  107. return self._name
  108. @property
  109. def unique_id(self) -> str:
  110. return self._projector._address
  111. @property
  112. def available(self) -> bool:
  113. return self._available
  114. @property
  115. def state(self) -> MediaPlayerState:
  116. return self._state
  117. @property
  118. def source(self) -> str | None:
  119. """Name of the current input source."""
  120. return self._current_source
  121. @property
  122. def source_list(self) -> list[str]:
  123. """List of available input sources."""
  124. return self._source_list
  125. @property
  126. def extra_state_attributes(self) -> dict[str, Any]:
  127. """Return the custom PJLink2 attributes."""
  128. return self.attrs
  129. async def async_turn_on(self) -> None:
  130. """Turn the projector on."""
  131. await Power(self._projector).set(Power.ON)
  132. self._state = MediaPlayerState.ON
  133. async def async_turn_off(self) -> None:
  134. """Turn the projector off."""
  135. await Power(self._projector).set(Power.OFF)
  136. self._state = MediaPlayerState.OFF
  137. async def async_select_source(self, source: str) -> None:
  138. """Select input source."""
  139. source_type = source[0]
  140. source_index = source[1]
  141. await Sources(self._projector).set(source_type, source_index)
  142. self._current_source = source
  143. async def async_update(self) -> None:
  144. """Update data from projector."""
  145. try:
  146. if not self._available:
  147. await self._projector.__aenter__()
  148. self._available = True
  149. info = await Information(self._projector).table()
  150. self.attrs[ATTR_PRODUCT_NAME] = info.get("product_name")
  151. self.attrs[ATTR_MANUFACTURER_NAME] = info.get(
  152. "manufacturer_name"
  153. )
  154. self.attrs[ATTR_PROJECTOR_NAME] = info.get("projector_name")
  155. if self._name is None:
  156. self._name = info.get("projector_name")
  157. pwr = await Power(self._projector).get()
  158. if pwr == Power.State.OFF:
  159. self._state = MediaPlayerState.OFF
  160. elif pwr == Power.State.ON:
  161. self._state = MediaPlayerState.ON
  162. elif pwr in (Power.State.COOLING, Power.State.WARMING):
  163. self._state = MediaPlayerState.ON
  164. if pwr == Power.ON:
  165. # 1. Fetch current source
  166. try:
  167. current = await Sources(self._projector).get()
  168. if isinstance(current, (tuple, list)):
  169. self._current_source = "".join(map(str, current))
  170. else:
  171. self._current_source = str(current)
  172. except PJLinkException:
  173. pass # Keep previous state if temporarily unavailable
  174. # 2. Fetch Lamp Hours
  175. try:
  176. self.attrs[ATTR_LAMP_HOURS] = await Lamp(
  177. self._projector
  178. ).hours()
  179. except PJLinkException:
  180. pass
  181. # 3. Fetch Resolution
  182. try:
  183. res = await Sources(self._projector).resolution()
  184. self.attrs[ATTR_RESOLUTION_X] = res[0]
  185. self.attrs[ATTR_RESOLUTION_Y] = res[1]
  186. except PJLinkException:
  187. self.attrs.pop(ATTR_RESOLUTION_X, None)
  188. self.attrs.pop(ATTR_RESOLUTION_Y, None)
  189. else:
  190. self.attrs.pop(ATTR_RESOLUTION_X, None)
  191. self.attrs.pop(ATTR_RESOLUTION_Y, None)
  192. self._current_source = None
  193. self._connectionErrorLogged = False
  194. except (PJLinkException, OSError) as err:
  195. err_str = repr(err)
  196. # If the projector is just busy switching inputs/states, ignore and wait for next poll
  197. if "ERR3" in err_str or "unavailable" in err_str:
  198. pass
  199. else:
  200. if not self._connectionErrorLogged:
  201. _LOGGER.error(
  202. "PJLink2 ERROR for %s: %s", self._name, err_str
  203. )
  204. self._connectionErrorLogged = True
  205. self._state = MediaPlayerState.OFF
  206. if self._available:
  207. self._available = False
  208. try:
  209. await self._projector.__aexit__(0, 0, 0)
  210. except Exception:
  211. pass