Files
ad_trident/apps/smartlight.py
2026-05-06 09:04:38 +02:00

229 lines
11 KiB
Python

from smartswitch import SmartSwitch
import ad_toolbox.smartcondition as SmartCondition
from ad_toolbox.eventhandler import EventHandler
# =============================================================================
# SmartLight — Light-specific extension of SmartSwitch
# =============================================================================
# Adds brightness control and smooth transitions on top of all SmartSwitch
# features. Use this class instead of SmartSwitch when the entity is a light.
#
# Inherits all SmartObject and SmartSwitch YAML keys.
# The "entity" key is required and must refer to a light entity.
#
# YAML CONFIGURATION
# ------------------
#
# brightness_pct_step: <int> # optional, default 5
# Step size (% of full brightness) used by increase/decrease events.
#
# brightness_step_transition: <seconds> # optional, default 0.5
# Transition duration applied to each increase/decrease brightness
# step. Smooths out the brightness change instead of jumping.
#
# increase_brightness_events: # optional
# <EventHandler events_block>
# HA events that increase brightness by brightness_pct_step.
# Has no effect while the light is off.
#
# decrease_brightness_events: # optional
# <EventHandler events_block>
# HA events that decrease brightness by brightness_pct_step.
# Has no effect while the light is off.
#
# on_events_with_transition: # optional
# <label>:
# events:
# <EventHandler events_block>
# brightness_pct: <0-100>
# Target brightness level.
# transition_time: <seconds>
# Duration of the ramp from 1 % to brightness_pct.
# Turns the light on with a smooth ramp-up. Multiple labelled
# transitions can be defined (e.g. "dim", "full").
#
# light_brightness_pct: # optional
# always_change_brightness: true|false # optional, default false
# When true, apply the resolved brightness even if the light
# is currently off. When false, only apply it while on.
# <label>:
# <SmartCondition block>
# The condition value (label) is used directly as the brightness
# percentage when the condition succeeds.
# Conditions are evaluated in declaration order; the first
# Succeeded result wins.
# Dynamically sets the brightness based on conditions. When the
# active condition changes, the new brightness is applied
# immediately (subject to always_change_brightness).
# Note: brightness 0 also calls light/turn_off for compatibility
# with integrations that do not honour brightness = 0.
#
# icon_override: # optional
# on_icon: <mdi:icon>
# off_icon: <mdi:icon>
# dest_entities: <entity_id | list of entity_ids>
# Updates the icon attribute on dest_entities whenever the light
# turns on or off. Useful for dashboard button cards.
#
# EXAMPLE YAML
# ------------
# living_light:
# module: smartlight
# class: SmartLight
# entity: light.living_room
#
# brightness_pct_step: 10
# brightness_step_transition: 0.5
#
# increase_brightness_events:
# btn_up:
# event_name: WALL_SWITCH
# event_data: {action: brightness_up}
# decrease_brightness_events:
# btn_down:
# event_name: WALL_SWITCH
# event_data: {action: brightness_down}
#
# on_events_with_transition:
# dim:
# events:
# btn:
# event_name: WALL_SWITCH
# event_data: {action: dim}
# brightness_pct: 20
# transition_time: 5
#
# light_brightness_pct:
# always_change_brightness: false
# "10":
# trigger:
# condition: sensor.lux > 500
# "60":
# trigger:
# condition: sensor.lux > 100
# "100":
# trigger:
# condition: "true"
#
# icon_override:
# on_icon: mdi:lightbulb
# off_icon: mdi:lightbulb-outline
# dest_entities: sensor.living_room_button
# =============================================================================
class SmartLight(SmartSwitch):
def on_initialize_smart_object(self):
# light_brightness_pct_list : ordered list of (Evaluator, label) pairs
# light_brightness_pct : currently active brightness % (str label)
self.light_brightness_pct_list = list()
self.light_brightness_pct = None
super().on_initialize_smart_object()
# increase/decrease brightness step events
if "increase_brightness_events" in self.args:
self.event_handlers.append(EventHandler(self, self.args["increase_brightness_events"], self.on_increase_brightness_event))
if "decrease_brightness_events" in self.args:
self.event_handlers.append(EventHandler(self, self.args["decrease_brightness_events"], self.on_decrease_brightness_event))
if "brightness_pct_step" in self.args:
self.brightness_pct_step = self.args["brightness_pct_step"]
else: self.brightness_pct_step = 5
self.brightness_step_transition = self.args.get("brightness_step_transition", 0.8)
# on_events_with_transition: one EventHandler per labelled transition
if "on_events_with_transition" in self.args:
for key in self.args["on_events_with_transition"]:
self.event_handlers.append(EventHandler(self, self.args["on_events_with_transition"][key]["events"], self.on_turn_on_with_transition, key))
# light_brightness_pct: build ordered evaluator list, run initial pass
if "light_brightness_pct" in self.args:
self.always_change_brightness = False
if isinstance(self.args["light_brightness_pct"], int):
self.light_brightness_pct = self.args["light_brightness_pct"]
else:
for key in self.args["light_brightness_pct"]:
if key == 'always_change_brightness':
self.always_change_brightness = bool(self.args["light_brightness_pct"][key])
else:
self.light_brightness_pct_list.append((SmartCondition.Evaluator(self,self.args["light_brightness_pct"][key],condition_name = key, on_update_cb = self.on_update_light_brightness_pct,constants = self.constants, templates_library = self.templates_library, log_callback_trigger_reason = False),key))
self.on_update_light_brightness_pct()
self.listen_state(self.on_state_change,self.entity_id)
# listen_state callback on self.entity_id. Applies icon_override when
# the light turns on or off.
def on_state_change(self, entity, attribute, old, new, *kwargs):
if "icon_override" in self.args:
override_data = self.args['icon_override']
def update_icon(entity_id,new_state): self.set_state(entity_id,attributes = { 'icon' : override_data['on_icon'] if new_state == 'on' else override_data['off_icon'] })
if isinstance(override_data['dest_entities'],list):
for target_entity in override_data['dest_entities']:
update_icon(target_entity,new)
else:
update_icon(override_data['dest_entities'],new)
# ------------------------------------------------------------------
# Brightness event callbacks
# ------------------------------------------------------------------
# Increase brightness by brightness_pct_step while the light is on.
def on_increase_brightness_event(self, event_name, data, kwargs):
if self.get_state(self.entity_id) != 'off':
self.call_service("light/turn_on", entity_id = self.entity_id, brightness_step_pct = self.brightness_pct_step, transition = self.brightness_step_transition)
# Decrease brightness by brightness_pct_step while the light is on.
def on_decrease_brightness_event(self, event_name, data, kwargs):
if self.get_state(self.entity_id) != 'off':
self.call_service("light/turn_on", entity_id = self.entity_id, brightness_step_pct = -self.brightness_pct_step, transition = self.brightness_step_transition)
# EventHandler callback for on_events_with_transition. Jumps to 1 %
# first so the ramp always starts from a known low level, then
# transitions to the configured brightness over transition_time seconds.
def on_turn_on_with_transition(self, event_name, data, event_category):
transition_time = self.args["on_events_with_transition"][event_category]["transition_time"]
brightness_pct = self.args["on_events_with_transition"][event_category]["brightness_pct"]
self.log(f"Turn on at {brightness_pct}% with a transition of {transition_time}s")
self.call_service("light/turn_on", entity_id = self.entity_id,brightness_pct = 1)
self.call_service("light/turn_on", entity_id = self.entity_id, transition = transition_time,brightness_pct = brightness_pct)
# ------------------------------------------------------------------
# SmartSwitch overrides
# ------------------------------------------------------------------
# Override: apply light_brightness_pct when turning on, if one is active.
def switch_on(self):
if self.light_brightness_pct != None:
self.log(f"Turn on {self.entity_id} at {self.light_brightness_pct}%")
self.call_service("light/turn_on", entity_id = self.entity_id,brightness_pct = self.light_brightness_pct)
else:
super().switch_on()
# on_update_cb for all light_brightness_pct evaluators. Walks the list
# in order and applies the first Succeeded result. Pushes the new
# brightness to the light if it is on (or always_change_brightness=true).
# Brightness 0 additionally calls light/turn_off for compatibility.
def on_update_light_brightness_pct(self):
for brightness_pct_evaluator in self.light_brightness_pct_list:
if brightness_pct_evaluator[0].evaluate(False) == SmartCondition.Result.Succeeded:
if self.light_brightness_pct != brightness_pct_evaluator[1]:
brightness_pct_evaluator[0].log_callback_trigger_reason()
brightness_pct_evaluator[0].log_evaluation_result()
self.light_brightness_pct = brightness_pct_evaluator[1]
self.log_info(f"New brightness : {self.light_brightness_pct}%")
break
if (self.always_change_brightness or self.get_state(self.entity_id) == "on") and self.light_brightness_pct != None:
self.call_service("light/turn_on", entity_id = self.entity_id,brightness_pct = self.light_brightness_pct)
if int(self.light_brightness_pct) == 0: #some integration seems to not turn off the light when you set up a brightness of 0
self.log_info("Turning off")
self.call_service("light/turn_off", entity_id = self.entity_id)