From 5ab19cc6900795840c8206ae539f84d8fb275ebb Mon Sep 17 00:00:00 2001 From: Chris Nussbaum Date: Wed, 18 Aug 2021 21:58:32 -0500 Subject: [PATCH] Refactor the TREO Light code as a component (#19) * Start making a component for Treo pool lights * More work on the Treo light component * TREO pool light is complete other than testing * Add color sync reset service and test Co-authored-by: Chris Nussbaum --- README.md | 5 +- components/treo_led_pool_light/README.md | 36 +++++ components/treo_led_pool_light/__init__.py | 0 components/treo_led_pool_light/light.py | 60 ++++++++ .../treo_led_pool_light.cpp | 144 ++++++++++++++++++ .../treo_led_pool_light/treo_led_pool_light.h | 60 ++++++++ custom/TreoLedPoolLight.h | 133 ---------------- devices/pool_and_patio_lights.yaml | 39 ++--- 8 files changed, 314 insertions(+), 163 deletions(-) create mode 100644 components/treo_led_pool_light/README.md create mode 100644 components/treo_led_pool_light/__init__.py create mode 100644 components/treo_led_pool_light/light.py create mode 100644 components/treo_led_pool_light/treo_led_pool_light.cpp create mode 100644 components/treo_led_pool_light/treo_led_pool_light.h delete mode 100644 custom/TreoLedPoolLight.h diff --git a/README.md b/README.md index 7a72fae..10bbce4 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Home Assistant is open source home automation that puts local control and privac ## Custom Components -I have been working on updating most of my custom code into components that can esily be pulled directly from GitHub into your device configuration using the [external components](https://esphome.io/components/external_components.html) component. I have run into frequent issues with changes in ESPHome breaking my components so I am now tagging my repo with the version of ESPHome it is compatible with. I generally upgrade pretty quickly so as soon as I have confirmed things are working and/or made the neccessary changes I will add a tag for the new version of ESPHome. +I have been working on updating most of my custom code into components that can easily be pulled directly from GitHub into your device configuration using the [external components](https://esphome.io/components/external_components.html) component. I have run into frequent issues with changes in ESPHome breaking my components so I am now tagging my repo with the version of ESPHome it is compatible with. I generally upgrade pretty quickly so as soon as I have confirmed things are working and/or made the neccessary changes I will add a tag for the new version of ESPHome. ### Binary Light With Power This an enhanced version of the standard [binary light](https://esphome.io/components/light/binary.html) component that adds an option to include a sensor to report current power usage based on a configured wattage of the light(s) it controls. More details on how to use this component are available [here](./components/binary_light_with_power/README.md). @@ -18,6 +18,9 @@ This an enhanced version of the standard [binary light](https://esphome.io/compo ### GPIO Light With Power This an enhanced version of the standard [gpio switch](https://esphome.io/components/switch/gpio.html) component that adds an option to include a sensor to report current power usage based on a configured wattage of the device(s) it controls. More details on how to use this component are available [here](./components/gpio_switch_with_power/README.md). +### TREO LED Pool Light +This is a custom light component that works with [TREO LED Pool Lights](https://www.srsmith.com/en-us/products/pool-lighting/treo-led-pool-light/) and exposes the different colors as "effects" so thay can be selected from Home Assistant. More details on how to use this component are available [here](./components/treo_led_pool_light/README.md). + ### Tuya This is a copy of the standard Tuya component with a couple of fixes/tweaks that are still getting hashed out in the main branch of ESPHome that are needed to get my custom Tuya Light Plus component to work reliably. Hopefully I can remove this component soon. diff --git a/components/treo_led_pool_light/README.md b/components/treo_led_pool_light/README.md new file mode 100644 index 0000000..13fffe6 --- /dev/null +++ b/components/treo_led_pool_light/README.md @@ -0,0 +1,36 @@ +# TREO LED Pool Light Component +## Overview +This is a custom light component that works with [TREO LED Pool Lights](https://www.srsmith.com/en-us/products/pool-lighting/treo-led-pool-light/) and exposes the different colors as "effects" so thay can be selected from Home Assistant. It also has an option to include a sensor to report current power usage based on a configured wattage of the light(s) it controls. + + +## Setup +Using the [External Components](https://esphome.io/components/external_components.html) feature in ESPHome you can add this component to your devices directly from my GitHub repo. +```yaml +external_components: + - source: github://nuttytree/esphome + components: [ treo_led_pool_light ] +``` + +Add and configure the Binary Light With Power Component +```yaml +light: + - platform: treo_led_pool_light + id: my_pool_lights + name: My Pool Lights + pin: 15 + power: + id: my_pool_lights_power + name: My Pool Lights Power + light_wattage: 10.0 + update_interval: 60s +``` + +## Configuration Variables (In addition to the standard variables) +* pin (Required, Pin) The output pin that controls power to the lights +* power.id (Optional, string) Manually specify the power sensor ID used for code generation. +* power.name (Optional, string) The name for the power sensor +* power.light_wattage (Optional, float) The total wattage of the light(s) controled by this dimmer +* power.update_interval (Optional, Time, default: 60s) Amount of time between updates of the power value while on. + +## Operation +It is possible for the color of the lights to get out of sync with each other and/or this component. To resolve this issue this component adds a service named esphome.{device_name}_color_sync_reset that goes through the series of power cycles defined in the lights user guide that will reset all lights and this component back to the slow color change "effect". diff --git a/components/treo_led_pool_light/__init__.py b/components/treo_led_pool_light/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/treo_led_pool_light/light.py b/components/treo_led_pool_light/light.py new file mode 100644 index 0000000..28604bf --- /dev/null +++ b/components/treo_led_pool_light/light.py @@ -0,0 +1,60 @@ +from esphome.components import light, sensor +from esphome import pins +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_OUTPUT_ID, + CONF_PIN, + CONF_POWER, + UNIT_WATT, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ICON_POWER, + CONF_UPDATE_INTERVAL, +) + +CONF_LIGHT_WATTAGE = "light_wattage" + +light_ns = cg.esphome_ns.namespace("light") +api_ns = cg.esphome_ns.namespace("api") +APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) +TreoLight = light_ns.class_("TreoLedPoolLight", light.LightOutput, cg.PollingComponent, APIServer) + +CONFIG_SCHEMA = cv.All( + light.LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TreoLight), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement_=UNIT_WATT, + accuracy_decimals_=1, + device_class_=DEVICE_CLASS_POWER, + state_class_=STATE_CLASS_MEASUREMENT, + icon_=ICON_POWER, + ).extend( + { + cv.Optional(CONF_LIGHT_WATTAGE): cv.positive_float, + cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.update_interval, + } + ), + } + ).extend(cv.COMPONENT_SCHEMA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await cg.register_component(var, config) + await light.register_light(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + + if CONF_POWER in config: + power_config = config[CONF_POWER] + power_sensor = await sensor.new_sensor(power_config) + cg.add(var.set_light_wattage(power_config[CONF_LIGHT_WATTAGE])) + cg.add(var.set_power_sensor(power_sensor)) + cg.add(var.set_update_interval(power_config[CONF_UPDATE_INTERVAL])) + else: + cg.add(var.set_update_interval(4294967295)) # uint32_t max diff --git a/components/treo_led_pool_light/treo_led_pool_light.cpp b/components/treo_led_pool_light/treo_led_pool_light.cpp new file mode 100644 index 0000000..3e845dc --- /dev/null +++ b/components/treo_led_pool_light/treo_led_pool_light.cpp @@ -0,0 +1,144 @@ +#include "esphome/core/log.h" +#include "treo_led_pool_light.h" + +namespace esphome { +namespace light { + +static const uint16_t COLOR_CHANGE_ON_OFF_TIME = 200; + +// Random 32bit value; If this changes existing restore color is invalidated +static const uint32_t RESTORE_COLOR_VERSION = 0x7B715952UL; + +LightTraits TreoLedPoolLight::get_traits() { + auto traits = light::LightTraits(); + return traits; +} + +void TreoLedPoolLight::setup() { + this->pin_->setup(); + + this->register_service(&TreoLedPoolLight::color_sync_reset, "color_sync_reset"); +} + +void TreoLedPoolLight::setup_state(light::LightState *state) { + auto *effectSlow = new TreoColorLightEffect("Slow Change", this, 1); + auto *effectWhite = new TreoColorLightEffect("White", this, 2); + auto *effectBlue = new TreoColorLightEffect("Blue", this, 3); + auto *effectGreen = new TreoColorLightEffect("Green", this, 4); + auto *effectRed = new TreoColorLightEffect("Red", this, 5); + auto *effectAmber = new TreoColorLightEffect("Amber", this, 6); + auto *effectMagenta = new TreoColorLightEffect("Magenta", this, 7); + auto *effectFast = new TreoColorLightEffect("Fast Change", this, 8); + state->add_effects({ effectSlow, effectWhite , effectBlue , effectGreen , effectRed , effectAmber , effectMagenta , effectFast }); + + this->rtc_ = global_preferences.make_preference(state->get_object_id_hash() ^ RESTORE_COLOR_VERSION); + this->rtc_.load(&this->current_color_); + this->target_color_ = this->current_color_; + + this->state_ = state; +} + +void TreoLedPoolLight::write_state(LightState *state) { + if (!this->is_changing_colors_) { + this->apply_state_(); + } +} + +void TreoLedPoolLight::update() { + if (this->light_wattage_.has_value() && this->power_sensor_ != nullptr && this->current_state_) + { + this->power_sensor_->publish_state(this->light_wattage_.value()); + } +} + +void TreoLedPoolLight::next_color() { + auto call = this->state_->turn_on(); + call.set_effect(this->current_color_ < 8 ? this->current_color_ + 1 : 1); + call.perform(); +} + +void TreoLedPoolLight::color_sync_reset() { + this->is_changing_colors_ = true; + this->target_color_ = 1; + this->pin_->digital_write(false); + + // After being off for at least 5 seconds we toggle on/off 3 times + // and then wait at least 5 seconds before allowing any other changes. + this->set_timeout("COLOR_RESET_ON_1", 5500, [this]() { + this->pin_->digital_write(true); + this->set_timeout("COLOR_RESET_OFF_1", 250 * 1, [this]() { this->pin_->digital_write(false); }); + + this->set_timeout("COLOR_RESET_ON_2", 500, [this]() { this->pin_->digital_write(true); }); + this->set_timeout("COLOR_RESET_OFF_2", 750, [this]() { this->pin_->digital_write(false); }); + + this->set_timeout("COLOR_RESET_ON_3", 1000, [this]() { this->pin_->digital_write(true); }); + this->set_timeout("COLOR_RESET_OFF_3", 1250, [this]() { this->pin_->digital_write(false); }); + + this->set_timeout("COLOR_RESET_FINISH", 6750, [this]() { + this->current_color_ = 1; + this->rtc_.save(&this->current_color_); + + this->is_changing_colors_ = false; + + bool curent_target_state; + this->state_->current_values_as_binary(&curent_target_state); + if (curent_target_state) { + auto call = this->state_->turn_on(); + call.set_effect(1); + call.perform(); + } + }); + }); +} + +void TreoLedPoolLight::apply_state_() { + bool target_state; + this->state_->current_values_as_binary(&target_state); + if (target_state != this->current_state_) { + this->pin_->digital_write(target_state); + this->current_state_ = target_state; + } + + // The default when the light is turned on is to have no effect but the Treo lights always + // have an "effect" of the current color so we update the effect to the current color. + if (target_state && this->state_->get_effect_name() == "None") { + auto call = this->state_->turn_on(); + call.set_effect(this->current_color_); + call.perform(); + } + + if (this->light_wattage_.has_value() && this->power_sensor_ != nullptr) + { + float power = target_state ? this->light_wattage_.value() : 0.0f; + this->power_sensor_->publish_state(power); + } +} + +void TreoLedPoolLight::set_color_(uint8_t color) { + this->target_color_ = color; + if (this->is_changing_colors_) { + return; + } else if (this->current_color_ != color) { + this->is_changing_colors_ = true; + this->pin_->digital_write(false); + this->set_timeout("COLOR_CHANGE_ON", COLOR_CHANGE_ON_OFF_TIME, [this]() { this->color_change_callback_(); }); + } +} + +void TreoLedPoolLight::color_change_callback_() { + this->pin_->digital_write(true); + this->current_color_ = this->current_color_ < 8 ? this->current_color_ + 1 : 1; + this->rtc_.save(&this->current_color_); + if (this->current_color_ != this->target_color_) { + this->set_timeout("COLOR_CHANGE_OFF", COLOR_CHANGE_ON_OFF_TIME, [this]() { + this->pin_->digital_write(false); + this->set_timeout("COLOR_CHANGE_ON", COLOR_CHANGE_ON_OFF_TIME, [this]() { this->color_change_callback_(); }); + }); + } else { + this->is_changing_colors_ = false; + this->apply_state_(); + } +} + +} // namespace light +} // namespace esphome diff --git a/components/treo_led_pool_light/treo_led_pool_light.h b/components/treo_led_pool_light/treo_led_pool_light.h new file mode 100644 index 0000000..3323f36 --- /dev/null +++ b/components/treo_led_pool_light/treo_led_pool_light.h @@ -0,0 +1,60 @@ +#pragma once + +#include "esphome/components/api/custom_api_device.h" +#include "esphome/components/light/light_output.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace light { + +class TreoColorLightEffect; + +class TreoLedPoolLight : public LightOutput, public PollingComponent, public api::CustomAPIDevice { + public: + void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_light_wattage(float light_wattage) { this->light_wattage_ = light_wattage; } + void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } + float get_setup_priority() const override { return setup_priority::HARDWARE; } + LightTraits get_traits() override; + void setup() override; + void setup_state(LightState *state) override; + void write_state(LightState *state) override; + void update() override; + + void next_color(); + void color_sync_reset(); + + TreoLedPoolLight& operator= (const LightOutput& x) {return *this;} + + protected: + friend TreoColorLightEffect; + + void apply_state_(); + void set_color_(uint8_t color); + void color_change_callback_(); + + GPIOPin *pin_; + ESPPreferenceObject rtc_; + LightState *state_{nullptr}; + optional light_wattage_{}; + sensor::Sensor *power_sensor_; + bool current_state_; + uint8_t current_color_; + uint8_t target_color_; + bool is_changing_colors_{false}; +}; + +class TreoColorLightEffect : public LightEffect { + public: + TreoColorLightEffect(const std::string &name, TreoLedPoolLight *treo_light, const uint8_t color) + : LightEffect(name), treo_light_(treo_light), color_(color) {} + void apply() override { this-> treo_light_->set_color_(this->color_); } + + protected: + TreoLedPoolLight *treo_light_{nullptr}; + uint8_t color_; +}; + +} // namespace light +} // namespace esphome diff --git a/custom/TreoLedPoolLight.h b/custom/TreoLedPoolLight.h deleted file mode 100644 index abf7f4b..0000000 --- a/custom/TreoLedPoolLight.h +++ /dev/null @@ -1,133 +0,0 @@ -#include "esphome.h" - -using namespace esphome; - -class TreoColorLightEffect; - -class TreoLedPoolLight : public Component, public light::LightOutput { - public: - TreoLedPoolLight(int gpio); - light::LightTraits get_traits() override; - void setup_state(light::LightState *state) override; - void write_state(light::LightState *state) override; - void next_color(); - void reset(); - - protected: - friend TreoColorLightEffect; - - void set_color(int color); - - int relayGpio; - int currentColor; - ESPPreferenceObject rtc; - light::LightState *parent; - TreoColorLightEffect *effectSlow; - TreoColorLightEffect *effectWhite; - TreoColorLightEffect *effectBlue; - TreoColorLightEffect *effectGreen; - TreoColorLightEffect *effectRed; - TreoColorLightEffect *effectAmber; - TreoColorLightEffect *effectMagenta; - TreoColorLightEffect *effectFast; -}; - -class TreoColorLightEffect : public light::LightEffect { - public: - TreoColorLightEffect(TreoLedPoolLight *treo, const std::string &name, const uint32_t color); - void apply() override; - - protected: - TreoLedPoolLight *treoLight; - uint32_t effectColor; -}; - -TreoLedPoolLight::TreoLedPoolLight(int gpio) : light::LightOutput(), relayGpio(gpio) {} - -light::LightTraits TreoLedPoolLight::get_traits() { - auto traits = light::LightTraits(); - traits.set_supports_brightness(false); - traits.set_supports_rgb(false); - traits.set_supports_rgb_white_value(false); - traits.set_supports_color_temperature(false); - return traits; -} - -void TreoLedPoolLight::setup_state(light::LightState *state) { - pinMode(this->relayGpio, OUTPUT); - this->rtc = global_preferences.make_preference(1944399030U ^ 12345); - this->rtc.load(&this->currentColor); - - this->parent = state; - - this->effectSlow = new TreoColorLightEffect(this, "Slow Change", 1); - this->effectWhite = new TreoColorLightEffect(this, "White", 2); - this->effectBlue = new TreoColorLightEffect(this, "Blue", 3); - this->effectGreen = new TreoColorLightEffect(this, "Green", 4); - this->effectRed = new TreoColorLightEffect(this, "Red", 5); - this->effectAmber = new TreoColorLightEffect(this, "Amber", 6); - this->effectMagenta = new TreoColorLightEffect(this, "Magenta", 7); - this->effectFast = new TreoColorLightEffect(this, "Fast Change", 8); - state->add_effects({ this->effectSlow, this->effectWhite , this->effectBlue , this->effectGreen , this->effectRed , this->effectAmber , this->effectMagenta , this->effectFast }); -} - -void TreoLedPoolLight::write_state(light::LightState *state) { - bool currentState; - state->current_values_as_binary(¤tState); - digitalWrite(relayGpio, currentState); - if (currentState && state->get_effect_name() == "None") { - auto call = state->turn_on(); - call.set_effect(this->currentColor); - call.perform(); - } -} - -void TreoLedPoolLight::next_color() { - auto call = this->parent->turn_on(); - call.set_effect(this->currentColor < 8 ? this->currentColor + 1 : 1); - call.perform(); -} - -void TreoLedPoolLight::reset() { - bool currentState; - this->parent->current_values_as_binary(¤tState); - digitalWrite(relayGpio, LOW); - delay(5500); - for (int i = 0; i < 3; i++) { - digitalWrite(relayGpio, HIGH); - delay(200); - digitalWrite(relayGpio, LOW); - delay(200); - } - delay(5500); - this->currentColor = 1; - this->rtc.save(&this->currentColor); - auto call = this->parent->turn_on(); - call.set_effect(1); - call.perform(); - call = this->parent->make_call(); - call.set_state(currentState); - call.perform(); -} - -void TreoLedPoolLight::set_color(int color) { - if (this->currentColor != color) { - while (this->currentColor != color) { - digitalWrite(this->relayGpio, LOW); - delay(200); - digitalWrite(this->relayGpio, HIGH); - delay(200); - this->currentColor = this->currentColor < 8 ? this->currentColor + 1 : 1; - } - this->rtc.save(&this->currentColor); - } -} - -TreoColorLightEffect::TreoColorLightEffect(TreoLedPoolLight *treo, const std::string &name, const uint32_t color) - : light::LightEffect(name), treoLight(treo), effectColor(color) {} - -void TreoColorLightEffect::apply() { - this->treoLight->set_color(this->effectColor); -} - -TreoLedPoolLight *Treo; \ No newline at end of file diff --git a/devices/pool_and_patio_lights.yaml b/devices/pool_and_patio_lights.yaml index ea8d167..d874860 100644 --- a/devices/pool_and_patio_lights.yaml +++ b/devices/pool_and_patio_lights.yaml @@ -11,15 +11,11 @@ substitutions: packages: device_base: !include ../packages/device_base.yaml -esphome: - includes: - - ../custom/TreoLedPoolLight.h - external_components: - source: type: local path: ../components - components: [ binary_light_with_power, total_daily_energy ] + components: [ treo_led_pool_light, binary_light_with_power, total_daily_energy ] - source: github://cbpowell/ESPSense components: [ espsense ] @@ -51,7 +47,7 @@ binary_sensor: on_press: then: - lambda: |- - Treo->next_color(); + static_cast(id(pool_lights).get_output())->next_color(); - platform: homeassistant id: patio_lights entity_id: light.patio_lights @@ -69,31 +65,16 @@ espsense: voltage: 120 light: - - platform: custom - lambda: |- - Treo = new TreoLedPoolLight(15); - App.register_component(Treo); - return {Treo}; - lights: - - id: pool_lights - name: "Pool Lights" - on_turn_on: - - sensor.template.publish: - id: pool_light_power - state: 10.0 - on_turn_off: - - sensor.template.publish: - id: pool_light_power - state: 0.0 + - platform: treo_led_pool_light + id: pool_lights + name: "Pool Lights" + pin: 15 + power: + id: pool_light_power + name: Pool Lights Power + light_wattage: 10.0 sensor: - - platform: template - name: Pool Lights Power - id: pool_light_power - unit_of_measurement: W - device_class: power - state_class: measurement - accuracy_decimals: 1 - platform: total_daily_energy name: Pool Lights power_id: pool_light_power