From 00f6b28342473635e5035127ed058a9770af3b81 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 27 Sep 2026 08:52:03 +0200 Subject: [PATCH] fix issue with vertex2 reconnection after HA reboot --- apps/vertex2.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/vertex2.py b/apps/vertex2.py index 0551215..3522c9e 100644 --- a/apps/vertex2.py +++ b/apps/vertex2.py @@ -11,6 +11,8 @@ from urllib.parse import urlparse class Vertex2(SmartObject): def on_initialize_smart_object(self): + # HASS can call initialize() again without calling terminate() first. + self._stop_interrupt_listener() self._interrupt_stop = threading.Event() self._status_requested = threading.Event() self._tx0_recovery_attempted = False @@ -143,22 +145,25 @@ class Vertex2(SmartObject): self.vertex2_tcp_port = int(self.args.get('vertex2_tcp_port', 2220)) self._interrupt_thread = threading.Thread( target=self._interrupt_listener, + args=(self._interrupt_stop,), name=f'{self.name}_interrupt_listener', daemon=True, ) self.update_vertex2_states() self._interrupt_thread.start() - def _interrupt_listener(self): + def _interrupt_listener(self, interrupt_stop): address = urlparse(self.args['vertex2_address']) host = address.hostname status_fields = ('rx0', 'rx1', 'tx0', 'tx1', 'tx0sink', 'tx1sink', 'aud0', 'aud1', 'audout', 'spd0', 'spd1') commands = tuple(f'get status {field}' for field in status_fields) + ('get insel',) expected = set(status_fields) | {'insel'} - while not self._interrupt_stop.is_set(): + while not interrupt_stop.is_set(): try: with socket.create_connection((host, self.vertex2_tcp_port), timeout=10) as connection: + if interrupt_stop.is_set(): + break # Detect a silent reboot/network loss without polling HDMI status. connection.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10) @@ -178,7 +183,7 @@ class Vertex2(SmartObject): previous_snapshot = None previous_snapshot_generation = None - while not self._interrupt_stop.is_set(): + while not interrupt_stop.is_set(): if status_started is None and self._status_requested.is_set(): self._status_requested.clear() status = {} @@ -195,6 +200,8 @@ class Vertex2(SmartObject): raise OSError('Vertex2 TCP status response timed out') continue + if interrupt_stop.is_set(): + break if not data: raise OSError('Vertex2 closed the IP INTERRUPT connection') @@ -251,7 +258,7 @@ class Vertex2(SmartObject): raise OSError('Vertex2 TCP status response timed out') except (OSError, ValueError) as error: - if self._interrupt_stop.is_set(): + if interrupt_stop.is_set(): break self.unreachable_sensor.set_state(state='on') self.video_tx_signal_sensor.set_state(state='unavailable') @@ -262,7 +269,7 @@ class Vertex2(SmartObject): sensor.set_state(state='unavailable') self.audio_out_signal_sensor.set_state(state='unavailable') self.log(f'Vertex2 IP INTERRUPT unavailable: {error}', level='WARNING') - self._interrupt_stop.wait(5) + interrupt_stop.wait(5) def trigger_tx0_stuck_workaround(self, kwargs): if self._tx0_recovery_attempted: @@ -288,13 +295,24 @@ class Vertex2(SmartObject): self.log_info('Vertex2 TX0 +5V workaround: restoring TX0 +5V FORCE') self.force_tx0_plus5(True) - def terminate(self): + def _stop_interrupt_listener(self): interrupt_stop = getattr(self, '_interrupt_stop', None) if interrupt_stop is not None: interrupt_stop.set() + interrupt_thread = getattr(self, '_interrupt_thread', None) + if interrupt_thread is not None: + # Allow the 10s connect timeout or 1s recv timeout to finish. + interrupt_thread.join(timeout=12) + if interrupt_thread.is_alive(): + raise RuntimeError('Previous Vertex2 IP INTERRUPT listener did not stop') + self._interrupt_thread = None restore_handle = getattr(self, '_tx0_recovery_restore_handle', None) if restore_handle is not None: self.cancel_timer(restore_handle) + self._tx0_recovery_restore_handle = None + + def terminate(self): + self._stop_interrupt_listener() super().terminate() def generate_input_selected_sensor_name(self,i):