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 <chris.nussbaum@protolabs.com>
This commit is contained in:
Chris Nussbaum 2021-08-18 21:58:32 -05:00 committed by GitHub
parent cf9716264f
commit 5ab19cc690
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 314 additions and 163 deletions

View File

@ -10,7 +10,7 @@ Home Assistant is open source home automation that puts local control and privac
## Custom Components ## 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 ### 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). 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 ### 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). 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 ### 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. 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.

View File

@ -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".

View File

@ -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

View File

@ -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<uint8_t>(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

View File

@ -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<float> 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

View File

@ -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<int>(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(&currentState);
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(&currentState);
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;

View File

@ -11,15 +11,11 @@ substitutions:
packages: packages:
device_base: !include ../packages/device_base.yaml device_base: !include ../packages/device_base.yaml
esphome:
includes:
- ../custom/TreoLedPoolLight.h
external_components: external_components:
- source: - source:
type: local type: local
path: ../components 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 - source: github://cbpowell/ESPSense
components: [ espsense ] components: [ espsense ]
@ -51,7 +47,7 @@ binary_sensor:
on_press: on_press:
then: then:
- lambda: |- - lambda: |-
Treo->next_color(); static_cast<TreoLedPoolLight *>(id(pool_lights).get_output())->next_color();
- platform: homeassistant - platform: homeassistant
id: patio_lights id: patio_lights
entity_id: light.patio_lights entity_id: light.patio_lights
@ -69,31 +65,16 @@ espsense:
voltage: 120 voltage: 120
light: light:
- platform: custom - platform: treo_led_pool_light
lambda: |- id: pool_lights
Treo = new TreoLedPoolLight(15); name: "Pool Lights"
App.register_component(Treo); pin: 15
return {Treo}; power:
lights: id: pool_light_power
- id: pool_lights name: Pool Lights Power
name: "Pool Lights" light_wattage: 10.0
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
sensor: 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 - platform: total_daily_energy
name: Pool Lights name: Pool Lights
power_id: pool_light_power power_id: pool_light_power