Energy monitoring fixes (#17)

* Fix energy monitoring for feit dimmers

* More energy reporting fixes/updates

* More energy monitoring updates

* Include missed readme update

* Final power monitoring updates

Co-authored-by: Chris Nussbaum <chris.nussbaum@protolabs.com>
This commit is contained in:
Chris Nussbaum 2021-08-17 21:55:21 -05:00 committed by GitHub
parent 3fdca0bf0f
commit adc77d67d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 768 additions and 116 deletions

View File

@ -10,14 +10,20 @@ Home Assistant is open source home automation that puts local control and privac
## Custom Components
### 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).
### 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).
### 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.
### Tuya Light Plus
This an enhanced version of the standard [Tuya](https://esphome.io/components/light/tuya.html) light component that adds a bunch of extra features. I use this component with [Feit Dimmers](https://www.amazon.com/gp/product/B07SXDFH38/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1) but it will likely work with other Tuya dimmers. More details on features and how to use this component are available [here](./components/tuya_light_plus/README.md).
### Tuya Dimmer as Fan
This a modified version of the Tuya fan component I use with [Feit Dimmers](https://www.amazon.com/gp/product/B07SXDFH38/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1) (but it will likely work with other Tuya dimmers) to control bathroom fans. The major change from the standard Tuya fan component (other than removing options for speed, oscillation, and direction) is adding a function to always change the dimmer back to the maximum "brightness" effectively making this only an on/off device. Details on how to use this component are available [here](./components/tuya_dimmer_as_fan/README.md).
This a modified version of the [Tuya fan](https://esphome.io/components/fan/tuya.html) component I use with [Feit Dimmers](https://www.amazon.com/gp/product/B07SXDFH38/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1) (but it will likely work with other Tuya dimmers) to control bathroom fans and adds several features. I created this component because I couldn't find a regular on/off switch with the same look and feel as the Feit dimmers so I decided to use the Feit dimmers but use this component to prevent "dimming" the fan. More details on features and how to use this component are available [here](./components/tuya_dimmer_as_fan/README.md).
### Tuya Light Plus
This an enhanced version of the standard [Tuya light](https://esphome.io/components/light/tuya.html) component that adds a bunch of extra features. I use this component with [Feit Dimmers](https://www.amazon.com/gp/product/B07SXDFH38/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1) but it will likely work with other Tuya dimmers. More details on features and how to use this component are available [here](./components/tuya_light_plus/README.md).
### ESPSense
This excellent component is not mine and doesn't live in this repository but most of my devices are using it so I felt it was worthy of a mention here. More details are available [here](https://github.com/cbpowell/ESPSense).
@ -55,7 +61,7 @@ This project was one of the first projects I have done that I would call woodwor
I plan to use dimmer switches for anything that is dimmable just for consistency and you never know when you might want to have the ability to dim a light. However there are times when a dimmer isn't an option.
### [SANA Dual Switch](https://www.amazon.com/gp/product/B07QC5ZCHP/ref=ppx_yo_dt_b_asin_title_o03_s01?ie=UTF8&th=1)
My basement bathroom has 4 devices (main light, fan, shower light, heat lamps) but only 2 single gang boxes (and not enough room to swith to dual gang) so these were a perfect fit. The prices is right, the buttons feel solid, and I was able to flash them using [Tuya-Convert](https://github.com/ct-Open-Source/tuya-convert). They do seem to have corners that are squarer then typical so I had a little bit of trouble getting a standard cover to fit but nothing I couldn't fix with a file.
My basement bathroom has 4 devices (main light, fan, shower light, and heat lamps) but only 2 single gang boxes (and not enough room to swith to dual gang) so these were a perfect fit. The price is right, the buttons feel solid, and I was able to flash them using [Tuya-Convert](https://github.com/ct-Open-Source/tuya-convert). They do seem to have corners that are squarer then typical so I had a little bit of trouble getting a standard cover to fit but nothing I couldn't fix with a file.
* [Basement Bathroom Light and Fan](./devices/basement_bathroom_light_fan.yaml)
* [Basement Bathroom Shower Light and Heater](./devices/basement_bathroom_shower_light_heater.yaml)

View File

@ -0,0 +1,40 @@
# Binary Light With Power Component
## Overview
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.
## 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: [ binary_light_with_power ]
```
Like the standard binary light component you need to have an [output](https://esphome.io/components/output.html).
```yaml
output:
- platform: gpio
id: my_light_output
pin: 13
```
Add and configure the Binary Light With Power Component
```yaml
light:
- platform: binary_light_with_power
id: my_light
name: My Light
output: my_light_output
power:
id: my_light_power
name: My Light Power
light_wattage: 48.0
update_interval: 60s
```
## Configuration Variables (In addition to the standard variables)
* 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.

View File

@ -0,0 +1,37 @@
#include "binary_light_with_power.h"
namespace esphome {
namespace binary_power {
light::LightTraits BinaryLightWithPower::get_traits() {
auto traits = light::LightTraits();
// TODO: Enable this with the 1.21.x version of ESPHome
// traits.set_supported_color_modes({light::ColorMode::ON_OFF});
return traits;
}
void BinaryLightWithPower::write_state(light::LightState *state) {
bool binary;
state->current_values_as_binary(&binary);
this->state_ = binary;
if (binary)
this->output_->turn_on();
else
this->output_->turn_off();
if (this->light_wattage_.has_value() && this->power_sensor_ != nullptr)
{
float power = this->state_ ? this->light_wattage_.value() : 0.0f;
this->power_sensor_->publish_state(power);
}
}
void BinaryLightWithPower::update() {
if (this->light_wattage_.has_value() && this->power_sensor_ != nullptr && this->state_)
{
this->power_sensor_->publish_state(this->light_wattage_.value());
}
}
} // namespace binary_power
} // namespace esphome

View File

@ -0,0 +1,28 @@
#pragma once
#include "esphome/components/light/light_output.h"
#include "esphome/components/output/binary_output.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace binary_power {
class BinaryLightWithPower : public light::LightOutput, public PollingComponent {
public:
void set_output(output::BinaryOutput *output) { output_ = output; }
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; }
light::LightTraits get_traits() override;
void write_state(light::LightState *state) override;
void update() override;
protected:
output::BinaryOutput *output_;
bool state_{false};
optional<float> light_wattage_{};
sensor::Sensor *power_sensor_;
};
} // namespace binary_power
} // namespace esphome

View File

@ -0,0 +1,55 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import core
from esphome.components import light, output, sensor
from esphome.const import (
CONF_OUTPUT_ID,
CONF_OUTPUT,
CONF_POWER,
UNIT_WATT,
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
ICON_POWER,
CONF_UPDATE_INTERVAL,
)
CONF_LIGHT_WATTAGE = "light_wattage"
binary_power_ns = cg.esphome_ns.namespace("binary_power")
LightWithPower = binary_power_ns.class_("BinaryLightWithPower", light.LightOutput, cg.PollingComponent)
CONFIG_SCHEMA = light.BINARY_LIGHT_SCHEMA.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LightWithPower),
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
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,
}
),
}
)
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)
out = await cg.get_variable(config[CONF_OUTPUT])
cg.add(var.set_output(out))
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,32 @@
# GPIO Switch With Power Component
## Overview
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.
## 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: [ gpio_switch_with_power ]
```
Add and configure the GPIO Switch With Power Component
```yaml
switch:
- platform: gpio_switch_with_power
id: my_switch
name: My Switch
pin: 4
power:
id: my_switch_power
name: My Switch Power
device_wattage: 48.0
update_interval: 60s
```
## Configuration Variables (In addition to the standard variables)
* 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.device_wattage (Optional, float) The total wattage of the device(s) controled by this dimmer
* power.update_interval (Optional, Time, default: 60s) Amount of time between updates of the power value while on.

View File

@ -0,0 +1,128 @@
#include "gpio_switch_with_power.h"
#include "esphome/core/log.h"
namespace esphome {
namespace gpio_power {
static const char *const TAG = "switch.gpio";
void GPIOSwitchWithPower::setup() {
ESP_LOGCONFIG(TAG, "Setting up GPIO Switch '%s'...", this->name_.c_str());
bool initial_state = false;
switch (this->restore_mode_) {
case GPIO_SWITCH_RESTORE_DEFAULT_OFF:
initial_state = this->get_initial_state().value_or(false);
break;
case GPIO_SWITCH_RESTORE_DEFAULT_ON:
initial_state = this->get_initial_state().value_or(true);
break;
case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF:
initial_state = !this->get_initial_state().value_or(true);
break;
case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON:
initial_state = !this->get_initial_state().value_or(false);
break;
case GPIO_SWITCH_ALWAYS_OFF:
initial_state = false;
break;
case GPIO_SWITCH_ALWAYS_ON:
initial_state = true;
break;
}
// write state before setup
if (initial_state)
this->turn_on();
else
this->turn_off();
this->pin_->setup();
// write after setup again for other IOs
if (initial_state)
this->turn_on();
else
this->turn_off();
}
void GPIOSwitchWithPower::dump_config() {
LOG_SWITCH("", "GPIO Switch", this);
LOG_PIN(" Pin: ", this->pin_);
const char *restore_mode = "";
switch (this->restore_mode_) {
case GPIO_SWITCH_RESTORE_DEFAULT_OFF:
restore_mode = "Restore (Defaults to OFF)";
break;
case GPIO_SWITCH_RESTORE_DEFAULT_ON:
restore_mode = "Restore (Defaults to ON)";
break;
case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON:
restore_mode = "Restore inverted (Defaults to ON)";
break;
case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF:
restore_mode = "Restore inverted (Defaults to OFF)";
break;
case GPIO_SWITCH_ALWAYS_OFF:
restore_mode = "Always OFF";
break;
case GPIO_SWITCH_ALWAYS_ON:
restore_mode = "Always ON";
break;
}
ESP_LOGCONFIG(TAG, " Restore Mode: %s", restore_mode);
if (!this->interlock_.empty()) {
ESP_LOGCONFIG(TAG, " Interlocks:");
for (auto *lock : this->interlock_) {
if (lock == this)
continue;
ESP_LOGCONFIG(TAG, " %s", lock->get_name().c_str());
}
}
}
void GPIOSwitchWithPower::update() {
if (this->device_wattage_.has_value() && this->power_sensor_ != nullptr && this->state)
{
this->power_sensor_->publish_state(this->device_wattage_.value());
}
}
void GPIOSwitchWithPower::write_state(bool state) {
if (state != this->inverted_) {
// Turning ON, check interlocking
bool found = false;
for (auto *lock : this->interlock_) {
if (lock == this)
continue;
if (lock->state) {
lock->turn_off();
found = true;
}
}
if (found && this->interlock_wait_time_ != 0) {
this->set_timeout("interlock", this->interlock_wait_time_, [this, state] {
// Don't write directly, call the function again
// (some other switch may have changed state while we were waiting)
this->write_state(state);
});
return;
}
} else if (this->interlock_wait_time_ != 0) {
// If we are switched off during the interlock wait time, cancel any pending
// re-activations
this->cancel_timeout("interlock");
}
this->pin_->digital_write(state);
this->publish_state(state);
if (this->device_wattage_.has_value() && this->power_sensor_ != nullptr)
{
float power = state ? this->device_wattage_.value() : 0.0f;
this->power_sensor_->publish_state(power);
}
}
} // namespace gpio_power
} // namespace esphome

View File

@ -0,0 +1,44 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/switch/switch.h"
namespace esphome {
namespace gpio_power {
enum GPIOSwitchRestoreMode {
GPIO_SWITCH_RESTORE_DEFAULT_OFF,
GPIO_SWITCH_RESTORE_DEFAULT_ON,
GPIO_SWITCH_ALWAYS_OFF,
GPIO_SWITCH_ALWAYS_ON,
GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF,
GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON,
};
class GPIOSwitchWithPower : public switch_::Switch, public PollingComponent {
public:
void set_pin(GPIOPin *pin) { pin_ = pin; }
void set_restore_mode(GPIOSwitchRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
void set_interlock(const std::vector<Switch *> &interlock) { this->interlock_ = interlock; }
void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; }
void set_device_wattage(float device_wattage) { this->device_wattage_ = device_wattage; }
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void setup() override;
void dump_config() override;
void update() override;
protected:
void write_state(bool state) override;
GPIOPin *pin_;
GPIOSwitchRestoreMode restore_mode_{GPIO_SWITCH_RESTORE_DEFAULT_OFF};
std::vector<Switch *> interlock_;
uint32_t interlock_wait_time_{0};
optional<float> device_wattage_{};
sensor::Sensor *power_sensor_;
};
} // namespace gpio_power
} // namespace esphome

View File

@ -0,0 +1,86 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import switch, sensor
from esphome.const import (
CONF_ID,
CONF_INTERLOCK,
CONF_PIN,
CONF_RESTORE_MODE,
CONF_POWER,
UNIT_WATT,
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
ICON_POWER,
CONF_UPDATE_INTERVAL,
)
gpio_power_ns = cg.esphome_ns.namespace("gpio_power")
GPIOSwitchWithPower = gpio_power_ns.class_("GPIOSwitchWithPower", switch.Switch, cg.PollingComponent)
GPIOSwitchRestoreMode = gpio_power_ns.enum("GPIOSwitchRestoreMode")
RESTORE_MODES = {
"RESTORE_DEFAULT_OFF": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_DEFAULT_OFF,
"RESTORE_DEFAULT_ON": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_DEFAULT_ON,
"ALWAYS_OFF": GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_OFF,
"ALWAYS_ON": GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_ON,
"RESTORE_INVERTED_DEFAULT_OFF": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF,
"RESTORE_INVERTED_DEFAULT_ON": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON,
}
CONF_INTERLOCK_WAIT_TIME = "interlock_wait_time"
CONF_DEVICE_WATTAGE = "device_wattage"
CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(GPIOSwitchWithPower),
cv.Required(CONF_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum(
RESTORE_MODES, upper=True, space="_"
),
cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)),
cv.Optional(
CONF_INTERLOCK_WAIT_TIME, default="0ms"
): cv.positive_time_period_milliseconds,
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_DEVICE_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_ID])
await cg.register_component(var, config)
await switch.register_switch(var, config)
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
if CONF_INTERLOCK in config:
interlock = []
for it in config[CONF_INTERLOCK]:
lock = await cg.get_variable(it)
interlock.append(lock)
cg.add(var.set_interlock(interlock))
cg.add(var.set_interlock_wait_time(config[CONF_INTERLOCK_WAIT_TIME]))
if CONF_POWER in config:
power_config = config[CONF_POWER]
power_sensor = await sensor.new_sensor(power_config)
cg.add(var.set_device_wattage(power_config[CONF_DEVICE_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,65 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, time
from esphome.const import (
CONF_ID,
CONF_TIME_ID,
DEVICE_CLASS_ENERGY,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
)
DEPENDENCIES = ["time"]
CONF_POWER_ID = "power_id"
CONF_MIN_SAVE_INTERVAL = "min_save_interval"
CONF_TOTAL_DAILY_ENERGY_METHOD = "method"
total_daily_energy_ns = cg.esphome_ns.namespace("total_daily_energy")
TotalDailyEnergyMethod = total_daily_energy_ns.enum("TotalDailyEnergyMethod")
TOTAL_DAILY_ENERGY_METHODS = {
"trapezoid": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID,
"left": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_LEFT,
"right": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_RIGHT,
}
TotalDailyEnergy = total_daily_energy_ns.class_(
"TotalDailyEnergy", sensor.Sensor, cg.Component
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
accuracy_decimals_=0,
device_class_=DEVICE_CLASS_ENERGY,
state_class_=STATE_CLASS_MEASUREMENT,
last_reset_type_=LAST_RESET_TYPE_AUTO,
icon_="",
unit_of_measurement_="",
)
.extend(
{
cv.GenerateID(): cv.declare_id(TotalDailyEnergy),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor),
cv.Optional(
CONF_MIN_SAVE_INTERVAL, default="0s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_TOTAL_DAILY_ENERGY_METHOD, default="left"): cv.enum(
TOTAL_DAILY_ENERGY_METHODS, lower=True
),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
sens = await cg.get_variable(config[CONF_POWER_ID])
cg.add(var.set_parent(sens))
time_ = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time(time_))
cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL]))
cg.add(var.set_method(config[CONF_TOTAL_DAILY_ENERGY_METHOD]))

View File

@ -0,0 +1,79 @@
#include "total_daily_energy.h"
#include "esphome/core/log.h"
namespace esphome {
namespace total_daily_energy {
static const char *const TAG = "total_daily_energy";
void TotalDailyEnergy::setup() {
this->pref_ = global_preferences.make_preference<float>(this->get_object_id_hash());
float recovered;
if (this->pref_.load(&recovered)) {
this->publish_state_and_save(recovered);
} else {
this->publish_state_and_save(0);
}
this->last_update_ = millis();
this->last_save_ = this->last_update_;
this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); });
}
void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); }
void TotalDailyEnergy::loop() {
auto t = this->time_->now();
if (!t.is_valid())
return;
if (this->last_day_of_year_ == 0) {
this->last_day_of_year_ = t.day_of_year;
return;
}
if (t.day_of_year != this->last_day_of_year_) {
this->last_day_of_year_ = t.day_of_year;
this->total_energy_ = 0;
this->publish_state_and_save(0);
}
}
void TotalDailyEnergy::publish_state_and_save(float state) {
this->total_energy_ = state;
this->publish_state(state);
const uint32_t now = millis();
if (now - this->last_save_ < this->min_save_interval_) {
return;
}
this->last_save_ = now;
this->pref_.save(&state);
}
void TotalDailyEnergy::process_new_state_(float state) {
if (isnan(state))
return;
const uint32_t now = millis();
const float old_state = this->last_power_state_;
const float new_state = state;
float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f;
float delta_energy = 0.0f;
switch (this->method_) {
case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID:
delta_energy = delta_hours * (old_state + new_state) / 2.0;
break;
case TOTAL_DAILY_ENERGY_METHOD_LEFT:
delta_energy = delta_hours * old_state;
break;
case TOTAL_DAILY_ENERGY_METHOD_RIGHT:
delta_energy = delta_hours * new_state;
break;
}
this->last_power_state_ = new_state;
this->last_update_ = now;
this->publish_state_and_save(this->total_energy_ + delta_energy);
}
} // namespace total_daily_energy
} // namespace esphome

