diff --git a/hass_mqtt_advanced_light_sync.py b/hass_mqtt_advanced_light_sync.py new file mode 100644 index 0000000..b31c109 --- /dev/null +++ b/hass_mqtt_advanced_light_sync.py @@ -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() \ No newline at end of file diff --git a/light_sync_config_example.yaml b/light_sync_config_example.yaml new file mode 100644 index 0000000..a02b1c4 --- /dev/null +++ b/light_sync_config_example.yaml @@ -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 \ No newline at end of file