From 8cc2ae32df8ef3ade8b3d1c99b238c15bc97f6ae Mon Sep 17 00:00:00 2001 From: Pierre Date: Sat, 17 Oct 2026 22:46:44 +0200 Subject: [PATCH] Shorter reconnection delay --- apps/global/homecinema.yaml | 36 ++++++++++++++++++----------- apps/samsungtheframeasync.py | 18 +++++++++++---- docs/samsungtheframeasync.md | 9 +++++++- tests/test_samsungtheframe_async.py | 26 +++++++++++++++++---- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/apps/global/homecinema.yaml b/apps/global/homecinema.yaml index 999a9e6..8f410a3 100644 --- a/apps/global/homecinema.yaml +++ b/apps/global/homecinema.yaml @@ -26,7 +26,6 @@ samsung_the_frame: trigger_conditions: - binary_sensor.is_everybody_sleeping or binary_sensor.is_living_room_sleeping or not binary_sensor.someone_home - binary_sensor.projector_screen_down - #hdmi: binary_sensor.vertex2_tx0_plus5_for_a_while and binary_sensor.vertex2_has_input_signal and not binary_sensor.projector_screen_down hdmi: binary_sensor.vertex2_has_input_signal and not binary_sensor.projector_screen_down manual: binary_sensor.is_tv_on and not binary_sensor.tv_auto_shutdown tv_off: not binary_sensor.living_room_occupancy @@ -127,6 +126,14 @@ projection_screen_switch: class: SmartSwitch entity: input_boolean.projection_screen + virtual_sensors: + sensors: + continuous_condition.projection_screen_on_for_a_while: + time: 300 + conditions: input_boolean.projection_screen + + off_conditions: binary_sensor.projection_screen_on_for_a_while and binary_sensor.projector_off_for_a_while + toggle_events: apple_tv_mute_long_press: event_name: state_changed @@ -153,6 +160,7 @@ vertex_2: virtual_sensors: default_values: binary_sensor.is_tv_on: false + binary_sensor.someone_home: false binary_sensor.vertex2_has_input_signal: false sensors: binary_sensor.projector_screen_down: input_boolean.projection_screen @@ -164,7 +172,6 @@ vertex_2: 0: name: AppleTV icon: mdi:apple - #reboot_when_leaving: True 1: name: Xbox icon: mdi:microsoft-xbox @@ -183,23 +190,20 @@ vertex_2: name: TV icon: mdi:television use_audio_out_audio_flags: input_boolean.use_bravia_theater_for_the_frame - force_plus5: - trigger_conditions: binary_sensor.is_tv_on or binary_sensor.vertex2_has_input_signal - blocking_conditions: not switch.the_frame_power_plug + force_plus5: not binary_sensor.vertex2_no_signal_spam 1: name: Projector icon: mdi:projector use_audio_out_audio_flags: True - force_plus5: switch.living_room_projector_plug + #force_plus5: switch.living_room_projector_plug - # vertex2_power_plug: - # entity: switch.fibaro_vertex_plug_51 - # # power_consumption: sensor.fibaro_vertex_plug_51_power - # conditions: - # trigger_conditions: binary_sensor.someone_home - # blocking_conditions: - # - binary_sensor.is_everybody_sleeping or binary_sensor.is_salon_sleeping - # - binary_sensor.vertex2_unreachable and not binary_sensor.is_tv_on and not binary_sensor.projector_on + vertex2_power_plug: + entity: switch.vertex2_power_plug + # power_consumption: sensor.fibaro_vertex_plug_51_power + conditions: + trigger_conditions: binary_sensor.someone_home #and (input_boolean.projection_screen or binary_sensor.the_frame_is_plugged_for_a_while) + blocking_conditions: (binary_sensor.is_everybody_sleeping or binary_sensor.is_living_room_sleeping) and not binary_sensor.vertex2_has_input_signal + #filmmaker_mode_conditions: sensor.vertex2_input_select_tx0 == 'AppleTV' @@ -216,12 +220,16 @@ vertex_2: vertex2_address: http://10.0.0.33 vertex2_tcp_port: 2220 + startup_delay: 30 # seconds after the power plug turns on video_tx_signal_sensor: sensor.vertex2_video_signal allm_sensor: binary_sensor.vertex2_allm + #tx0_plus5_sensor: binary_sensor.vertex2_tx0_plus5 + #tx1_plus5_sensor: binary_sensor.vertex2_tx1_plus5 audio_out_signal_sensor: sensor.vertex2_audio_signal output_selected_sensor: sensor.vertex2_output_selected input_selected_sensor: sensor.vertex2_input_selected + no_signal_spam_sensor: binary_sensor.vertex2_no_signal_spam unreachable_sensor: binary_sensor.vertex2_unreachable command_processing_sensor: binary_sensor.vertex2_command_processing command_processing_timeout: 20 diff --git a/apps/samsungtheframeasync.py b/apps/samsungtheframeasync.py index 1bb02d0..b2774ae 100644 --- a/apps/samsungtheframeasync.py +++ b/apps/samsungtheframeasync.py @@ -308,6 +308,8 @@ class SamsungTheFrame(SmartObject): if self._stopped: return self._generation += 1 + # A fresh target must not inherit a long idle reconnection delay. + self._retry_at = min(self._retry_at, self._loop.time() + self._transition_retry_delay()) self.requested_state = None if state == 'manual' else state previous = self._target_task if previous is not None and not previous.cancelling(): @@ -422,7 +424,10 @@ class SamsungTheFrame(SmartObject): await self._publish(*self._observed) except NETWORK_ERRORS as error: self.log_info(f'TV refresh failed: {type(error).__name__}: {error}') - self._retry_at = self._loop.time() + self._backoff + retry_delay = self._backoff + if self.requested_state is not None: + retry_delay = min(retry_delay, self._transition_retry_delay()) + self._retry_at = self._loop.time() + retry_delay self._backoff = min(60, self._backoff * 2) # One refresh worker coalesces power events. WebSocket # notifications wake it immediately; no network polling @@ -490,6 +495,9 @@ class SamsungTheFrame(SmartObject): selected = random.choice(available) await self._client.art_request('select_image', content_id=selected['content_id'], show=True) + def _transition_retry_delay(self): + return max(0.2, float(self.args.get('transition_retry_delay', 2))) + async def _target_runner(self, previous, target, generation): if previous is not None: with suppress(asyncio.CancelledError): @@ -510,7 +518,7 @@ class SamsungTheFrame(SmartObject): return except NETWORK_ERRORS + (KeyError, TypeError) as error: self.log_info(f'TV transition failed: {type(error).__name__}: {error}') - delay = 30 + delay = self._transition_retry_delay() # Cancellation interrupts both network waits and confirmation delays. await asyncio.sleep(delay) self._refresh_at = 0 @@ -519,17 +527,17 @@ class SamsungTheFrame(SmartObject): async def _transition(self, target): plugged, powered = await self._power() if plugged is None or powered is None: - return 30 + return self._transition_retry_delay() if not plugged: if target == 'power_off': return None await self.turn_on(self.power_plug) - return 60 + return 5 if not powered: if target == 'tv_off': return None await self._wake_up() - return 60 + return 10 if self._loop.time() < self._retry_at: return max(1, self._retry_at - self._loop.time()) # Always confirm the current state before a toggle. Serialize Art reads diff --git a/docs/samsungtheframeasync.md b/docs/samsungtheframeasync.md index 4a1f1d7..12fa04b 100644 --- a/docs/samsungtheframeasync.md +++ b/docs/samsungtheframeasync.md @@ -43,7 +43,9 @@ lors de l'ajout de ce module. connexions. Une consommation indisponible n'est pas assimilée à un arrêt. - Après un échec de lecture, reconnexion avec attente progressive : 5, 10, 20, 40, puis 60 secondes. Les événements de consommation ne contournent pas cette - attente. Les transitions échouées sont revérifiées après 30 secondes. + attente en surveillance seule. Pendant une transition, cette attente est plafonnée + à 2 secondes, y compris après une erreur de transition. Une nouvelle consigne + raccourcit aussi une attente de reconnexion déjà en cours. - Les cinq consignes sont conservées : `manual`, `art_mode`, `tv_off`, `hdmi`, `power_off`. Une nouvelle consigne annule la transition précédente, y compris l'attente de 5 secondes avant coupure électrique. Une commande Power échouée @@ -64,6 +66,7 @@ basculement : ```yaml network_timeout: 2 # secondes, connexion/autorisation et requêtes TV reconnect_delay: 5 # attente initiale, doublée jusqu'à 60 secondes +transition_retry_delay: 2 # attente entre tentatives pendant une transition fallback_refresh: 60 # intervalle de lecture de secours, en secondes power_threshold: 35 # watts, même seuil que l'ancienne app ``` @@ -82,3 +85,7 @@ Les tests simulent les messages et sockets TV : persistance, notifications, concurrence, délais, reconnexion, transitions et annulation. Ils ne contactent pas la TV réelle. La réception des événements et les commandes doivent encore être validées sur le téléviseur lors du basculement manuel. + +Après allumage de la prise, la transition est revérifiée après 5 secondes ; +après une commande de réveil, après 10 secondes (au lieu de 60). Les délais +de confirmation après une touche et avant coupure électrique sont conservés. diff --git a/tests/test_samsungtheframe_async.py b/tests/test_samsungtheframe_async.py index b1840b6..45cd6f9 100644 --- a/tests/test_samsungtheframe_async.py +++ b/tests/test_samsungtheframe_async.py @@ -11,7 +11,7 @@ import aiohttp sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'apps')) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'apps' / 'ad_toolbox')) -from SamsungTheFrameAsync import SamsungTheFrame, TVError, _TVConnection +from samsungtheframeasync import SamsungTheFrame, TVError, _TVConnection def message(data): @@ -231,6 +231,22 @@ class ApplicationTests(unittest.IsolatedAsyncioTestCase): app.app_list = [] return app + async def test_fresh_target_shortens_idle_reconnect_delay(self): + app = self.app() + app._retry_at = app._loop.time() + 60 + app._transition = AsyncMock(return_value=None) + app._set_target('hdmi') + self.assertLessEqual(app._retry_at - app._loop.time(), 2) + await app._target_task + + async def test_wake_rechecks_after_ten_seconds(self): + app = self.app() + app.args['wake_up_service'] = 'esphome/wake_tv' + app.get_state = AsyncMock(side_effect=lambda e: 'on' if e == app.power_plug else '0') + self.assertEqual(await app._transition('hdmi'), 10) + app.call_service.assert_awaited_once_with('esphome/wake_tv') + app._client.send_key.assert_not_awaited() + async def asyncTearDown(self): tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] for task in tasks: @@ -252,7 +268,7 @@ class ApplicationTests(unittest.IsolatedAsyncioTestCase): app.listen_state = Mock() app._post = Mock() app.create_task = Mock(side_effect=lambda coro: coro.close()) - with patch('SamsungTheFrameAsync.EventHandler') as handler: + with patch('samsungtheframeasync.EventHandler') as handler: app.on_initialize_smart_object() self.assertEqual(app.listen_state.call_count, 3) self.assertEqual(handler.call_count, 2) @@ -343,7 +359,7 @@ class ApplicationTests(unittest.IsolatedAsyncioTestCase): app.get_state.side_effect = None self.assertIsNone(await app._transition('power_off')) app.turn_on.assert_not_called() - self.assertEqual(await app._transition('art_mode'), 60) + self.assertEqual(await app._transition('art_mode'), 5) app.turn_on.assert_awaited_once_with(app.power_plug) app._client.art_request.assert_not_called() @@ -379,7 +395,7 @@ class ApplicationTests(unittest.IsolatedAsyncioTestCase): async def test_monitor_coalesces_sensor_bursts_and_shutdown_closes_client(self): app = self.app() client = app._client - with patch('SamsungTheFrameAsync._TVConnection', return_value=client): + with patch('samsungtheframeasync._TVConnection', return_value=client): task = asyncio.create_task(app._run()) await asyncio.sleep(.02) for _ in range(30): @@ -402,7 +418,7 @@ class ApplicationTests(unittest.IsolatedAsyncioTestCase): app.args['reconnect_delay'] = .5 client.alive.return_value = False client.art_request.side_effect = TVError('offline') - with patch('SamsungTheFrameAsync._TVConnection', return_value=client): + with patch('samsungtheframeasync._TVConnection', return_value=client): task = asyncio.create_task(app._run()) await asyncio.sleep(.01) for _ in range(10):