View File

@ -0,0 +1,49 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/time/real_time_clock.h"
namespace esphome {
namespace total_daily_energy {
enum TotalDailyEnergyMethod {
TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0,
TOTAL_DAILY_ENERGY_METHOD_LEFT,
TOTAL_DAILY_ENERGY_METHOD_RIGHT,
};
class TotalDailyEnergy : public sensor::Sensor, public Component {
public:
void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; }
void set_time(time::RealTimeClock *time) { time_ = time; }
void set_parent(Sensor *parent) { parent_ = parent; }
void set_method(TotalDailyEnergyMethod method) { method_ = method; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
std::string unit_of_measurement() override { return this->parent_->get_unit_of_measurement() + "h"; }
std::string icon() override { return this->parent_->get_icon(); }
int8_t accuracy_decimals() override { return this->parent_->get_accuracy_decimals() + 2; }
void loop() override;
void publish_state_and_save(float state);
protected:
void process_new_state_(float state);
ESPPreferenceObject pref_;
time::RealTimeClock *time_;
Sensor *parent_;
TotalDailyEnergyMethod method_;
uint16_t last_day_of_year_{};
uint32_t last_update_{0};
uint32_t last_save_{0};
uint32_t min_save_interval_{0};
float total_energy_{0.0f};
float last_power_state_{0.0f};
};
} // namespace total_daily_energy
} // namespace esphome

