From ed59a72c3fa94e6fcdb6d764cefa64e1e8200651 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sat, 26 Sep 2026 16:43:24 +0200 Subject: [PATCH] Still iterating on the home cinema configuration --- apps/ad_toolbox | 2 +- apps/global/homecinema.yaml | 99 +++++++++++++++++------ apps/global/virtualsensors.yaml | 13 +-- apps/smartprojector_benq_over_telnet.py | 9 ++- apps/smartshutter.py | 41 ++++++++++ apps/vertex2.py | 103 ++++++++++++++++++++++-- 6 files changed, 232 insertions(+), 35 deletions(-) diff --git a/apps/ad_toolbox b/apps/ad_toolbox index 3e7ae75..a387a13 160000 --- a/apps/ad_toolbox +++ b/apps/ad_toolbox @@ -1 +1 @@ -Subproject commit 3e7ae7550b88f67c2a14aba31a3140055d200c04 +Subproject commit a387a13fa27f06a8982bfa84ed8692bb7c4fb6a0 diff --git a/apps/global/homecinema.yaml b/apps/global/homecinema.yaml index 2379688..790f0fa 100644 --- a/apps/global/homecinema.yaml +++ b/apps/global/homecinema.yaml @@ -14,7 +14,7 @@ samsung_the_frame: virtual_sensors: default_values: binary_sensor.tv_auto_shutdown: false - binary_sensor.tv_on: false + binary_sensor.is_tv_on: false sensors: value_selector.samsung_the_frame_target_state: power_off: @@ -22,14 +22,14 @@ samsung_the_frame: - binary_sensor.is_everybody_sleeping or binary_sensor.is_living_room_sleeping or not binary_sensor.someone_home - binary_sensor.projector_on or binary_sensor.projector_screen_down hdmi: binary_sensor.vertex2_has_input_signal and not binary_sensor.projector_screen_down - manual: binary_sensor.tv_on and not binary_sensor.tv_auto_shutdown + manual: binary_sensor.is_tv_on and not binary_sensor.tv_auto_shutdown tv_off: not binary_sensor.living_room_occupancy art_mode: True tv_state: sensor.samsung_the_frame_target_state output_sensors: - is_on: binary_sensor.tv_on + is_on: binary_sensor.is_tv_on is_art_mode: binary_sensor.tv_in_art_mode tv_auto_shutdown: binary_sensor.tv_auto_shutdown @@ -67,12 +67,30 @@ w4000i: continuous_condition.projector_ready: conditions: switch.living_room_projector_plug time: 30 + continuous_condition.has_allm_signal_for_a_while: + conditions: binary_sensor.vertex2_allm + time: 5 attributes_override: icon: sensor.projector_icon friendly_name: Projecteur press_enter_event: send_projector_enter + press_enter_events: + vertex2_allm: + event_name: state_changed + event_data: + entity_id: binary_sensor.has_allm_signal_for_a_while + new_state: + state: "on" + apple_tv_mute_short_press: + event_name: state_changed + event_data: + entity_id: binary_sensor.ir_bridge_apple_tv_mute_button_short_press + old_state: + state: "off" + new_state: + state: "on" projector_ready_conditions: binary_sensor.projector_ready @@ -80,10 +98,10 @@ w4000i: #cooling_down_sensor: binary_sensor.projector_cooling_down smart_conditions: - trigger_conditions: binary_sensor.vertex2_has_input_signal + trigger_conditions: binary_sensor.vertex2_has_input_signal and binary_sensor.projector_screen_down disable_conditions: - self == 'unavailable' - - self == 'off' and not binary_sensor.projector_screen_down + #- self == 'off' and not binary_sensor.projector_screen_down #- not binary_sensor.projector_screen_down #- binary_sensor.projector_cooling_down - switch.projector.warming_up @@ -93,7 +111,10 @@ bravia_theatre: class: SmartSwitch entity: media_player.bravia_theatre - smart_conditions: binary_sensor.vertex2_has_input_signal + smart_conditions: + #trigger_conditions: binary_sensor.vertex2_has_input_signal + trigger_conditions: sensor.vertex2_output_selected == 'Projector' or input_boolean.use_bravia_theater_for_the_frame + blocking_conditions: not binary_sensor.vertex2_has_input_signal projection_screen: module: smartshutter @@ -101,13 +122,26 @@ projection_screen: entity: cover.living_room_projection_screen close_position: 22 - close_conditions: # close mean screen down - # when the projector is turn back on, it can happen that this condition is evaluated before projector_off_for_a_while is updated - # so I need to test both binary_sensor.projector_on and not binary_sensor.projector_off_for_a_while - trigger_conditions: binary_sensor.projector_on or not binary_sensor.projector_off_for_a_while - disable_conditions: - - binary_sensor.day_interval_night and not binary_sensor.projector_on - - binary_sensor.is_everybody_sleeping and not binary_sensor.projector_on + toggle_events: + apple_tv_mute_long_press: + event_name: state_changed + event_data: + entity_id: binary_sensor.ir_bridge_apple_tv_mute_button_long_press + old_state: + state: "off" + new_state: + state: "on" + + # close_conditions: # close mean screen down + # # when the projector is turn back on, it can happen that this condition is evaluated before projector_off_for_a_while is updated + # # so I need to test both binary_sensor.projector_on and not binary_sensor.projector_off_for_a_while + # trigger_conditions: binary_sensor.projector_on or not binary_sensor.projector_off_for_a_while + # disable_conditions: + # - binary_sensor.day_interval_night and not binary_sensor.projector_on + # - binary_sensor.is_everybody_sleeping and not binary_sensor.projector_on + + open_conditions: # close mean screen up + trigger_conditions: binary_sensor.projector_off_for_a_while vertex_2: module: vertex2 @@ -128,9 +162,9 @@ vertex_2: 2: name: Playstation icon: mdi:sony-playstation - # 3: - # name: Switch - # icon: mdi:nintendo-switch + 3: + name: Switch + icon: mdi:nintendo-switch output_config: default: @@ -150,17 +184,14 @@ vertex_2: # 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.tv_on and not binary_sensor.projector_on + # - binary_sensor.vertex2_unreachable and not binary_sensor.is_tv_on and not binary_sensor.projector_on - # mutetx0_conditions: - # trigger_conditions: - # - binary_sensor.is_sonos_amp_playing_tv and not media_player.piece_a_vivre.is_volume_muted - # - binary_sensor.is_hw_700_on + mutetx0_conditions: input_boolean.use_bravia_theater_for_the_frame #filmmaker_mode_conditions: sensor.vertex2_input_select_tx0 == 'AppleTV' output_selection: - Projector: binary_sensor.projector_on + Projector: binary_sensor.projector_screen_down TV: True # input_selection: @@ -174,7 +205,29 @@ vertex_2: vertex2_tcp_port: 2220 video_tx_signal_sensor: sensor.vertex2_video_signal + allm_sensor: binary_sensor.vertex2_allm audio_out_signal_sensor: sensor.vertex2_audio_signal output_selected_sensor: sensor.vertex2_output_selected input_selected_sensor: sensor.vertex2_input_selected - unreachable_sensor: binary_sensor.vertex2_unreachable \ No newline at end of file + unreachable_sensor: binary_sensor.vertex2_unreachable + +use_bravia_theater_for_the_frame: + module: smartswitch + class: SmartSwitch + + entity: input_boolean.use_bravia_theater_for_the_frame + + toggle_events: + - toggle_use_bravia_theater_for_the_frame + + +apple_remote: + module: virtualevents + class: VirtualEvents + + priority: 100 # default priority app is 50, since the virtual_events doesn't create any sensor but is based on sensor created by many app, it's important it's created last + + virtual_events: + toggle_use_bravia_theater_for_the_frame: + event_name: toggle_use_bravia_theater_for_the_frame + event_condition: binary_sensor.is_tv_on and binary_sensor.ir_bridge_apple_tv_mute_button_short_press \ No newline at end of file diff --git a/apps/global/virtualsensors.yaml b/apps/global/virtualsensors.yaml index 02d3d7e..131f776 100644 --- a/apps/global/virtualsensors.yaml +++ b/apps/global/virtualsensors.yaml @@ -4,7 +4,7 @@ virtual_sensors: priority: 10 # default priority app is 50, since most of them require sensors created by virtual sensors, it's important that virtual_sensors start first default_values: - binary_sensor.tv_on: false + binary_sensor.is_tv_on: false sensors: binary_sensor.someone_home: person.maeva == 'home' or person.pierre == 'home' or (input_boolean.use_red_shelly_presence and binary_sensor.red_shelly_home) @@ -23,7 +23,7 @@ virtual_sensors: 0: - not binary_sensor.living_room_occupancy - binary_sensor.projector_on and not binary_sensor.is_vertex2_content_idle - - binary_sensor.tv_on + - binary_sensor.is_tv_on - light.living_room - sensor.mezzanine_motion_light_level > 15 - binary_sensor.is_living_room_sleeping @@ -34,7 +34,7 @@ virtual_sensors: # home cinema sensors binary_sensor.projector_on: switch.living_room_projector_plug and sensor.living_room_projector_plug_power > 50 - binary_sensor.projector_screen_down: cover.living_room_projection_screen.current_position < 50 + binary_sensor.projector_screen_down: cover.living_room_projection_screen.current_position < 90 continuous_condition.projector_off_for_a_while: time: 90 default_value: on @@ -44,11 +44,14 @@ virtual_sensors: binary_sensor.is_apple_tv_on: media_player.apple_tv != 'off' and media_player.apple_tv != 'idle' binary_sensor.is_xbox_on: binary_sensor.yetanotherpedro #media_player.xbox != 'off' and media_player.xbox != 'unavailable' binary_sensor.is_playstation_on: switch.ps5_136_power - retain_condition.vertex2_has_input_signal: - retain_time: 10 + sensor.vertex2_input_selected_icon: sensor.vertex2_input_selected.icon + retain_condition.vertex2_has_input_signal: + retain_time: 15 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 + 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 select.hdfury_vertex2_07_port_select_tx0 == 0 binary_sensor.is_using_xbox: binary_sensor.is_xbox_on and binary_sensor.vertex2_has_input_signal #and select.hdfury_vertex2_07_port_select_tx0 == 1 binary_sensor.is_using_playstation: binary_sensor.is_playstation_on and binary_sensor.vertex2_has_input_signal #and select.hdfury_vertex2_07_port_select_tx0 == 2 diff --git a/apps/smartprojector_benq_over_telnet.py b/apps/smartprojector_benq_over_telnet.py index 6f73f71..bed92ad 100644 --- a/apps/smartprojector_benq_over_telnet.py +++ b/apps/smartprojector_benq_over_telnet.py @@ -1,5 +1,6 @@ import appdaemon.plugins.hass.hassapi as hass from smartswitch import SmartSwitch +from ad_toolbox.eventhandler import EventHandler import ad_toolbox.smartcondition as SmartCondition import telnetlib import time @@ -36,6 +37,8 @@ class SmartProjector(SmartSwitch): if 'press_enter_event' in self.args: self.listen_event(lambda event_name, data, kwargs: self.press_enter(),self.args['press_enter_event']) + if 'press_enter_events' in self.args: + self.event_handlers.append(EventHandler(self, self.args['press_enter_events'], self.on_press_enter_event)) self.projector_ready_conditions = SmartCondition.Evaluator(self,self.args["projector_ready_conditions"],condition_name = 'projector_ready_conditions') @@ -195,8 +198,12 @@ class SmartProjector(SmartSwitch): self.attributes['warming_up'] = False self.entity.set_state(state = self.get_projector_state(),attributes = self.attributes) + def on_press_enter_event(self, event_name, data, event_context): + self.press_enter() + def press_enter(self): - self.send_cmd("enter") + if self.entity.get_state() == 'on': + self.send_cmd("enter") def send_cmd(self,cmd, log_command = False): cmd = f"*{cmd}#" diff --git a/apps/smartshutter.py b/apps/smartshutter.py index bfb3f51..adb4c64 100644 --- a/apps/smartshutter.py +++ b/apps/smartshutter.py @@ -1,6 +1,7 @@ import appdaemon.plugins.hass.hassapi as hass import ad_toolbox.smartcondition as SmartCondition from ad_toolbox.smartobject import SmartObject +from ad_toolbox.eventhandler import EventHandler import time class SmartShutter(SmartObject): @@ -75,12 +76,52 @@ class SmartShutter(SmartObject): self.ready = True self.on_update() + # toggle_events uses the same event filters as SmartSwitch. + self.event_handlers = [] + if "toggle_events" in self.args: + self.event_handlers.append(EventHandler(self, self.args["toggle_events"], self.on_toggle_event)) + def terminate(self): + self.event_handlers = [] self.emergency_close_conditions = None self.close_conditions = None self.open_conditions = None super().terminate() + def on_toggle_event(self, event_name, data, event_context): + if not self.ready: + return + + current_position = self.get_state(self.entity_id, attribute='current_position') + try: + current_position = float(current_position) + except (TypeError, ValueError): + self.log("Toggle ignored: current position is unavailable", level="WARNING") + return + + # Reverse the requested movement when another press arrives in transit. + if self.target_position is not None: + should_close = self.target_position == self.open_position + else: + midpoint = (self.open_position + self.close_position) / 2 + should_close = current_position >= midpoint + + if not should_close and self.emergency_close_conditions and self.emergency_close_conditions.evaluate(False) == SmartCondition.Result.Succeeded: + self.on_update() + return + + self.stop_slow_transition() + if self.auto_switch: + self.set_state(self.auto_switch, state='off') + self.target_position = self.close_position if should_close else self.open_position + self.log(f"Toggle shutter to {self.target_position} (previous position was {current_position})") + if self.target_position == 0: + self.call_service("cover/close_cover", entity_id=self.entity_id) + elif self.target_position == 100: + self.call_service("cover/open_cover", entity_id=self.entity_id) + else: + self.call_service("cover/set_cover_position", entity_id=self.entity_id, position=self.target_position) + def on_state_change(self, entity, attribute, old, new, kwargs): if self.ready == False: self.log(f"on_state_change called before the app is ready",level = "WARNING") diff --git a/apps/vertex2.py b/apps/vertex2.py index 5e4edba..92a30c3 100644 --- a/apps/vertex2.py +++ b/apps/vertex2.py @@ -6,6 +6,7 @@ import ad_toolbox.smartcondition as SmartCondition import time import socket import threading +import re from urllib.parse import urlparse class Vertex2(SmartObject): @@ -16,9 +17,25 @@ class Vertex2(SmartObject): self._tx0_recovery_restore_handle = None self.video_tx_signal_sensor = self.create_entity( - self.args['video_tx_signal_sensor'], + self.args['video_tx_signal_sensor'], + state='unavailable', + ) + self.video_tx_signal_sensors = { + index: self.create_entity( + self.generate_output_sensor_name(self.args['video_tx_signal_sensor'], index), state='unavailable', ) + for index in range(2) + } + self.allm_sensor = self.create_entity(self.args['allm_sensor'], state='unavailable') + self.allm_sensors = { + index: self.create_entity( + self.generate_output_sensor_name(self.args['allm_sensor'], index), + state='unavailable', + ) + for index in range(2) + } + self._tx_video_signal_values = {0: 'unavailable', 1: 'unavailable'} self.audio_out_signal_sensor = self.create_entity( self.args['audio_out_signal_sensor'], @@ -61,6 +78,13 @@ class Vertex2(SmartObject): if "input_config" in self.args: self.listen_state(self.on_input_selected_sensor_change,sensor_name) + default_input_config = self.get_input_config(None) + self.input_selected_sensor = self.create_entity( + self.args['input_selected_sensor'], + state=default_input_config['name'], + icon=default_input_config['icon'], + ) + self.unreachable_sensor = self.create_entity( self.args['unreachable_sensor'], state='off', @@ -187,6 +211,7 @@ class Vertex2(SmartObject): snapshot = dict(status) self.run_in(self._apply_vertex2_status, 0, rx0=snapshot['rx0'], rx1=snapshot['rx1'], + tx0=snapshot['tx0'], tx1=snapshot['tx1'], audout=snapshot['audout'], insel=snapshot['insel']) status_started = None consecutive = (previous_snapshot_generation is not None @@ -215,6 +240,11 @@ class Vertex2(SmartObject): break self.unreachable_sensor.set_state(state='on') self.video_tx_signal_sensor.set_state(state='unavailable') + for sensor in self.video_tx_signal_sensors.values(): + sensor.set_state(state='unavailable') + self.allm_sensor.set_state(state='unavailable') + for sensor in self.allm_sensors.values(): + 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) @@ -255,6 +285,9 @@ class Vertex2(SmartObject): def generate_input_selected_sensor_name(self,i): return f"{self.args['input_selected_sensor']}_tx{i}" + def generate_output_sensor_name(self, sensor_name, output_index): + return f'{sensor_name}_tx{output_index}' + def on_input_selected_sensor_change(self,entity, attribute, old, new, kwargs): if old != new: self.log(f"input_select switch from {old} to {new}") @@ -323,6 +356,7 @@ class Vertex2(SmartObject): state=output_config["name"], attributes={"icon": output_config["icon"]}, ) + self._refresh_selected_output_signal() self.log(f"Selecting TX{output_index} as the active output") if output_index == 0: @@ -340,6 +374,7 @@ class Vertex2(SmartObject): state=default_output_config["name"], attributes={"icon": default_output_config["icon"]}, ) + self._refresh_selected_output_signal() self.force_tx0_plus5(False) self.force_tx1_plus5(False) @@ -437,13 +472,70 @@ class Vertex2(SmartObject): def update_vertex2_states(self): if self.power_plug_entity is not None and self.get_state(self.power_plug_entity) != 'on': - self.run_in(self._apply_vertex2_status, 0, rx0='', rx1='', audout='', insel='0 0') + self.run_in(self._apply_vertex2_status, 0, + rx0='', rx1='', tx0='', tx1='', audout='', insel='0 0') return self._status_requested.set() + def _refresh_selected_output_signal(self): + output_name = self.output_selected_sensor.get_state() + output_index = self.output_names.get(output_name) + self._refresh_selected_input_sensor(output_index) + signal = self._tx_video_signal_values.get(output_index, 'unavailable') + if self.video_tx_signal_sensor.get_state() != signal: + self.video_tx_signal_sensor.set_state(state=signal) + + allm_state = self.allm_sensors[output_index].get_state() if output_index in self.allm_sensors else 'unavailable' + if self.allm_sensor.get_state() != allm_state: + self.allm_sensor.set_state(state=allm_state) + + def _refresh_selected_input_sensor(self, output_index=None): + if output_index not in (0, 1): + config = self.get_input_config(None) + else: + selected_name = self.input_selected_sensors[ + self.generate_input_selected_sensor_name(output_index) + ].get_state() + input_number = self.input_names.get(selected_name) + config = self.get_input_config(input_number) + + attributes = {} + if config.get('icon') is not None: + attributes['icon'] = config['icon'] + if self.input_selected_sensor.get_state() != config['name']: + self.input_selected_sensor.set_state(state=config['name'], attributes=attributes) + elif attributes: + self.input_selected_sensor.set_state(attributes=attributes) + + def _update_output_video_sensors(self, tx0_signal, tx1_signal): + values = {0: tx0_signal.strip() or 'unavailable', 1: tx1_signal.strip() or 'unavailable'} + self._tx_video_signal_values = values + allm_states = {} + + for output_index, signal in values.items(): + if self.video_tx_signal_sensors[output_index].get_state() != signal: + self.video_tx_signal_sensors[output_index].set_state(state=signal) + allm_state = 'unavailable' if signal == 'unavailable' else ( + 'on' if re.search(r'\bALLM\b', signal, re.IGNORECASE) else 'off' + ) + allm_states[output_index] = allm_state + if self.allm_sensors[output_index].get_state() != allm_state: + self.allm_sensors[output_index].set_state(state=allm_state) + + selected_name = self.output_selected_sensor.get_state() + selected_index = self.output_names.get(selected_name) + selected_signal = values.get(selected_index, 'unavailable') + selected_allm = allm_states.get(selected_index, 'unavailable') + signal_changed = self.video_tx_signal_sensor.get_state() != selected_signal + if signal_changed: + self.video_tx_signal_sensor.set_state(state=selected_signal) + if self.allm_sensor.get_state() != selected_allm: + self.allm_sensor.set_state(state=selected_allm) + def _apply_vertex2_status(self, kwargs): self.log_info( f"Vertex2 TCP status: RX0={kwargs['rx0']!r}, RX1={kwargs['rx1']!r}, " + f"TX0={kwargs['tx0']!r}, TX1={kwargs['tx1']!r}, " f"AUDOUT={kwargs['audout']!r}, insel={kwargs['insel']!r}" ) try: @@ -469,7 +561,7 @@ class Vertex2(SmartObject): for i in range(2): sensor = self.input_selected_sensors[self.generate_input_selected_sensor_name(i)] - input_number = None if input_signal in ('unavailable', 'no signal') else portselect[i] + input_number = portselect[i] input_config = self.get_input_config(input_number) name = input_config['name'] attributes = {} @@ -481,8 +573,9 @@ class Vertex2(SmartObject): elif attributes: sensor.set_state(attributes=attributes) - if self.video_tx_signal_sensor.get_state() != input_signal: - self.video_tx_signal_sensor.set_state(state=input_signal) + 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']) 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')