From dd4335c811ac2735483588219657dc5e4d2be0cf Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 9 Oct 2026 22:47:26 +0200 Subject: [PATCH] Vertex2 tweaking --- apps/ad_toolbox | 2 +- apps/global/homecinema.yaml | 9 +++ apps/global/virtualsensors.yaml | 4 +- apps/vertex2.py | 137 +++++++++++++++----------------- 4 files changed, 76 insertions(+), 76 deletions(-) diff --git a/apps/ad_toolbox b/apps/ad_toolbox index ece851f..cf8839d 160000 --- a/apps/ad_toolbox +++ b/apps/ad_toolbox @@ -1 +1 @@ -Subproject commit ece851f5a01fcbc64ab19781a2d464fc2950e64a +Subproject commit cf8839db6a542defb3e174957b62a2613182cd0a diff --git a/apps/global/homecinema.yaml b/apps/global/homecinema.yaml index 93b2606..e219e97 100644 --- a/apps/global/homecinema.yaml +++ b/apps/global/homecinema.yaml @@ -154,6 +154,9 @@ vertex_2: priority: 5 virtual_sensors: + default_values: + binary_sensor.is_tv_on: false + binary_sensor.vertex2_has_input_signal: false sensors: binary_sensor.projector_screen_down: cover.living_room_projection_screen.current_position < 90 @@ -183,10 +186,14 @@ 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 1: name: Projector icon: mdi:projector use_audio_out_audio_flags: True + force_plus5: switch.living_room_projector_plug # vertex2_power_plug: # entity: switch.fibaro_vertex_plug_51 @@ -219,6 +226,8 @@ vertex_2: output_selected_sensor: sensor.vertex2_output_selected input_selected_sensor: sensor.vertex2_input_selected unreachable_sensor: binary_sensor.vertex2_unreachable + command_processing_sensor: binary_sensor.vertex2_command_processing + command_processing_timeout: 20 use_bravia_theater_for_the_frame: module: smartswitch diff --git a/apps/global/virtualsensors.yaml b/apps/global/virtualsensors.yaml index 064c1bd..505015c 100644 --- a/apps/global/virtualsensors.yaml +++ b/apps/global/virtualsensors.yaml @@ -110,10 +110,10 @@ virtual_sensors: binary_sensor.is_playstation_on: switch.ps5_136_power sensor.vertex2_input_selected_icon: sensor.vertex2_input_selected.icon retain_condition.vertex2_has_input_signal: - retain_time: 20 + retain_time: 10 conditions: trigger_conditions: (sensor.vertex2_video_signal and sensor.vertex2_video_signal != 'no signal') or sensor.vertex2_audio_signal - disable_conditions: binary_sensor.vertex2_unreachable + disable_conditions: binary_sensor.vertex2_unreachable or binary_sensor.vertex2_command_processing sensor_attributes: icon: sensor.vertex2_input_selected_icon binary_sensor.is_using_apple_tv: binary_sensor.is_apple_tv_on and binary_sensor.vertex2_has_input_signal and sensor.vertex2_input_selected == 'AppleTV' diff --git a/apps/vertex2.py b/apps/vertex2.py index 7bdae80..db4c199 100644 --- a/apps/vertex2.py +++ b/apps/vertex2.py @@ -15,10 +15,16 @@ class Vertex2(SmartObject): self._stop_interrupt_listener() self._interrupt_stop = threading.Event() self._status_requested = threading.Event() - self._plus5_off_handle = None - self._plus5_active_index = None - self._input_signal_present = False - self.plus5_signal_timeout = max(0, float(self.args.get('plus5_signal_timeout', 60))) + self._command_processing_lock = threading.Lock() + self._command_processing_handle = None + self._command_processing_deadline = None + self.command_processing_timeout = max(0, float(self.args.get('command_processing_timeout', 30))) + self.command_processing_sensor = None + if self.args.get('command_processing_sensor'): + self.command_processing_sensor = self.create_entity( + self.args['command_processing_sensor'], + state='off', + ) self.video_tx_signal_sensor = self.create_entity( self.args['video_tx_signal_sensor'], state='unavailable', @@ -112,17 +118,23 @@ class Vertex2(SmartObject): output_index = int(output_key) except (TypeError, ValueError): continue - for condition_key in ('use_audio_out_audio_flags',): + for condition_key in ('use_audio_out_audio_flags', 'force_plus5'): condition_config = config.get(condition_key) - if not condition_config: + if condition_config is None: continue + callback_options = ( + {'on_change_cb': self.on_output_plus5_condition_change, + 'trigger_callback_on_activation': True} + if condition_key == 'force_plus5' + else {'on_update_cb': self.on_output_config_conditions_update, + 'trigger_callback_on_activation': False} + ) self.output_config_conditions.setdefault(output_index, {})[condition_key] = SmartCondition.Evaluator( self, condition_config, condition_name=f'output_config_tx{output_index}_{condition_key}', - on_update_cb=self.on_output_config_conditions_update, pass_condition_name_to_cb=str(output_index), - trigger_callback_on_activation=False, + **callback_options, ) self.output_selection_evaluators = [] @@ -205,6 +217,7 @@ class Vertex2(SmartObject): line, buffer = buffer.split('\n', 1) line = line.strip() if line == 'int': + self._refresh_command_processing() self.log_info('Vertex2 IP INTERRUPT received') self._status_requested.set() elif status_started is not None: @@ -253,15 +266,16 @@ class Vertex2(SmartObject): if interrupt_thread.is_alive(): raise RuntimeError('Previous Vertex2 IP INTERRUPT listener did not stop') self._interrupt_thread = None - plus5_off_handle = getattr(self, '_plus5_off_handle', None) - if plus5_off_handle is not None: - self.cancel_timer(plus5_off_handle) - self._plus5_off_handle = None + processing_lock = getattr(self, '_command_processing_lock', None) + if processing_lock is not None: + with processing_lock: + self._command_processing_deadline = None + if self._command_processing_handle is not None: + self.cancel_timer(self._command_processing_handle) + self._command_processing_handle = None def terminate(self): self._stop_interrupt_listener() - if getattr(self, '_plus5_active_index', None) is not None: - self._set_active_output_plus5(None) super().terminate() def generate_input_selected_sensor_name(self,i): @@ -309,7 +323,7 @@ class Vertex2(SmartObject): break if output_selected is None: - self.log_warning("No output_selection condition succeeded; disabling TX0 and TX1 +5V FORCE") + self.log_warning("No output_selection condition succeeded; clearing output selection") self.disable_output_selection() return @@ -346,7 +360,6 @@ class Vertex2(SmartObject): self.set_edid_video_flags_to_tx0() else: self.set_edid_video_flags_to_tx1() - self._set_active_output_plus5(output_index if self._input_signal_present else None) def disable_output_selection(self): default_output_config = self.get_output_config(None) @@ -355,60 +368,15 @@ class Vertex2(SmartObject): attributes={"icon": default_output_config["icon"]}, ) self._refresh_selected_output_signal() - self._set_active_output_plus5(None) - def _set_active_output_plus5(self, output_index): - if self._plus5_off_handle is not None: - self.cancel_timer(self._plus5_off_handle) - self._plus5_off_handle = None - - previous_index = self._plus5_active_index - if previous_index == output_index: + def on_output_plus5_condition_change(self, output_index, previous_result, result): + if result == SmartCondition.Result.Disabled: return - - if previous_index == 0: - self.force_tx0_plus5(False) - elif previous_index == 1: - self.force_tx1_plus5(False) - - self._plus5_active_index = output_index - if output_index == 0: - # At startup the previous hardware state is unknown; disable the - # other output only after a signal confirms the selected TX. - self.force_tx1_plus5(False) - self.force_tx0_plus5(True) - elif output_index == 1: - self.force_tx0_plus5(False) - self.force_tx1_plus5(True) - - def _update_plus5_from_input_signal(self, has_signal): - self._input_signal_present = has_signal - selected_name = self.output_selected_sensor.get_state() - selected_index = self.output_names.get(selected_name) - - if has_signal: - self._set_active_output_plus5(selected_index) - elif selected_index in (0, 1) and self._plus5_off_handle is None: - self.log_info( - f"No input signal; turning selected TX +5V FORCE off in {self.plus5_signal_timeout:g}s" - ) - self._plus5_off_handle = self.run_in( - self._turn_off_plus5_after_signal_loss, - self.plus5_signal_timeout, - ) - - def _turn_off_plus5_after_signal_loss(self, kwargs): - self._plus5_off_handle = None - if self._input_signal_present: - return - self.log_info("Input signal absent for configured timeout; turning selected TX +5V FORCE off") - selected_index = self.output_names.get(self.output_selected_sensor.get_state()) - if selected_index == 0: - self.force_tx0_plus5(False) - elif selected_index == 1: - self.force_tx1_plus5(False) - if self._plus5_active_index == selected_index: - self._plus5_active_index = None + enabled = result == SmartCondition.Result.Succeeded + if int(output_index) == 0: + self.force_tx0_plus5(enabled) + elif int(output_index) == 1: + self.force_tx1_plus5(enabled) def on_filmmaker_mode_conditions_change(self,prev_result,result): if result == SmartCondition.Result.Succeeded: @@ -422,8 +390,35 @@ class Vertex2(SmartObject): else: self.turn_off(self.power_plug_entity) + def _refresh_command_processing(self, start=False): + if self.command_processing_sensor is None: + return + with self._command_processing_lock: + if not start and self.command_processing_sensor.get_state() != 'on': + return + if self._command_processing_handle is not None: + self.cancel_timer(self._command_processing_handle) + self._command_processing_deadline = time.monotonic() + self.command_processing_timeout + if self.command_processing_sensor.get_state() != 'on': + self.command_processing_sensor.set_state(state='on') + self._command_processing_handle = self.run_in( + self._on_command_processing_timeout, + self.command_processing_timeout, + deadline=self._command_processing_deadline, + ) + + def _on_command_processing_timeout(self, kwargs): + with self._command_processing_lock: + # Ignore a callback already queued before a timer reset or shutdown. + if kwargs['deadline'] != self._command_processing_deadline: + return + self._command_processing_handle = None + self._command_processing_deadline = None + self.command_processing_sensor.set_state(state='off') + def send_cmd(self,cmd,value = ''): request = f'{self.args["vertex2_address"]}/cmd?{cmd}={value}' + self._refresh_command_processing(start=True) try: r = get(request, timeout=5) if cmd == 'edid' and value.startswith(('audiotx', 'audioout')): @@ -535,6 +530,7 @@ class Vertex2(SmartObject): "name": config.get("name", default_config["name"]), "icon": config.get("icon", default_config["icon"]), "use_audio_out_audio_flags": config.get("use_audio_out_audio_flags"), + "force_plus5": config.get("force_plus5"), } return {"name": config, "icon": None} @@ -644,11 +640,6 @@ class Vertex2(SmartObject): selected_output_index = self.output_names.get(self.output_selected_sensor.get_state()) self._refresh_selected_input_sensor(selected_output_index) self._update_output_video_sensors(kwargs['rx0'], kwargs['rx1']) - has_input_signal = any( - signal.strip().lower() not in ('', 'no signal', 'unavailable') - for signal in (kwargs['rx0'], kwargs['rx1']) - ) - self._update_plus5_from_input_signal(has_input_signal) if self.audio_out_signal_sensor.get_state() != audio_out_signal: self.audio_out_signal_sensor.set_state(state=audio_out_signal) self.unreachable_sensor.set_state(state='off')