View File

@ -1,6 +1,9 @@
# Tuya Light Plus Component
# Tuya Dimmer as Fan Component
## Overview
This a modified version of the Tuya fan component I use with [Feit Dimmers](https://www.amazon.com/gp/product/B07SXDFH38/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1) (but it will likely work with other Tuya dimmers) to control bathroom fans. The major change from the standard Tuya fan component (other than removing options for speed, oscillation, and direction) is adding a function to always change the dimmer back to the maximum "brightness" effectively making this only an on/off device. Similar to the Tuya Light Plus component this component can also add a power sensor based on configured wattage of the fan, this could be done with a templat sensor and automations but it was easy to add here so I figured why not.
This a modified version of the [Tuya fan](https://esphome.io/components/fan/tuya.html) component I use with [Feit Dimmers](https://www.amazon.com/gp/product/B07SXDFH38/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1) (but it will likely work with other Tuya dimmers) to control bathroom fans. Changes from the standard Tuya fan component include the following:
* Remove options for speed, oscillation, and direction as they don't apply.
* Always change the "brightness" back to the maximum value effectively making this only an on/off device.
* Can add a sensor to report current power usage based on a configured wattage of the fan(s) it controls.
## Setup
@ -32,15 +35,14 @@ fan:
power:
id: my_fan_power
name: My Fan Power
light_wattage: 21.6
fan_wattage: 21.6
update_interval: 60s
```
## Configuration Variables
* id (Optional, ID): Manually specify the ID used for code generation.
* name (Required, string): The name of the light.
* switch_datapoint (Required, int): The datapoint id number of the power switch.
## Configuration Variables (In addition to the standard variables)
* dimmer_datapoint (Required, int): The datapoint id number of the dimmer value.
* dimmer_max_value (Optional, int, default 255): The highest dimmer value allowed.
* 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.fan_wattage (Optional, float) The total wattage of the fan(s) controled by this dimmer.
* power.update_interval (Optional, Time, default: 60s) Amount of time between updates of the power value while on.

View File

@ -10,6 +10,7 @@ from esphome.const import (
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
ICON_POWER,
CONF_UPDATE_INTERVAL,
)
DEPENDENCIES = ["tuya"]
@ -38,6 +39,7 @@ CONFIG_SCHEMA = cv.All(
).extend(
{
cv.Optional(CONF_FAN_WATTAGE): cv.positive_float,
cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.update_interval,
}
),
}
@ -63,3 +65,6 @@ async def to_code(config):
power_sensor = await sensor.new_sensor(power_config)
cg.add(var.set_fan_wattage(power_config[CONF_FAN_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

@ -42,5 +42,13 @@ void TuyaDimmerAsFan::dump_config() {
ESP_LOGCONFIG(TAG, " Dimmer has datapoint ID %u", *this->dimmer_id_);
}
void TuyaDimmerAsFan::update()
{
if (this->fan_wattage_.has_value() && this->power_sensor_ != nullptr && this->fan_->state)
{
this->power_sensor_->publish_state(this->fan_wattage_.value());
}
}
} // namespace tuya
} // namespace esphome

View File

@ -8,7 +8,7 @@
namespace esphome {
namespace tuya {
class TuyaDimmerAsFan : public Component {
class TuyaDimmerAsFan : public PollingComponent {
public:
TuyaDimmerAsFan(Tuya *parent, fan::FanState *fan) : parent_(parent), fan_(fan) {}
void setup() override;
@ -18,6 +18,7 @@ class TuyaDimmerAsFan : public Component {
void set_dimmer_max_value(uint32_t max_value) { this->dimmer_max_value_ = max_value; }
void set_fan_wattage(float fan_wattage) { this->fan_wattage_ = fan_wattage; }
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
void update() override;
protected:
Tuya *parent_;

View File

@ -1,6 +1,6 @@
# Tuya Light Plus Component
## Overview
This an enhanced version of the standard Tuya light component that adds a bunch of extra features. I use this component with [Feit Dimmers](https://www.amazon.com/gp/product/B07SXDFH38/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1) but it will likely work with other Tuya dimmers. Extra features include the following:
This an enhanced version of the standard [Tuya light](https://esphome.io/components/light/tuya.html) component that adds a bunch of extra features. I use this component with [Feit Dimmers](https://www.amazon.com/gp/product/B07SXDFH38/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1) but it will likely work with other Tuya dimmers. Extra features include the following:
* Resets the brightness level back to a default level when turned off so that it always comes on at the same level instead of the level it was at when turned off.
* The default level can be different during the "day" vs at "night" when everyone is in bed. Day and Night are based on a sensor (binary or text)in Home Assistant.
* The default level can be updated via a service that is added to Home Assistant by this component.
@ -11,7 +11,7 @@ This an enhanced version of the standard Tuya light component that adds a bunch
* Double clicking the dimmer while off can be configured to leave the light in an off or on state.
* Adds an option to configure action(s) to run when the dimmer is double clicked while on (this double click always turns the light off otherwise you get strange flash when double clicking).
* Allows you to "link" other light(s) in Home Assistant that will be controlled by this dimmer (on/off and level).
* Can add a sensor to report current power usage based on a configured wattage of the lights it controls. Currently this reports the specified wattage regardless of the dimmer level (my lights run at the max level 95% of the time so for me this is pretty accurate). Eventually I want to determine approximately what the dimmer level to power reduction ratio is so that it can more accurately report the power.
* Can add a sensor to report current power usage based on a configured wattage of the light(s) it controls. Currently this reports the specified wattage regardless of the dimmer level (my lights run at the max level 95% of the time so for me this is pretty accurate). Eventually I want to determine approximately what the dimmer level to power reduction ratio is so that it can more accurately report the power.
## Setup
@ -60,17 +60,10 @@ light:
id: my_light_power
name: My Light Power
light_wattage: 21.6
update_interval: 60s
```
## Configuration Variables
* id (Optional, ID): Manually specify the ID used for code generation.
* name (Required, string): The name of the light.
* switch_datapoint (Required, int): The datapoint id number of the power switch.
* dimmer_datapoint (Required, int): The datapoint id number of the dimmer value.
* min_value_datapoint (Optional, int): The datapoint id number of the MCU minimum value setting. If this is set then ESPHome will sync the min_value to the MCU on startup.
* min_value (Optional, int, default 0): The lowest dimmer value allowed.
* max_value (Optional, int, default 255): The highest dimmer value allowed.
## Configuration Variables (In addition to the standard variables)
* default_brightness (Optional, int 1-255): The default brightness level for the light.
* auto_off_time (Optional, Time): The amount of time to wait before automatically turning the light off, 0 disables auto off
* linked_lights (Optional, string): List of lights that will be controlled by this dimmer (note this one direction, changes to the linked light will not be applied to this light).
@ -88,6 +81,7 @@ light:
* 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
This component adds 2 services to Home Assistant that can be used to update the settings of the dimmer:

View File

@ -17,6 +17,7 @@ from esphome.const import (
DEVICE_CLASS_POWER,
STATE_CLASS_MEASUREMENT,
ICON_POWER,
CONF_UPDATE_INTERVAL,
)
from esphome.components.tuya import CONF_TUYA_ID, Tuya
@ -43,7 +44,7 @@ CONF_LIGHT_WATTAGE = "light_wattage"
tuya_ns = cg.esphome_ns.namespace("tuya")
api_ns = cg.esphome_ns.namespace("api")
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
TuyaLight = tuya_ns.class_("TuyaLightPlus", light.LightOutput, cg.Component, APIServer)
TuyaLight = tuya_ns.class_("TuyaLightPlus", light.LightOutput, cg.PollingComponent, APIServer)
DoubleClickWhileOffTrigger = tuya_ns.class_('DoubleClickWhileOffTrigger', auto.Trigger.template())
DoubleClickWhileOnTrigger = tuya_ns.class_('DoubleClickWhileOnTrigger', auto.Trigger.template())
@ -104,6 +105,7 @@ CONFIG_SCHEMA = cv.All(
).extend(
{
cv.Optional(CONF_LIGHT_WATTAGE): cv.positive_float,
cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.update_interval,
}
),
}
@ -166,5 +168,8 @@ async def to_code(config):
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
paren = await cg.get_variable(config[CONF_TUYA_ID])
cg.add(var.set_tuya_parent(paren))

View File

@ -89,6 +89,14 @@ void TuyaLightPlus::loop()
}
}
void TuyaLightPlus::update()
{
if (this->light_wattage_.has_value() && this->power_sensor_ != nullptr && this->state_->current_values.is_on())
{
this->power_sensor_->publish_state(this->light_wattage_.value());
}
}
void TuyaLightPlus::add_new_double_click_while_off_callback(std::function<void()> &&callback)
{
this->has_double_click_while_off_ = true;

View File

@ -1,9 +1,9 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/tuya/tuya.h"
#include "esphome/components/light/light_output.h"
#include "esphome/components/api/custom_api_device.h"
#include "esphome/core/component.h"
namespace esphome {
namespace tuya {
@ -13,7 +13,7 @@ enum DayNightSensorType {
TEXT,
};
class TuyaLightPlus : public Component, public light::LightOutput, public api::CustomAPIDevice {
class TuyaLightPlus : public light::LightOutput, public PollingComponent, public api::CustomAPIDevice {
public:
void setup() override;
void dump_config() override;
@ -29,6 +29,7 @@ class TuyaLightPlus : public Component, public light::LightOutput, public api::C
void setup_state(light::LightState *state) override;
void write_state(light::LightState *state) override;
void loop() override;
void update() override;
void set_linked_lights(const std::string linked_lights) { this->linked_lights_ = linked_lights; }
void set_day_night_sensor(const std::string day_night_sensor) { this->day_night_sensor_ = day_night_sensor; }

View File

@ -12,6 +12,10 @@ packages:
device_base: !include ../packages/device_base.yaml
external_components:
- source:
type: local
path: ../components
components: [ binary_light_with_power, total_daily_energy ]
- source: github://cbpowell/ESPSense
components: [ espsense ]
@ -46,18 +50,14 @@ fan:
name: "Basement Bathroom Fan"
light:
- platform: binary
- platform: binary_light_with_power
id: the_light
name: "Basement Bathroom Light"
output: light_output
on_turn_on:
- sensor.template.publish:
id: light_power
state: 48.0
on_turn_off:
- sensor.template.publish:
id: light_power
state: 0.0
power:
id: light_power
name: Basement Bathroom Light Power
light_wattage: 48.0
output:
- platform: gpio
@ -73,17 +73,11 @@ status_led:
inverted: true
sensor:
- platform: template
name: Basement Bathroom Light Power
id: light_power
unit_of_measurement: W
device_class: power
state_class: measurement
accuracy_decimals: 1
- platform: total_daily_energy
name: Basement Bathroom Light
power_id: light_power
state_class: measurement
method: left
min_save_interval: 10min
time:
- platform: homeassistant

View File

@ -14,6 +14,10 @@ packages:
external_components:
- source: github://cbpowell/ESPSense
components: [ espsense ]
- source:
type: local
path: ../components
components: [ binary_light_with_power, gpio_switch_with_power, total_daily_energy ]
binary_sensor:
- platform: gpio
@ -35,7 +39,7 @@ binary_sensor:
espsense:
plugs:
- name: Basement Bathroom Shower Light
- name: Basement Shower Light
power_sensor: light_power
voltage: 120
- name: Basement Bathroom Heater
@ -43,18 +47,14 @@ espsense:
voltage: 120
light:
- platform: binary
- platform: binary_light_with_power
id: the_light
name: "Basement Shower Light"
name: Basement Shower Light
output: light_output
on_turn_on:
- sensor.template.publish:
id: light_power
state: 11.5
on_turn_off:
- sensor.template.publish:
id: light_power
state: 0.0
power:
id: light_power
name: Basement Shower Light Power
light_wattage: 11.5
output:
- platform: gpio
@ -67,43 +67,27 @@ status_led:
inverted: true
switch:
- platform: gpio
- platform: gpio_switch_with_power
id: heater
name: "Basement Bathroom Heater"
name: Basement Bathroom Heater
icon: mdi:radiator
pin: 4
on_turn_on:
- sensor.template.publish:
id: heat_power
state: 500
on_turn_off:
- sensor.template.publish:
id: heat_power
state: 0.0
power:
id: heat_power
name: Basement Bathroom Heater Power
device_wattage: 500.0
sensor:
- platform: template
name: Basement Bathroom Shower Light
id: light_power
unit_of_measurement: W
device_class: power
state_class: measurement
accuracy_decimals: 1
- platform: template
name: Basement Bathroom Heater
id: heat_power
unit_of_measurement: W
device_class: power
state_class: measurement
accuracy_decimals: 1
- platform: total_daily_energy
name: Basement Bathroom Shower Light
name: Basement Shower Light
power_id: light_power
state_class: measurement
method: left
min_save_interval: 10min
- platform: total_daily_energy
name: Basement Bathroom Heater
power_id: heat_power
state_class: measurement
method: left
min_save_interval: 10min
time:
- platform: homeassistant

View File

@ -22,11 +22,11 @@ light:
sensor_type: text
sensor_day_value: Day
sensor_night_value: Night
day_default_brightness: 255
day_default_brightness: 128
night_default_brightness: 1
day_auto_off_time: 0 min
night_auto_off_time: 15 min
power:
id: power
name: ${device_name} Power
light_wattage: 35.5
light_wattage: 81.5

View File

@ -22,7 +22,7 @@ light:
sensor_type: text
sensor_day_value: Day
sensor_night_value: Night
day_default_brightness: 255
day_default_brightness: 128
night_default_brightness: 1
day_auto_off_time: 0 min
night_auto_off_time: 15 min

View File

@ -12,27 +12,27 @@ packages:
device_base: !include ../packages/device_base.yaml
external_components:
- source:
type: local
path: ../components
components: [ binary_light_with_power, total_daily_energy ]
- source: github://cbpowell/ESPSense
components: [ espsense ]
espsense:
plugs:
- name: Patio Lights
- name: ${device_name}
power_sensor: power
voltage: 120
light:
- platform: binary
name: "Patio Lights"
- platform: binary_light_with_power
name: ${device_name}
output: patio_lights_output
on_turn_on:
- sensor.template.publish:
id: power
state: 15.3
on_turn_off:
- sensor.template.publish:
id: power
state: 0.0
power:
id: power
name: ${device_name} Power
light_wattage: 15.3
output:
- platform: gpio
@ -40,17 +40,11 @@ output:
pin: D1
sensor:
- platform: template
name: Patio Lights Power
id: power
unit_of_measurement: W
device_class: power
state_class: measurement
accuracy_decimals: 1
- platform: total_daily_energy
name: Patio Lights
name: ${device_name}
power_id: power
state_class: measurement
method: left
min_save_interval: 10min
time:
- platform: homeassistant

View File

@ -16,6 +16,10 @@ esphome:
- ../custom/TreoLedPoolLight.h
external_components:
- source:
type: local
path: ../components
components: [ binary_light_with_power, total_daily_energy ]
- source: github://cbpowell/ESPSense
components: [ espsense ]
@ -93,7 +97,8 @@ sensor:
- platform: total_daily_energy
name: Pool Lights
power_id: pool_light_power
state_class: measurement
method: left
min_save_interval: 10min
status_led:
pin:

View File

@ -177,7 +177,7 @@ time:
int hour = now.hour;
int minute = now.minute;
if (hour == 4 && minute == 45)
if (hour == 3 && minute == 45)
{
auto ha_service = new api::HomeAssistantServiceCallAction<>(api_apiserver, false);
ha_service->set_service("switch.turn_off");
@ -186,7 +186,7 @@ time:
id(pool_cleaner).turn_off();
id(pool_pump).turn_on();
}
else if (hour >= 5 && hour <= 6)
else if (hour >= 4 && hour <= 5)
{
auto ha_service = new api::HomeAssistantServiceCallAction<>(api_apiserver, false);
ha_service->set_service("switch.turn_off");
@ -201,7 +201,7 @@ time:
id(pool_pump).turn_on();
}
}
else if (hour == 7)
else if (hour == 6)
{
auto ha_service = new api::HomeAssistantServiceCallAction<>(api_apiserver, false);
ha_service->set_service("switch.turn_on");
@ -210,11 +210,11 @@ time:
id(pool_cleaner).turn_off();
id(pool_pump).turn_on();
}
else if (hour >= 8 && hour <= 21 && (minute == 0 || minute == 30))
else if (hour >= 7 && hour <= 20 && (minute == 0 || minute == 30))
{
id(pool_pump).turn_off();
}
else if (hour >= 8 && hour <= 21 && (minute == 15 || minute == 45))
else if (hour >= 8 && hour <= 20 && (minute == 15 || minute == 45))
{
auto ha_service = new api::HomeAssistantServiceCallAction<>(api_apiserver, false);
ha_service->set_service("switch.turn_on");
@ -223,7 +223,7 @@ time:
id(pool_cleaner).turn_off();
id(pool_pump).turn_on();
}
else if (hour == 22 && minute == 0)
else if (hour == 21 && minute == 0)
{
auto ha_service = new api::HomeAssistantServiceCallAction<>(api_apiserver, false);
ha_service->set_service("switch.turn_off");

View File

@ -6,7 +6,7 @@ external_components:
- source:
type: local
path: ../components
components: [ tuya, tuya_light_plus ]
components: [ tuya, tuya_light_plus, total_daily_energy ]
- source: github://cbpowell/ESPSense
components: [ espsense ]
@ -25,7 +25,8 @@ sensor:
- platform: total_daily_energy
name: ${device_name}
power_id: power
state_class: measurement
method: left
min_save_interval: 10min
time:
- platform: homeassistant

View File

@ -6,7 +6,7 @@ external_components:
- source:
type: local
path: ../components
components: [ tuya, tuya_dimmer_as_fan ]
components: [ tuya, tuya_dimmer_as_fan, total_daily_energy ]
- source: github://cbpowell/ESPSense
components: [ espsense ]
@ -37,7 +37,8 @@ sensor:
- platform: total_daily_energy
name: ${device_name}
power_id: power
state_class: measurement
method: left
min_save_interval: 10min
time:
- platform: homeassistant