|
@@ -74,7 +74,6 @@ async def async_setup_platform(
|
|
|
async_add_entities: Callable,
|
|
async_add_entities: Callable,
|
|
|
discovery_info: DiscoveryInfoType | None = None,
|
|
discovery_info: DiscoveryInfoType | None = None,
|
|
|
) -> None:
|
|
) -> None:
|
|
|
- """Set up the media_player platform."""
|
|
|
|
|
host = config.get(CONF_HOST)
|
|
host = config.get(CONF_HOST)
|
|
|
port = config.get(CONF_PORT)
|
|
port = config.get(CONF_PORT)
|
|
|
password = config.get(CONF_PASSWORD)
|
|
password = config.get(CONF_PASSWORD)
|
|
@@ -86,7 +85,6 @@ async def async_setup_platform(
|
|
|
|
|
|
|
|
|
|
|
|
|
class PJLink2MediaPlayer(MediaPlayerEntity):
|
|
class PJLink2MediaPlayer(MediaPlayerEntity):
|
|
|
- """Representation of a PJLink2 media player."""
|
|
|
|
|
|
|
|
|
|
_attr_supported_features = (
|
|
_attr_supported_features = (
|
|
|
MediaPlayerEntityFeature.TURN_ON
|
|
MediaPlayerEntityFeature.TURN_ON
|
|
@@ -100,12 +98,14 @@ class PJLink2MediaPlayer(MediaPlayerEntity):
|
|
|
self.attrs: dict[str, Any] = {}
|
|
self.attrs: dict[str, Any] = {}
|
|
|
self._name = name
|
|
self._name = name
|
|
|
self._state = MediaPlayerState.OFF
|
|
self._state = MediaPlayerState.OFF
|
|
|
- self._available = False
|
|
|
|
|
|
|
+
|
|
|
|
|
+ # The Fix: Decoupling TCP socket state from HA's availability state
|
|
|
|
|
+ self._socket_open = False
|
|
|
|
|
+ self._is_available = False
|
|
|
|
|
+
|
|
|
self._connectionErrorLogged = False
|
|
self._connectionErrorLogged = False
|
|
|
self._current_source = None
|
|
self._current_source = None
|
|
|
|
|
|
|
|
- # --- FRIENDLY NAMES MAPPING ---
|
|
|
|
|
- # Change these values to whatever you want them to be called in Home Assistant
|
|
|
|
|
self._source_mapping = {
|
|
self._source_mapping = {
|
|
|
"31": "HDMI 1",
|
|
"31": "HDMI 1",
|
|
|
"32": "HDMI 2",
|
|
"32": "HDMI 2",
|
|
@@ -116,9 +116,8 @@ class PJLink2MediaPlayer(MediaPlayerEntity):
|
|
|
self._source_list = list(self._source_mapping.values())
|
|
self._source_list = list(self._source_mapping.values())
|
|
|
|
|
|
|
|
async def async_will_remove_from_hass(self) -> None:
|
|
async def async_will_remove_from_hass(self) -> None:
|
|
|
- """Close connection."""
|
|
|
|
|
await super().async_will_remove_from_hass()
|
|
await super().async_will_remove_from_hass()
|
|
|
- if self._available:
|
|
|
|
|
|
|
+ if self._socket_open:
|
|
|
try:
|
|
try:
|
|
|
await self._projector.__aexit__(0, 0, 0)
|
|
await self._projector.__aexit__(0, 0, 0)
|
|
|
except Exception:
|
|
except Exception:
|
|
@@ -134,7 +133,7 @@ class PJLink2MediaPlayer(MediaPlayerEntity):
|
|
|
|
|
|
|
|
@property
|
|
@property
|
|
|
def available(self) -> bool:
|
|
def available(self) -> bool:
|
|
|
- return self._available
|
|
|
|
|
|
|
+ return self._is_available
|
|
|
|
|
|
|
|
@property
|
|
@property
|
|
|
def state(self) -> MediaPlayerState:
|
|
def state(self) -> MediaPlayerState:
|
|
@@ -161,8 +160,6 @@ class PJLink2MediaPlayer(MediaPlayerEntity):
|
|
|
self._state = MediaPlayerState.OFF
|
|
self._state = MediaPlayerState.OFF
|
|
|
|
|
|
|
|
async def async_select_source(self, source: str) -> None:
|
|
async def async_select_source(self, source: str) -> None:
|
|
|
- """Select input source."""
|
|
|
|
|
- # Convert friendly name (e.g. "HDMI 1") back to raw PJLink code (e.g. "31")
|
|
|
|
|
raw_source = self._reverse_mapping.get(source, source)
|
|
raw_source = self._reverse_mapping.get(source, source)
|
|
|
source_type = raw_source[0]
|
|
source_type = raw_source[0]
|
|
|
source_index = raw_source[1]
|
|
source_index = raw_source[1]
|
|
@@ -171,11 +168,11 @@ class PJLink2MediaPlayer(MediaPlayerEntity):
|
|
|
self._current_source = source
|
|
self._current_source = source
|
|
|
|
|
|
|
|
async def async_update(self) -> None:
|
|
async def async_update(self) -> None:
|
|
|
- """Update data from projector."""
|
|
|
|
|
try:
|
|
try:
|
|
|
- if not self._available:
|
|
|
|
|
|
|
+ if not self._socket_open:
|
|
|
await self._projector.__aenter__()
|
|
await self._projector.__aenter__()
|
|
|
- self._available = True
|
|
|
|
|
|
|
+ self._socket_open = True
|
|
|
|
|
+ self._is_available = True
|
|
|
info = await Information(self._projector).table()
|
|
info = await Information(self._projector).table()
|
|
|
self.attrs[ATTR_PRODUCT_NAME] = info.get("product_name")
|
|
self.attrs[ATTR_PRODUCT_NAME] = info.get("product_name")
|
|
|
self.attrs[ATTR_MANUFACTURER_NAME] = info.get(
|
|
self.attrs[ATTR_MANUFACTURER_NAME] = info.get(
|
|
@@ -194,26 +191,20 @@ class PJLink2MediaPlayer(MediaPlayerEntity):
|
|
|
self._state = MediaPlayerState.ON
|
|
self._state = MediaPlayerState.ON
|
|
|
|
|
|
|
|
if pwr == Power.ON:
|
|
if pwr == Power.ON:
|
|
|
- # 1. Fetch current source
|
|
|
|
|
try:
|
|
try:
|
|
|
current = await Sources(self._projector).get()
|
|
current = await Sources(self._projector).get()
|
|
|
if isinstance(current, (tuple, list)):
|
|
if isinstance(current, (tuple, list)):
|
|
|
raw_source = "".join(map(str, current))
|
|
raw_source = "".join(map(str, current))
|
|
|
else:
|
|
else:
|
|
|
raw_source = str(current)
|
|
raw_source = str(current)
|
|
|
-
|
|
|
|
|
- # Apply friendly name mapping
|
|
|
|
|
self._current_source = self._source_mapping.get(
|
|
self._current_source = self._source_mapping.get(
|
|
|
raw_source, raw_source
|
|
raw_source, raw_source
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- # THE FIX: If the projector is busy (ERR3), bubble the error up so we force a disconnect
|
|
|
|
|
if "ERR3" in repr(e) or "unavailable" in repr(e):
|
|
if "ERR3" in repr(e) or "unavailable" in repr(e):
|
|
|
raise e
|
|
raise e
|
|
|
_LOGGER.debug("Ignored error getting source: %s", repr(e))
|
|
_LOGGER.debug("Ignored error getting source: %s", repr(e))
|
|
|
|
|
|
|
|
- # 2. Fetch Lamp Hours
|
|
|
|
|
try:
|
|
try:
|
|
|
self.attrs[ATTR_LAMP_HOURS] = await Lamp(
|
|
self.attrs[ATTR_LAMP_HOURS] = await Lamp(
|
|
|
self._projector
|
|
self._projector
|
|
@@ -221,7 +212,6 @@ class PJLink2MediaPlayer(MediaPlayerEntity):
|
|
|
except Exception:
|
|
except Exception:
|
|
|
pass
|
|
pass
|
|
|
|
|
|
|
|
- # 3. Fetch Resolution
|
|
|
|
|
try:
|
|
try:
|
|
|
res = await Sources(self._projector).resolution()
|
|
res = await Sources(self._projector).resolution()
|
|
|
self.attrs[ATTR_RESOLUTION_X] = res[0]
|
|
self.attrs[ATTR_RESOLUTION_X] = res[0]
|
|
@@ -230,7 +220,7 @@ class PJLink2MediaPlayer(MediaPlayerEntity):
|
|
|
self.attrs.pop(ATTR_RESOLUTION_X, None)
|
|
self.attrs.pop(ATTR_RESOLUTION_X, None)
|
|
|
self.attrs.pop(ATTR_RESOLUTION_Y, None)
|
|
self.attrs.pop(ATTR_RESOLUTION_Y, None)
|
|
|
|
|
|
|
|
- else:
|
|
|
|
|
|
|
+ elif pwr == Power.State.OFF:
|
|
|
self.attrs.pop(ATTR_RESOLUTION_X, None)
|
|
self.attrs.pop(ATTR_RESOLUTION_X, None)
|
|
|
self.attrs.pop(ATTR_RESOLUTION_Y, None)
|
|
self.attrs.pop(ATTR_RESOLUTION_Y, None)
|
|
|
self._current_source = None
|
|
self._current_source = None
|
|
@@ -240,10 +230,8 @@ class PJLink2MediaPlayer(MediaPlayerEntity):
|
|
|
except Exception as err:
|
|
except Exception as err:
|
|
|
err_str = repr(err)
|
|
err_str = repr(err)
|
|
|
|
|
|
|
|
- # THE FIX: Forcefully drop the connection on any error.
|
|
|
|
|
- # This prevents the projector from getting "stuck" on a stale socket.
|
|
|
|
|
- if self._available:
|
|
|
|
|
- self._available = False
|
|
|
|
|
|
|
+ if self._socket_open:
|
|
|
|
|
+ self._socket_open = False
|
|
|
try:
|
|
try:
|
|
|
await self._projector.__aexit__(0, 0, 0)
|
|
await self._projector.__aexit__(0, 0, 0)
|
|
|
except Exception:
|
|
except Exception:
|
|
@@ -253,10 +241,13 @@ class PJLink2MediaPlayer(MediaPlayerEntity):
|
|
|
_LOGGER.debug(
|
|
_LOGGER.debug(
|
|
|
"Projector is busy switching inputs. Reconnecting next poll."
|
|
"Projector is busy switching inputs. Reconnecting next poll."
|
|
|
)
|
|
)
|
|
|
|
|
+ # Notice we do NOT set self._is_available = False here!
|
|
|
|
|
+ # This keeps the attributes stable in Home Assistant.
|
|
|
else:
|
|
else:
|
|
|
if not self._connectionErrorLogged:
|
|
if not self._connectionErrorLogged:
|
|
|
_LOGGER.error(
|
|
_LOGGER.error(
|
|
|
"PJLink2 ERROR for %s: %s", self._name, err_str
|
|
"PJLink2 ERROR for %s: %s", self._name, err_str
|
|
|
)
|
|
)
|
|
|
self._connectionErrorLogged = True
|
|
self._connectionErrorLogged = True
|
|
|
|
|
+ self._is_available = False
|
|
|
self._state = MediaPlayerState.OFF
|
|
self._state = MediaPlayerState.OFF
|