1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-04 09:01:49 +00:00

Compare commits

..

29 Commits

Author SHA1 Message Date
Jesse Hills
833affc1bf Merge pull request #6326 from esphome/bump-2024.2.2
2024.2.2
2024-03-06 10:28:28 +13:00
Jesse Hills
e2b197dc2c Bump version to 2024.2.2 2024-03-06 09:34:48 +13:00
Jesse Hills
f39dc49f49 Add wake word phrase to voice assistant start command (#6290) 2024-03-06 09:34:47 +13:00
Samuel Sieb
b0a25401f7 auto load output for now (#6309)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2024-03-06 09:34:47 +13:00
Jesse Hills
37d2b3c797 Merge pull request from GHSA-9p43-hj5j-96h5 2024-03-06 09:34:26 +13:00
Samuel Sieb
63cce916e2 fix tmp102 negative calculation (#6320) 2024-03-06 09:34:08 +13:00
星野SKY
1aab87b41c handling with the negative temperature in the sensor tmp102 (#6316) 2024-03-06 09:33:55 +13:00
puuu
a3fc1acdcb CSE7766: Fix energy calculation (#6286)
Co-authored-by: DAVe3283 <DAVe3283+GitHub@gmail.com>
2024-03-06 09:32:23 +13:00
Jesse Hills
cc115e7cc9 Merge pull request #6284 from esphome/bump-2024.2.1
2024.2.1
2024-02-26 09:04:36 +13:00
Jesse Hills
badac933ae Bump version to 2024.2.1 2024-02-26 07:52:25 +13:00
Keith Burzinski
b1b8217713 Fix thermostat supplemental actions (#6282) 2024-02-26 07:52:25 +13:00
Samuel Sieb
f3174c58bc fix throttle average nan handling (#6275)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2024-02-26 07:52:25 +13:00
Samuel Sieb
e66e135a63 make output optional for speed fan (#6274)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2024-02-26 07:52:25 +13:00
Jesse Hills
d814ed1d4a Merge pull request from GHSA-8p25-3q46-8q2p 2024-02-26 07:52:08 +13:00
J. Nick Koston
84c6e52be2 dashboard: move storage json update to a background task in edit save (#6280)
* dashboard: move storage json update to a background task in edit save

* dashboard: move storage json update to a background task in edit save

* fix typing

* docs
2024-02-26 07:50:28 +13:00
Keith Burzinski
2cf6393161 Fix RP2040 SPI pin validation (#6277) 2024-02-26 07:47:24 +13:00
Samuel Sieb
5a7759f1c4 allow multiple emc2101 (#6272) 2024-02-26 07:47:24 +13:00
Daniel Baulig
db5205931b web_server: Add a position property for cover entities that have the supports position trait (#6269) 2024-02-26 07:47:24 +13:00
Jesse Hills
62d59cffcc Bump zeroconf timeout to 3000 (#6270) 2024-02-26 07:47:24 +13:00
Jesse Hills
2e7129e816 Add missing timeout to "async_request" (#6267) 2024-02-26 07:47:24 +13:00
Jesse Hills
2d22a2d1c2 Merge pull request #6252 from esphome/bump-2024.2.0
2024.2.0
2024-02-21 13:48:58 +13:00
Jesse Hills
c92968da8a Bump version to 2024.2.0 2024-02-21 12:51:13 +13:00
Jesse Hills
86580d07cb Merge pull request #6249 from esphome/bump-2024.2.0b3
2024.2.0b3
2024-02-21 11:38:08 +13:00
Jesse Hills
03ea71034f Bump version to 2024.2.0b3 2024-02-21 10:57:43 +13:00
sibowler
7bf676abfa Tuya Fan component fix to handle enum datapoint type (#6135) 2024-02-21 10:57:43 +13:00
Michael Hansen
fb16e6b027 Voice Assistant: add on_idle trigger and fix nevermind (#6141) 2024-02-21 10:57:43 +13:00
SmartShackMaster
4eb04afa62 Clear UART read buffer before sending next command (#6200) 2024-02-21 10:57:43 +13:00
Keith Burzinski
841a831c63 Fix tm1651 enum (#6248) 2024-02-21 10:57:43 +13:00
Samuel Sieb
ae4af2966a hold interrupt disable for dallas one-wire (#6244)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2024-02-21 10:57:43 +13:00
33 changed files with 204 additions and 96 deletions

View File

@@ -1449,6 +1449,7 @@ message VoiceAssistantRequest {
string conversation_id = 2;
uint32 flags = 3;
VoiceAssistantAudioSettings audio_settings = 4;
string wake_word_phrase = 5;
}
message VoiceAssistantResponse {

View File

@@ -6594,6 +6594,10 @@ bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimite
this->audio_settings = value.as_message<VoiceAssistantAudioSettings>();
return true;
}
case 5: {
this->wake_word_phrase = value.as_string();
return true;
}
default:
return false;
}
@@ -6603,6 +6607,7 @@ void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(2, this->conversation_id);
buffer.encode_uint32(3, this->flags);
buffer.encode_message<VoiceAssistantAudioSettings>(4, this->audio_settings);
buffer.encode_string(5, this->wake_word_phrase);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantRequest::dump_to(std::string &out) const {
@@ -6624,6 +6629,10 @@ void VoiceAssistantRequest::dump_to(std::string &out) const {
out.append(" audio_settings: ");
this->audio_settings.dump_to(out);
out.append("\n");
out.append(" wake_word_phrase: ");
out.append("'").append(this->wake_word_phrase).append("'");
out.append("\n");
out.append("}");
}
#endif

View File

@@ -1701,6 +1701,7 @@ class VoiceAssistantRequest : public ProtoMessage {
std::string conversation_id{};
uint32_t flags{0};
VoiceAssistantAudioSettings audio_settings{};
std::string wake_word_phrase{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;

View File

@@ -118,7 +118,7 @@ void CSE7766Component::parse_data_() {
uint32_t power_coeff = this->get_24_bit_uint_(14);
uint32_t power_cycle = this->get_24_bit_uint_(17);
uint8_t adj = this->raw_data_[20];
uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22];
uint16_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22];
bool have_power = adj & 0x10;
bool have_current = adj & 0x20;
@@ -132,8 +132,19 @@ void CSE7766Component::parse_data_() {
}
}
float energy = 0.0;
if (this->energy_sensor_ != nullptr) {
if (this->cf_pulses_last_ == 0 && !this->energy_sensor_->has_state()) {
this->cf_pulses_last_ = cf_pulses;
}
uint16_t cf_diff = cf_pulses - this->cf_pulses_last_;
this->cf_pulses_total_ += cf_diff;
this->cf_pulses_last_ = cf_pulses;
energy = this->cf_pulses_total_ * float(power_coeff) / 1000000.0f / 3600.0f;
this->energy_sensor_->publish_state(energy);
}
float power = 0.0f;
float energy = 0.0f;
if (power_cycle_exceeds_range) {
// Datasheet: power cycle exceeding range means active power is 0
if (this->power_sensor_ != nullptr) {
@@ -144,27 +155,6 @@ void CSE7766Component::parse_data_() {
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(power);
}
// Add CF pulses to the total energy only if we have Power coefficient to multiply by
if (this->cf_pulses_last_ == 0) {
this->cf_pulses_last_ = cf_pulses;
}
uint32_t cf_diff;
if (cf_pulses < this->cf_pulses_last_) {
cf_diff = cf_pulses + (0x10000 - this->cf_pulses_last_);
} else {
cf_diff = cf_pulses - this->cf_pulses_last_;
}
this->cf_pulses_last_ = cf_pulses;
energy = cf_diff * float(power_coeff) / 1000000.0f / 3600.0f;
this->energy_total_ += energy;
if (this->energy_sensor_ != nullptr)
this->energy_sensor_->publish_state(this->energy_total_);
} else if ((this->energy_sensor_ != nullptr) && !this->energy_sensor_->has_state()) {
this->energy_sensor_->publish_state(0);
}
float current = 0.0f;

View File

@@ -30,8 +30,8 @@ class CSE7766Component : public Component, public uart::UARTDevice {
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *energy_sensor_{nullptr};
float energy_total_{0.0f};
uint32_t cf_pulses_last_{0};
uint32_t cf_pulses_total_{0};
uint16_t cf_pulses_last_{0};
};
} // namespace cse7766

View File

@@ -168,10 +168,6 @@ bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() {
if (!wire->reset()) {
return false;
}
}
{
InterruptLock lock;
wire->select(this->address_);
wire->write8(DALLAS_COMMAND_READ_SCRATCH_PAD);

View File

@@ -7,6 +7,8 @@ CODEOWNERS = ["@ellull"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
CONF_PWM = "pwm"
CONF_DIVIDER = "divider"
CONF_DAC = "dac"

View File

@@ -336,6 +336,8 @@ void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, ui
}
uint8_t FingerprintGrowComponent::send_command_() {
while (this->available())
this->read();
this->write((uint8_t) (START_CODE >> 8));
this->write((uint8_t) (START_CODE & 0xFF));
this->write(this->address_[0]);

View File

@@ -134,7 +134,7 @@ void MicroWakeWord::loop() {
this->set_state_(State::IDLE);
if (this->detected_) {
this->detected_ = false;
this->wake_word_detected_trigger_->trigger("");
this->wake_word_detected_trigger_->trigger(this->wake_word_);
}
}
break;

View File

@@ -252,7 +252,9 @@ ThrottleAverageFilter::ThrottleAverageFilter(uint32_t time_period) : time_period
optional<float> ThrottleAverageFilter::new_value(float value) {
ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::new_value(value=%f)", this, value);
if (!std::isnan(value)) {
if (std::isnan(value)) {
this->have_nan_ = true;
} else {
this->sum_ += value;
this->n_++;
}
@@ -262,12 +264,14 @@ void ThrottleAverageFilter::setup() {
this->set_interval("throttle_average", this->time_period_, [this]() {
ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::interval(sum=%f, n=%i)", this, this->sum_, this->n_);
if (this->n_ == 0) {
this->output(NAN);
if (this->have_nan_)
this->output(NAN);
} else {
this->output(this->sum_ / this->n_);
this->sum_ = 0.0f;
this->n_ = 0;
}
this->have_nan_ = false;
});
}
float ThrottleAverageFilter::get_setup_priority() const { return setup_priority::HARDWARE; }

View File

@@ -245,6 +245,7 @@ class ThrottleAverageFilter : public Filter, public Component {
uint32_t time_period_;
float sum_{0.0f};
unsigned int n_{0};
bool have_nan_{false};
};
using lambda_filter_t = std::function<optional<float>(float)>;

View File

@@ -14,12 +14,14 @@ from esphome.const import (
from .. import speed_ns
AUTO_LOAD = ["output"]
SpeedFan = speed_ns.class_("SpeedFan", cg.Component, fan.Fan)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan),
cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput),
cv.Optional(CONF_OUTPUT): cv.use_id(output.FloatOutput),
cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Optional(CONF_SPEED): cv.invalid(
@@ -32,11 +34,14 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
async def to_code(config):
output_ = await cg.get_variable(config[CONF_OUTPUT])
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], output_, config[CONF_SPEED_COUNT])
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_SPEED_COUNT])
await cg.register_component(var, config)
await fan.register_fan(var, config)
if CONF_OUTPUT in config:
output_ = await cg.get_variable(config[CONF_OUTPUT])
cg.add(var.set_output(output_))
if CONF_OSCILLATION_OUTPUT in config:
oscillation_output = await cg.get_variable(config[CONF_OSCILLATION_OUTPUT])
cg.add(var.set_oscillating(oscillation_output))

View File

@@ -36,9 +36,10 @@ void SpeedFan::control(const fan::FanCall &call) {
}
void SpeedFan::write_state_() {
float speed = this->state ? static_cast<float>(this->speed) / static_cast<float>(this->speed_count_) : 0.0f;
this->output_->set_level(speed);
if (this->output_ != nullptr) {
float speed = this->state ? static_cast<float>(this->speed) / static_cast<float>(this->speed_count_) : 0.0f;
this->output_->set_level(speed);
}
if (this->oscillating_ != nullptr)
this->oscillating_->set_state(this->oscillating);
if (this->direction_ != nullptr)

View File

@@ -12,9 +12,10 @@ namespace speed {
class SpeedFan : public Component, public fan::Fan {
public:
SpeedFan(output::FloatOutput *output, int speed_count) : output_(output), speed_count_(speed_count) {}
SpeedFan(int speed_count) : speed_count_(speed_count) {}
void setup() override;
void dump_config() override;
void set_output(output::FloatOutput *output) { this->output_ = output; }
void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; }
void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; }
void set_preset_modes(const std::set<std::string> &presets) { this->preset_modes_ = presets; }
@@ -24,7 +25,7 @@ class SpeedFan : public Component, public fan::Fan {
void control(const fan::FanCall &call) override;
void write_state_();
output::FloatOutput *output_;
output::FloatOutput *output_{nullptr};
output::BinaryOutput *oscillating_{nullptr};
output::BinaryOutput *direction_{nullptr};
int speed_count_{};

View File

@@ -74,7 +74,8 @@ CONF_FORCE_SW = "force_sw"
CONF_INTERFACE = "interface"
CONF_INTERFACE_INDEX = "interface_index"
# RP2040 SPI pin assignments are complicated. Refer to https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
# RP2040 SPI pin assignments are complicated;
# refer to GPIO function select table in https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
RP_SPI_PINSETS = [
{
@@ -85,7 +86,7 @@ RP_SPI_PINSETS = [
{
CONF_MISO_PIN: [8, 12, 24, 28, -1],
CONF_CLK_PIN: [10, 14, 26],
CONF_MOSI_PIN: [11, 23, 27, -1],
CONF_MOSI_PIN: [11, 15, 27, -1],
},
]

View File

@@ -1,6 +1,5 @@
#include "thermostat_climate.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace thermostat {
@@ -63,6 +62,15 @@ void ThermostatClimate::setup() {
this->publish_state();
}
void ThermostatClimate::loop() {
for (auto &timer : this->timer_) {
if (timer.active && (timer.started + timer.time < millis())) {
timer.active = false;
timer.func();
}
}
}
float ThermostatClimate::cool_deadband() { return this->cooling_deadband_; }
float ThermostatClimate::cool_overrun() { return this->cooling_overrun_; }
float ThermostatClimate::heat_deadband() { return this->heating_deadband_; }
@@ -439,6 +447,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu
this->start_timer_(thermostat::TIMER_FANNING_ON);
trig_fan = this->fan_only_action_trigger_;
}
this->cooling_max_runtime_exceeded_ = false;
trig = this->cool_action_trigger_;
ESP_LOGVV(TAG, "Switching to COOLING action");
action_ready = true;
@@ -452,6 +461,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu
this->start_timer_(thermostat::TIMER_FANNING_ON);
trig_fan = this->fan_only_action_trigger_;
}
this->heating_max_runtime_exceeded_ = false;
trig = this->heat_action_trigger_;
ESP_LOGVV(TAG, "Switching to HEATING action");
action_ready = true;
@@ -752,15 +762,15 @@ bool ThermostatClimate::heating_action_ready_() {
void ThermostatClimate::start_timer_(const ThermostatClimateTimerIndex timer_index) {
if (this->timer_duration_(timer_index) > 0) {
this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index),
this->timer_cbf_(timer_index));
this->timer_[timer_index].started = millis();
this->timer_[timer_index].active = true;
}
}
bool ThermostatClimate::cancel_timer_(ThermostatClimateTimerIndex timer_index) {
auto ret = this->timer_[timer_index].active;
this->timer_[timer_index].active = false;
return this->cancel_timeout(this->timer_[timer_index].name);
return ret;
}
bool ThermostatClimate::timer_active_(ThermostatClimateTimerIndex timer_index) {
@@ -777,7 +787,6 @@ std::function<void()> ThermostatClimate::timer_cbf_(ThermostatClimateTimerIndex
void ThermostatClimate::cooling_max_run_time_timer_callback_() {
ESP_LOGVV(TAG, "cooling_max_run_time timer expired");
this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].active = false;
this->cooling_max_runtime_exceeded_ = true;
this->trigger_supplemental_action_();
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
@@ -785,21 +794,18 @@ void ThermostatClimate::cooling_max_run_time_timer_callback_() {
void ThermostatClimate::cooling_off_timer_callback_() {
ESP_LOGVV(TAG, "cooling_off timer expired");
this->timer_[thermostat::TIMER_COOLING_OFF].active = false;
this->switch_to_action_(this->compute_action_());
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
}
void ThermostatClimate::cooling_on_timer_callback_() {
ESP_LOGVV(TAG, "cooling_on timer expired");
this->timer_[thermostat::TIMER_COOLING_ON].active = false;
this->switch_to_action_(this->compute_action_());
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
}
void ThermostatClimate::fan_mode_timer_callback_() {
ESP_LOGVV(TAG, "fan_mode timer expired");
this->timer_[thermostat::TIMER_FAN_MODE].active = false;
this->switch_to_fan_mode_(this->fan_mode.value_or(climate::CLIMATE_FAN_ON));
if (this->supports_fan_only_action_uses_fan_mode_timer_)
this->switch_to_action_(this->compute_action_());
@@ -807,19 +813,16 @@ void ThermostatClimate::fan_mode_timer_callback_() {
void ThermostatClimate::fanning_off_timer_callback_() {
ESP_LOGVV(TAG, "fanning_off timer expired");
this->timer_[thermostat::TIMER_FANNING_OFF].active = false;
this->switch_to_action_(this->compute_action_());
}
void ThermostatClimate::fanning_on_timer_callback_() {
ESP_LOGVV(TAG, "fanning_on timer expired");
this->timer_[thermostat::TIMER_FANNING_ON].active = false;
this->switch_to_action_(this->compute_action_());
}
void ThermostatClimate::heating_max_run_time_timer_callback_() {
ESP_LOGVV(TAG, "heating_max_run_time timer expired");
this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].active = false;
this->heating_max_runtime_exceeded_ = true;
this->trigger_supplemental_action_();
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
@@ -827,21 +830,18 @@ void ThermostatClimate::heating_max_run_time_timer_callback_() {
void ThermostatClimate::heating_off_timer_callback_() {
ESP_LOGVV(TAG, "heating_off timer expired");
this->timer_[thermostat::TIMER_HEATING_OFF].active = false;
this->switch_to_action_(this->compute_action_());
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
}
void ThermostatClimate::heating_on_timer_callback_() {
ESP_LOGVV(TAG, "heating_on timer expired");
this->timer_[thermostat::TIMER_HEATING_ON].active = false;
this->switch_to_action_(this->compute_action_());
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
}
void ThermostatClimate::idle_on_timer_callback_() {
ESP_LOGVV(TAG, "idle_on timer expired");
this->timer_[thermostat::TIMER_IDLE_ON].active = false;
this->switch_to_action_(this->compute_action_());
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
}

View File

@@ -1,10 +1,12 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/sensor/sensor.h"
#include <cinttypes>
#include <map>
#include <vector>
@@ -26,9 +28,9 @@ enum ThermostatClimateTimerIndex : size_t {
enum OnBootRestoreFrom : size_t { MEMORY = 0, DEFAULT_PRESET = 1 };
struct ThermostatClimateTimer {
const std::string name;
bool active;
uint32_t time;
uint32_t started;
std::function<void()> func;
};
@@ -59,6 +61,7 @@ class ThermostatClimate : public climate::Climate, public Component {
ThermostatClimate();
void setup() override;
void dump_config() override;
void loop() override;
void set_default_preset(const std::string &custom_preset);
void set_default_preset(climate::ClimatePreset preset);
@@ -441,16 +444,17 @@ class ThermostatClimate : public climate::Climate, public Component {
/// Climate action timers
std::vector<ThermostatClimateTimer> timer_{
{"cool_run", false, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)},
{"cool_off", false, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)},
{"cool_on", false, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)},
{"fan_mode", false, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)},
{"fan_off", false, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)},
{"fan_on", false, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)},
{"heat_run", false, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)},
{"heat_off", false, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)},
{"heat_on", false, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)},
{"idle_on", false, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}};
{false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)},
};
/// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc)
std::map<climate::ClimatePreset, ThermostatClimateTargetTempConfig> preset_config_{};

View File

@@ -13,6 +13,7 @@ from esphome.const import (
CODEOWNERS = ["@freekode"]
tm1651_ns = cg.esphome_ns.namespace("tm1651")
TM1651Brightness = tm1651_ns.enum("TM1651Brightness")
TM1651Display = tm1651_ns.class_("TM1651Display", cg.Component)
SetLevelPercentAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action)
@@ -24,9 +25,9 @@ TurnOffAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action)
CONF_LEVEL_PERCENT = "level_percent"
TM1651_BRIGHTNESS_OPTIONS = {
1: TM1651Display.TM1651_BRIGHTNESS_LOW,
2: TM1651Display.TM1651_BRIGHTNESS_MEDIUM,
3: TM1651Display.TM1651_BRIGHTNESS_HIGH,
1: TM1651Brightness.TM1651_BRIGHTNESS_LOW,
2: TM1651Brightness.TM1651_BRIGHTNESS_MEDIUM,
3: TM1651Brightness.TM1651_BRIGHTNESS_HIGH,
}
CONFIG_SCHEMA = cv.All(

View File

@@ -12,9 +12,9 @@ static const char *const TAG = "tm1651.display";
static const uint8_t MAX_INPUT_LEVEL_PERCENT = 100;
static const uint8_t TM1651_MAX_LEVEL = 7;
static const uint8_t TM1651_BRIGHTNESS_LOW = 0;
static const uint8_t TM1651_BRIGHTNESS_MEDIUM = 2;
static const uint8_t TM1651_BRIGHTNESS_HIGH = 7;
static const uint8_t TM1651_BRIGHTNESS_LOW_HW = 0;
static const uint8_t TM1651_BRIGHTNESS_MEDIUM_HW = 2;
static const uint8_t TM1651_BRIGHTNESS_HIGH_HW = 7;
void TM1651Display::setup() {
ESP_LOGCONFIG(TAG, "Setting up TM1651...");
@@ -78,14 +78,14 @@ uint8_t TM1651Display::calculate_level_(uint8_t new_level) {
uint8_t TM1651Display::calculate_brightness_(uint8_t new_brightness) {
if (new_brightness <= 1) {
return TM1651_BRIGHTNESS_LOW;
return TM1651_BRIGHTNESS_LOW_HW;
} else if (new_brightness == 2) {
return TM1651_BRIGHTNESS_MEDIUM;
return TM1651_BRIGHTNESS_MEDIUM_HW;
} else if (new_brightness >= 3) {
return TM1651_BRIGHTNESS_HIGH;
return TM1651_BRIGHTNESS_HIGH_HW;
}
return TM1651_BRIGHTNESS_LOW;
return TM1651_BRIGHTNESS_LOW_HW;
}
} // namespace tm1651

View File

@@ -13,6 +13,12 @@
namespace esphome {
namespace tm1651 {
enum TM1651Brightness : uint8_t {
TM1651_BRIGHTNESS_LOW = 1,
TM1651_BRIGHTNESS_MEDIUM = 2,
TM1651_BRIGHTNESS_HIGH = 3,
};
class TM1651Display : public Component {
public:
void set_clk_pin(InternalGPIOPin *pin) { clk_pin_ = pin; }
@@ -24,6 +30,7 @@ class TM1651Display : public Component {
void set_level_percent(uint8_t new_level);
void set_level(uint8_t new_level);
void set_brightness(uint8_t new_brightness);
void set_brightness(TM1651Brightness new_brightness) { this->set_brightness(static_cast<uint8_t>(new_brightness)); }
void turn_on();
void turn_off();

View File

@@ -28,7 +28,7 @@ void TMP102Component::dump_config() {
}
void TMP102Component::update() {
uint16_t raw_temperature;
int16_t raw_temperature;
if (this->write(&TMP102_REGISTER_TEMPERATURE, 1) != i2c::ERROR_OK) {
this->status_set_warning();
return;
@@ -39,7 +39,6 @@ void TMP102Component::update() {
return;
}
raw_temperature = i2c::i2ctohs(raw_temperature);
raw_temperature = raw_temperature >> 4;
float temperature = raw_temperature * TMP102_CONVERSION_FACTOR;
ESP_LOGD(TAG, "Got Temperature=%.1f°C", temperature);

View File

@@ -34,9 +34,13 @@ void TuyaFan::setup() {
}
if (this->oscillation_id_.has_value()) {
this->parent_->register_listener(*this->oscillation_id_, [this](const TuyaDatapoint &datapoint) {
// Whether data type is BOOL or ENUM, it will still be a 1 or a 0, so the functions below are valid in both
// scenarios
ESP_LOGV(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool));
this->oscillating = datapoint.value_bool;
this->publish_state();
this->oscillation_type_ = datapoint.type;
});
}
if (this->direction_id_.has_value()) {
@@ -80,7 +84,11 @@ void TuyaFan::control(const fan::FanCall &call) {
this->parent_->set_boolean_datapoint_value(*this->switch_id_, *call.get_state());
}
if (this->oscillation_id_.has_value() && call.get_oscillating().has_value()) {
this->parent_->set_boolean_datapoint_value(*this->oscillation_id_, *call.get_oscillating());
if (this->oscillation_type_ == TuyaDatapointType::ENUM) {
this->parent_->set_enum_datapoint_value(*this->oscillation_id_, *call.get_oscillating());
} else if (this->speed_type_ == TuyaDatapointType::BOOLEAN) {
this->parent_->set_boolean_datapoint_value(*this->oscillation_id_, *call.get_oscillating());
}
}
if (this->direction_id_.has_value() && call.get_direction().has_value()) {
bool enable = *call.get_direction() == fan::FanDirection::REVERSE;

View File

@@ -29,6 +29,7 @@ class TuyaFan : public Component, public fan::Fan {
optional<uint8_t> direction_id_{};
int speed_count_{};
TuyaDatapointType speed_type_{};
TuyaDatapointType oscillation_type_{};
};
} // namespace tuya

View File

@@ -32,6 +32,7 @@ CONF_ON_TTS_START = "on_tts_start"
CONF_ON_TTS_STREAM_START = "on_tts_stream_start"
CONF_ON_TTS_STREAM_END = "on_tts_stream_end"
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
CONF_ON_IDLE = "on_idle"
CONF_SILENCE_DETECTION = "silence_detection"
CONF_USE_WAKE_WORD = "use_wake_word"
@@ -41,6 +42,8 @@ CONF_AUTO_GAIN = "auto_gain"
CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level"
CONF_VOLUME_MULTIPLIER = "volume_multiplier"
CONF_WAKE_WORD = "wake_word"
voice_assistant_ns = cg.esphome_ns.namespace("voice_assistant")
VoiceAssistant = voice_assistant_ns.class_("VoiceAssistant", cg.Component)
@@ -127,6 +130,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ON_TTS_STREAM_END): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_IDLE): automation.validate_automation(single=True),
}
).extend(cv.COMPONENT_SCHEMA),
tts_stream_validate,
@@ -259,6 +263,13 @@ async def to_code(config):
config[CONF_ON_TTS_STREAM_END],
)
if CONF_ON_IDLE in config:
await automation.build_automation(
var.get_idle_trigger(),
[],
config[CONF_ON_IDLE],
)
cg.add_define("USE_VOICE_ASSISTANT")
@@ -276,6 +287,7 @@ VOICE_ASSISTANT_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(VoiceAssis
VOICE_ASSISTANT_ACTION_SCHEMA.extend(
{
cv.Optional(CONF_SILENCE_DETECTION, default=True): cv.boolean,
cv.Optional(CONF_WAKE_WORD): cv.templatable(cv.string),
}
),
)
@@ -284,6 +296,9 @@ async def voice_assistant_listen_to_code(config, action_id, template_arg, args):
await cg.register_parented(var, config[CONF_ID])
if CONF_SILENCE_DETECTION in config:
cg.add(var.set_silence_detection(config[CONF_SILENCE_DETECTION]))
if wake_word := config.get(CONF_WAKE_WORD):
templ = await cg.templatable(wake_word, args, cg.std_string)
cg.add(var.set_wake_word(templ))
return var

View File

@@ -135,6 +135,8 @@ void VoiceAssistant::loop() {
switch (this->state_) {
case State::IDLE: {
if (this->continuous_ && this->desired_state_ == State::IDLE) {
this->idle_trigger_->trigger();
this->ring_buffer_->reset();
#ifdef USE_ESP_ADF
if (this->use_wake_word_) {
@@ -213,6 +215,8 @@ void VoiceAssistant::loop() {
msg.conversation_id = this->conversation_id_;
msg.flags = flags;
msg.audio_settings = audio_settings;
msg.wake_word_phrase = this->wake_word_;
this->wake_word_ = "";
if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) {
ESP_LOGW(TAG, "Could not request start");
@@ -618,6 +622,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
{
this->set_state_(State::IDLE, State::IDLE);
}
} else if (this->state_ == State::AWAITING_RESPONSE) {
// No TTS start event ("nevermind")
this->set_state_(State::IDLE, State::IDLE);
}
this->defer([this]() { this->end_trigger_->trigger(); });
break;

View File

@@ -116,6 +116,7 @@ class VoiceAssistant : public Component {
Trigger<std::string> *get_tts_end_trigger() const { return this->tts_end_trigger_; }
Trigger<std::string> *get_tts_start_trigger() const { return this->tts_start_trigger_; }
Trigger<std::string, std::string> *get_error_trigger() const { return this->error_trigger_; }
Trigger<> *get_idle_trigger() const { return this->idle_trigger_; }
Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
Trigger<> *get_client_disconnected_trigger() const { return this->client_disconnected_trigger_; }
@@ -123,6 +124,8 @@ class VoiceAssistant : public Component {
void client_subscription(api::APIConnection *client, bool subscribe);
api::APIConnection *get_api_connection() const { return this->api_client_; }
void set_wake_word(const std::string &wake_word) { this->wake_word_ = wake_word; }
protected:
int read_microphone_();
void set_state_(State state);
@@ -148,6 +151,7 @@ class VoiceAssistant : public Component {
Trigger<std::string> *tts_end_trigger_ = new Trigger<std::string>();
Trigger<std::string> *tts_start_trigger_ = new Trigger<std::string>();
Trigger<std::string, std::string> *error_trigger_ = new Trigger<std::string, std::string>();
Trigger<> *idle_trigger_ = new Trigger<>();
Trigger<> *client_connected_trigger_ = new Trigger<>();
Trigger<> *client_disconnected_trigger_ = new Trigger<>();
@@ -173,6 +177,8 @@ class VoiceAssistant : public Component {
std::string conversation_id_{""};
std::string wake_word_{""};
HighFrequencyLoopRequester high_freq_;
#ifdef USE_ESP_ADF
@@ -198,8 +204,13 @@ class VoiceAssistant : public Component {
};
template<typename... Ts> class StartAction : public Action<Ts...>, public Parented<VoiceAssistant> {
TEMPLATABLE_VALUE(std::string, wake_word);
public:
void play(Ts... x) override { this->parent_->request_start(false, this->silence_detection_); }
void play(Ts... x) override {
this->parent_->set_wake_word(this->wake_word_.value(x...));
this->parent_->request_start(false, this->silence_detection_);
}
void set_silence_detection(bool silence_detection) { this->silence_detection_ = silence_detection; }

View File

@@ -785,6 +785,8 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
obj->position, start_config);
root["current_operation"] = cover::cover_operation_to_str(obj->current_operation);
if (obj->get_traits().get_supports_position())
root["position"] = obj->position;
if (obj->get_traits().get_supports_tilt())
root["tilt"] = obj->tilt;
});

View File

@@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2024.2.0b2"
__version__ = "2024.2.2"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -8,3 +8,5 @@ MAX_EXECUTOR_WORKERS = 48
SENTINEL = object()
DASHBOARD_COMMAND = ["esphome", "--dashboard"]

View File

@@ -1,11 +1,13 @@
from __future__ import annotations
import asyncio
import contextlib
import logging
import threading
from dataclasses import dataclass
from functools import partial
from typing import TYPE_CHECKING, Any, Callable
from collections.abc import Coroutine
from ..zeroconf import DiscoveredImport
from .dns import DNSCache
@@ -71,6 +73,7 @@ class ESPHomeDashboard:
"mdns_status",
"settings",
"dns_cache",
"_background_tasks",
)
def __init__(self) -> None:
@@ -85,6 +88,7 @@ class ESPHomeDashboard:
self.mdns_status: MDNSStatus | None = None
self.settings = DashboardSettings()
self.dns_cache = DNSCache()
self._background_tasks: set[asyncio.Task] = set()
async def async_setup(self) -> None:
"""Setup the dashboard."""
@@ -132,7 +136,19 @@ class ESPHomeDashboard:
if settings.status_use_mqtt:
status_thread_mqtt.join()
self.mqtt_ping_request.set()
for task in self._background_tasks:
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
await asyncio.sleep(0)
def async_create_background_task(
self, coro: Coroutine[Any, Any, Any]
) -> asyncio.Task:
"""Create a background task."""
task = self.loop.create_task(coro)
task.add_done_callback(self._background_tasks.discard)
return task
DASHBOARD = ESPHomeDashboard()

View File

@@ -10,12 +10,14 @@ from esphome import const, util
from esphome.storage_json import StorageJSON, ext_storage_path
from .const import (
DASHBOARD_COMMAND,
EVENT_ENTRY_ADDED,
EVENT_ENTRY_REMOVED,
EVENT_ENTRY_STATE_CHANGED,
EVENT_ENTRY_UPDATED,
)
from .enum import StrEnum
from .util.subprocess import async_run_system_command
if TYPE_CHECKING:
from .core import ESPHomeDashboard
@@ -235,6 +237,14 @@ class DashboardEntries:
)
return path_to_cache_key
def async_schedule_storage_json_update(self, filename: str) -> None:
"""Schedule a task to update the storage JSON file."""
self._dashboard.async_create_background_task(
async_run_system_command(
[*DASHBOARD_COMMAND, "compile", "--only-generate", filename]
)
)
class DashboardEntry:
"""Represents a single dashboard entry.

View File

@@ -9,11 +9,11 @@ import hashlib
import json
import logging
import os
import time
import secrets
import shutil
import subprocess
import threading
import time
from collections.abc import Iterable
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, TypeVar
@@ -40,6 +40,7 @@ from esphome.storage_json import StorageJSON, ext_storage_path, trash_storage_pa
from esphome.util import get_serial_ports, shlex_quote
from esphome.yaml_util import FastestAvailableSafeLoader
from .const import DASHBOARD_COMMAND
from .core import DASHBOARD
from .entries import EntryState, entry_state_to_bool
from .util.file import write_file
@@ -286,9 +287,6 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
raise NotImplementedError
DASHBOARD_COMMAND = ["esphome", "--dashboard"]
class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
"""Base class for commands that require a port."""
@@ -808,12 +806,21 @@ class EditRequestHandler(BaseHandler):
@bind_config
async def get(self, configuration: str | None = None) -> None:
"""Get the content of a file."""
loop = asyncio.get_running_loop()
if not configuration.endswith((".yaml", ".yml")):
self.send_error(404)
return
filename = settings.rel_path(configuration)
if Path(filename).resolve().parent != settings.absolute_config_dir:
self.send_error(404)
return
loop = asyncio.get_running_loop()
content = await loop.run_in_executor(
None, self._read_file, filename, configuration
)
if content is not None:
self.set_header("Content-Type", "application/yaml")
self.write(content)
def _read_file(self, filename: str, configuration: str) -> bytes | None:
@@ -835,15 +842,19 @@ class EditRequestHandler(BaseHandler):
@bind_config
async def post(self, configuration: str | None = None) -> None:
"""Write the content of a file."""
if not configuration.endswith((".yaml", ".yml")):
self.send_error(404)
return
filename = settings.rel_path(configuration)
if Path(filename).resolve().parent != settings.absolute_config_dir:
self.send_error(404)
return
loop = asyncio.get_running_loop()
config_file = settings.rel_path(configuration)
await loop.run_in_executor(
None, self._write_file, config_file, self.request.body
)
await loop.run_in_executor(None, self._write_file, filename, self.request.body)
# Ensure the StorageJSON is updated as well
await async_run_system_command(
[*DASHBOARD_COMMAND, "compile", "--only-generate", config_file]
)
DASHBOARD.entries.async_schedule_storage_json_update(filename)
self.set_status(200)

View File

@@ -110,7 +110,7 @@ class DashboardImportDiscovery:
self, zeroconf: Zeroconf, info: AsyncServiceInfo, service_type: str, name: str
) -> None:
"""Process a service info."""
if await info.async_request(zeroconf):
if await info.async_request(zeroconf, timeout=3000):
self._process_service_info(name, info)
def _process_service_info(self, name: str, info: ServiceInfo) -> None: