Compare commits
6
Commits
e4c63e19a0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3eaeeaf644 | ||
|
|
0a285ecba6 | ||
|
|
ad7c0ac596 | ||
|
|
b129854f54 | ||
|
|
fa1ace8731 | ||
|
|
736c03a034 |
@@ -0,0 +1,59 @@
|
||||
# Bug AppDaemon 4.5.10 : trafic HASS constant (~400-450 KB/s)
|
||||
|
||||
## Symptôme
|
||||
Après ~10 minutes de fonctionnement (temps depuis le démarrage d'AppDaemon,
|
||||
pas de Home Assistant), le trafic réseau du container grimpe à ~400-450 KB/s
|
||||
et reste stable indéfiniment, même sans aucune app chargée.
|
||||
|
||||
## Cause
|
||||
Bug upstream corrigé par [AppDaemon/appdaemon#2429](https://github.com/AppDaemon/appdaemon/pull/2429),
|
||||
inclus depuis la version **4.5.12**. Dans `plugin_management.py`,
|
||||
`update_plugin_state()` appelle `refresh_update_time(plugin)` (l'objet plugin)
|
||||
au lieu de `refresh_update_time(plugin.name)` (la string). Le timestamp utilisé
|
||||
par `time_since_plugin_update(plugin.name)` n'est donc jamais remis à jour :
|
||||
une fois le `refresh_delay` par défaut (10 min) dépassé, AppDaemon redemande
|
||||
l'état complet de Home Assistant (`get_states`) à **chaque tick de la boucle
|
||||
utilitaire (1x/seconde)**, indéfiniment.
|
||||
|
||||
## Statut
|
||||
- Corrigé depuis AppDaemon 4.5.12 (12 oct. 2025). Si l'image utilisée est en
|
||||
4.5.12+, ce fix n'est plus nécessaire.
|
||||
- Toujours utile si on reste bloqué sur une version < 4.5.12.
|
||||
|
||||
## Comment réappliquer le patch (si toujours sur AppDaemon < 4.5.12)
|
||||
|
||||
Fichier concerné : `/usr/local/lib/python3.12/site-packages/appdaemon/plugin_management.py`
|
||||
|
||||
Chercher (dans la méthode `update_plugin_state`) :
|
||||
|
||||
```python
|
||||
finally:
|
||||
await self.refresh_update_time(plugin)
|
||||
```
|
||||
|
||||
Remplacer par :
|
||||
|
||||
```python
|
||||
finally:
|
||||
await self.refresh_update_time(plugin.name)
|
||||
```
|
||||
|
||||
Une seule ligne à changer. Une commande one-liner pour l'appliquer dans le
|
||||
container :
|
||||
|
||||
```sh
|
||||
sed -i 's/await self.refresh_update_time(plugin)$/await self.refresh_update_time(plugin.name)/' \
|
||||
/usr/local/lib/python3.12/site-packages/appdaemon/plugin_management.py
|
||||
```
|
||||
|
||||
⚠️ Ce patch vit dans l'image du container (site-packages), pas dans `/conf`.
|
||||
Il est donc perdu à chaque recréation du container et doit être réappliqué
|
||||
manuellement tant que l'image reste en 4.5.10/4.5.11.
|
||||
|
||||
## Comment vérifier que le patch est actif
|
||||
|
||||
```sh
|
||||
grep -n "refresh_update_time(plugin" /usr/local/lib/python3.12/site-packages/appdaemon/plugin_management.py
|
||||
```
|
||||
|
||||
Doit afficher `refresh_update_time(plugin.name)` (pas `refresh_update_time(plugin)` seul).
|
||||
@@ -111,7 +111,7 @@ bedroom_heatpump:
|
||||
auto: binary_sensor.heatpump_winter_mode or room_temperature > target_temperature + 1
|
||||
quiet: True
|
||||
|
||||
target_temperature: sensor.target_temperature_heatpump # + 0.5
|
||||
target_temperature: sensor.target_temperature_heatpump - 0.5
|
||||
hvac_mode: "binary_sensor.heatpump_winter_mode ? 'heat' : 'cool'"
|
||||
fan_mode: sensor.bedroom_heatpump_fan_mode
|
||||
|
||||
@@ -139,7 +139,7 @@ living_room_heatpump:
|
||||
virtual_sensors:
|
||||
sensors:
|
||||
binary_sensor.living_room_any_window_open: binary_sensor.living_room_french_door or binary_sensor.living_room_window
|
||||
binary_sensor.living_room_cant_open_window: binary_sensor.is_living_room_sleeping or binary_sensor.projector_on
|
||||
binary_sensor.living_room_cant_open_window: binary_sensor.is_living_room_sleeping or binary_sensor.projector_on or sensor.ext_back_humidity > 65
|
||||
value_selector.living_room_heatpump_fan_mode:
|
||||
auto: binary_sensor.heatpump_winter_mode or room_temperature > target_temperature + 1
|
||||
quiet: True
|
||||
@@ -172,4 +172,47 @@ living_room_heatpump:
|
||||
|
||||
quick_resume:
|
||||
sensor: binary_sensor.living_room_heatpump_quick_resume
|
||||
reset_events: good_morning
|
||||
reset_events: good_morning
|
||||
|
||||
guestroom_heatpump:
|
||||
module: smartheatpump
|
||||
class: SmartHeatpump
|
||||
entity: climate.guestroom_heatpump
|
||||
|
||||
templates_library: smart_heating_templates_library
|
||||
|
||||
constants:
|
||||
window_open: binary_sensor.guestroom_window
|
||||
ext_temperature: sensor.ext_front_temperature
|
||||
room_temperature: sensor.guestroom_temperature
|
||||
room_occupancy: binary_sensor.guestroom_occupancy
|
||||
target_temperature: sensor.target_temperature_heatpump
|
||||
cant_open_window: binary_sensor.is_guestroom_sleeping
|
||||
sleeping_in_the_room: binary_sensor.is_guestroom_sleeping
|
||||
quick_resume: binary_sensor.guestroom_heatpump_quick_resume
|
||||
occupied_tonight: input_boolean.guestroom_occupied_tonight
|
||||
stay_on_while_sleeping: false
|
||||
|
||||
virtual_sensors:
|
||||
sensors:
|
||||
value_selector.guestroom_heatpump_fan_mode:
|
||||
auto: binary_sensor.heatpump_winter_mode or room_temperature > target_temperature + 1
|
||||
quiet: True
|
||||
|
||||
target_temperature: sensor.target_temperature_heatpump - 0.5
|
||||
hvac_mode: "binary_sensor.heatpump_winter_mode ? 'heat' : 'cool'"
|
||||
fan_mode: sensor.guestroom_heatpump_fan_mode
|
||||
|
||||
smart_conditions:
|
||||
callback_delay: 10 # on the morning it can happen that we ask to start and stop the heat pump immediately afterwards which in result in the heat pump ignoring the second command
|
||||
template_conditions: <clim_conditions>
|
||||
|
||||
quick_resume:
|
||||
sensor: binary_sensor.guestroom_heatpump_quick_resume
|
||||
reset_events: good_morning
|
||||
|
||||
control_mode_selector:
|
||||
sensor: input_select.guestroom_heatpump_override
|
||||
reset_events:
|
||||
- good_morning
|
||||
- good_bye
|
||||
@@ -56,6 +56,9 @@ occupancy_sensors:
|
||||
binary_sensor.bedroom_occupancy:
|
||||
trigger_conditions: binary_sensor.is_bedroom_sleeping or binary_sensor.day_interval_night
|
||||
blocking_conditions: not binary_sensor.someone_home
|
||||
binary_sensor.guestroom_occupancy:
|
||||
trigger_conditions: binary_sensor.is_guestroom_sleeping or binary_sensor.day_interval_night
|
||||
blocking_conditions: not binary_sensor.someone_home or not input_boolean.guestroom_occupied_tonight
|
||||
retain_condition.desk_occupancy:
|
||||
retain_time: 5 * 60
|
||||
conditions:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,4 +3,6 @@ light_ambiance:
|
||||
class: SmartLight
|
||||
entity: light.desk_ambiance
|
||||
|
||||
smart_conditions: sensor.desk_last_motion < 3 and binary_sensor.is_dark_outside
|
||||
smart_conditions:
|
||||
trigger_conditions: sensor.desk_last_motion < 3 and binary_sensor.is_dark_outside
|
||||
blocking_conditions: binary_sensor.is_desk_sleeping
|
||||
@@ -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
|
||||
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
|
||||
@@ -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
|
||||
@@ -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("#")
|
||||
@@ -0,0 +1 @@
|
||||
16442958
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
debugpy
|
||||
icalendar
|
||||
icalendar
|
||||
samsungtvws
|
||||
Reference in New Issue
Block a user