Add advanced light sync configuration for multiple downlights and Nanoleaf
This commit is contained in:
parent
176633899f
commit
d5183b2d64
2 changed files with 406 additions and 0 deletions
285
hass_mqtt_advanced_light_sync.py
Normal file
285
hass_mqtt_advanced_light_sync.py
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import appdaemon.plugins.mqtt.mqttapi as mqtt
|
||||||
|
import appdaemon.adbase as base
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class hass_mqtt_advanced_light_sync(hass.Hass, mqtt.Mqtt):
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.entity_id = self.args["entity_id"]
|
||||||
|
self.entity = self.get_entity(self.entity_id)
|
||||||
|
self.mqtt_topic_base = self.args["mqtt_topic_base"] # e.g., "/commonroom/dl1"
|
||||||
|
|
||||||
|
# MQTT Topics
|
||||||
|
self.mqtt_topic_state = self.mqtt_topic_base
|
||||||
|
self.mqtt_topic_state_set = f"{self.mqtt_topic_base}/set"
|
||||||
|
self.mqtt_topic_state_request = f"{self.mqtt_topic_base}/request"
|
||||||
|
|
||||||
|
self.mqtt_topic_brightness = f"{self.mqtt_topic_base}/brightness"
|
||||||
|
self.mqtt_topic_brightness_set = f"{self.mqtt_topic_base}/brightness/set"
|
||||||
|
|
||||||
|
self.mqtt_topic_color_r = f"{self.mqtt_topic_base}/color/r"
|
||||||
|
self.mqtt_topic_color_r_set = f"{self.mqtt_topic_base}/color/r/set"
|
||||||
|
self.mqtt_topic_color_g = f"{self.mqtt_topic_base}/color/g"
|
||||||
|
self.mqtt_topic_color_g_set = f"{self.mqtt_topic_base}/color/g/set"
|
||||||
|
self.mqtt_topic_color_b = f"{self.mqtt_topic_base}/color/b"
|
||||||
|
self.mqtt_topic_color_b_set = f"{self.mqtt_topic_base}/color/b/set"
|
||||||
|
|
||||||
|
self.mqtt_topic_temp = f"{self.mqtt_topic_base}/temp"
|
||||||
|
self.mqtt_topic_temp_set = f"{self.mqtt_topic_base}/temp/set"
|
||||||
|
|
||||||
|
self.mqtt_topic_mode = f"{self.mqtt_topic_base}/mode"
|
||||||
|
self.mqtt_topic_mode_set = f"{self.mqtt_topic_base}/mode/set"
|
||||||
|
|
||||||
|
# Light capabilities from config
|
||||||
|
self.has_brightness = self.args.get("has_brightness", True)
|
||||||
|
self.has_rgb = self.args.get("has_rgb", False)
|
||||||
|
self.has_ct = self.args.get("has_ct", False)
|
||||||
|
self.ct_min = self.args.get("ct_min", 2700) # Kelvin
|
||||||
|
self.ct_max = self.args.get("ct_max", 6500) # Kelvin
|
||||||
|
|
||||||
|
# Subscribe to MQTT set topics
|
||||||
|
self.mqtt_subscribe(self.mqtt_topic_state_set, namespace="mqtt")
|
||||||
|
self.mqtt_subscribe(self.mqtt_topic_state_request, namespace="mqtt")
|
||||||
|
|
||||||
|
if self.has_brightness:
|
||||||
|
self.mqtt_subscribe(self.mqtt_topic_brightness_set, namespace="mqtt")
|
||||||
|
|
||||||
|
if self.has_rgb:
|
||||||
|
self.mqtt_subscribe(self.mqtt_topic_color_r_set, namespace="mqtt")
|
||||||
|
self.mqtt_subscribe(self.mqtt_topic_color_g_set, namespace="mqtt")
|
||||||
|
self.mqtt_subscribe(self.mqtt_topic_color_b_set, namespace="mqtt")
|
||||||
|
|
||||||
|
if self.has_ct:
|
||||||
|
self.mqtt_subscribe(self.mqtt_topic_temp_set, namespace="mqtt")
|
||||||
|
|
||||||
|
if self.has_rgb and self.has_ct:
|
||||||
|
self.mqtt_subscribe(self.mqtt_topic_mode_set, namespace="mqtt")
|
||||||
|
|
||||||
|
# Listen to MQTT events
|
||||||
|
self.listen_event(self.handle_state_set, "MQTT_MESSAGE", topic=self.mqtt_topic_state_set, namespace="mqtt")
|
||||||
|
self.listen_event(self.handle_state_request, "MQTT_MESSAGE", topic=self.mqtt_topic_state_request, namespace="mqtt")
|
||||||
|
|
||||||
|
if self.has_brightness:
|
||||||
|
self.listen_event(self.handle_brightness_set, "MQTT_MESSAGE", topic=self.mqtt_topic_brightness_set, namespace="mqtt")
|
||||||
|
|
||||||
|
if self.has_rgb:
|
||||||
|
self.listen_event(self.handle_color_r_set, "MQTT_MESSAGE", topic=self.mqtt_topic_color_r_set, namespace="mqtt")
|
||||||
|
self.listen_event(self.handle_color_g_set, "MQTT_MESSAGE", topic=self.mqtt_topic_color_g_set, namespace="mqtt")
|
||||||
|
self.listen_event(self.handle_color_b_set, "MQTT_MESSAGE", topic=self.mqtt_topic_color_b_set, namespace="mqtt")
|
||||||
|
|
||||||
|
if self.has_ct:
|
||||||
|
self.listen_event(self.handle_temp_set, "MQTT_MESSAGE", topic=self.mqtt_topic_temp_set, namespace="mqtt")
|
||||||
|
|
||||||
|
if self.has_rgb and self.has_ct:
|
||||||
|
self.listen_event(self.handle_mode_set, "MQTT_MESSAGE", topic=self.mqtt_topic_mode_set, namespace="mqtt")
|
||||||
|
|
||||||
|
# Listen to Home Assistant state changes
|
||||||
|
self.entity.listen_state(self.handle_hass_state_change)
|
||||||
|
|
||||||
|
# Initial state publish
|
||||||
|
self.publish_all_states_to_mqtt()
|
||||||
|
|
||||||
|
def publish_all_states_to_mqtt(self):
|
||||||
|
"""Publish all current Home Assistant states to MQTT"""
|
||||||
|
try:
|
||||||
|
# Main state (on/off)
|
||||||
|
state = self.get_state(self.entity_id)
|
||||||
|
self.mqtt_publish(self.mqtt_topic_state, str(state), namespace="mqtt")
|
||||||
|
|
||||||
|
if state == "on":
|
||||||
|
# Brightness
|
||||||
|
if self.has_brightness:
|
||||||
|
brightness = self.get_state(self.entity_id, attribute="brightness")
|
||||||
|
if brightness is not None:
|
||||||
|
self.mqtt_publish(self.mqtt_topic_brightness, str(brightness), namespace="mqtt")
|
||||||
|
|
||||||
|
# RGB Color
|
||||||
|
if self.has_rgb:
|
||||||
|
rgb_color = self.get_state(self.entity_id, attribute="rgb_color")
|
||||||
|
if rgb_color is not None and len(rgb_color) >= 3:
|
||||||
|
self.mqtt_publish(self.mqtt_topic_color_r, str(rgb_color[0]), namespace="mqtt")
|
||||||
|
self.mqtt_publish(self.mqtt_topic_color_g, str(rgb_color[1]), namespace="mqtt")
|
||||||
|
self.mqtt_publish(self.mqtt_topic_color_b, str(rgb_color[2]), namespace="mqtt")
|
||||||
|
|
||||||
|
# Color Temperature
|
||||||
|
if self.has_ct:
|
||||||
|
color_temp = self.get_state(self.entity_id, attribute="color_temp")
|
||||||
|
if color_temp is not None:
|
||||||
|
# Convert from Kelvin to 0-4095 range
|
||||||
|
ct_normalized = self.kelvin_to_4095(color_temp)
|
||||||
|
self.mqtt_publish(self.mqtt_topic_temp, str(ct_normalized), namespace="mqtt")
|
||||||
|
|
||||||
|
# Mode determination
|
||||||
|
if self.has_rgb and self.has_ct:
|
||||||
|
# Determine mode based on current light state
|
||||||
|
color_mode = self.get_state(self.entity_id, attribute="color_mode")
|
||||||
|
if color_mode == "rgb":
|
||||||
|
self.mqtt_publish(self.mqtt_topic_mode, "rgb", namespace="mqtt")
|
||||||
|
elif color_mode in ["color_temp", "white"]:
|
||||||
|
self.mqtt_publish(self.mqtt_topic_mode, "ct", namespace="mqtt")
|
||||||
|
else:
|
||||||
|
# Default to RGB mode
|
||||||
|
self.mqtt_publish(self.mqtt_topic_mode, "rgb", namespace="mqtt")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Error publishing states: {e}")
|
||||||
|
|
||||||
|
def kelvin_to_4095(self, kelvin):
|
||||||
|
"""Convert Kelvin temperature to 0-4095 range"""
|
||||||
|
if kelvin <= self.ct_min:
|
||||||
|
return 0
|
||||||
|
elif kelvin >= self.ct_max:
|
||||||
|
return 4095
|
||||||
|
else:
|
||||||
|
# Linear interpolation
|
||||||
|
ratio = (kelvin - self.ct_min) / (self.ct_max - self.ct_min)
|
||||||
|
return int(ratio * 4095)
|
||||||
|
|
||||||
|
def range_4095_to_kelvin(self, value_4095):
|
||||||
|
"""Convert 0-4095 range to Kelvin temperature"""
|
||||||
|
if value_4095 <= 0:
|
||||||
|
return self.ct_min
|
||||||
|
elif value_4095 >= 4095:
|
||||||
|
return self.ct_max
|
||||||
|
else:
|
||||||
|
# Linear interpolation
|
||||||
|
ratio = value_4095 / 4095
|
||||||
|
return int(self.ct_min + ratio * (self.ct_max - self.ct_min))
|
||||||
|
|
||||||
|
# MQTT → Home Assistant handlers
|
||||||
|
def handle_state_set(self, event_name, data, cb_args):
|
||||||
|
"""Handle on/off state changes from MQTT"""
|
||||||
|
state = data["payload"].lower()
|
||||||
|
if state == "on":
|
||||||
|
self.call_service("homeassistant/turn_on", entity_id=self.entity_id)
|
||||||
|
elif state == "off":
|
||||||
|
self.call_service("homeassistant/turn_off", entity_id=self.entity_id)
|
||||||
|
|
||||||
|
def handle_state_request(self, event_name, data, cb_args):
|
||||||
|
"""Handle state request from MQTT"""
|
||||||
|
self.publish_all_states_to_mqtt()
|
||||||
|
|
||||||
|
def handle_brightness_set(self, event_name, data, cb_args):
|
||||||
|
"""Handle brightness changes from MQTT"""
|
||||||
|
if not self.has_brightness:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
brightness = int(data["payload"])
|
||||||
|
brightness = max(0, min(255, brightness)) # Clamp to 0-255
|
||||||
|
self.call_service("light/turn_on", entity_id=self.entity_id, brightness=brightness)
|
||||||
|
except ValueError:
|
||||||
|
self.log(f"Invalid brightness value: {data['payload']}")
|
||||||
|
|
||||||
|
def handle_color_r_set(self, event_name, data, cb_args):
|
||||||
|
"""Handle red color changes from MQTT"""
|
||||||
|
self.handle_color_component_set("r", data)
|
||||||
|
|
||||||
|
def handle_color_g_set(self, event_name, data, cb_args):
|
||||||
|
"""Handle green color changes from MQTT"""
|
||||||
|
self.handle_color_component_set("g", data)
|
||||||
|
|
||||||
|
def handle_color_b_set(self, event_name, data, cb_args):
|
||||||
|
"""Handle blue color changes from MQTT"""
|
||||||
|
self.handle_color_component_set("b", data)
|
||||||
|
|
||||||
|
def handle_color_component_set(self, component, data):
|
||||||
|
"""Handle RGB color component changes from MQTT"""
|
||||||
|
if not self.has_rgb:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
value = int(data["payload"])
|
||||||
|
value = max(0, min(255, value)) # Clamp to 0-255
|
||||||
|
|
||||||
|
# Get current RGB values
|
||||||
|
current_rgb = self.get_state(self.entity_id, attribute="rgb_color")
|
||||||
|
if current_rgb is None:
|
||||||
|
current_rgb = [255, 255, 255] # Default to white
|
||||||
|
|
||||||
|
# Update the specific component
|
||||||
|
new_rgb = list(current_rgb)
|
||||||
|
if component == "r":
|
||||||
|
new_rgb[0] = value
|
||||||
|
elif component == "g":
|
||||||
|
new_rgb[1] = value
|
||||||
|
elif component == "b":
|
||||||
|
new_rgb[2] = value
|
||||||
|
|
||||||
|
# Set the new RGB color
|
||||||
|
self.call_service("light/turn_on", entity_id=self.entity_id, rgb_color=new_rgb)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
self.log(f"Invalid color {component} value: {data['payload']}")
|
||||||
|
|
||||||
|
def handle_temp_set(self, event_name, data, cb_args):
|
||||||
|
"""Handle color temperature changes from MQTT"""
|
||||||
|
if not self.has_ct:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
temp_4095 = int(data["payload"])
|
||||||
|
temp_4095 = max(0, min(4095, temp_4095)) # Clamp to 0-4095
|
||||||
|
|
||||||
|
# Convert to Kelvin
|
||||||
|
kelvin = self.range_4095_to_kelvin(temp_4095)
|
||||||
|
|
||||||
|
# Set color temperature
|
||||||
|
self.call_service("light/turn_on", entity_id=self.entity_id, color_temp=kelvin)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
self.log(f"Invalid color temperature value: {data['payload']}")
|
||||||
|
|
||||||
|
def handle_mode_set(self, event_name, data, cb_args):
|
||||||
|
"""Handle mode changes from MQTT"""
|
||||||
|
if not (self.has_rgb and self.has_ct):
|
||||||
|
return
|
||||||
|
|
||||||
|
mode = data["payload"].lower()
|
||||||
|
if mode == "rgb":
|
||||||
|
# Switch to RGB mode - set a default RGB color
|
||||||
|
self.call_service("light/turn_on", entity_id=self.entity_id, rgb_color=[255, 255, 255])
|
||||||
|
elif mode == "ct":
|
||||||
|
# Switch to CT mode - set a default color temperature
|
||||||
|
default_kelvin = (self.ct_min + self.ct_max) // 2
|
||||||
|
self.call_service("light/turn_on", entity_id=self.entity_id, color_temp=default_kelvin)
|
||||||
|
|
||||||
|
# Home Assistant → MQTT handler
|
||||||
|
def handle_hass_state_change(self, entity, attribute, old, new, cb_args):
|
||||||
|
"""Handle Home Assistant state changes and publish to MQTT"""
|
||||||
|
self.log(f"State change detected: {attribute} {old} -> {new}")
|
||||||
|
|
||||||
|
# Publish the changed state
|
||||||
|
if attribute == "state":
|
||||||
|
self.mqtt_publish(self.mqtt_topic_state, str(new), namespace="mqtt")
|
||||||
|
|
||||||
|
# If turning on, publish all attributes
|
||||||
|
if new == "on":
|
||||||
|
self.run_in(self.delayed_publish_all, 0.5) # Small delay to ensure attributes are updated
|
||||||
|
|
||||||
|
elif attribute == "brightness" and self.has_brightness:
|
||||||
|
if new is not None:
|
||||||
|
self.mqtt_publish(self.mqtt_topic_brightness, str(new), namespace="mqtt")
|
||||||
|
|
||||||
|
elif attribute == "rgb_color" and self.has_rgb:
|
||||||
|
if new is not None and len(new) >= 3:
|
||||||
|
self.mqtt_publish(self.mqtt_topic_color_r, str(new[0]), namespace="mqtt")
|
||||||
|
self.mqtt_publish(self.mqtt_topic_color_g, str(new[1]), namespace="mqtt")
|
||||||
|
self.mqtt_publish(self.mqtt_topic_color_b, str(new[2]), namespace="mqtt")
|
||||||
|
|
||||||
|
# Update mode to RGB
|
||||||
|
if self.has_ct:
|
||||||
|
self.mqtt_publish(self.mqtt_topic_mode, "rgb", namespace="mqtt")
|
||||||
|
|
||||||
|
elif attribute == "color_temp" and self.has_ct:
|
||||||
|
if new is not None:
|
||||||
|
# Convert to 0-4095 range
|
||||||
|
ct_normalized = self.kelvin_to_4095(new)
|
||||||
|
self.mqtt_publish(self.mqtt_topic_temp, str(ct_normalized), namespace="mqtt")
|
||||||
|
|
||||||
|
# Update mode to CT
|
||||||
|
if self.has_rgb:
|
||||||
|
self.mqtt_publish(self.mqtt_topic_mode, "ct", namespace="mqtt")
|
||||||
|
|
||||||
|
def delayed_publish_all(self, cb_args):
|
||||||
|
"""Delayed publish all states - used when light turns on"""
|
||||||
|
self.publish_all_states_to_mqtt()
|
121
light_sync_config_example.yaml
Normal file
121
light_sync_config_example.yaml
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
# AppDaemon Configuration Example for Advanced Light Sync
|
||||||
|
# Add these configurations to your apps.yaml file
|
||||||
|
|
||||||
|
# RGB + Color Temperature Lights (Downlights)
|
||||||
|
downlight_1_sync:
|
||||||
|
module: hass_mqtt_advanced_light_sync
|
||||||
|
class: hass_mqtt_advanced_light_sync
|
||||||
|
entity_id: light.downlight_1
|
||||||
|
mqtt_topic_base: "/commonroom/dl1"
|
||||||
|
has_brightness: true
|
||||||
|
has_rgb: true
|
||||||
|
has_ct: true
|
||||||
|
ct_min: 2700 # Kelvin
|
||||||
|
ct_max: 6500 # Kelvin
|
||||||
|
|
||||||
|
downlight_2_sync:
|
||||||
|
module: hass_mqtt_advanced_light_sync
|
||||||
|
class: hass_mqtt_advanced_light_sync
|
||||||
|
entity_id: light.downlight_2
|
||||||
|
mqtt_topic_base: "/commonroom/dl2"
|
||||||
|
has_brightness: true
|
||||||
|
has_rgb: true
|
||||||
|
has_ct: true
|
||||||
|
ct_min: 2700
|
||||||
|
ct_max: 6500
|
||||||
|
|
||||||
|
downlight_3_sync:
|
||||||
|
module: hass_mqtt_advanced_light_sync
|
||||||
|
class: hass_mqtt_advanced_light_sync
|
||||||
|
entity_id: light.downlight_3
|
||||||
|
mqtt_topic_base: "/commonroom/dl3"
|
||||||
|
has_brightness: true
|
||||||
|
has_rgb: true
|
||||||
|
has_ct: true
|
||||||
|
ct_min: 2700
|
||||||
|
ct_max: 6500
|
||||||
|
|
||||||
|
downlight_4_sync:
|
||||||
|
module: hass_mqtt_advanced_light_sync
|
||||||
|
class: hass_mqtt_advanced_light_sync
|
||||||
|
entity_id: light.downlight_4
|
||||||
|
mqtt_topic_base: "/commonroom/dl4"
|
||||||
|
has_brightness: true
|
||||||
|
has_rgb: true
|
||||||
|
has_ct: true
|
||||||
|
ct_min: 2700
|
||||||
|
ct_max: 6500
|
||||||
|
|
||||||
|
downlight_5_sync:
|
||||||
|
module: hass_mqtt_advanced_light_sync
|
||||||
|
class: hass_mqtt_advanced_light_sync
|
||||||
|
entity_id: light.downlight_5
|
||||||
|
mqtt_topic_base: "/commonroom/dl5"
|
||||||
|
has_brightness: true
|
||||||
|
has_rgb: true
|
||||||
|
has_ct: true
|
||||||
|
ct_min: 2700
|
||||||
|
ct_max: 6500
|
||||||
|
|
||||||
|
downlight_6_sync:
|
||||||
|
module: hass_mqtt_advanced_light_sync
|
||||||
|
class: hass_mqtt_advanced_light_sync
|
||||||
|
entity_id: light.downlight_6
|
||||||
|
mqtt_topic_base: "/commonroom/dl6"
|
||||||
|
has_brightness: true
|
||||||
|
has_rgb: true
|
||||||
|
has_ct: true
|
||||||
|
ct_min: 2700
|
||||||
|
ct_max: 6500
|
||||||
|
|
||||||
|
# Nanoleaf RGB + Color Temperature Light
|
||||||
|
nanoleaf_sync:
|
||||||
|
module: hass_mqtt_advanced_light_sync
|
||||||
|
class: hass_mqtt_advanced_light_sync
|
||||||
|
entity_id: light.nanoleaf_panels
|
||||||
|
mqtt_topic_base: "/commonroom/nanoleaf"
|
||||||
|
has_brightness: true
|
||||||
|
has_rgb: true
|
||||||
|
has_ct: true
|
||||||
|
ct_min: 1200 # Nanoleaf supports wider CT range
|
||||||
|
ct_max: 6500
|
||||||
|
|
||||||
|
# Brightness-Only Lights (Lamp & Table Light)
|
||||||
|
lamp_sync:
|
||||||
|
module: hass_mqtt_advanced_light_sync
|
||||||
|
class: hass_mqtt_advanced_light_sync
|
||||||
|
entity_id: light.desk_lamp
|
||||||
|
mqtt_topic_base: "/commonroom/lamp"
|
||||||
|
has_brightness: true
|
||||||
|
has_rgb: false
|
||||||
|
has_ct: false
|
||||||
|
|
||||||
|
table_lamp_sync:
|
||||||
|
module: hass_mqtt_advanced_light_sync
|
||||||
|
class: hass_mqtt_advanced_light_sync
|
||||||
|
entity_id: light.outdoor_table_lamp
|
||||||
|
mqtt_topic_base: "/3rdbuilding/tablelamp"
|
||||||
|
has_brightness: true
|
||||||
|
has_rgb: false
|
||||||
|
has_ct: false
|
||||||
|
|
||||||
|
# For Local Lights (if you want Home Assistant integration)
|
||||||
|
# These would use the basic sync script since they're controlled directly by GPIO
|
||||||
|
front_flood_light_sync:
|
||||||
|
module: hass_mqtt_light_sync
|
||||||
|
class: hass_mqtt_state_sync
|
||||||
|
entity_id: light.front_flood_light
|
||||||
|
mqtt_topic_state: "/serverroom/front_flood/state"
|
||||||
|
mqtt_topic_set_state: "/serverroom/front_flood/set"
|
||||||
|
mqtt_topic_request: "/serverroom/front_flood/request"
|
||||||
|
|
||||||
|
# Example of how local lights with brightness could be set up
|
||||||
|
# (if you want to sync GPIO PWM brightness with Home Assistant)
|
||||||
|
local_light_with_brightness_sync:
|
||||||
|
module: hass_mqtt_advanced_light_sync
|
||||||
|
class: hass_mqtt_advanced_light_sync
|
||||||
|
entity_id: light.local_dimmable_light
|
||||||
|
mqtt_topic_base: "/serverroom/local_light"
|
||||||
|
has_brightness: true
|
||||||
|
has_rgb: false
|
||||||
|
has_ct: false
|
Loading…
Add table
Add a link
Reference in a new issue