mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	Compare commits
	
		
			29 Commits
		
	
	
		
			2024.2.0b2
			...
			2024.2.2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					833affc1bf | ||
| 
						 | 
					e2b197dc2c | ||
| 
						 | 
					f39dc49f49 | ||
| 
						 | 
					b0a25401f7 | ||
| 
						 | 
					37d2b3c797 | ||
| 
						 | 
					63cce916e2 | ||
| 
						 | 
					1aab87b41c | ||
| 
						 | 
					a3fc1acdcb | ||
| 
						 | 
					cc115e7cc9 | ||
| 
						 | 
					badac933ae | ||
| 
						 | 
					b1b8217713 | ||
| 
						 | 
					f3174c58bc | ||
| 
						 | 
					e66e135a63 | ||
| 
						 | 
					d814ed1d4a | ||
| 
						 | 
					84c6e52be2 | ||
| 
						 | 
					2cf6393161 | ||
| 
						 | 
					5a7759f1c4 | ||
| 
						 | 
					db5205931b | ||
| 
						 | 
					62d59cffcc | ||
| 
						 | 
					2e7129e816 | ||
| 
						 | 
					2d22a2d1c2 | ||
| 
						 | 
					c92968da8a | ||
| 
						 | 
					86580d07cb | ||
| 
						 | 
					03ea71034f | ||
| 
						 | 
					7bf676abfa | ||
| 
						 | 
					fb16e6b027 | ||
| 
						 | 
					4eb04afa62 | ||
| 
						 | 
					841a831c63 | ||
| 
						 | 
					ae4af2966a | 
@@ -1449,6 +1449,7 @@ message VoiceAssistantRequest {
 | 
			
		||||
  string conversation_id = 2;
 | 
			
		||||
  uint32 flags = 3;
 | 
			
		||||
  VoiceAssistantAudioSettings audio_settings = 4;
 | 
			
		||||
  string wake_word_phrase = 5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message VoiceAssistantResponse {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,8 @@ CODEOWNERS = ["@ellull"]
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 | 
			
		||||
MULTI_CONF = True
 | 
			
		||||
 | 
			
		||||
CONF_PWM = "pwm"
 | 
			
		||||
CONF_DIVIDER = "divider"
 | 
			
		||||
CONF_DAC = "dac"
 | 
			
		||||
 
 | 
			
		||||
@@ -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]);
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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; }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)>;
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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_{};
 | 
			
		||||
 
 | 
			
		||||
@@ -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],
 | 
			
		||||
    },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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_());
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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_{};
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
"""Constants used by esphome."""
 | 
			
		||||
 | 
			
		||||
__version__ = "2024.2.0b2"
 | 
			
		||||
__version__ = "2024.2.2"
 | 
			
		||||
 | 
			
		||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 | 
			
		||||
VALID_SUBSTITUTIONS_CHARACTERS = (
 | 
			
		||||
 
 | 
			
		||||
@@ -8,3 +8,5 @@ MAX_EXECUTOR_WORKERS = 48
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SENTINEL = object()
 | 
			
		||||
 | 
			
		||||
DASHBOARD_COMMAND = ["esphome", "--dashboard"]
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user