media_player.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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["product_name"]
  151. self.attrs[ATTR_MANUFACTURER_NAME] = info["manufacturer_name"]
  152. self.attrs[ATTR_PROJECTOR_NAME] = info["projector_name"]
  153. if self._name == None:
  154. self._name = info["projector_name"]
  155. pwr = await Power(self._projector).get()
  156. if pwr == Power.State.OFF:
  157. self._state = MediaPlayerState.OFF
  158. elif pwr == Power.State.ON:
  159. self._state = MediaPlayerState.ON
  160. elif pwr in (Power.State.COOLING, Power.State.WARMING):
  161. self._state = (
  162. MediaPlayerState.ON
  163. ) # Keeps UI active during transition
  164. if pwr == Power.ON:
  165. res = await Sources(self._projector).resolution()
  166. self.attrs[ATTR_RESOLUTION_X] = res[0]
  167. self.attrs[ATTR_RESOLUTION_Y] = res[1]
  168. self.attrs[ATTR_LAMP_HOURS] = await Lamp(
  169. self._projector
  170. ).hours()
  171. # Fetch current source
  172. current = await Sources(self._projector).get()
  173. if isinstance(current, (tuple, list)):
  174. self._current_source = "".join(map(str, current))
  175. else:
  176. self._current_source = str(current)
  177. else:
  178. self.attrs.pop(ATTR_RESOLUTION_X, None)
  179. self.attrs.pop(ATTR_RESOLUTION_Y, None)
  180. self._current_source = None
  181. self._connectionErrorLogged = False
  182. except PJLinkProjectorError:
  183. self.attrs.pop(ATTR_RESOLUTION_X, None)
  184. self.attrs.pop(ATTR_RESOLUTION_Y, None)
  185. except (PJLinkException, OSError) as err:
  186. if not self._connectionErrorLogged:
  187. _LOGGER.error("PJLink2 ERROR for %s: %s", self._name, repr(err))
  188. self._connectionErrorLogged = True
  189. self._state = MediaPlayerState.OFF
  190. if self._available:
  191. self._available = False
  192. try:
  193. await self._projector.__aexit__(0, 0, 0)
  194. except Exception:
  195. pass