Shorter reconnection delay

This commit is contained in:
Pierre committed 2026-10-17 22:46:44 +02:00
1 parent 8a8cb02296
commit 8cc2ae32df
4 files changed
+64 -25

No files matched your search

+22 -14
View File
@@ -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.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
# 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
#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
+13 -5
View File
@@ -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
+8 -1
View File
@@ -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.
+21 -5
View File
@@ -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):