From 736c03a034cbfd48a78736d8b903ea4e8e929e94 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sat, 1 Aug 2026 11:26:40 +0200 Subject: [PATCH] add The Frame and w4000i integration --- apps/global/virtualsensors.yaml | 4 + apps/rooms/living_room.yaml | 102 ++++++++- apps/samsungtheframe.py | 290 ++++++++++++++++++++++++ apps/smartprojector_benq_over_telnet.py | 241 ++++++++++++++++++++ apps/theframe_token.txt | 1 + requirements.txt | 3 +- 6 files changed, 634 insertions(+), 7 deletions(-) create mode 100644 apps/samsungtheframe.py create mode 100644 apps/smartprojector_benq_over_telnet.py create mode 100644 apps/theframe_token.txt diff --git a/apps/global/virtualsensors.yaml b/apps/global/virtualsensors.yaml index 030b3aa..ede5605 100644 --- a/apps/global/virtualsensors.yaml +++ b/apps/global/virtualsensors.yaml @@ -3,6 +3,9 @@ virtual_sensors: class: VirtualSensorsApp 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 + 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) binary_sensor.is_dark_outside: sun.sun == 'below_horizon' @@ -20,6 +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 - light.living_room - sensor.mezzanine_motion_light_level > 15 - binary_sensor.is_living_room_sleeping diff --git a/apps/rooms/living_room.yaml b/apps/rooms/living_room.yaml index 6f5a8c6..558716c 100644 --- a/apps/rooms/living_room.yaml +++ b/apps/rooms/living_room.yaml @@ -109,12 +109,62 @@ bravia_theatre: smart_conditions: binary_sensor.vertex2_has_input_signal -w4000i: - module: smartswitch - class: SmartSwitch - entity: media_player.benq_w4000i +# w4000i: +# module: smartswitch +# class: SmartSwitch +# entity: media_player.benq_w4000i - smart_conditions: binary_sensor.vertex2_has_input_signal +# smart_conditions: binary_sensor.vertex2_has_input_signal + +w4000i: + module: smartprojector_benq_over_telnet + class: SmartProjector + entity: switch.projector + + telnet_connection: + ip: 10.0.0.40 + port: 8000 + + power_plug: + entity: switch.living_room_projector_plug + power_sensor: sensor.living_room_projector_plug_power + power_threeshold: 5 + conditions: + trigger_conditions: + - binary_sensor.someone_home and not (binary_sensor.is_everybody_sleeping or binary_sensor.is_living_room_sleeping) + - sensor.living_room_projector_plug_power > 10 or binary_sensor.projector_on # don't remove this one or it will fry the projector + + virtual_sensors: + sensors: + value_selector.projector_icon: + projector_off: + conditions: not super + value: "'mdi:projector-off'" + projector_on: + conditions: True + value: "'mdi:projector'" + continuous_condition.projector_ready: + conditions: switch.living_room_projector_plug + time: 30 + + attributes_override: + icon: sensor.projector_icon + friendly_name: Projecteur + + press_enter_event: send_projector_enter + + projector_ready_conditions: binary_sensor.projector_ready + + #state_conditions: binary_sensor.projector_on + #cooling_down_sensor: binary_sensor.projector_cooling_down + + smart_conditions: + trigger_conditions: binary_sensor.vertex2_has_input_signal + disable_conditions: + - self == 'unavailable' + #- not binary_sensor.projector_screen_down + #- binary_sensor.projector_cooling_down + - switch.projector.warming_up mezzanine_curtains: module: smartshutter @@ -139,4 +189,44 @@ living_room_sleep_switch: entity_id: event.living_room_bilresa_button_bouton_2 new_state: attributes: - event_type: multi_press_1 \ No newline at end of file + event_type: multi_press_1 + +samsung_the_frame: + module: samsungtheframe + class: SamsungTheFrame + + ip_address: 10.0.0.58 + #broadcast_address: 10.0.0.255 + #mac_address: 98:06:3C:85:93:38 + #wol_switch: switch.theframewol + wake_up_service: "esphome/ir_bridge_turn_on_the_frame" + app_list: # https://github.com/tavicu/homebridge-samsung-tizen/issues/291 + - 3201907018807 # Netflix + - 3201910019365 # Prime + + virtual_sensors: + default_values: + binary_sensor.tv_auto_shutdown: false + binary_sensor.tv_on: false + sensors: + value_selector.samsung_the_frame_target_state: + power_off: + trigger_conditions: + - 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.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_art_mode: binary_sensor.tv_in_art_mode + tv_auto_shutdown: binary_sensor.tv_auto_shutdown + + input_sensors: + power_plug: switch.the_frame_power_plug + power_plug_power: sensor.the_frame_power_plug_power \ No newline at end of file diff --git a/apps/samsungtheframe.py b/apps/samsungtheframe.py new file mode 100644 index 0000000..3126fd4 --- /dev/null +++ b/apps/samsungtheframe.py @@ -0,0 +1,290 @@ +import appdaemon.plugins.hass.hassapi as hass +from ad_toolbox.smartobject import SmartObject +import ad_toolbox.smartcondition as SmartCondition +from samsungtvws import SamsungTVWS +from samsungtvws import exceptions as tv_exceptions +import os +import random +import time +import websocket + + +class SamsungTheFrame(SmartObject): + + def on_initialize_smart_object(self): + super().on_initialize_smart_object() + + self.log(f"Initializing TheFrame with ip {self.args['ip_address']}") + #self.need_to_turn_off_when_hdmi_gone = False + self.requested_state = None + self.requested_state_confirm_cb_handle = None + + self.power_plug = self.args['input_sensors']['power_plug'] + self.power_plug_power = self.args['input_sensors']['power_plug_power'] + self.is_on_sensor = self.args['output_sensors']['is_on'] + self.is_art_mode_sensor = self.args['output_sensors']['is_art_mode'] + self.tv_auto_shutdown = self.args['output_sensors']['tv_auto_shutdown'] + + if self.get_state(self.tv_auto_shutdown) == None: + self.set_state(self.tv_auto_shutdown,state = 'off') + + if 'app_list' in self.args: self.app_list = self.args['app_list'] + else: self.app_list = [] + + self.tv_states_evaluators = list() + + if "tv_state" in self.args: + self.listen_state(self.on_update_tv_state_sensor,self.args["tv_state"]) + self.request_tv_state(self.get_state(self.args["tv_state"])) + + if "tv_states" in self.args: + for key in self.args["tv_states"]: + self.tv_states_evaluators.append((SmartCondition.Evaluator(self,self.args["tv_states"][key], condition_name = key, on_update_cb = self.on_update_tv_states),key)) + self.on_update_tv_states() + + self.listen_state(self.update_sensors_cb,self.power_plug) + self.listen_state(self.update_sensors_cb,self.power_plug_power) + self.update_sensors() + + def on_update_tv_states(self): + for state_evaluator in self.tv_states_evaluators: + if state_evaluator[0].evaluate(False) == SmartCondition.Result.Succeeded: + state_evaluator[0].log_evaluation_result() + new_state = state_evaluator[1] + break + + self.request_tv_state(new_state) + + def on_update_tv_state_sensor(self, entity, attribute, old, new, kwargs): + if new != old: self.request_tv_state(new) + + def connect(self): + #self.log(f"Connecting to {self.args['ip_address']}") + return SamsungTVWS(self.args['ip_address'],port = 8002, token_file = os.path.join(str(self.AD.app_dir),"theframe_token.txt")) + + def has_power(self): + return self.get_state(self.power_plug) == 'on' and float(self.get_state(self.power_plug_power)) > 35 + + def wake_up(self): + if 'wake_up_service' in self.args: + service = self.args['wake_up_service'] + self.log(f"Calling service {service}") + self.call_service(service) + elif 'wol_switch' in self.args: + self.log(f"Switching on {self.args['wol_switch']}") + self.turn_on(self.args['wol_switch']) + if 'mac_address' in self.args: + wakeonlan.send_magic_packet(self.args['mac_address']) + if 'ip_address' in self.args: + wakeonlan.send_magic_packet(self.args['mac_address'],ip_address= self.args['ip_address'],port = 9) + if 'broadcast_address' in self.args: + wakeonlan.send_magic_packet(self.args['mac_address'],ip_address= self.args['broadcast_address']) + + def select_random_art(self): + try: + tv = self.connect() + available_art = tv.art().available() + if len(available_art): + selected_art = random.choice(available_art) + tv.art().select_image(selected_art['content_id'], show=True) + except tv_exceptions.ResponseError as e: + self.log(f"select_random_art with exception ResponseError {e}") + except TimeoutError: + self.log(f"select_random_art with exception TimeoutError") + + def request_tv_state(self,requested_state): + valid_states = ['art_mode','tv_off','hdmi','power_off','manual'] + assert requested_state in valid_states, f"{requested_state} is not a valid states. Valid states are {valid_states}" + + self.log(f"Requesting TV to switch to {requested_state}") + + if requested_state == 'hdmi' and self.get_state(self.is_on_sensor) == 'off': + self.log(f"setting {self.tv_auto_shutdown} to 'on'") + self.set_state(self.tv_auto_shutdown,state = 'on') + + #if we already have a confirm callback, let's cancel it + if self.requested_state_confirm_cb_handle: + self.log("Canceling previous confirm CB") + self.cancel_timer(self.requested_state_confirm_cb_handle) + self.requested_state_confirm_cb_handle = None + + if requested_state == 'manual': + self.requested_state = None # 'manual' is not a real state + else: + self.requested_state = requested_state + if self._process_state_request(): + self.log(f"The TV was already in the right state") + + def _process_state_request(self): + try: + success, wait_time = self.update_sensors() + if not success: + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, wait_time) + return False + is_art_mode = self.get_state(self.is_art_mode_sensor) == 'on' + is_on = self.get_state(self.is_on_sensor) == 'on' + is_plugged = self.get_state(self.power_plug) == 'on' + + if self.requested_state == 'tv_off': + if not is_plugged: + self.log("Powering on TV") + self.turn_on(self.power_plug) + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 60) + return False + elif is_on: + tv = self.connect() + self.log("Switching to art mode") + tv.shortcuts().power() + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 10) + return False + elif is_art_mode: + tv = self.connect() + self.log("Turning off TV") + tv.hold_key('KEY_POWER',4) + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 10) + return False + elif self.requested_state == 'art_mode': + if not is_art_mode: + if not is_plugged: + self.log("Powering on TV") + self.turn_on(self.power_plug) + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 60) + return False + elif is_on: + tv = self.connect() + self.log("Switching to art mode") + tv.shortcuts().power() + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 10) + return False + elif not self.has_power(): + self.log("Waking up TV") + self.wake_up() + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 60) + return False + + elif self.requested_state == "hdmi": + if not is_plugged: + self.log("Powering on TV") + self.turn_on(self.power_plug) + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 60) + return False + elif not self.has_power(): + self.log("Waking up TV") + self.wake_up() + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 60) + return False + elif not is_on: + tv = self.connect() + self.log("Turning on TV") + tv.shortcuts().power() + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 5) + return False + else: + tv = self.connect() + for app in self.app_list: + # {'id': '3201907018807', 'name': 'Netflix', 'running': True, 'version': '5.2.59020', 'visible': True} + app_status = tv.rest_app_status(str(app)) + if app_status['visible']: + self.log(f"{app_status['name']} is active") + self.log("Switching to Hdmi") + tv.send_key('KEY_HDMI') + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 5) + return False + return True + elif self.requested_state == 'power_off': + if is_plugged: + if is_on: + tv = self.connect() + self.log("Switching to art mode") + tv.shortcuts().power() + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 10) + return False + elif not self.has_power(): + self.log("Waking up TV") + self.wake_up() + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 60) + return False + elif is_art_mode: #it's better to turn off while it's art mode to ensure the TV will wake up in art mode and support properly wake on lan + self.select_random_art() + time.sleep(5) + self.log("Powering off TV") + self.turn_off(self.power_plug) + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 5) + return False + return True + except (BrokenPipeError,websocket.WebSocketConnectionClosedException): + self.requested_state_confirm_cb_handle = self.run_in(self.confirm_change_state_cb, 5) + return False + + + def confirm_change_state_cb(self,*kwargs): + self.requested_state_confirm_cb_handle = None + if self._process_state_request(): + self.log(f"The TV is now confirm to be in {self.requested_state}") + self.requested_state = None + + # def on_art_mode_condition_change(self,prev_result,result): + # if result == SmartCondition.Result.Succeeded: + # self.request_tv_state('art_mode') + # else: + # if self.get_state(self.is_art_mode_sensor) == 'on': + # self.select_random_art() + # #I need to request state if I'm not in art_mode as it will cancel a potential request pending + # self.request_tv_state('off') + + def update_sensors_cb(self,*args): + success, wait_time = self.update_sensors() + if not success: + self.run_in(self.update_sensors_cb, wait_time) + + def update_sensors(self): + is_art_mode = "off" + is_on = "off" + if self.has_power(): + try: + tv = self.connect() + + try: + if tv.art().get_artmode() == 'on': + is_art_mode = "on" + else: + is_on = "on" + except KeyError as e: #sometimes the lib throw an exception in File "/samsungtvws/art.py", line 318, in get_artmode + self.log(f"get_artmode() throw a KeyError Exception {e}") + return False , 10 + + except websocket.WebSocketConnectionClosedException: + self.log(f"update sensors failed with exception WebSocketConnectionClosedException") + return False , 30 + except ConnectionRefusedError: + self.log(f"update sensors failed with exception ConnectionRefusedError") + return False , 30 + except tv_exceptions.ConnectionFailure: + self.log(f"update sensors failed with exception ConnectionFailure") + return False , 30 + except OSError as error: + self.log(f"update sensors failed with exception OsError {error}") + return False , 30 + except tv_exceptions.ResponseError as e: + self.log(f"update sensors failed with exception ResponseError {e}") + return False , 30 + # except: + # import traceback + # exception_string = traceback.format_exc().splitlines()[-1] + # self.disconnect() + # self.log(f"update sensors failed with exception {exception_string}") + # return False , 5 + + if self.get_state(self.is_art_mode_sensor) != is_art_mode: + self.log(f"Updating sensor: {self.is_art_mode_sensor} = '{is_art_mode}'") + self.set_state(self.is_art_mode_sensor,state = is_art_mode) + + if self.get_state(self.is_on_sensor) != is_on: + self.log(f"Updating sensor: {self.is_on_sensor} = '{is_on}'") + self.set_state(self.is_on_sensor,state = is_on) + if is_on == 'off' and self.get_state(self.tv_auto_shutdown) != 'off': + self.log(f"setting {self.tv_auto_shutdown} to 'off'") + self.set_state(self.tv_auto_shutdown,state = 'off') + + + return True, 0 \ No newline at end of file diff --git a/apps/smartprojector_benq_over_telnet.py b/apps/smartprojector_benq_over_telnet.py new file mode 100644 index 0000000..6f73f71 --- /dev/null +++ b/apps/smartprojector_benq_over_telnet.py @@ -0,0 +1,241 @@ +import appdaemon.plugins.hass.hassapi as hass +from smartswitch import SmartSwitch +import ad_toolbox.smartcondition as SmartCondition +import telnetlib +import time +import datetime + +class SmartProjector(SmartSwitch): + + WARM_UP_TIME = 35 + def get_default_entity_state(self): return 'off' + + def on_initialize_smart_object(self): + #those variable are use withing get_default_entity_state() + self.telnet_ip = self.args['telnet_connection']['ip'] + self.telnet_port = self.args['telnet_connection']['port'] + self.update_state_cb_handle = None + self.warming_up_done_cb_handle = None + self.power_plug_entity = None + self.power_plug_sensor = None + self.attributes = { 'warming_up' : False, 'picture_mode' : None} + + if "power_plug" in self.args: + self.power_plug_entity = self.args["power_plug"]["entity"] + self.power_plug_conditions_evaluator = SmartCondition.Evaluator(self,self.args["power_plug"]["conditions"],condition_name = 'power_plug',on_change_cb = self.on_change_power_plug_conditions,unavaibility_result = SmartCondition.Result.Unavailable) + if "power_sensor" in self.args["power_plug"]: + self.power_threeshold = self.args["power_plug"]["power_threeshold"] + self.power_plug_sensor = self.get_entity(self.args["power_plug"]["power_sensor"]) + + super().on_initialize_smart_object() + + self.entity.set_state(attributes = self.attributes) + + self.listen_event(self.on_turn_on_service_call,"call_service",domain = "switch", service = "turn_on", service_data = {'entity_id' : self.entity_id}) + self.listen_event(self.on_turn_off_service_call,"call_service",domain = "switch", service = "turn_off", service_data = {'entity_id' : self.entity_id}) + + if 'press_enter_event' in self.args: + self.listen_event(lambda event_name, data, kwargs: self.press_enter(),self.args['press_enter_event']) + + self.projector_ready_conditions = SmartCondition.Evaluator(self,self.args["projector_ready_conditions"],condition_name = 'projector_ready_conditions') + + # self.send_cmd("menu=?",True) + + self.lamp_mode_evaluators = list() + if "lamp_mode" in self.args: + for key in self.args["lamp_mode"]: + self.lamp_mode_evaluators.append((SmartCondition.Evaluator(self,self.args["lamp_mode"][key], condition_name = f"lamp_mode][{key}", on_update_cb = self.on_update_lamp_mode,constants = self.constants, templates_library = self.templates_library),key)) + self.on_update_lamp_mode() + + self.picture_mode_evaluators = list() + if "picture_mode" in self.args: + for key in self.args["picture_mode"]: + self.picture_mode_evaluators.append((SmartCondition.Evaluator(self,self.args["picture_mode"][key], condition_name = f"picture_mode][{key}", on_update_cb = self.on_update_picture_mode,constants = self.constants, templates_library = self.templates_library),key)) + self.on_update_picture_mode() + + + if self.power_plug_sensor != None: + self.power_plug_sensor.listen_state(self.on_power_change) + + self.reset_update_state_cb() + #self.entity.listen_state(self.on_state_change) + self.update_state() + + def on_power_change(self, entity, attribute, old, new, kwargs): + self.update_state() + + def on_change_power_plug_conditions(self,prev_result,result): + if result != SmartCondition.Result.Unavailable: + if result == SmartCondition.Result.Succeeded: + self.turn_on(self.power_plug_entity) + else: + self.turn_off(self.power_plug_entity) + + def on_turn_on_service_call(self, event_name, data, kwargs): + self.log_info("on_turn_on_service_call") + self.switch_on() + + def on_turn_off_service_call(self, event_name, data, kwargs): + self.log_info("on_turn_off_service_call") + self.switch_off() + + def reset_update_state_cb(self): + # when we use a power sensor, the change of power consuption is responsible of triggering the update + if self.power_plug_sensor == None: + if self.update_state_cb_handle != None: + self.cancel_timer(self.update_state_cb_handle) + self.update_state_cb_handle = self.run_every(self.update_state,datetime.datetime.now(), 30) + + def is_available(self): return self.entity.get_state() != 'unavailable' + + def switch_on(self): + if self.is_available(): + self.log(f"Turn on {self.entity_id}") + # when we turn on the projector, pow=? will return OFF during the warm up period (a few seconds) + # it can result in a switch hickup if update_state is called right after we turn on the projector + self.reset_update_state_cb() + + #send_cmd can take 1-2s when the projector is off, so it's better to optimisticaly set the state and correct afterwards + self.set_warming_up_attribute(True) + self.entity.set_state(state = 'on',attributes = self.attributes) + cmd_result = self.send_cmd('pow=on') + if cmd_result is not None and cmd_result != "POW=ON": + # Got a definitive bad response — revert + self.set_warming_up_attribute(False) + self.entity.set_state(state = 'off',attributes = self.attributes) + # If cmd_result is None (projector dropped connection during power-on sequence), + # keep optimistic 'on' state — update_state() will correct after warm-up + else: + self.log_warning("Turn On ignored as the projector is unavailable") + + def switch_off(self): + if self.is_available(): + self.log(f"Turn off {self.entity_id}") + if self.send_cmd('pow=off') == "POW=OFF": + self.set_warming_up_attribute(False) + self.entity.set_state(state = 'off',attributes = self.attributes) + else: + self.log_warning("Turn Off ignored as the projector is unavailable") + + def get_projector_state(self): + if self.projector_ready_conditions.evaluate(False) == SmartCondition.Result.Succeeded: + if self.power_plug_sensor != None: + if float(self.power_plug_sensor.get_state()) > self.power_threeshold: return 'on' + else: return 'off' + else: + try: + cmd_result = self.send_cmd('pow=?') + if cmd_result == "POW=ON": + return 'on' + if cmd_result == "POW=OFF": + return 'off' + self.log_info(f"pow=? returned and invalid result : {cmd_result}, the projector will stay in the previous state") + return self.entity.get_state() + except OSError as error: + # telenetlib raise an OsError [Errno 113] Host is unreachable when the projector is unreachable (most likely unplugged) + if error.errno == 113: return 'unavailable' + else: raise + else: return 'off' + + def on_update_picture_mode(self): + for evaluator in self.picture_mode_evaluators: + if evaluator[0].evaluate(False) == SmartCondition.Result.Succeeded: + evaluator[0].log_evaluation_result() + if evaluator[1] != "None": + cmd_result = self.send_cmd("appmod=?") + if cmd_result == 'APPMOD=FIMMAKER': + self.log_info(f"Can't switch picture mode to {evaluator[1]} because the projector is in Filmmaker mode") + else: + cmd_result = self.send_cmd(f"appmod={evaluator[1]}",True) + + self.attributes['picture_mode'] = cmd_result.split("=")[1] if cmd_result else None + self.entity.set_state(attributes = self.attributes) + break + + def on_update_lamp_mode(self): + for evaluator in self.lamp_mode_evaluators: + if evaluator[0].evaluate(False) == SmartCondition.Result.Succeeded: + evaluator[0].log_evaluation_result() + if evaluator[1] != "None": + result = self.send_cmd(f"lampm={evaluator[1]}",True) + break + + def update_state(self,kwargs = None): + new_state = self.get_projector_state() + + if new_state != self.entity.get_state(): + self.set_warming_up_attribute(new_state == 'on') + + if new_state == 'on': + # the projector is not responding during power on sequence + if self.attributes['warming_up'] == 'off': + cmd_result = self.send_cmd("appmod=?") + if cmd_result: self.attributes['picture_mode'] = cmd_result.split("=")[1] + else: self.attributes['picture_mode'] = "unavailable" + else: self.attributes['picture_mode'] = "unavailable" + + + + self.entity.set_state(state = new_state,attributes = self.attributes) + + def set_warming_up_attribute(self, new_value): + if new_value != self.attributes['warming_up']: + # let's cancel previous CB + if self.warming_up_done_cb_handle != None: + self.cancel_timer(self.warming_up_done_cb_handle) + self.warming_up_done_cb_handle = None + + if new_value: + self.warming_up_done_cb_handle = self.run_in(self.on_warm_up_done,self.WARM_UP_TIME) + + self.attributes['warming_up'] = new_value + + def on_warm_up_done(self,*kwargs): + self.warming_up_done_cb_handle = None + self.attributes['warming_up'] = False + self.entity.set_state(state = self.get_projector_state(),attributes = self.attributes) + + def press_enter(self): + self.send_cmd("enter") + + def send_cmd(self,cmd, log_command = False): + cmd = f"*{cmd}#" + return self._send_cmd(cmd,log_command,3) + + def _send_cmd(self,cmd,log_command,try_count): + invalid_result = [ "*Unsupported Item#", "*Block Item#", "*Illegal format#" ] + + should_retry = False + with telnetlib.Telnet(self.telnet_ip,self.telnet_port) as session: + if log_command: self.log_info(f'Sending command "{cmd}"') + try: + session.write(bytes(cmd,'utf-8')) + output = session.read_until(b"#",timeout = 5).decode('ascii') + except (ConnectionResetError,EOFError): + self.log_warning(f"Connection Reset While sending command {cmd}. {try_count} try left") + if try_count <= 0: + self.log_error(f"Could not send command {cmd}",dump_stack = True) + return None + else: should_retry = True + + + # opening session recursivly inside the with doesn't sound like a good idea + if should_retry: + if try_count == 1: + # I know calling sleep() is in callback is evil + # but this is a very small sleep time and working with event would make the code much much more complexe + time.sleep(1) + return self._send_cmd(cmd,log_command,try_count - 1) + + if log_command: self.log_info(f"Result = {output}") + if output in invalid_result: + self.log_error(f"Command rejected. {cmd} returned {output}",dump_stack = True) + return None + if not output: + if try_count > 0: + self.log_warning(f"Command output is empty. {cmd}. {try_count} try left") + return self._send_cmd(cmd,log_command,try_count - 1) + else: + self.log_error(f"Command output is empty. {cmd}. {try_count} try left",dump_stack = True) + return None + return output.strip("*").rstrip("#") \ No newline at end of file diff --git a/apps/theframe_token.txt b/apps/theframe_token.txt new file mode 100644 index 0000000..7584140 --- /dev/null +++ b/apps/theframe_token.txt @@ -0,0 +1 @@ +16442958 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b8c5aa7..11e95a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ debugpy -icalendar \ No newline at end of file +icalendar +samsungtvws \ No newline at end of file