mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 06:33:51 +00:00 
			
		
		
		
	Merge branch 'ble_cleanups' into integration
This commit is contained in:
		| @@ -11,7 +11,7 @@ ci: | ||||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
|     rev: v0.12.12 | ||||
|     rev: v0.13.0 | ||||
|     hooks: | ||||
|       # Run the linter. | ||||
|       - id: ruff | ||||
|   | ||||
| @@ -66,7 +66,7 @@ esphome/components/binary_sensor/* @esphome/core | ||||
| esphome/components/bk72xx/* @kuba2k2 | ||||
| esphome/components/bl0906/* @athom-tech @jesserockz @tarontop | ||||
| esphome/components/bl0939/* @ziceva | ||||
| esphome/components/bl0940/* @tobias- | ||||
| esphome/components/bl0940/* @dan-s-github @tobias- | ||||
| esphome/components/bl0942/* @dbuezas @dwmw2 | ||||
| esphome/components/ble_client/* @buxtronix @clydebarrow | ||||
| esphome/components/bluetooth_proxy/* @bdraco @jesserockz | ||||
| @@ -234,7 +234,7 @@ esphome/components/ina2xx_base/* @latonita | ||||
| esphome/components/ina2xx_i2c/* @latonita | ||||
| esphome/components/ina2xx_spi/* @latonita | ||||
| esphome/components/inkbird_ibsth1_mini/* @fkirill | ||||
| esphome/components/inkplate6/* @jesserockz | ||||
| esphome/components/inkplate/* @jesserockz @JosipKuci | ||||
| esphome/components/integration/* @OttoWinter | ||||
| esphome/components/internal_temperature/* @Mat931 | ||||
| esphome/components/interval/* @esphome/core | ||||
|   | ||||
							
								
								
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							| @@ -48,7 +48,7 @@ PROJECT_NAME           = ESPHome | ||||
| # could be handy for archiving the generated documentation or if some version | ||||
| # control system is used. | ||||
|  | ||||
| PROJECT_NUMBER         = 2025.9.0-dev | ||||
| PROJECT_NUMBER         = 2025.10.0-dev | ||||
|  | ||||
| # Using the PROJECT_BRIEF tag one can provide an optional one line description | ||||
| # for a project that appears at the top of each page and should give viewer a | ||||
|   | ||||
| @@ -1 +1,6 @@ | ||||
| CODEOWNERS = ["@tobias-"] | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| CODEOWNERS = ["@tobias-", "@dan-s-github"] | ||||
|  | ||||
| CONF_BL0940_ID = "bl0940_id" | ||||
| bl0940_ns = cg.esphome_ns.namespace("bl0940") | ||||
|   | ||||
| @@ -7,28 +7,26 @@ namespace bl0940 { | ||||
|  | ||||
| static const char *const TAG = "bl0940"; | ||||
|  | ||||
| static const uint8_t BL0940_READ_COMMAND = 0x50;  // 0x58 according to documentation | ||||
| static const uint8_t BL0940_FULL_PACKET = 0xAA; | ||||
| static const uint8_t BL0940_PACKET_HEADER = 0x55;  // 0x58 according to documentation | ||||
| static const uint8_t BL0940_PACKET_HEADER = 0x55;  // 0x58 according to en doc but 0x55 in cn doc | ||||
|  | ||||
| static const uint8_t BL0940_WRITE_COMMAND = 0xA0;  // 0xA8 according to documentation | ||||
| static const uint8_t BL0940_REG_I_FAST_RMS_CTRL = 0x10; | ||||
| static const uint8_t BL0940_REG_MODE = 0x18; | ||||
| static const uint8_t BL0940_REG_SOFT_RESET = 0x19; | ||||
| static const uint8_t BL0940_REG_USR_WRPROT = 0x1A; | ||||
| static const uint8_t BL0940_REG_TPS_CTRL = 0x1B; | ||||
|  | ||||
| const uint8_t BL0940_INIT[5][6] = { | ||||
| static const uint8_t BL0940_INIT[5][5] = { | ||||
|     // Reset to default | ||||
|     {BL0940_WRITE_COMMAND, BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, | ||||
|     {BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, | ||||
|     // Enable User Operation Write | ||||
|     {BL0940_WRITE_COMMAND, BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, | ||||
|     {BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, | ||||
|     // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS | ||||
|     {BL0940_WRITE_COMMAND, BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37}, | ||||
|     {BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37}, | ||||
|     // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS | ||||
|     {BL0940_WRITE_COMMAND, BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, | ||||
|     {BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, | ||||
|     // 0x181C = Half cycle, Fast RMS threshold 6172 | ||||
|     {BL0940_WRITE_COMMAND, BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; | ||||
|     {BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; | ||||
|  | ||||
| void BL0940::loop() { | ||||
|   DataPacket buffer; | ||||
| @@ -36,8 +34,8 @@ void BL0940::loop() { | ||||
|     return; | ||||
|   } | ||||
|   if (read_array((uint8_t *) &buffer, sizeof(buffer))) { | ||||
|     if (validate_checksum(&buffer)) { | ||||
|       received_package_(&buffer); | ||||
|     if (this->validate_checksum_(&buffer)) { | ||||
|       this->received_package_(&buffer); | ||||
|     } | ||||
|   } else { | ||||
|     ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); | ||||
| @@ -46,35 +44,151 @@ void BL0940::loop() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool BL0940::validate_checksum(const DataPacket *data) { | ||||
|   uint8_t checksum = BL0940_READ_COMMAND; | ||||
| bool BL0940::validate_checksum_(DataPacket *data) { | ||||
|   uint8_t checksum = this->read_command_; | ||||
|   // Whole package but checksum | ||||
|   for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { | ||||
|     checksum += data->raw[i]; | ||||
|   uint8_t *raw = (uint8_t *) data; | ||||
|   for (uint32_t i = 0; i < sizeof(*data) - 1; i++) { | ||||
|     checksum += raw[i]; | ||||
|   } | ||||
|   checksum ^= 0xFF; | ||||
|   if (checksum != data->checksum) { | ||||
|     ESP_LOGW(TAG, "BL0940 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); | ||||
|     ESP_LOGW(TAG, "Invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); | ||||
|   } | ||||
|   return checksum == data->checksum; | ||||
| } | ||||
|  | ||||
| void BL0940::update() { | ||||
|   this->flush(); | ||||
|   this->write_byte(BL0940_READ_COMMAND); | ||||
|   this->write_byte(this->read_command_); | ||||
|   this->write_byte(BL0940_FULL_PACKET); | ||||
| } | ||||
|  | ||||
| void BL0940::setup() { | ||||
| #ifdef USE_NUMBER | ||||
|   // add calibration callbacks | ||||
|   if (this->voltage_calibration_number_ != nullptr) { | ||||
|     this->voltage_calibration_number_->add_on_state_callback( | ||||
|         [this](float state) { this->voltage_calibration_callback_(state); }); | ||||
|     if (this->voltage_calibration_number_->has_state()) { | ||||
|       this->voltage_calibration_callback_(this->voltage_calibration_number_->state); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (this->current_calibration_number_ != nullptr) { | ||||
|     this->current_calibration_number_->add_on_state_callback( | ||||
|         [this](float state) { this->current_calibration_callback_(state); }); | ||||
|     if (this->current_calibration_number_->has_state()) { | ||||
|       this->current_calibration_callback_(this->current_calibration_number_->state); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (this->power_calibration_number_ != nullptr) { | ||||
|     this->power_calibration_number_->add_on_state_callback( | ||||
|         [this](float state) { this->power_calibration_callback_(state); }); | ||||
|     if (this->power_calibration_number_->has_state()) { | ||||
|       this->power_calibration_callback_(this->power_calibration_number_->state); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (this->energy_calibration_number_ != nullptr) { | ||||
|     this->energy_calibration_number_->add_on_state_callback( | ||||
|         [this](float state) { this->energy_calibration_callback_(state); }); | ||||
|     if (this->energy_calibration_number_->has_state()) { | ||||
|       this->energy_calibration_callback_(this->energy_calibration_number_->state); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   // calculate calibrated reference values | ||||
|   this->voltage_reference_cal_ = this->voltage_reference_ / this->voltage_cal_; | ||||
|   this->current_reference_cal_ = this->current_reference_ / this->current_cal_; | ||||
|   this->power_reference_cal_ = this->power_reference_ / this->power_cal_; | ||||
|   this->energy_reference_cal_ = this->energy_reference_ / this->energy_cal_; | ||||
|  | ||||
|   for (auto *i : BL0940_INIT) { | ||||
|     this->write_array(i, 6); | ||||
|     this->write_byte(this->write_command_), this->write_array(i, 5); | ||||
|     delay(1); | ||||
|   } | ||||
|   this->flush(); | ||||
| } | ||||
|  | ||||
| float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { | ||||
|   auto tb = (float) (temperature.h << 8 | temperature.l); | ||||
| float BL0940::calculate_power_reference_() { | ||||
|   // calculate power reference based on voltage and current reference | ||||
|   return this->voltage_reference_cal_ * this->current_reference_cal_ * 4046 / 324004 / 79931; | ||||
| } | ||||
|  | ||||
| float BL0940::calculate_energy_reference_() { | ||||
|   // formula: 3600000 * 4046 * RL * R1 * 1000 / (1638.4 * 256) / Vref² / (R1 + R2) | ||||
|   // or:  power_reference_ * 3600000 / (1638.4 * 256) | ||||
|   return this->power_reference_cal_ * 3600000 / (1638.4 * 256); | ||||
| } | ||||
|  | ||||
| float BL0940::calculate_calibration_value_(float state) { return (100 + state) / 100; } | ||||
|  | ||||
| void BL0940::reset_calibration() { | ||||
| #ifdef USE_NUMBER | ||||
|   if (this->current_calibration_number_ != nullptr && this->current_cal_ != 1) { | ||||
|     this->current_calibration_number_->make_call().set_value(0).perform(); | ||||
|   } | ||||
|   if (this->voltage_calibration_number_ != nullptr && this->voltage_cal_ != 1) { | ||||
|     this->voltage_calibration_number_->make_call().set_value(0).perform(); | ||||
|   } | ||||
|   if (this->power_calibration_number_ != nullptr && this->power_cal_ != 1) { | ||||
|     this->power_calibration_number_->make_call().set_value(0).perform(); | ||||
|   } | ||||
|   if (this->energy_calibration_number_ != nullptr && this->energy_cal_ != 1) { | ||||
|     this->energy_calibration_number_->make_call().set_value(0).perform(); | ||||
|   } | ||||
| #endif | ||||
|   ESP_LOGD(TAG, "external calibration values restored to initial state"); | ||||
| } | ||||
|  | ||||
| void BL0940::current_calibration_callback_(float state) { | ||||
|   this->current_cal_ = this->calculate_calibration_value_(state); | ||||
|   ESP_LOGV(TAG, "update current calibration state: %f", this->current_cal_); | ||||
|   this->recalibrate_(); | ||||
| } | ||||
| void BL0940::voltage_calibration_callback_(float state) { | ||||
|   this->voltage_cal_ = this->calculate_calibration_value_(state); | ||||
|   ESP_LOGV(TAG, "update voltage calibration state: %f", this->voltage_cal_); | ||||
|   this->recalibrate_(); | ||||
| } | ||||
| void BL0940::power_calibration_callback_(float state) { | ||||
|   this->power_cal_ = this->calculate_calibration_value_(state); | ||||
|   ESP_LOGV(TAG, "update power calibration state: %f", this->power_cal_); | ||||
|   this->recalibrate_(); | ||||
| } | ||||
| void BL0940::energy_calibration_callback_(float state) { | ||||
|   this->energy_cal_ = this->calculate_calibration_value_(state); | ||||
|   ESP_LOGV(TAG, "update energy calibration state: %f", this->energy_cal_); | ||||
|   this->recalibrate_(); | ||||
| } | ||||
|  | ||||
| void BL0940::recalibrate_() { | ||||
|   ESP_LOGV(TAG, "Recalibrating reference values"); | ||||
|   this->voltage_reference_cal_ = this->voltage_reference_ / this->voltage_cal_; | ||||
|   this->current_reference_cal_ = this->current_reference_ / this->current_cal_; | ||||
|  | ||||
|   if (this->voltage_cal_ != 1 || this->current_cal_ != 1) { | ||||
|     this->power_reference_ = this->calculate_power_reference_(); | ||||
|   } | ||||
|   this->power_reference_cal_ = this->power_reference_ / this->power_cal_; | ||||
|  | ||||
|   if (this->voltage_cal_ != 1 || this->current_cal_ != 1 || this->power_cal_ != 1) { | ||||
|     this->energy_reference_ = this->calculate_energy_reference_(); | ||||
|   } | ||||
|   this->energy_reference_cal_ = this->energy_reference_ / this->energy_cal_; | ||||
|  | ||||
|   ESP_LOGD(TAG, | ||||
|            "Recalibrated reference values:\n" | ||||
|            "Voltage: %f\n, Current: %f\n, Power: %f\n, Energy: %f\n", | ||||
|            this->voltage_reference_cal_, this->current_reference_cal_, this->power_reference_cal_, | ||||
|            this->energy_reference_cal_); | ||||
| } | ||||
|  | ||||
| float BL0940::update_temp_(sensor::Sensor *sensor, uint16_le_t temperature) const { | ||||
|   auto tb = (float) temperature; | ||||
|   float converted_temp = ((float) 170 / 448) * (tb / 2 - 32) - 45; | ||||
|   if (sensor != nullptr) { | ||||
|     if (sensor->has_state() && std::abs(converted_temp - sensor->get_state()) > max_temperature_diff_) { | ||||
| @@ -87,33 +201,40 @@ float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { | ||||
|   return converted_temp; | ||||
| } | ||||
|  | ||||
| void BL0940::received_package_(const DataPacket *data) const { | ||||
| void BL0940::received_package_(DataPacket *data) { | ||||
|   // Bad header | ||||
|   if (data->frame_header != BL0940_PACKET_HEADER) { | ||||
|     ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; | ||||
|   float i_rms = (float) to_uint32_t(data->i_rms) / current_reference_; | ||||
|   float watt = (float) to_int32_t(data->watt) / power_reference_; | ||||
|   uint32_t cf_cnt = to_uint32_t(data->cf_cnt); | ||||
|   float total_energy_consumption = (float) cf_cnt / energy_reference_; | ||||
|   // cf_cnt is only 24 bits, so track overflows | ||||
|   uint32_t cf_cnt = (uint24_t) data->cf_cnt; | ||||
|   cf_cnt |= this->prev_cf_cnt_ & 0xff000000; | ||||
|   if (cf_cnt < this->prev_cf_cnt_) { | ||||
|     cf_cnt += 0x1000000; | ||||
|   } | ||||
|   this->prev_cf_cnt_ = cf_cnt; | ||||
|  | ||||
|   float tps1 = update_temp_(internal_temperature_sensor_, data->tps1); | ||||
|   float tps2 = update_temp_(external_temperature_sensor_, data->tps2); | ||||
|   float v_rms = (uint24_t) data->v_rms / this->voltage_reference_cal_; | ||||
|   float i_rms = (uint24_t) data->i_rms / this->current_reference_cal_; | ||||
|   float watt = (int24_t) data->watt / this->power_reference_cal_; | ||||
|   float total_energy_consumption = cf_cnt / this->energy_reference_cal_; | ||||
|  | ||||
|   if (voltage_sensor_ != nullptr) { | ||||
|     voltage_sensor_->publish_state(v_rms); | ||||
|   float tps1 = update_temp_(this->internal_temperature_sensor_, data->tps1); | ||||
|   float tps2 = update_temp_(this->external_temperature_sensor_, data->tps2); | ||||
|  | ||||
|   if (this->voltage_sensor_ != nullptr) { | ||||
|     this->voltage_sensor_->publish_state(v_rms); | ||||
|   } | ||||
|   if (current_sensor_ != nullptr) { | ||||
|     current_sensor_->publish_state(i_rms); | ||||
|   if (this->current_sensor_ != nullptr) { | ||||
|     this->current_sensor_->publish_state(i_rms); | ||||
|   } | ||||
|   if (power_sensor_ != nullptr) { | ||||
|     power_sensor_->publish_state(watt); | ||||
|   if (this->power_sensor_ != nullptr) { | ||||
|     this->power_sensor_->publish_state(watt); | ||||
|   } | ||||
|   if (energy_sensor_ != nullptr) { | ||||
|     energy_sensor_->publish_state(total_energy_consumption); | ||||
|   if (this->energy_sensor_ != nullptr) { | ||||
|     this->energy_sensor_->publish_state(total_energy_consumption); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGV(TAG, "BL0940: U %fV, I %fA, P %fW, Cnt %" PRId32 ", ∫P %fkWh, T1 %f°C, T2 %f°C", v_rms, i_rms, watt, cf_cnt, | ||||
| @@ -121,7 +242,27 @@ void BL0940::received_package_(const DataPacket *data) const { | ||||
| } | ||||
|  | ||||
| void BL0940::dump_config() {  // NOLINT(readability-function-cognitive-complexity) | ||||
|   ESP_LOGCONFIG(TAG, "BL0940:"); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "BL0940:\n" | ||||
|                 "  LEGACY MODE: %s\n" | ||||
|                 "  READ  CMD: 0x%02X\n" | ||||
|                 "  WRITE CMD: 0x%02X\n" | ||||
|                 "  ------------------\n" | ||||
|                 "  Current reference: %f\n" | ||||
|                 "  Energy reference: %f\n" | ||||
|                 "  Power reference: %f\n" | ||||
|                 "  Voltage reference: %f\n", | ||||
|                 TRUEFALSE(this->legacy_mode_enabled_), this->read_command_, this->write_command_, | ||||
|                 this->current_reference_, this->energy_reference_, this->power_reference_, this->voltage_reference_); | ||||
| #ifdef USE_NUMBER | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "BL0940:\n" | ||||
|                 "  Current calibration: %f\n" | ||||
|                 "  Energy calibration: %f\n" | ||||
|                 "  Power calibration: %f\n" | ||||
|                 "  Voltage calibration: %f\n", | ||||
|                 this->current_cal_, this->energy_cal_, this->power_cal_, this->voltage_cal_); | ||||
| #endif | ||||
|   LOG_SENSOR("", "Voltage", this->voltage_sensor_); | ||||
|   LOG_SENSOR("", "Current", this->current_sensor_); | ||||
|   LOG_SENSOR("", "Power", this->power_sensor_); | ||||
| @@ -130,9 +271,5 @@ void BL0940::dump_config() {  // NOLINT(readability-function-cognitive-complexit | ||||
|   LOG_SENSOR("", "External temperature", this->external_temperature_sensor_); | ||||
| } | ||||
|  | ||||
| uint32_t BL0940::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } | ||||
|  | ||||
| int32_t BL0940::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } | ||||
|  | ||||
| }  // namespace bl0940 | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -1,66 +1,48 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/uart/uart.h" | ||||
| #include "esphome/core/datatypes.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #ifdef USE_BUTTON | ||||
| #include "esphome/components/button/button.h" | ||||
| #endif | ||||
| #ifdef USE_NUMBER | ||||
| #include "esphome/components/number/number.h" | ||||
| #endif | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/uart/uart.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bl0940 { | ||||
|  | ||||
| static const float BL0940_PREF = 1430; | ||||
| static const float BL0940_UREF = 33000; | ||||
| static const float BL0940_IREF = 275000;  // 2750 from tasmota. Seems to generate values 100 times too high | ||||
|  | ||||
| // Measured to 297J  per click according to power consumption of 5 minutes | ||||
| // Converted to kWh (3.6MJ per kwH). Used to be 256 * 1638.4 | ||||
| static const float BL0940_EREF = 3.6e6 / 297; | ||||
|  | ||||
| struct ube24_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align) | ||||
|   uint8_t l; | ||||
|   uint8_t m; | ||||
|   uint8_t h; | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| struct ube16_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align) | ||||
|   uint8_t l; | ||||
|   uint8_t h; | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| struct sbe24_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align) | ||||
|   uint8_t l; | ||||
|   uint8_t m; | ||||
|   int8_t h; | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| // Caveat: All these values are big endian (low - middle - high) | ||||
|  | ||||
| union DataPacket {  // NOLINT(altera-struct-pack-align) | ||||
|   uint8_t raw[35]; | ||||
|   struct { | ||||
|     uint8_t frame_header;  // value of 0x58 according to docs. 0x55 according to Tasmota real world tests. Reality wins. | ||||
|     ube24_t i_fast_rms;    // 0x00 | ||||
|     ube24_t i_rms;         // 0x04 | ||||
|     ube24_t RESERVED0;     // reserved | ||||
|     ube24_t v_rms;         // 0x06 | ||||
|     ube24_t RESERVED1;     // reserved | ||||
|     sbe24_t watt;          // 0x08 | ||||
|     ube24_t RESERVED2;     // reserved | ||||
|     ube24_t cf_cnt;        // 0x0A | ||||
|     ube24_t RESERVED3;     // reserved | ||||
|     ube16_t tps1;          // 0x0c | ||||
|     uint8_t RESERVED4;     // value of 0x00 | ||||
|     ube16_t tps2;          // 0x0c | ||||
|     uint8_t RESERVED5;     // value of 0x00 | ||||
|     uint8_t checksum;      // checksum | ||||
|   }; | ||||
| struct DataPacket { | ||||
|   uint8_t frame_header;    // Packet header (0x58 in EN docs, 0x55 in CN docs and Tasmota tests) | ||||
|   uint24_le_t i_fast_rms;  // Fast RMS current | ||||
|   uint24_le_t i_rms;       // RMS current | ||||
|   uint24_t RESERVED0;      // Reserved | ||||
|   uint24_le_t v_rms;       // RMS voltage | ||||
|   uint24_t RESERVED1;      // Reserved | ||||
|   int24_le_t watt;         // Active power (can be negative for bidirectional measurement) | ||||
|   uint24_t RESERVED2;      // Reserved | ||||
|   uint24_le_t cf_cnt;      // Energy pulse count | ||||
|   uint24_t RESERVED3;      // Reserved | ||||
|   uint16_le_t tps1;        // Internal temperature sensor 1 | ||||
|   uint8_t RESERVED4;       // Reserved (should be 0x00) | ||||
|   uint16_le_t tps2;        // Internal temperature sensor 2 | ||||
|   uint8_t RESERVED5;       // Reserved (should be 0x00) | ||||
|   uint8_t checksum;        // Packet checksum | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| class BL0940 : public PollingComponent, public uart::UARTDevice { | ||||
|  public: | ||||
|   // Sensor setters | ||||
|   void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } | ||||
|   void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } | ||||
|   void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } | ||||
|   void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } | ||||
|  | ||||
|   // Temperature sensor setters | ||||
|   void set_internal_temperature_sensor(sensor::Sensor *internal_temperature_sensor) { | ||||
|     internal_temperature_sensor_ = internal_temperature_sensor; | ||||
|   } | ||||
| @@ -68,42 +50,105 @@ class BL0940 : public PollingComponent, public uart::UARTDevice { | ||||
|     external_temperature_sensor_ = external_temperature_sensor; | ||||
|   } | ||||
|  | ||||
|   void loop() override; | ||||
|   // Configuration setters | ||||
|   void set_legacy_mode(bool enable) { this->legacy_mode_enabled_ = enable; } | ||||
|   void set_read_command(uint8_t read_command) { this->read_command_ = read_command; } | ||||
|   void set_write_command(uint8_t write_command) { this->write_command_ = write_command; } | ||||
|  | ||||
|   // Reference value setters (used for calibration and conversion) | ||||
|   void set_current_reference(float current_ref) { this->current_reference_ = current_ref; } | ||||
|   void set_energy_reference(float energy_ref) { this->energy_reference_ = energy_ref; } | ||||
|   void set_power_reference(float power_ref) { this->power_reference_ = power_ref; } | ||||
|   void set_voltage_reference(float voltage_ref) { this->voltage_reference_ = voltage_ref; } | ||||
|  | ||||
| #ifdef USE_NUMBER | ||||
|   // Calibration number setters (for Home Assistant number entities) | ||||
|   void set_current_calibration_number(number::Number *num) { this->current_calibration_number_ = num; } | ||||
|   void set_voltage_calibration_number(number::Number *num) { this->voltage_calibration_number_ = num; } | ||||
|   void set_power_calibration_number(number::Number *num) { this->power_calibration_number_ = num; } | ||||
|   void set_energy_calibration_number(number::Number *num) { this->energy_calibration_number_ = num; } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_BUTTON | ||||
|   // Resets all calibration values to defaults (can be triggered by a button) | ||||
|   void reset_calibration(); | ||||
| #endif | ||||
|  | ||||
|   // Core component methods | ||||
|   void loop() override; | ||||
|   void update() override; | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   sensor::Sensor *voltage_sensor_{nullptr}; | ||||
|   sensor::Sensor *current_sensor_{nullptr}; | ||||
|   // NB This may be negative as the circuits is seemingly able to measure | ||||
|   // power in both directions | ||||
|   sensor::Sensor *power_sensor_{nullptr}; | ||||
|   sensor::Sensor *energy_sensor_{nullptr}; | ||||
|   sensor::Sensor *internal_temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *external_temperature_sensor_{nullptr}; | ||||
|   // --- Sensor pointers --- | ||||
|   sensor::Sensor *voltage_sensor_{nullptr};               // Voltage sensor | ||||
|   sensor::Sensor *current_sensor_{nullptr};               // Current sensor | ||||
|   sensor::Sensor *power_sensor_{nullptr};                 // Power sensor (can be negative for bidirectional) | ||||
|   sensor::Sensor *energy_sensor_{nullptr};                // Energy sensor | ||||
|   sensor::Sensor *internal_temperature_sensor_{nullptr};  // Internal temperature sensor | ||||
|   sensor::Sensor *external_temperature_sensor_{nullptr};  // External temperature sensor | ||||
|  | ||||
|   // Max difference between two measurements of the temperature. Used to avoid noise. | ||||
|   float max_temperature_diff_{0}; | ||||
|   // Divide by this to turn into Watt | ||||
|   float power_reference_ = BL0940_PREF; | ||||
|   // Divide by this to turn into Volt | ||||
|   float voltage_reference_ = BL0940_UREF; | ||||
|   // Divide by this to turn into Ampere | ||||
|   float current_reference_ = BL0940_IREF; | ||||
|   // Divide by this to turn into kWh | ||||
|   float energy_reference_ = BL0940_EREF; | ||||
| #ifdef USE_NUMBER | ||||
|   // --- Calibration number entities (for dynamic calibration via HA UI) --- | ||||
|   number::Number *voltage_calibration_number_{nullptr}; | ||||
|   number::Number *current_calibration_number_{nullptr}; | ||||
|   number::Number *power_calibration_number_{nullptr}; | ||||
|   number::Number *energy_calibration_number_{nullptr}; | ||||
| #endif | ||||
|  | ||||
|   float update_temp_(sensor::Sensor *sensor, ube16_t packed_temperature) const; | ||||
|   // --- Internal state --- | ||||
|   uint32_t prev_cf_cnt_ = 0;       // Previous energy pulse count (for energy calculation) | ||||
|   float max_temperature_diff_{0};  // Max allowed temperature difference between two measurements (noise filter) | ||||
|  | ||||
|   static uint32_t to_uint32_t(ube24_t input); | ||||
|   // --- Reference values for conversion --- | ||||
|   float power_reference_;        // Divider for raw power to get Watts | ||||
|   float power_reference_cal_;    // Calibrated power reference | ||||
|   float voltage_reference_;      // Divider for raw voltage to get Volts | ||||
|   float voltage_reference_cal_;  // Calibrated voltage reference | ||||
|   float current_reference_;      // Divider for raw current to get Amperes | ||||
|   float current_reference_cal_;  // Calibrated current reference | ||||
|   float energy_reference_;       // Divider for raw energy to get kWh | ||||
|   float energy_reference_cal_;   // Calibrated energy reference | ||||
|  | ||||
|   static int32_t to_int32_t(sbe24_t input); | ||||
|   // --- Home Assistant calibration values (multipliers, default 1) --- | ||||
|   float current_cal_{1}; | ||||
|   float voltage_cal_{1}; | ||||
|   float power_cal_{1}; | ||||
|   float energy_cal_{1}; | ||||
|  | ||||
|   static bool validate_checksum(const DataPacket *data); | ||||
|   // --- Protocol commands --- | ||||
|   uint8_t read_command_; | ||||
|   uint8_t write_command_; | ||||
|  | ||||
|   void received_package_(const DataPacket *data) const; | ||||
|   // --- Mode flags --- | ||||
|   bool legacy_mode_enabled_ = true; | ||||
|  | ||||
|   // --- Methods --- | ||||
|   // Converts packed temperature value to float and updates the sensor | ||||
|   float update_temp_(sensor::Sensor *sensor, uint16_le_t packed_temperature) const; | ||||
|  | ||||
|   // Validates the checksum of a received data packet | ||||
|   bool validate_checksum_(DataPacket *data); | ||||
|  | ||||
|   // Handles a received data packet | ||||
|   void received_package_(DataPacket *data); | ||||
|  | ||||
|   // Calculates reference values for calibration and conversion | ||||
|   float calculate_energy_reference_(); | ||||
|   float calculate_power_reference_(); | ||||
|   float calculate_calibration_value_(float state); | ||||
|  | ||||
|   // Calibration update callbacks (used with number entities) | ||||
|   void current_calibration_callback_(float state); | ||||
|   void voltage_calibration_callback_(float state); | ||||
|   void power_calibration_callback_(float state); | ||||
|   void energy_calibration_callback_(float state); | ||||
|   void reset_calibration_callback_(); | ||||
|  | ||||
|   // Recalculates all reference values after calibration changes | ||||
|   void recalibrate_(); | ||||
| }; | ||||
|  | ||||
| }  // namespace bl0940 | ||||
| }  // namespace esphome | ||||
|   | ||||
							
								
								
									
										27
									
								
								esphome/components/bl0940/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/bl0940/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import button | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART | ||||
|  | ||||
| from .. import CONF_BL0940_ID, bl0940_ns | ||||
| from ..sensor import BL0940 | ||||
|  | ||||
| CalibrationResetButton = bl0940_ns.class_( | ||||
|     "CalibrationResetButton", button.Button, cg.Component | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     button.button_schema( | ||||
|         CalibrationResetButton, | ||||
|         entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|         icon=ICON_RESTART, | ||||
|     ) | ||||
|     .extend({cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940)}) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await button.new_button(config) | ||||
|     await cg.register_component(var, config) | ||||
|     await cg.register_parented(var, config[CONF_BL0940_ID]) | ||||
| @@ -0,0 +1,20 @@ | ||||
| #include "calibration_reset_button.h" | ||||
| #include "../bl0940.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bl0940 { | ||||
|  | ||||
| static const char *const TAG = "bl0940.button.calibration_reset"; | ||||
|  | ||||
| void CalibrationResetButton::dump_config() { LOG_BUTTON("", "Calibration Reset Button", this); } | ||||
|  | ||||
| void CalibrationResetButton::press_action() { | ||||
|   ESP_LOGI(TAG, "Resetting calibration defaults..."); | ||||
|   this->parent_->reset_calibration(); | ||||
| } | ||||
|  | ||||
| }  // namespace bl0940 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										19
									
								
								esphome/components/bl0940/button/calibration_reset_button.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								esphome/components/bl0940/button/calibration_reset_button.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/button/button.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bl0940 { | ||||
|  | ||||
| class BL0940;  // Forward declaration of BL0940 class | ||||
|  | ||||
| class CalibrationResetButton : public button::Button, public Component, public Parented<BL0940> { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|  | ||||
|   void press_action() override; | ||||
| }; | ||||
|  | ||||
| }  // namespace bl0940 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										94
									
								
								esphome/components/bl0940/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								esphome/components/bl0940/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import number | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_MAX_VALUE, | ||||
|     CONF_MIN_VALUE, | ||||
|     CONF_MODE, | ||||
|     CONF_RESTORE_VALUE, | ||||
|     CONF_STEP, | ||||
|     ENTITY_CATEGORY_CONFIG, | ||||
|     UNIT_PERCENT, | ||||
| ) | ||||
|  | ||||
| from .. import CONF_BL0940_ID, bl0940_ns | ||||
| from ..sensor import BL0940 | ||||
|  | ||||
| # Define calibration types | ||||
| CONF_CURRENT_CALIBRATION = "current_calibration" | ||||
| CONF_VOLTAGE_CALIBRATION = "voltage_calibration" | ||||
| CONF_POWER_CALIBRATION = "power_calibration" | ||||
| CONF_ENERGY_CALIBRATION = "energy_calibration" | ||||
|  | ||||
| BL0940Number = bl0940_ns.class_("BL0940Number") | ||||
|  | ||||
| CalibrationNumber = bl0940_ns.class_( | ||||
|     "CalibrationNumber", number.Number, cg.PollingComponent | ||||
| ) | ||||
|  | ||||
|  | ||||
| def validate_min_max(config): | ||||
|     if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: | ||||
|         raise cv.Invalid("max_value must be greater than min_value") | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CALIBRATION_SCHEMA = cv.All( | ||||
|     number.number_schema( | ||||
|         CalibrationNumber, | ||||
|         entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|         unit_of_measurement=UNIT_PERCENT, | ||||
|     ) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Optional(CONF_MODE, default="BOX"): cv.enum(number.NUMBER_MODES), | ||||
|             cv.Optional(CONF_MAX_VALUE, default=10): cv.All( | ||||
|                 cv.float_, cv.Range(max=50) | ||||
|             ), | ||||
|             cv.Optional(CONF_MIN_VALUE, default=-10): cv.All( | ||||
|                 cv.float_, cv.Range(min=-50) | ||||
|             ), | ||||
|             cv.Optional(CONF_STEP, default=0.1): cv.positive_float, | ||||
|             cv.Optional(CONF_RESTORE_VALUE): cv.boolean, | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.COMPONENT_SCHEMA), | ||||
|     validate_min_max, | ||||
| ) | ||||
|  | ||||
| # Configuration schema for BL0940 numbers | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(BL0940Number), | ||||
|         cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940), | ||||
|         cv.Optional(CONF_CURRENT_CALIBRATION): CALIBRATION_SCHEMA, | ||||
|         cv.Optional(CONF_VOLTAGE_CALIBRATION): CALIBRATION_SCHEMA, | ||||
|         cv.Optional(CONF_POWER_CALIBRATION): CALIBRATION_SCHEMA, | ||||
|         cv.Optional(CONF_ENERGY_CALIBRATION): CALIBRATION_SCHEMA, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     # Get the BL0940 component instance | ||||
|     bl0940 = await cg.get_variable(config[CONF_BL0940_ID]) | ||||
|  | ||||
|     # Process all calibration types | ||||
|     for cal_type, setter_method in [ | ||||
|         (CONF_CURRENT_CALIBRATION, "set_current_calibration_number"), | ||||
|         (CONF_VOLTAGE_CALIBRATION, "set_voltage_calibration_number"), | ||||
|         (CONF_POWER_CALIBRATION, "set_power_calibration_number"), | ||||
|         (CONF_ENERGY_CALIBRATION, "set_energy_calibration_number"), | ||||
|     ]: | ||||
|         if conf := config.get(cal_type): | ||||
|             var = await number.new_number( | ||||
|                 conf, | ||||
|                 min_value=conf.get(CONF_MIN_VALUE), | ||||
|                 max_value=conf.get(CONF_MAX_VALUE), | ||||
|                 step=conf.get(CONF_STEP), | ||||
|             ) | ||||
|             await cg.register_component(var, conf) | ||||
|  | ||||
|             if restore_value := config.get(CONF_RESTORE_VALUE): | ||||
|                 cg.add(var.set_restore_value(restore_value)) | ||||
|             cg.add(getattr(bl0940, setter_method)(var)) | ||||
							
								
								
									
										29
									
								
								esphome/components/bl0940/number/calibration_number.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								esphome/components/bl0940/number/calibration_number.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| #include "calibration_number.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bl0940 { | ||||
|  | ||||
| static const char *const TAG = "bl0940.number"; | ||||
|  | ||||
| void CalibrationNumber::setup() { | ||||
|   float value = 0.0f; | ||||
|   if (this->restore_value_) { | ||||
|     this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash()); | ||||
|     if (!this->pref_.load(&value)) { | ||||
|       value = 0.0f; | ||||
|     } | ||||
|   } | ||||
|   this->publish_state(value); | ||||
| } | ||||
|  | ||||
| void CalibrationNumber::control(float value) { | ||||
|   this->publish_state(value); | ||||
|   if (this->restore_value_) | ||||
|     this->pref_.save(&value); | ||||
| } | ||||
|  | ||||
| void CalibrationNumber::dump_config() { LOG_NUMBER("", "Calibration Number", this); } | ||||
|  | ||||
| }  // namespace bl0940 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										26
									
								
								esphome/components/bl0940/number/calibration_number.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								esphome/components/bl0940/number/calibration_number.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/number/number.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/preferences.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bl0940 { | ||||
|  | ||||
| class CalibrationNumber : public number::Number, public Component { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|   void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } | ||||
|  | ||||
|  protected: | ||||
|   void control(float value) override; | ||||
|   bool restore_value_{true}; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|  | ||||
| }  // namespace bl0940 | ||||
| }  // namespace esphome | ||||
| @@ -8,6 +8,7 @@ from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_INTERNAL_TEMPERATURE, | ||||
|     CONF_POWER, | ||||
|     CONF_REFERENCE_VOLTAGE, | ||||
|     CONF_VOLTAGE, | ||||
|     DEVICE_CLASS_CURRENT, | ||||
|     DEVICE_CLASS_ENERGY, | ||||
| @@ -23,12 +24,133 @@ from esphome.const import ( | ||||
|     UNIT_WATT, | ||||
| ) | ||||
|  | ||||
| from . import bl0940_ns | ||||
|  | ||||
| DEPENDENCIES = ["uart"] | ||||
|  | ||||
|  | ||||
| bl0940_ns = cg.esphome_ns.namespace("bl0940") | ||||
| BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice) | ||||
|  | ||||
| CONF_LEGACY_MODE = "legacy_mode" | ||||
|  | ||||
| CONF_READ_COMMAND = "read_command" | ||||
| CONF_WRITE_COMMAND = "write_command" | ||||
|  | ||||
| CONF_RESISTOR_SHUNT = "resistor_shunt" | ||||
| CONF_RESISTOR_ONE = "resistor_one" | ||||
| CONF_RESISTOR_TWO = "resistor_two" | ||||
|  | ||||
| CONF_CURRENT_REFERENCE = "current_reference" | ||||
| CONF_ENERGY_REFERENCE = "energy_reference" | ||||
| CONF_POWER_REFERENCE = "power_reference" | ||||
| CONF_VOLTAGE_REFERENCE = "voltage_reference" | ||||
|  | ||||
| DEFAULT_BL0940_READ_COMMAND = 0x58 | ||||
| DEFAULT_BL0940_WRITE_COMMAND = 0xA1 | ||||
|  | ||||
| # Values according to BL0940 application note: | ||||
| # https://www.belling.com.cn/media/file_object/bel_product/BL0940/guide/BL0940_APPNote_TSSOP14_V1.04_EN.pdf | ||||
| DEFAULT_BL0940_VREF = 1.218  # Vref = 1.218 | ||||
| DEFAULT_BL0940_RL = 1  # RL = 1 mΩ | ||||
| DEFAULT_BL0940_R1 = 0.51  # R1 = 0.51 kΩ | ||||
| DEFAULT_BL0940_R2 = 1950  # R2 = 5 x 390 kΩ -> 1950 kΩ | ||||
|  | ||||
| # ---------------------------------------------------- | ||||
| # values from initial implementation | ||||
| DEFAULT_BL0940_LEGACY_READ_COMMAND = 0x50 | ||||
| DEFAULT_BL0940_LEGACY_WRITE_COMMAND = 0xA0 | ||||
|  | ||||
| DEFAULT_BL0940_LEGACY_UREF = 33000 | ||||
| DEFAULT_BL0940_LEGACY_IREF = 275000 | ||||
| DEFAULT_BL0940_LEGACY_PREF = 1430 | ||||
| # Measured to 297J  per click according to power consumption of 5 minutes | ||||
| # Converted to kWh (3.6MJ per kwH). Used to be 256 * 1638.4 | ||||
| DEFAULT_BL0940_LEGACY_EREF = 3.6e6 / 297 | ||||
| # ---------------------------------------------------- | ||||
|  | ||||
|  | ||||
| # methods to calculate voltage and current reference values | ||||
| def calculate_voltage_reference(vref, r_one, r_two): | ||||
|     # formula: 79931 / Vref * (R1 * 1000) / (R1 + R2) | ||||
|     return 79931 / vref * (r_one * 1000) / (r_one + r_two) | ||||
|  | ||||
|  | ||||
| def calculate_current_reference(vref, r_shunt): | ||||
|     # formula: 324004 * RL / Vref | ||||
|     return 324004 * r_shunt / vref | ||||
|  | ||||
|  | ||||
| def calculate_power_reference(voltage_reference, current_reference): | ||||
|     # calculate power reference based on voltage and current reference | ||||
|     return voltage_reference * current_reference * 4046 / 324004 / 79931 | ||||
|  | ||||
|  | ||||
| def calculate_energy_reference(power_reference): | ||||
|     # formula: power_reference * 3600000 / (1638.4 * 256) | ||||
|     return power_reference * 3600000 / (1638.4 * 256) | ||||
|  | ||||
|  | ||||
| def validate_legacy_mode(config): | ||||
|     # Only allow schematic calibration options if legacy_mode is False | ||||
|     if config.get(CONF_LEGACY_MODE, True): | ||||
|         forbidden = [ | ||||
|             CONF_REFERENCE_VOLTAGE, | ||||
|             CONF_RESISTOR_SHUNT, | ||||
|             CONF_RESISTOR_ONE, | ||||
|             CONF_RESISTOR_TWO, | ||||
|         ] | ||||
|         for key in forbidden: | ||||
|             if key in config: | ||||
|                 raise cv.Invalid( | ||||
|                     f"Option '{key}' is only allowed when legacy_mode: false" | ||||
|                 ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| def set_command_defaults(config): | ||||
|     # Set defaults for read_command and write_command based on legacy_mode | ||||
|     legacy = config.get(CONF_LEGACY_MODE, True) | ||||
|     if legacy: | ||||
|         config.setdefault(CONF_READ_COMMAND, DEFAULT_BL0940_LEGACY_READ_COMMAND) | ||||
|         config.setdefault(CONF_WRITE_COMMAND, DEFAULT_BL0940_LEGACY_WRITE_COMMAND) | ||||
|     else: | ||||
|         config.setdefault(CONF_READ_COMMAND, DEFAULT_BL0940_READ_COMMAND) | ||||
|         config.setdefault(CONF_WRITE_COMMAND, DEFAULT_BL0940_WRITE_COMMAND) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| def set_reference_values(config): | ||||
|     # Set default reference values based on legacy_mode | ||||
|     if config.get(CONF_LEGACY_MODE, True): | ||||
|         config.setdefault(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_LEGACY_UREF) | ||||
|         config.setdefault(CONF_CURRENT_REFERENCE, DEFAULT_BL0940_LEGACY_IREF) | ||||
|         config.setdefault(CONF_POWER_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) | ||||
|         config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) | ||||
|     else: | ||||
|         vref = config.get(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_VREF) | ||||
|         r_one = config.get(CONF_RESISTOR_ONE, DEFAULT_BL0940_R1) | ||||
|         r_two = config.get(CONF_RESISTOR_TWO, DEFAULT_BL0940_R2) | ||||
|         r_shunt = config.get(CONF_RESISTOR_SHUNT, DEFAULT_BL0940_RL) | ||||
|  | ||||
|         config.setdefault( | ||||
|             CONF_VOLTAGE_REFERENCE, calculate_voltage_reference(vref, r_one, r_two) | ||||
|         ) | ||||
|         config.setdefault( | ||||
|             CONF_CURRENT_REFERENCE, calculate_current_reference(vref, r_shunt) | ||||
|         ) | ||||
|         config.setdefault( | ||||
|             CONF_POWER_REFERENCE, | ||||
|             calculate_power_reference( | ||||
|                 config.get(CONF_VOLTAGE_REFERENCE), config.get(CONF_CURRENT_REFERENCE) | ||||
|             ), | ||||
|         ) | ||||
|         config.setdefault( | ||||
|             CONF_ENERGY_REFERENCE, | ||||
|             calculate_energy_reference(config.get(CONF_POWER_REFERENCE)), | ||||
|         ) | ||||
|  | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
| @@ -69,10 +191,24 @@ CONFIG_SCHEMA = ( | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_LEGACY_MODE, default=True): cv.boolean, | ||||
|             cv.Optional(CONF_READ_COMMAND): cv.hex_uint8_t, | ||||
|             cv.Optional(CONF_WRITE_COMMAND): cv.hex_uint8_t, | ||||
|             cv.Optional(CONF_REFERENCE_VOLTAGE): cv.float_, | ||||
|             cv.Optional(CONF_RESISTOR_SHUNT): cv.float_, | ||||
|             cv.Optional(CONF_RESISTOR_ONE): cv.float_, | ||||
|             cv.Optional(CONF_RESISTOR_TWO): cv.float_, | ||||
|             cv.Optional(CONF_CURRENT_REFERENCE): cv.float_, | ||||
|             cv.Optional(CONF_ENERGY_REFERENCE): cv.float_, | ||||
|             cv.Optional(CONF_POWER_REFERENCE): cv.float_, | ||||
|             cv.Optional(CONF_VOLTAGE_REFERENCE): cv.float_, | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(uart.UART_DEVICE_SCHEMA) | ||||
|     .add_extra(validate_legacy_mode) | ||||
|     .add_extra(set_command_defaults) | ||||
|     .add_extra(set_reference_values) | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -99,3 +235,16 @@ async def to_code(config): | ||||
|     if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): | ||||
|         sens = await sensor.new_sensor(external_temperature_config) | ||||
|         cg.add(var.set_external_temperature_sensor(sens)) | ||||
|  | ||||
|     # enable legacy mode | ||||
|     cg.add(var.set_legacy_mode(config.get(CONF_LEGACY_MODE))) | ||||
|  | ||||
|     # Set bl0940 commands after validator has determined which defaults to use if not set | ||||
|     cg.add(var.set_read_command(config.get(CONF_READ_COMMAND))) | ||||
|     cg.add(var.set_write_command(config.get(CONF_WRITE_COMMAND))) | ||||
|  | ||||
|     # Set reference values after validator has set the values either from defaults or calculated | ||||
|     cg.add(var.set_current_reference(config.get(CONF_CURRENT_REFERENCE))) | ||||
|     cg.add(var.set_voltage_reference(config.get(CONF_VOLTAGE_REFERENCE))) | ||||
|     cg.add(var.set_power_reference(config.get(CONF_POWER_REFERENCE))) | ||||
|     cg.add(var.set_energy_reference(config.get(CONF_ENERGY_REFERENCE))) | ||||
|   | ||||
| @@ -7,83 +7,83 @@ namespace esphome { | ||||
| namespace captive_portal { | ||||
|  | ||||
| const uint8_t INDEX_GZ[] PROGMEM = { | ||||
|     0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x95, 0x56, 0xeb, 0x6f, 0xd4, 0x38, 0x10, 0xff, 0xce, | ||||
|     0x5f, 0xe1, 0x33, 0x8f, 0x26, 0xd0, 0x3c, 0xb7, 0xdb, 0x96, 0x6c, 0x12, 0x04, 0xdc, 0x21, 0x90, 0x28, 0x20, 0xb5, | ||||
|     0x70, 0x1f, 0x10, 0x52, 0xbd, 0xc9, 0x64, 0x63, 0x9a, 0x38, 0x39, 0xdb, 0xfb, 0x62, 0xb5, 0xf7, 0xb7, 0xdf, 0x38, | ||||
|     0xc9, 0x6e, 0xb7, 0x15, 0x9c, 0xee, 0x5a, 0x35, 0x1d, 0xdb, 0xf3, 0xf8, 0xcd, 0x78, 0x1e, 0x8e, 0x7f, 0xcb, 0x9b, | ||||
|     0x4c, 0xaf, 0x5b, 0x20, 0xa5, 0xae, 0xab, 0x34, 0x36, 0x5f, 0x52, 0x31, 0x31, 0x4b, 0x40, 0xe0, 0x0a, 0x58, 0x9e, | ||||
|     0xc6, 0x35, 0x68, 0x46, 0xb2, 0x92, 0x49, 0x05, 0x3a, 0xf9, 0x7c, 0xf5, 0xc6, 0x39, 0x4f, 0xe3, 0x8a, 0x8b, 0x1b, | ||||
|     0x22, 0xa1, 0x4a, 0x78, 0xd6, 0x08, 0x52, 0x4a, 0x28, 0x92, 0x9c, 0x69, 0x16, 0xf1, 0x9a, 0xcd, 0x60, 0x10, 0x11, | ||||
|     0xac, 0x86, 0x64, 0xc1, 0x61, 0xd9, 0x36, 0x52, 0x13, 0xe4, 0xd3, 0x20, 0x74, 0x42, 0x97, 0x3c, 0xd7, 0x65, 0x92, | ||||
|     0xc3, 0x82, 0x67, 0xe0, 0x74, 0x8b, 0x63, 0x2e, 0xb8, 0xe6, 0xac, 0x72, 0x54, 0xc6, 0x2a, 0x48, 0x82, 0xe3, 0xb9, | ||||
|     0x02, 0xd9, 0x2d, 0xd8, 0x14, 0xd7, 0xa2, 0xa1, 0x69, 0xac, 0x32, 0xc9, 0x5b, 0x4d, 0x0c, 0xd4, 0xa4, 0x6e, 0xf2, | ||||
|     0x79, 0x05, 0xa9, 0xe7, 0x31, 0x85, 0x90, 0x94, 0xc7, 0x45, 0x0e, 0x2b, 0x77, 0xea, 0x67, 0x99, 0x0f, 0xe7, 0xe7, | ||||
|     0xee, 0x77, 0xf5, 0x00, 0x9d, 0x9a, 0xd7, 0x68, 0xcd, 0xad, 0x9a, 0x8c, 0x69, 0xde, 0x08, 0x57, 0x01, 0x93, 0x59, | ||||
|     0x99, 0x24, 0x09, 0x7d, 0xa1, 0xd8, 0x02, 0xe8, 0x93, 0x27, 0xd6, 0x9e, 0x69, 0x06, 0xfa, 0x8f, 0x0a, 0x0c, 0xa9, | ||||
|     0x5e, 0xad, 0xaf, 0xd8, 0xec, 0x03, 0x02, 0xb7, 0x28, 0x53, 0x3c, 0x07, 0x6a, 0x7f, 0xf5, 0xbf, 0xb9, 0x4a, 0xaf, | ||||
|     0x2b, 0x70, 0x73, 0xae, 0xda, 0x8a, 0xad, 0x13, 0x3a, 0x45, 0xad, 0x37, 0xd4, 0x9e, 0x14, 0x73, 0x91, 0x19, 0xe5, | ||||
|     0x44, 0x59, 0x60, 0x6f, 0x2a, 0x40, 0x78, 0xc9, 0x05, 0xd3, 0xa5, 0x5b, 0xb3, 0x95, 0xd5, 0x13, 0x5c, 0x58, 0xe1, | ||||
|     0x53, 0x0b, 0x9e, 0x05, 0xbe, 0x6f, 0x1f, 0x77, 0x1f, 0xdf, 0xf6, 0xf0, 0xff, 0x44, 0x82, 0x9e, 0x4b, 0x41, 0x98, | ||||
|     0x75, 0x1d, 0xb7, 0xc8, 0x49, 0xf2, 0x84, 0x5e, 0x04, 0x21, 0x09, 0x9e, 0xbb, 0xe1, 0xf8, 0xbd, 0x7b, 0x46, 0x4e, | ||||
|     0xf0, 0x7f, 0x76, 0xe6, 0x8c, 0x49, 0x70, 0x82, 0x9f, 0x30, 0x74, 0xc7, 0xc4, 0xff, 0x41, 0x49, 0xc1, 0xab, 0x2a, | ||||
|     0xa1, 0xa2, 0x11, 0x40, 0x89, 0xd2, 0xb2, 0xb9, 0x81, 0x84, 0x66, 0x73, 0x29, 0x11, 0xfb, 0xeb, 0xa6, 0x6a, 0x24, | ||||
|     0xf5, 0xd2, 0x07, 0xff, 0x4b, 0xa1, 0x96, 0x4c, 0xa8, 0xa2, 0x91, 0x75, 0x42, 0xbb, 0xe8, 0x5b, 0x8f, 0x36, 0x7a, | ||||
|     0x4b, 0xcc, 0xc7, 0x3e, 0x38, 0x74, 0x1a, 0xc9, 0x67, 0x5c, 0x24, 0xd4, 0x68, 0x3c, 0x47, 0x23, 0xd7, 0xf6, 0x76, | ||||
|     0xef, 0x3d, 0x33, 0xde, 0x0f, 0xfe, 0x34, 0xd6, 0xd7, 0xeb, 0x58, 0x2d, 0x66, 0x64, 0x55, 0x57, 0x42, 0x25, 0xb4, | ||||
|     0xd4, 0xba, 0x8d, 0x3c, 0x6f, 0xb9, 0x5c, 0xba, 0xcb, 0x91, 0xdb, 0xc8, 0x99, 0x17, 0xfa, 0xbe, 0xef, 0x21, 0x07, | ||||
|     0x25, 0x7d, 0x22, 0xd0, 0xf0, 0x84, 0x92, 0x12, 0xf8, 0xac, 0xd4, 0x1d, 0x9d, 0x3e, 0xda, 0xc0, 0x36, 0x36, 0x1c, | ||||
|     0xe9, 0xf5, 0xb7, 0x03, 0x2b, 0xfc, 0xc0, 0x0a, 0xbc, 0x60, 0x16, 0xdd, 0xb9, 0x79, 0xd4, 0xb9, 0x79, 0xc6, 0x42, | ||||
|     0x12, 0x12, 0xbf, 0xfb, 0x0d, 0x1d, 0x43, 0x0f, 0x2b, 0xe7, 0xde, 0x8a, 0x1c, 0xac, 0x0c, 0x55, 0x9f, 0x3a, 0xcf, | ||||
|     0xf7, 0xb2, 0x81, 0xd9, 0x59, 0x04, 0xfe, 0xed, 0x86, 0x11, 0x78, 0x7b, 0x7a, 0xb8, 0x76, 0xc2, 0x2f, 0x87, 0x0c, | ||||
|     0xc6, 0x5a, 0x19, 0x7c, 0x39, 0x65, 0x63, 0x32, 0x1e, 0x76, 0xc6, 0x8e, 0xa1, 0xf7, 0x2b, 0x32, 0x5e, 0x20, 0x47, | ||||
|     0xed, 0x9c, 0x3a, 0x63, 0x36, 0x22, 0xa3, 0x01, 0x08, 0x52, 0xb8, 0x7d, 0x8a, 0x82, 0x07, 0x7b, 0xce, 0xe8, 0xc7, | ||||
|     0x91, 0x97, 0x52, 0x3b, 0xa2, 0xf4, 0xd6, 0xf3, 0xe6, 0xd0, 0x73, 0xf7, 0x7b, 0x83, 0x39, 0x45, 0x29, 0x46, 0x06, | ||||
|     0x74, 0x56, 0x5a, 0xd4, 0xc3, 0xc2, 0x2a, 0xf8, 0x0c, 0xb3, 0xbe, 0x11, 0xd4, 0x76, 0x75, 0x09, 0xc2, 0xda, 0x89, | ||||
|     0x1a, 0x41, 0xe8, 0x4e, 0xac, 0xfb, 0x27, 0xda, 0xde, 0xec, 0xf3, 0x5f, 0x73, 0x8d, 0x65, 0xa6, 0x5d, 0x53, 0xb0, | ||||
|     0xc7, 0xfb, 0xdd, 0x69, 0x93, 0xaf, 0x7f, 0x51, 0x1a, 0x65, 0xd0, 0xd7, 0x05, 0x17, 0x02, 0xe4, 0x15, 0xac, 0xf0, | ||||
|     0xe6, 0x2e, 0x5e, 0xbe, 0x26, 0x2f, 0xf3, 0x5c, 0x82, 0x52, 0x11, 0xa1, 0xcf, 0x34, 0xd6, 0x40, 0xf6, 0xdf, 0x75, | ||||
|     0x05, 0x77, 0x74, 0xfd, 0xc9, 0xdf, 0x70, 0xf2, 0x01, 0xf4, 0xb2, 0x91, 0x37, 0x83, 0x36, 0x03, 0x6d, 0x62, 0x2a, | ||||
|     0x4c, 0x22, 0x4e, 0xd6, 0x2a, 0x57, 0x55, 0xd8, 0x3e, 0xac, 0xc0, 0x46, 0x3b, 0xed, 0xad, 0x57, 0x62, 0x17, 0xa8, | ||||
|     0xeb, 0x38, 0xe7, 0x0b, 0x92, 0x55, 0xd8, 0x21, 0xb0, 0x5c, 0x7a, 0x55, 0x94, 0x3c, 0x20, 0xdd, 0x4f, 0x23, 0x32, | ||||
|     0x94, 0xbe, 0x49, 0xe8, 0x4f, 0x3a, 0xc0, 0xab, 0xf5, 0xbb, 0xdc, 0x3a, 0x52, 0x58, 0xfb, 0x47, 0xb6, 0xbb, 0x60, | ||||
|     0xd5, 0x1c, 0x48, 0x42, 0x74, 0xc9, 0xd5, 0x2d, 0xc0, 0xc9, 0x2f, 0xc5, 0x5a, 0x75, 0x83, 0x52, 0x05, 0x1e, 0x2b, | ||||
|     0xcb, 0xa6, 0xe9, 0x60, 0x2e, 0x66, 0x7d, 0x83, 0xa4, 0x0f, 0xe9, 0x3d, 0x44, 0x4e, 0x05, 0x85, 0xde, 0xf3, 0x11, | ||||
|     0x2c, 0x3b, 0x65, 0x09, 0x57, 0xa2, 0x75, 0x7b, 0xbb, 0xdf, 0x8c, 0x55, 0xcb, 0xc4, 0x7d, 0x41, 0x03, 0xd0, 0x94, | ||||
|     0x0a, 0x36, 0x36, 0xa4, 0x4c, 0xbd, 0x20, 0xd3, 0xde, 0xa0, 0xc7, 0x76, 0xe4, 0xa3, 0x0d, 0x47, 0x8d, 0xa6, 0x5f, | ||||
|     0xed, 0x35, 0xc6, 0x1e, 0x86, 0x26, 0xbd, 0xde, 0xda, 0xb7, 0x7e, 0xfc, 0x35, 0x07, 0xb9, 0xbe, 0x84, 0x0a, 0x32, | ||||
|     0xdd, 0x48, 0x8b, 0x3e, 0x44, 0x2b, 0x98, 0x4a, 0x9d, 0xc3, 0x6f, 0xaf, 0x2e, 0xde, 0x27, 0x8d, 0x25, 0xed, 0xe3, | ||||
|     0x5f, 0x71, 0x9b, 0x51, 0xf0, 0x15, 0x47, 0xc1, 0xdf, 0xc9, 0x91, 0x19, 0x06, 0x47, 0xdf, 0x50, 0xb4, 0xf3, 0xf7, | ||||
|     0xfa, 0x76, 0x22, 0x98, 0x72, 0x7e, 0x86, 0x2d, 0xe1, 0xd8, 0x78, 0xe8, 0x9c, 0x8e, 0xed, 0x2d, 0xda, 0x47, 0x04, | ||||
|     0x88, 0xbb, 0xeb, 0xeb, 0xd8, 0xdf, 0x4d, 0x8b, 0x4d, 0x9f, 0x6e, 0xa6, 0xcd, 0xca, 0x51, 0xfc, 0x07, 0x17, 0xb3, | ||||
|     0x88, 0x8b, 0x12, 0x24, 0xd7, 0x5b, 0x84, 0x8b, 0x13, 0xa2, 0x9d, 0xeb, 0x4d, 0xcb, 0xf2, 0xdc, 0x9c, 0x8c, 0xdb, | ||||
|     0xd5, 0xa4, 0xc0, 0x79, 0x62, 0x38, 0x21, 0x0a, 0xa0, 0xde, 0xf6, 0xe7, 0x5d, 0x47, 0x89, 0x9e, 0x8f, 0x1f, 0x6f, | ||||
|     0x4d, 0xc2, 0x6d, 0x34, 0x5e, 0x96, 0xc3, 0x2a, 0x3e, 0x13, 0x51, 0x86, 0xc0, 0x41, 0xf6, 0x42, 0x05, 0xab, 0x79, | ||||
|     0xb5, 0x8e, 0x14, 0xf6, 0x36, 0x07, 0x07, 0x0d, 0x2f, 0xb6, 0xd3, 0xb9, 0xd6, 0x8d, 0x40, 0xdb, 0x32, 0x07, 0x19, | ||||
|     0xf9, 0x93, 0x9e, 0x70, 0x24, 0xcb, 0xf9, 0x5c, 0x45, 0xee, 0x48, 0x42, 0x3d, 0x99, 0xb2, 0xec, 0x66, 0x26, 0x9b, | ||||
|     0xb9, 0xc8, 0x9d, 0xcc, 0x74, 0xda, 0xe8, 0x61, 0x50, 0xb0, 0x11, 0x64, 0x93, 0x61, 0x55, 0x14, 0xc5, 0x04, 0x43, | ||||
|     0x01, 0x4e, 0xdf, 0xcb, 0xa2, 0xd0, 0x3d, 0x31, 0x62, 0x07, 0x30, 0xdd, 0xd0, 0x6c, 0xf4, 0x18, 0x71, 0x04, 0x3c, | ||||
|     0x9e, 0xec, 0xdc, 0xf1, 0x27, 0xd8, 0xc2, 0x15, 0x2a, 0x69, 0xb1, 0xb6, 0x11, 0xe6, 0xb6, 0x66, 0x5c, 0x1c, 0xa2, | ||||
|     0x37, 0x69, 0x32, 0x19, 0xc6, 0x0f, 0x86, 0xa5, 0x33, 0xd3, 0x0d, 0xa1, 0x09, 0x0e, 0x98, 0x7e, 0x86, 0x46, 0xe1, | ||||
|     0xa9, 0xdf, 0xae, 0xb6, 0xee, 0x90, 0x20, 0x9b, 0x1d, 0x77, 0x51, 0xc1, 0x6a, 0xf2, 0x7d, 0xae, 0x34, 0x2f, 0xd6, | ||||
|     0xce, 0x30, 0x83, 0x23, 0x4c, 0x16, 0x9c, 0xbd, 0x53, 0x64, 0x05, 0x10, 0x93, 0xce, 0x86, 0xc3, 0x35, 0xd4, 0x6a, | ||||
|     0x88, 0xd3, 0x5e, 0x4d, 0x97, 0xa0, 0x77, 0x75, 0xfd, 0x1b, 0xb7, 0xc9, 0xc5, 0x4d, 0xcd, 0x24, 0x8e, 0x0a, 0x67, | ||||
|     0xda, 0x60, 0x4c, 0xeb, 0xc8, 0x39, 0xc3, 0xbb, 0x1a, 0xb6, 0x8c, 0x32, 0xf4, 0x1c, 0x61, 0x76, 0xb3, 0x75, 0x17, | ||||
|     0xef, 0xa0, 0x5d, 0x11, 0xd5, 0x54, 0x3c, 0x1f, 0xf8, 0x3a, 0x16, 0xe2, 0xef, 0xc3, 0x13, 0xe0, 0x75, 0x13, 0xb3, | ||||
|     0xb7, 0x0b, 0xf5, 0x49, 0x71, 0xce, 0x02, 0xff, 0x27, 0x37, 0x92, 0x17, 0x45, 0x38, 0x2d, 0xf6, 0x91, 0x32, 0x63, | ||||
|     0xd2, 0x94, 0x46, 0x97, 0x5a, 0xb1, 0xd7, 0xbf, 0x66, 0x4c, 0x66, 0xe0, 0x03, 0x05, 0x23, 0x8c, 0xef, 0x9b, 0x80, | ||||
|     0xf0, 0x3c, 0xc1, 0x4e, 0x95, 0x1e, 0xb4, 0x2f, 0x64, 0x0c, 0x76, 0x47, 0x48, 0xdd, 0x69, 0x46, 0xfd, 0x59, 0x87, | ||||
|     0x3e, 0x7d, 0xdd, 0x60, 0x7d, 0x60, 0xdb, 0x11, 0x33, 0xa2, 0x1b, 0x32, 0x84, 0xc0, 0x75, 0xdd, 0x78, 0x2a, 0xd3, | ||||
|     0x4f, 0x15, 0x30, 0x05, 0x64, 0xc9, 0xb8, 0x76, 0xb1, 0x1a, 0x3b, 0xfe, 0xbe, 0x8e, 0x51, 0x29, 0xb2, 0xa6, 0x43, | ||||
|     0xc1, 0xc6, 0xe5, 0xa8, 0x37, 0x70, 0x09, 0xda, 0x68, 0x32, 0x06, 0x46, 0x69, 0x6c, 0x46, 0x2e, 0x61, 0x5d, 0x4b, | ||||
|     0x4b, 0xbc, 0x25, 0x2f, 0xb8, 0x79, 0xb2, 0xa4, 0x71, 0x97, 0xe4, 0x46, 0x83, 0x89, 0x73, 0xff, 0xbc, 0xea, 0xa8, | ||||
|     0x0a, 0xc4, 0x0c, 0x27, 0xe9, 0x28, 0x24, 0xe8, 0x76, 0x06, 0x65, 0x53, 0x61, 0x58, 0x93, 0xcb, 0xcb, 0x77, 0xbf, | ||||
|     0xa7, 0x06, 0xcc, 0xad, 0x1c, 0xf6, 0xa7, 0x5e, 0xcc, 0x10, 0x83, 0xd4, 0xe9, 0x49, 0xff, 0xa8, 0x6a, 0xb1, 0xbf, | ||||
|     0xa0, 0x07, 0xf9, 0x1d, 0x1d, 0x9f, 0x86, 0xcd, 0x5e, 0x4f, 0xf7, 0xd7, 0x95, 0x4a, 0x7a, 0x89, 0x80, 0x62, 0x6f, | ||||
|     0x58, 0xc4, 0x9e, 0x01, 0xdc, 0x9f, 0x97, 0x03, 0x1f, 0xc6, 0xe9, 0xe3, 0xd5, 0x4b, 0xf2, 0xb9, 0xc5, 0x26, 0x00, | ||||
|     0x7d, 0xd8, 0x3a, 0xaf, 0xf0, 0x65, 0x58, 0x36, 0x79, 0xf2, 0xe9, 0xe3, 0xe5, 0xd5, 0xde, 0xc3, 0x79, 0xc7, 0x44, | ||||
|     0x40, 0x64, 0xfd, 0xf3, 0x6e, 0x5e, 0x69, 0xde, 0x32, 0xa9, 0x3b, 0xb5, 0x8e, 0xe9, 0x22, 0x3b, 0x1f, 0xba, 0x73, | ||||
|     0x7c, 0x03, 0x41, 0xef, 0x46, 0x2f, 0x98, 0x92, 0x1d, 0xaa, 0x9d, 0xb5, 0x7b, 0xb8, 0xbc, 0xfe, 0xb6, 0xbd, 0xfe, | ||||
|     0xea, 0xbd, 0xee, 0xa5, 0xfb, 0x0f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; | ||||
|     0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, | ||||
|     0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, | ||||
|     0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, | ||||
|     0x7e, 0xa0, 0x24, 0x7b, 0x9d, 0x45, 0x73, 0xb8, 0xb3, 0x60, 0x61, 0x38, 0xef, 0x19, 0xcd, 0x83, 0xc5, 0xdf, 0x2a, | ||||
|     0xc5, 0xec, 0xbe, 0x03, 0xd4, 0xd8, 0x56, 0x94, 0x85, 0x7b, 0x23, 0x41, 0xe5, 0x8a, 0x80, 0x2c, 0x8b, 0x06, 0x68, | ||||
|     0x55, 0x16, 0x2d, 0x58, 0x8a, 0x58, 0x43, 0xb5, 0x01, 0x4b, 0xfe, 0xbc, 0xff, 0x39, 0xb8, 0x2d, 0x0b, 0xc1, 0xe5, | ||||
|     0x1a, 0x69, 0x10, 0x84, 0x33, 0x25, 0x51, 0xa3, 0xa1, 0x26, 0x15, 0xb5, 0x34, 0xe3, 0x2d, 0x5d, 0xc1, 0x24, 0x22, | ||||
|     0x69, 0x0b, 0x64, 0xc3, 0x61, 0xdb, 0x29, 0x6d, 0x11, 0x53, 0xd2, 0x82, 0xb4, 0x04, 0x6f, 0x79, 0x65, 0x1b, 0x52, | ||||
|     0xc1, 0x86, 0x33, 0x08, 0x86, 0xc3, 0x35, 0x97, 0xdc, 0x72, 0x2a, 0x02, 0xc3, 0xa8, 0x00, 0x92, 0x5c, 0xf7, 0x06, | ||||
|     0xf4, 0x70, 0xa0, 0x4b, 0x01, 0x44, 0x2a, 0x5c, 0x16, 0x86, 0x69, 0xde, 0x59, 0xe4, 0x5c, 0x25, 0xad, 0xaa, 0x7a, | ||||
|     0x01, 0x65, 0x14, 0x51, 0x63, 0xc0, 0x9a, 0x88, 0xcb, 0x0a, 0x76, 0xe1, 0x32, 0x66, 0x2c, 0x86, 0xdb, 0xdb, 0xf0, | ||||
|     0xb3, 0x79, 0x56, 0x29, 0xd6, 0xb7, 0x20, 0x6d, 0x28, 0x14, 0xa3, 0x96, 0x2b, 0x19, 0x1a, 0xa0, 0x9a, 0x35, 0x84, | ||||
|     0x10, 0xfc, 0xa3, 0xa1, 0x1b, 0xc0, 0xdf, 0x7f, 0xef, 0x9d, 0x99, 0x56, 0x60, 0xff, 0x21, 0xc0, 0x81, 0xe6, 0xa7, | ||||
|     0xfd, 0x3d, 0x5d, 0xfd, 0x4e, 0x5b, 0xf0, 0x30, 0x35, 0xbc, 0x02, 0xec, 0x7f, 0x8c, 0x3f, 0x85, 0xc6, 0xee, 0x05, | ||||
|     0x84, 0x15, 0x37, 0x9d, 0xa0, 0x7b, 0x82, 0x97, 0x42, 0xb1, 0x35, 0xf6, 0xf3, 0xba, 0x97, 0xcc, 0x29, 0x47, 0xc6, | ||||
|     0x03, 0xff, 0x20, 0xc0, 0x22, 0x4b, 0xde, 0x51, 0xdb, 0x84, 0x2d, 0xdd, 0x79, 0x23, 0xc0, 0xa5, 0x97, 0xfe, 0xe0, | ||||
|     0xc1, 0xcb, 0x24, 0x8e, 0xfd, 0xeb, 0xe1, 0x15, 0xfb, 0x51, 0x12, 0xc7, 0xb9, 0x06, 0xdb, 0x6b, 0x89, 0xa8, 0xf7, | ||||
|     0x50, 0x74, 0xd4, 0x36, 0xa8, 0x22, 0xf8, 0x5d, 0x92, 0xa2, 0xe4, 0x75, 0x98, 0xce, 0x7f, 0x0b, 0x5f, 0xa1, 0x9b, | ||||
|     0x30, 0x9d, 0xb3, 0x57, 0xc1, 0x1c, 0x25, 0x37, 0xc1, 0x1c, 0xa5, 0x69, 0x38, 0x47, 0xf1, 0x17, 0x8c, 0x6a, 0x2e, | ||||
|     0x04, 0xc1, 0x52, 0x49, 0xc0, 0xc8, 0x58, 0xad, 0xd6, 0x40, 0x30, 0xeb, 0xb5, 0x06, 0x69, 0xdf, 0x2a, 0xa1, 0x34, | ||||
|     0x8e, 0xca, 0x67, 0xff, 0x97, 0x42, 0xab, 0xa9, 0x34, 0xb5, 0xd2, 0x2d, 0xc1, 0x43, 0xf6, 0xbd, 0x17, 0x07, 0x7b, | ||||
|     0x44, 0xee, 0xe5, 0x5f, 0x10, 0x03, 0xa5, 0xf9, 0x8a, 0x4b, 0x82, 0x9d, 0xc6, 0x5b, 0x1c, 0x95, 0x0f, 0xfe, 0xf1, | ||||
|     0x1c, 0x3d, 0x75, 0xd1, 0x4f, 0xf1, 0x28, 0xef, 0xe3, 0x43, 0x61, 0x36, 0x2b, 0xb4, 0x6b, 0x85, 0x34, 0x04, 0x37, | ||||
|     0xd6, 0x76, 0x59, 0x14, 0x6d, 0xb7, 0xdb, 0x70, 0x3b, 0x0b, 0x95, 0x5e, 0x45, 0x69, 0x1c, 0xc7, 0x91, 0xd9, 0xac, | ||||
|     0x30, 0x1a, 0x0b, 0x01, 0xa7, 0x37, 0x18, 0x35, 0xc0, 0x57, 0x8d, 0x1d, 0xe0, 0xf2, 0xc5, 0x01, 0x8e, 0x85, 0xe3, | ||||
|     0x28, 0x1f, 0x3e, 0x5d, 0x58, 0xe1, 0x17, 0x56, 0xe0, 0x47, 0xea, 0xe1, 0x53, 0x98, 0x57, 0x43, 0x98, 0xaf, 0x68, | ||||
|     0x8a, 0x52, 0x14, 0x0f, 0x4f, 0x1a, 0x38, 0x78, 0x3a, 0x05, 0x4f, 0x4e, 0xe8, 0xe2, 0xe4, 0xa0, 0x76, 0x11, 0xbc, | ||||
|     0x3e, 0xcb, 0x26, 0x0e, 0xb3, 0x49, 0xe2, 0x47, 0x84, 0x13, 0xf8, 0x65, 0x71, 0x79, 0x0e, 0xd2, 0x0f, 0x97, 0x0c, | ||||
|     0xce, 0x5a, 0x93, 0x7c, 0x58, 0xd0, 0x39, 0x9a, 0x4f, 0x98, 0x79, 0xe0, 0xe0, 0xf3, 0x09, 0xcd, 0x37, 0x69, 0x93, | ||||
|     0xb4, 0xc1, 0x22, 0x98, 0xd3, 0x19, 0x9a, 0x4d, 0x8e, 0xcc, 0xd0, 0x6c, 0x93, 0x36, 0x8b, 0x0f, 0x8b, 0x4b, 0x5c, | ||||
|     0x30, 0xfb, 0x72, 0x15, 0x95, 0xd8, 0xcf, 0x30, 0x7e, 0x8c, 0x5c, 0x5d, 0x46, 0x1e, 0x7e, 0x56, 0x5c, 0x7a, 0x18, | ||||
|     0xfb, 0xc7, 0x1a, 0x2c, 0x6b, 0x3c, 0x1c, 0x31, 0x25, 0x6b, 0xbe, 0x0a, 0x3f, 0x1b, 0x25, 0xb1, 0x1f, 0xda, 0x06, | ||||
|     0xa4, 0x77, 0x12, 0x75, 0x82, 0x30, 0x50, 0xbc, 0xa7, 0x14, 0xeb, 0x1f, 0xce, 0xf5, 0x6f, 0xb9, 0x15, 0x40, 0x6c, | ||||
|     0xe8, 0x1a, 0xf6, 0xfa, 0x8c, 0x5d, 0xaa, 0x6a, 0xff, 0x8d, 0xd6, 0x68, 0x92, 0xb1, 0x2f, 0xb8, 0x94, 0xa0, 0xef, | ||||
|     0x61, 0x67, 0x09, 0x7e, 0xf7, 0xe6, 0x2d, 0x7a, 0x53, 0x55, 0x1a, 0x8c, 0xc9, 0x10, 0x7e, 0x69, 0xc3, 0x96, 0xb2, | ||||
|     0xff, 0x5d, 0x57, 0xf2, 0x95, 0xae, 0x7f, 0xf2, 0x9f, 0x39, 0xfa, 0x1d, 0xec, 0x56, 0xe9, 0xf5, 0xa4, 0xcd, 0xb9, | ||||
|     0x96, 0xbb, 0x0e, 0xd3, 0xc4, 0x86, 0xb4, 0x33, 0xa1, 0x11, 0x9c, 0x81, 0x97, 0xf8, 0x61, 0x4b, 0xbb, 0xc7, 0xa8, | ||||
|     0xe4, 0x29, 0x51, 0x0f, 0x45, 0xc5, 0x37, 0x88, 0x09, 0x6a, 0x0c, 0xc1, 0x72, 0x54, 0x85, 0xd1, 0x33, 0x34, 0xfc, | ||||
|     0x94, 0x64, 0x82, 0xb3, 0x35, 0xc1, 0x7f, 0x31, 0x01, 0x7e, 0xda, 0xff, 0x5a, 0x79, 0x57, 0xc6, 0xf0, 0xea, 0xca, | ||||
|     0x0f, 0x37, 0x54, 0xf4, 0x80, 0x08, 0xb2, 0x0d, 0x37, 0x8f, 0x0e, 0xe6, 0xdf, 0x14, 0xeb, 0xcc, 0xfa, 0xca, 0x0f, | ||||
|     0x6b, 0xc5, 0x7a, 0xe3, 0xf9, 0xb8, 0x9c, 0xcc, 0x15, 0x74, 0x1c, 0x90, 0xf8, 0x39, 0x7e, 0xe2, 0x51, 0x20, 0xa0, | ||||
|     0xb6, 0x67, 0x3e, 0x84, 0x5e, 0x1c, 0x8c, 0x27, 0x43, 0x6d, 0x0c, 0xf7, 0x8f, 0x67, 0x64, 0x61, 0x3a, 0x2a, 0x9f, | ||||
|     0x0a, 0x3a, 0x07, 0x5d, 0xab, 0xc8, 0xd0, 0x41, 0xae, 0x5f, 0x3a, 0x2a, 0xcf, 0x06, 0x23, 0x7a, 0x02, 0x5f, 0x1c, | ||||
|     0xb8, 0x27, 0xdd, 0x14, 0x5c, 0x9f, 0x35, 0x16, 0x51, 0xc5, 0x37, 0xe5, 0xc3, 0xd1, 0x7f, 0x8c, 0xe3, 0x5f, 0x3d, | ||||
|     0xe8, 0xfd, 0x1d, 0x08, 0x60, 0x56, 0x69, 0x0f, 0x3f, 0x97, 0x60, 0xb1, 0x3f, 0x06, 0xfc, 0xcb, 0xfd, 0xbb, 0xdf, | ||||
|     0x88, 0xf2, 0xb4, 0x7f, 0xfd, 0x2d, 0x6e, 0xb7, 0x0a, 0x3e, 0x6a, 0x10, 0xff, 0x26, 0x57, 0x6e, 0x19, 0x5c, 0x7d, | ||||
|     0xc2, 0x7e, 0x38, 0xc4, 0xfb, 0xf0, 0xb8, 0x11, 0x5c, 0x3b, 0xbf, 0xdc, 0xb5, 0xe2, 0xda, 0x45, 0x18, 0x2c, 0xe6, | ||||
|     0xfe, 0xf1, 0xe1, 0xe8, 0x1f, 0xfd, 0xbc, 0x88, 0xc6, 0xb9, 0x5e, 0x16, 0xc3, 0x88, 0x2d, 0x7f, 0x38, 0x2c, 0xd5, | ||||
|     0x2e, 0x30, 0xfc, 0x0b, 0x97, 0xab, 0x8c, 0xcb, 0x06, 0x34, 0xb7, 0xc7, 0x8a, 0x6f, 0xae, 0xb9, 0xec, 0x7a, 0x7b, | ||||
|     0xe8, 0x68, 0x55, 0x39, 0xca, 0xbc, 0xdb, 0xe5, 0xb5, 0x92, 0xd6, 0x71, 0x42, 0x96, 0x40, 0x7b, 0x1c, 0xe9, 0xc3, | ||||
|     0x44, 0xc9, 0x5e, 0xcf, 0xbf, 0x3b, 0xba, 0x82, 0x3b, 0x58, 0xd8, 0xd9, 0x80, 0x0a, 0xbe, 0x92, 0x19, 0x03, 0x69, | ||||
|     0x41, 0x8f, 0x42, 0x35, 0x6d, 0xb9, 0xd8, 0x67, 0x86, 0x4a, 0x13, 0x18, 0xd0, 0xbc, 0x3e, 0x2e, 0x7b, 0x6b, 0x95, | ||||
|     0x3c, 0x2c, 0x95, 0xae, 0x40, 0x67, 0x71, 0x3e, 0x02, 0x81, 0xa6, 0x15, 0xef, 0x4d, 0x16, 0xce, 0x34, 0xb4, 0xf9, | ||||
|     0x92, 0xb2, 0xf5, 0x4a, 0xab, 0x5e, 0x56, 0x01, 0x73, 0x93, 0x36, 0x7b, 0x9e, 0xd4, 0x74, 0x06, 0x2c, 0x9f, 0x4e, | ||||
|     0x75, 0x5d, 0xe7, 0x82, 0x4b, 0x08, 0xc6, 0x59, 0x96, 0xa5, 0xe1, 0x8d, 0x13, 0xbb, 0x70, 0x33, 0x4c, 0x1d, 0x62, | ||||
|     0xf4, 0x31, 0x89, 0xe3, 0xef, 0xf2, 0x53, 0x38, 0x71, 0xce, 0x7a, 0x6d, 0x94, 0xce, 0x3a, 0xc5, 0x9d, 0x9b, 0xc7, | ||||
|     0x96, 0x72, 0x79, 0xe9, 0xbd, 0x2b, 0x93, 0x7c, 0x5a, 0x3f, 0x19, 0x97, 0x83, 0x99, 0x61, 0x09, 0xe5, 0x2d, 0x97, | ||||
|     0xe3, 0x0e, 0xcd, 0xd2, 0x45, 0xdc, 0xed, 0x8e, 0xe1, 0x54, 0x20, 0x87, 0x13, 0x77, 0x2d, 0x60, 0x97, 0x7f, 0xee, | ||||
|     0x8d, 0xe5, 0xf5, 0x3e, 0x98, 0x76, 0x70, 0x66, 0x3a, 0xca, 0x20, 0x58, 0x82, 0xdd, 0x02, 0xc8, 0x7c, 0xb0, 0x11, | ||||
|     0x70, 0x0b, 0xad, 0x99, 0xf2, 0x74, 0x56, 0x33, 0x14, 0xe8, 0xd7, 0xba, 0xfe, 0x1b, 0xb7, 0xab, 0xc5, 0x43, 0x4b, | ||||
|     0xf5, 0x8a, 0xcb, 0x60, 0xa9, 0xac, 0x55, 0x6d, 0x16, 0xbc, 0xea, 0x76, 0xf9, 0x84, 0x72, 0xca, 0xb2, 0xc4, 0xb9, | ||||
|     0x39, 0xec, 0xd6, 0x53, 0xbe, 0x93, 0x6e, 0x87, 0x8c, 0x12, 0xbc, 0x9a, 0xf8, 0x06, 0x16, 0x14, 0x9f, 0xd3, 0x93, | ||||
|     0xcc, 0xbb, 0x1d, 0x72, 0xb8, 0x53, 0xaa, 0x6f, 0xea, 0x5b, 0x9a, 0xc4, 0x7f, 0xf1, 0x45, 0xaa, 0xba, 0x4e, 0x97, | ||||
|     0xf5, 0x39, 0x53, 0x6e, 0x4d, 0xba, 0xd6, 0x18, 0x4a, 0xab, 0x88, 0xc6, 0xdb, 0x8c, 0xab, 0x8c, 0xb2, 0x70, 0x19, | ||||
|     0x2e, 0x8b, 0x26, 0x41, 0xbc, 0x22, 0x2d, 0x65, 0xe5, 0xc5, 0xf8, 0x2a, 0xa2, 0x26, 0x39, 0x91, 0x9a, 0xa4, 0xfc, | ||||
|     0x6a, 0x18, 0x8d, 0xb4, 0xc1, 0xfb, 0xf2, 0xad, 0x92, 0x12, 0x98, 0xe5, 0x72, 0x85, 0xac, 0x42, 0x53, 0x0a, 0xc2, | ||||
|     0x30, 0x2c, 0x96, 0xba, 0x7c, 0x2f, 0x80, 0x1a, 0x40, 0x5b, 0xca, 0x6d, 0x58, 0x44, 0x23, 0xff, 0xd8, 0xc7, 0xbc, | ||||
|     0x22, 0x12, 0x6c, 0x39, 0x35, 0x6c, 0xd1, 0xcc, 0x46, 0x03, 0x77, 0x60, 0x9d, 0x26, 0x67, 0x60, 0x56, 0x16, 0x6e, | ||||
|     0xe5, 0x22, 0x3a, 0x8c, 0x34, 0x12, 0x6d, 0x79, 0xcd, 0xdd, 0x95, 0xa5, 0x2c, 0x86, 0x22, 0x77, 0x1a, 0x5c, 0x9e, | ||||
|     0xc7, 0xeb, 0xd5, 0x00, 0x09, 0x90, 0x2b, 0xdb, 0x90, 0x59, 0x8a, 0x3a, 0x41, 0x19, 0x34, 0x4a, 0x54, 0xa0, 0xc9, | ||||
|     0xdd, 0xdd, 0xaf, 0x7f, 0x2f, 0x9d, 0x33, 0x8f, 0x72, 0x9d, 0x59, 0x8f, 0x62, 0x0e, 0x98, 0xa4, 0x16, 0x37, 0xe3, | ||||
|     0xa5, 0xaa, 0xa3, 0xc6, 0x6c, 0x95, 0xae, 0xbe, 0xd2, 0xf1, 0x7e, 0x42, 0x8e, 0x7a, 0x86, 0xff, 0xd0, 0x2a, 0xe5, | ||||
|     0x1d, 0xdd, 0x40, 0x11, 0x4d, 0x87, 0x22, 0x72, 0x0e, 0x8f, 0xf4, 0x66, 0xe2, 0x6b, 0x92, 0xf2, 0x8f, 0xfb, 0x37, | ||||
|     0xe8, 0xcf, 0xae, 0xa2, 0x16, 0xc6, 0xb4, 0x0d, 0x51, 0xb5, 0x60, 0x1b, 0x55, 0x91, 0xf7, 0x7f, 0xdc, 0xdd, 0x9f, | ||||
|     0x23, 0xec, 0x07, 0x26, 0x04, 0x92, 0x8d, 0xd7, 0xbb, 0x5e, 0x58, 0xde, 0x51, 0x6d, 0x07, 0xb5, 0x81, 0x9b, 0x22, | ||||
|     0xa7, 0x18, 0x06, 0x7a, 0xcd, 0x05, 0x8c, 0x61, 0x8c, 0x82, 0x25, 0x3a, 0x79, 0x75, 0xb2, 0xf6, 0xc4, 0xaf, 0x68, | ||||
|     0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; | ||||
|  | ||||
| }  // namespace captive_portal | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -43,13 +43,6 @@ void BLEClientBase::setup() { | ||||
| void BLEClientBase::set_state(espbt::ClientState st) { | ||||
|   ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st); | ||||
|   ESPBTClient::set_state(st); | ||||
|  | ||||
|   if (st == espbt::ClientState::READY_TO_CONNECT) { | ||||
|     // Enable loop for state processing | ||||
|     this->enable_loop(); | ||||
|     // Connect immediately instead of waiting for next loop | ||||
|     this->connect(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BLEClientBase::loop() { | ||||
| @@ -65,8 +58,8 @@ void BLEClientBase::loop() { | ||||
|     } | ||||
|     this->set_state(espbt::ClientState::IDLE); | ||||
|   } | ||||
|   // If its idle, we can disable the loop as set_state | ||||
|   // will enable it again when we need to connect. | ||||
|   // If idle, we can disable the loop as connect() | ||||
|   // will enable it again when a connection is needed. | ||||
|   else if (this->state_ == espbt::ClientState::IDLE) { | ||||
|     this->disable_loop(); | ||||
|   } | ||||
| @@ -108,9 +101,20 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { | ||||
| #endif | ||||
|  | ||||
| void BLEClientBase::connect() { | ||||
|   // Prevent duplicate connection attempts | ||||
|   if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED || | ||||
|       this->state_ == espbt::ClientState::ESTABLISHED) { | ||||
|     ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, | ||||
|              this->address_str_.c_str(), espbt::client_state_to_string(this->state_)); | ||||
|     return; | ||||
|   } | ||||
|   ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_.c_str(), | ||||
|            this->remote_addr_type_); | ||||
|   this->paired_ = false; | ||||
|   // Enable loop for state processing | ||||
|   this->enable_loop(); | ||||
|   // Immediately transition to CONNECTING to prevent duplicate connection attempts | ||||
|   this->set_state(espbt::ClientState::CONNECTING); | ||||
|  | ||||
|   // Determine connection parameters based on connection type | ||||
|   if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { | ||||
| @@ -168,7 +172,7 @@ void BLEClientBase::unconditional_disconnect() { | ||||
|     this->log_gattc_warning_("esp_ble_gattc_close", err); | ||||
|   } | ||||
|  | ||||
|   if (this->state_ == espbt::ClientState::READY_TO_CONNECT || this->state_ == espbt::ClientState::DISCOVERED) { | ||||
|   if (this->state_ == espbt::ClientState::DISCOVERED) { | ||||
|     this->set_address(0); | ||||
|     this->set_state(espbt::ClientState::IDLE); | ||||
|   } else { | ||||
| @@ -212,8 +216,6 @@ void BLEClientBase::handle_connection_result_(esp_err_t ret) { | ||||
|   if (ret) { | ||||
|     this->log_gattc_warning_("esp_ble_gattc_open", ret); | ||||
|     this->set_state(espbt::ClientState::IDLE); | ||||
|   } else { | ||||
|     this->set_state(espbt::ClientState::CONNECTING); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -51,8 +51,6 @@ const char *client_state_to_string(ClientState state) { | ||||
|       return "IDLE"; | ||||
|     case ClientState::DISCOVERED: | ||||
|       return "DISCOVERED"; | ||||
|     case ClientState::READY_TO_CONNECT: | ||||
|       return "READY_TO_CONNECT"; | ||||
|     case ClientState::CONNECTING: | ||||
|       return "CONNECTING"; | ||||
|     case ClientState::CONNECTED: | ||||
| @@ -795,7 +793,7 @@ void ESP32BLETracker::try_promote_discovered_clients_() { | ||||
| #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE | ||||
|     this->update_coex_preference_(true); | ||||
| #endif | ||||
|     client->set_state(ClientState::READY_TO_CONNECT); | ||||
|     client->connect(); | ||||
|     break; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -159,8 +159,6 @@ enum class ClientState : uint8_t { | ||||
|   IDLE, | ||||
|   // Device advertisement found. | ||||
|   DISCOVERED, | ||||
|   // Device is discovered and the scanner is stopped | ||||
|   READY_TO_CONNECT, | ||||
|   // Connection in progress. | ||||
|   CONNECTING, | ||||
|   // Initial connection established. | ||||
| @@ -313,7 +311,6 @@ class ESP32BLETracker : public Component, | ||||
|           counts.discovered++; | ||||
|           break; | ||||
|         case ClientState::CONNECTING: | ||||
|         case ClientState::READY_TO_CONNECT: | ||||
|           counts.connecting++; | ||||
|           break; | ||||
|         default: | ||||
|   | ||||
							
								
								
									
										1
									
								
								esphome/components/inkplate/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/inkplate/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@jesserockz", "@JosipKuci"] | ||||
							
								
								
									
										105
									
								
								esphome/components/inkplate/const.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								esphome/components/inkplate/const.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| WAVEFORMS = { | ||||
|     "inkplate_6": ( | ||||
|         (0, 1, 1, 0, 0, 1, 1, 0, 0), | ||||
|         (0, 1, 2, 1, 1, 2, 1, 0, 0), | ||||
|         (1, 1, 1, 2, 2, 1, 0, 0, 0), | ||||
|         (0, 0, 0, 1, 1, 1, 2, 0, 0), | ||||
|         (2, 1, 1, 1, 2, 1, 2, 0, 0), | ||||
|         (2, 2, 1, 1, 2, 1, 2, 0, 0), | ||||
|         (1, 1, 1, 2, 1, 2, 2, 0, 0), | ||||
|         (0, 0, 0, 0, 0, 0, 2, 0, 0), | ||||
|     ), | ||||
|     "inkplate_10": ( | ||||
|         (0, 0, 0, 0, 0, 0, 0, 1, 0), | ||||
|         (0, 0, 0, 2, 2, 2, 1, 1, 0), | ||||
|         (0, 0, 2, 1, 1, 2, 2, 1, 0), | ||||
|         (0, 1, 2, 2, 1, 2, 2, 1, 0), | ||||
|         (0, 0, 2, 1, 2, 2, 2, 1, 0), | ||||
|         (0, 2, 2, 2, 2, 2, 2, 1, 0), | ||||
|         (0, 0, 0, 0, 0, 2, 1, 2, 0), | ||||
|         (0, 0, 0, 2, 2, 2, 2, 2, 0), | ||||
|     ), | ||||
|     "inkplate_6_plus": ( | ||||
|         (0, 0, 0, 0, 0, 2, 1, 1, 0), | ||||
|         (0, 0, 2, 1, 1, 1, 2, 1, 0), | ||||
|         (0, 2, 2, 2, 1, 1, 2, 1, 0), | ||||
|         (0, 0, 2, 2, 2, 1, 2, 1, 0), | ||||
|         (0, 0, 0, 0, 2, 2, 2, 1, 0), | ||||
|         (0, 0, 2, 1, 2, 1, 1, 2, 0), | ||||
|         (0, 0, 2, 2, 2, 1, 1, 2, 0), | ||||
|         (0, 0, 0, 0, 2, 2, 2, 2, 0), | ||||
|     ), | ||||
|     "inkplate_6_v2": ( | ||||
|         (1, 0, 1, 0, 1, 1, 1, 0, 0), | ||||
|         (0, 0, 0, 1, 1, 1, 1, 0, 0), | ||||
|         (1, 1, 1, 1, 0, 2, 1, 0, 0), | ||||
|         (1, 1, 1, 2, 2, 1, 1, 0, 0), | ||||
|         (1, 1, 1, 1, 2, 2, 1, 0, 0), | ||||
|         (0, 1, 1, 1, 2, 2, 1, 0, 0), | ||||
|         (0, 0, 0, 0, 1, 1, 2, 0, 0), | ||||
|         (0, 0, 0, 0, 0, 1, 2, 0, 0), | ||||
|     ), | ||||
|     "inkplate_5": ( | ||||
|         (0, 0, 1, 1, 0, 1, 1, 1, 0), | ||||
|         (0, 1, 1, 1, 1, 2, 0, 1, 0), | ||||
|         (1, 2, 2, 0, 2, 1, 1, 1, 0), | ||||
|         (1, 1, 1, 2, 0, 1, 1, 2, 0), | ||||
|         (0, 1, 1, 1, 2, 0, 1, 2, 0), | ||||
|         (0, 0, 0, 1, 1, 2, 1, 2, 0), | ||||
|         (1, 1, 1, 2, 0, 2, 1, 2, 0), | ||||
|         (0, 0, 0, 0, 0, 0, 0, 0, 0), | ||||
|     ), | ||||
|     "inkplate_5_v2": ( | ||||
|         (0, 0, 1, 1, 2, 1, 1, 1, 0), | ||||
|         (1, 1, 2, 2, 1, 2, 1, 1, 0), | ||||
|         (0, 1, 2, 2, 1, 1, 2, 1, 0), | ||||
|         (0, 0, 1, 1, 1, 1, 1, 2, 0), | ||||
|         (1, 2, 1, 2, 1, 1, 1, 2, 0), | ||||
|         (0, 1, 1, 1, 2, 0, 1, 2, 0), | ||||
|         (1, 1, 1, 2, 2, 2, 1, 2, 0), | ||||
|         (0, 0, 0, 0, 0, 0, 0, 0, 0), | ||||
|     ), | ||||
| } | ||||
|  | ||||
| INKPLATE_10_CUSTOM_WAVEFORMS = ( | ||||
|     ( | ||||
|         (0, 0, 0, 0, 0, 0, 0, 0, 0), | ||||
|         (0, 0, 0, 2, 1, 2, 1, 1, 0), | ||||
|         (0, 0, 0, 2, 2, 1, 2, 1, 0), | ||||
|         (0, 0, 2, 2, 1, 2, 2, 1, 0), | ||||
|         (0, 0, 0, 2, 1, 1, 1, 2, 0), | ||||
|         (0, 0, 2, 2, 2, 1, 1, 2, 0), | ||||
|         (0, 0, 0, 0, 0, 1, 2, 2, 0), | ||||
|         (0, 0, 0, 0, 2, 2, 2, 2, 0), | ||||
|     ), | ||||
|     ( | ||||
|         (0, 3, 3, 3, 3, 3, 3, 3, 0), | ||||
|         (0, 1, 2, 1, 1, 2, 2, 1, 0), | ||||
|         (0, 2, 2, 2, 1, 2, 2, 1, 0), | ||||
|         (0, 0, 2, 2, 2, 2, 2, 1, 0), | ||||
|         (0, 3, 3, 2, 1, 1, 1, 2, 0), | ||||
|         (0, 3, 3, 2, 2, 1, 1, 2, 0), | ||||
|         (0, 2, 1, 2, 1, 2, 1, 2, 0), | ||||
|         (0, 3, 3, 3, 2, 2, 2, 2, 0), | ||||
|     ), | ||||
|     ( | ||||
|         (0, 0, 0, 0, 0, 0, 0, 1, 0), | ||||
|         (0, 0, 0, 2, 2, 2, 1, 1, 0), | ||||
|         (0, 0, 2, 1, 1, 2, 2, 1, 0), | ||||
|         (1, 1, 2, 2, 1, 2, 2, 1, 0), | ||||
|         (0, 0, 2, 1, 2, 2, 2, 1, 0), | ||||
|         (0, 1, 2, 2, 2, 2, 2, 1, 0), | ||||
|         (0, 0, 0, 2, 2, 2, 1, 2, 0), | ||||
|         (0, 0, 0, 2, 2, 2, 2, 2, 0), | ||||
|     ), | ||||
|     ( | ||||
|         (0, 0, 0, 0, 0, 0, 0, 1, 0), | ||||
|         (0, 0, 0, 2, 2, 2, 1, 1, 0), | ||||
|         (2, 2, 2, 1, 0, 2, 1, 0, 0), | ||||
|         (2, 1, 1, 2, 1, 1, 1, 2, 0), | ||||
|         (2, 2, 2, 1, 1, 1, 0, 2, 0), | ||||
|         (2, 2, 2, 1, 1, 2, 1, 2, 0), | ||||
|         (0, 0, 0, 0, 2, 1, 2, 2, 0), | ||||
|         (0, 0, 0, 0, 2, 2, 2, 2, 0), | ||||
|     ), | ||||
| ) | ||||
							
								
								
									
										238
									
								
								esphome/components/inkplate/display.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								esphome/components/inkplate/display.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,238 @@ | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import display, i2c | ||||
| from esphome.components.esp32 import CONF_CPU_FREQUENCY | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_FULL_UPDATE_EVERY, | ||||
|     CONF_ID, | ||||
|     CONF_LAMBDA, | ||||
|     CONF_MIRROR_X, | ||||
|     CONF_MIRROR_Y, | ||||
|     CONF_MODEL, | ||||
|     CONF_OE_PIN, | ||||
|     CONF_PAGES, | ||||
|     CONF_TRANSFORM, | ||||
|     CONF_WAKEUP_PIN, | ||||
|     PLATFORM_ESP32, | ||||
| ) | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| from .const import INKPLATE_10_CUSTOM_WAVEFORMS, WAVEFORMS | ||||
|  | ||||
| DEPENDENCIES = ["i2c", "esp32"] | ||||
| AUTO_LOAD = ["psram"] | ||||
|  | ||||
| CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" | ||||
| CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" | ||||
| CONF_DISPLAY_DATA_2_PIN = "display_data_2_pin" | ||||
| CONF_DISPLAY_DATA_3_PIN = "display_data_3_pin" | ||||
| CONF_DISPLAY_DATA_4_PIN = "display_data_4_pin" | ||||
| CONF_DISPLAY_DATA_5_PIN = "display_data_5_pin" | ||||
| CONF_DISPLAY_DATA_6_PIN = "display_data_6_pin" | ||||
| CONF_DISPLAY_DATA_7_PIN = "display_data_7_pin" | ||||
|  | ||||
| CONF_CL_PIN = "cl_pin" | ||||
| CONF_CKV_PIN = "ckv_pin" | ||||
| CONF_GREYSCALE = "greyscale" | ||||
| CONF_GMOD_PIN = "gmod_pin" | ||||
| CONF_GPIO0_ENABLE_PIN = "gpio0_enable_pin" | ||||
| CONF_LE_PIN = "le_pin" | ||||
| CONF_PARTIAL_UPDATING = "partial_updating" | ||||
| CONF_POWERUP_PIN = "powerup_pin" | ||||
| CONF_SPH_PIN = "sph_pin" | ||||
| CONF_SPV_PIN = "spv_pin" | ||||
| CONF_VCOM_PIN = "vcom_pin" | ||||
|  | ||||
| inkplate_ns = cg.esphome_ns.namespace("inkplate") | ||||
| Inkplate = inkplate_ns.class_( | ||||
|     "Inkplate", | ||||
|     cg.PollingComponent, | ||||
|     i2c.I2CDevice, | ||||
|     display.Display, | ||||
|     display.DisplayBuffer, | ||||
| ) | ||||
|  | ||||
| InkplateModel = inkplate_ns.enum("InkplateModel") | ||||
|  | ||||
| MODELS = { | ||||
|     "inkplate_6": InkplateModel.INKPLATE_6, | ||||
|     "inkplate_10": InkplateModel.INKPLATE_10, | ||||
|     "inkplate_6_plus": InkplateModel.INKPLATE_6_PLUS, | ||||
|     "inkplate_6_v2": InkplateModel.INKPLATE_6_V2, | ||||
|     "inkplate_5": InkplateModel.INKPLATE_5, | ||||
|     "inkplate_5_v2": InkplateModel.INKPLATE_5_V2, | ||||
| } | ||||
|  | ||||
| CONF_CUSTOM_WAVEFORM = "custom_waveform" | ||||
|  | ||||
|  | ||||
| def _validate_custom_waveform(config): | ||||
|     if CONF_CUSTOM_WAVEFORM in config and config[CONF_MODEL] != "inkplate_10": | ||||
|         raise cv.Invalid("Custom waveforms are only supported on the Inkplate 10") | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     display.FULL_DISPLAY_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(Inkplate), | ||||
|             cv.Optional(CONF_GREYSCALE, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_CUSTOM_WAVEFORM): cv.All( | ||||
|                 cv.uint8_t, cv.Range(min=1, max=len(INKPLATE_10_CUSTOM_WAVEFORMS)) | ||||
|             ), | ||||
|             cv.Optional(CONF_TRANSFORM): cv.Schema( | ||||
|                 { | ||||
|                     cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, | ||||
|                     cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_PARTIAL_UPDATING, default=True): cv.boolean, | ||||
|             cv.Optional(CONF_FULL_UPDATE_EVERY, default=10): cv.uint32_t, | ||||
|             cv.Optional(CONF_MODEL, default="inkplate_6"): cv.enum( | ||||
|                 MODELS, lower=True, space="_" | ||||
|             ), | ||||
|             # Control pins | ||||
|             cv.Required(CONF_CKV_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_GMOD_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_GPIO0_ENABLE_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_OE_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_POWERUP_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_SPH_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_SPV_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_VCOM_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_WAKEUP_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Optional(CONF_CL_PIN, default=0): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional(CONF_LE_PIN, default=2): pins.internal_gpio_output_pin_schema, | ||||
|             # Data pins | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_0_PIN, default=4 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_1_PIN, default=5 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_2_PIN, default=18 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_3_PIN, default=19 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_4_PIN, default=23 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_5_PIN, default=25 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_6_PIN, default=26 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_7_PIN, default=27 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("5s")) | ||||
|     .extend(i2c.i2c_device_schema(0x48)), | ||||
|     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), | ||||
|     _validate_custom_waveform, | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _validate_cpu_frequency(config): | ||||
|     esp32_config = fv.full_config.get()[PLATFORM_ESP32] | ||||
|     if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ": | ||||
|         raise cv.Invalid( | ||||
|             "Inkplate requires 240MHz CPU frequency (set in esp32 component)" | ||||
|         ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _validate_cpu_frequency | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|  | ||||
|     await display.register_display(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     if CONF_LAMBDA in config: | ||||
|         lambda_ = await cg.process_lambda( | ||||
|             config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void | ||||
|         ) | ||||
|         cg.add(var.set_writer(lambda_)) | ||||
|  | ||||
|     cg.add(var.set_greyscale(config[CONF_GREYSCALE])) | ||||
|     if transform := config.get(CONF_TRANSFORM): | ||||
|         cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) | ||||
|         cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) | ||||
|     cg.add(var.set_partial_updating(config[CONF_PARTIAL_UPDATING])) | ||||
|     cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) | ||||
|  | ||||
|     cg.add(var.set_model(config[CONF_MODEL])) | ||||
|  | ||||
|     if custom_waveform := config.get(CONF_CUSTOM_WAVEFORM): | ||||
|         waveform = INKPLATE_10_CUSTOM_WAVEFORMS[custom_waveform - 1] | ||||
|         waveform = [element for tupl in waveform for element in tupl] | ||||
|         cg.add(var.set_waveform(waveform, True)) | ||||
|     else: | ||||
|         waveform = WAVEFORMS[config[CONF_MODEL]] | ||||
|         waveform = [element for tupl in waveform for element in tupl] | ||||
|         cg.add(var.set_waveform(waveform, False)) | ||||
|  | ||||
|     ckv = await cg.gpio_pin_expression(config[CONF_CKV_PIN]) | ||||
|     cg.add(var.set_ckv_pin(ckv)) | ||||
|  | ||||
|     gmod = await cg.gpio_pin_expression(config[CONF_GMOD_PIN]) | ||||
|     cg.add(var.set_gmod_pin(gmod)) | ||||
|  | ||||
|     gpio0_enable = await cg.gpio_pin_expression(config[CONF_GPIO0_ENABLE_PIN]) | ||||
|     cg.add(var.set_gpio0_enable_pin(gpio0_enable)) | ||||
|  | ||||
|     oe = await cg.gpio_pin_expression(config[CONF_OE_PIN]) | ||||
|     cg.add(var.set_oe_pin(oe)) | ||||
|  | ||||
|     powerup = await cg.gpio_pin_expression(config[CONF_POWERUP_PIN]) | ||||
|     cg.add(var.set_powerup_pin(powerup)) | ||||
|  | ||||
|     sph = await cg.gpio_pin_expression(config[CONF_SPH_PIN]) | ||||
|     cg.add(var.set_sph_pin(sph)) | ||||
|  | ||||
|     spv = await cg.gpio_pin_expression(config[CONF_SPV_PIN]) | ||||
|     cg.add(var.set_spv_pin(spv)) | ||||
|  | ||||
|     vcom = await cg.gpio_pin_expression(config[CONF_VCOM_PIN]) | ||||
|     cg.add(var.set_vcom_pin(vcom)) | ||||
|  | ||||
|     wakeup = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) | ||||
|     cg.add(var.set_wakeup_pin(wakeup)) | ||||
|  | ||||
|     cl = await cg.gpio_pin_expression(config[CONF_CL_PIN]) | ||||
|     cg.add(var.set_cl_pin(cl)) | ||||
|  | ||||
|     le = await cg.gpio_pin_expression(config[CONF_LE_PIN]) | ||||
|     cg.add(var.set_le_pin(le)) | ||||
|  | ||||
|     display_data_0 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_0_PIN]) | ||||
|     cg.add(var.set_display_data_0_pin(display_data_0)) | ||||
|  | ||||
|     display_data_1 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_1_PIN]) | ||||
|     cg.add(var.set_display_data_1_pin(display_data_1)) | ||||
|  | ||||
|     display_data_2 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_2_PIN]) | ||||
|     cg.add(var.set_display_data_2_pin(display_data_2)) | ||||
|  | ||||
|     display_data_3 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_3_PIN]) | ||||
|     cg.add(var.set_display_data_3_pin(display_data_3)) | ||||
|  | ||||
|     display_data_4 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_4_PIN]) | ||||
|     cg.add(var.set_display_data_4_pin(display_data_4)) | ||||
|  | ||||
|     display_data_5 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_5_PIN]) | ||||
|     cg.add(var.set_display_data_5_pin(display_data_5)) | ||||
|  | ||||
|     display_data_6 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_6_PIN]) | ||||
|     cg.add(var.set_display_data_6_pin(display_data_6)) | ||||
|  | ||||
|     display_data_7 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_7_PIN]) | ||||
|     cg.add(var.set_display_data_7_pin(display_data_7)) | ||||
| @@ -6,11 +6,11 @@ | ||||
| #include <hal/gpio_hal.h> | ||||
| 
 | ||||
| namespace esphome { | ||||
| namespace inkplate6 { | ||||
| namespace inkplate { | ||||
| 
 | ||||
| static const char *const TAG = "inkplate"; | ||||
| 
 | ||||
| void Inkplate6::setup() { | ||||
| void Inkplate::setup() { | ||||
|   for (uint32_t i = 0; i < 256; i++) { | ||||
|     this->pin_lut_[i] = ((i & 0b00000011) << 4) | (((i & 0b00001100) >> 2) << 18) | (((i & 0b00010000) >> 4) << 23) | | ||||
|                         (((i & 0b11100000) >> 5) << 25); | ||||
| @@ -56,7 +56,7 @@ void Inkplate6::setup() { | ||||
| /**
 | ||||
|  * Allocate buffers. May be called after setup to re-initialise if e.g. greyscale is changed. | ||||
|  */ | ||||
| void Inkplate6::initialize_() { | ||||
| void Inkplate::initialize_() { | ||||
|   RAMAllocator<uint8_t> allocator; | ||||
|   RAMAllocator<uint32_t> allocator32; | ||||
|   uint32_t buffer_size = this->get_buffer_length_(); | ||||
| @@ -81,29 +81,25 @@ void Inkplate6::initialize_() { | ||||
|     return; | ||||
|   } | ||||
|   if (this->greyscale_) { | ||||
|     uint8_t glut_size = 9; | ||||
| 
 | ||||
|     this->glut_ = allocator32.allocate(256 * glut_size); | ||||
|     this->glut_ = allocator32.allocate(256 * GLUT_SIZE); | ||||
|     if (this->glut_ == nullptr) { | ||||
|       ESP_LOGE(TAG, "Could not allocate glut!"); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|     this->glut2_ = allocator32.allocate(256 * glut_size); | ||||
|     this->glut2_ = allocator32.allocate(256 * GLUT_SIZE); | ||||
|     if (this->glut2_ == nullptr) { | ||||
|       ESP_LOGE(TAG, "Could not allocate glut2!"); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const auto *const waveform3_bit = waveform3BitAll[this->model_]; | ||||
| 
 | ||||
|     for (int i = 0; i < glut_size; i++) { | ||||
|     for (uint8_t i = 0; i < GLUT_SIZE; i++) { | ||||
|       for (uint32_t j = 0; j < 256; j++) { | ||||
|         uint8_t z = (waveform3_bit[j & 0x07][i] << 2) | (waveform3_bit[(j >> 4) & 0x07][i]); | ||||
|         uint8_t z = (this->waveform_[j & 0x07][i] << 2) | (this->waveform_[(j >> 4) & 0x07][i]); | ||||
|         this->glut_[i * 256 + j] = ((z & 0b00000011) << 4) | (((z & 0b00001100) >> 2) << 18) | | ||||
|                                    (((z & 0b00010000) >> 4) << 23) | (((z & 0b11100000) >> 5) << 25); | ||||
|         z = ((waveform3_bit[j & 0x07][i] << 2) | (waveform3_bit[(j >> 4) & 0x07][i])) << 4; | ||||
|         z = ((this->waveform_[j & 0x07][i] << 2) | (this->waveform_[(j >> 4) & 0x07][i])) << 4; | ||||
|         this->glut2_[i * 256 + j] = ((z & 0b00000011) << 4) | (((z & 0b00001100) >> 2) << 18) | | ||||
|                                     (((z & 0b00010000) >> 4) << 23) | (((z & 0b11100000) >> 5) << 25); | ||||
|       } | ||||
| @@ -130,9 +126,9 @@ void Inkplate6::initialize_() { | ||||
|   memset(this->buffer_, 0, buffer_size); | ||||
| } | ||||
| 
 | ||||
| float Inkplate6::get_setup_priority() const { return setup_priority::PROCESSOR; } | ||||
| float Inkplate::get_setup_priority() const { return setup_priority::PROCESSOR; } | ||||
| 
 | ||||
| size_t Inkplate6::get_buffer_length_() { | ||||
| size_t Inkplate::get_buffer_length_() { | ||||
|   if (this->greyscale_) { | ||||
|     return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 2u; | ||||
|   } else { | ||||
| @@ -140,7 +136,7 @@ size_t Inkplate6::get_buffer_length_() { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::update() { | ||||
| void Inkplate::update() { | ||||
|   this->do_update_(); | ||||
| 
 | ||||
|   if (this->full_update_every_ > 0 && this->partial_updates_ >= this->full_update_every_) { | ||||
| @@ -150,7 +146,7 @@ void Inkplate6::update() { | ||||
|   this->display(); | ||||
| } | ||||
| 
 | ||||
| void HOT Inkplate6::draw_absolute_pixel_internal(int x, int y, Color color) { | ||||
| void HOT Inkplate::draw_absolute_pixel_internal(int x, int y, Color color) { | ||||
|   if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) | ||||
|     return; | ||||
| 
 | ||||
| @@ -171,18 +167,18 @@ void HOT Inkplate6::draw_absolute_pixel_internal(int x, int y, Color color) { | ||||
|     // uint8_t gs = (uint8_t)(px*7);
 | ||||
| 
 | ||||
|     uint8_t gs = ((color.red * 2126 / 10000) + (color.green * 7152 / 10000) + (color.blue * 722 / 10000)) >> 5; | ||||
|     this->buffer_[pos] = (pixelMaskGLUT[x_sub] & current) | (x_sub ? gs : gs << 4); | ||||
|     this->buffer_[pos] = (PIXEL_MASK_GLUT[x_sub] & current) | (x_sub ? gs : gs << 4); | ||||
| 
 | ||||
|   } else { | ||||
|     int x1 = x / 8; | ||||
|     int x_sub = x % 8; | ||||
|     uint32_t pos = (x1 + y * (this->get_width_internal() / 8)); | ||||
|     uint8_t current = this->partial_buffer_[pos]; | ||||
|     this->partial_buffer_[pos] = (~pixelMaskLUT[x_sub] & current) | (color.is_on() ? 0 : pixelMaskLUT[x_sub]); | ||||
|     this->partial_buffer_[pos] = (~PIXEL_MASK_LUT[x_sub] & current) | (color.is_on() ? 0 : PIXEL_MASK_LUT[x_sub]); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::dump_config() { | ||||
| void Inkplate::dump_config() { | ||||
|   LOG_DISPLAY("", "Inkplate", this); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Greyscale: %s\n" | ||||
| @@ -214,7 +210,7 @@ void Inkplate6::dump_config() { | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::eink_off_() { | ||||
| void Inkplate::eink_off_() { | ||||
|   ESP_LOGV(TAG, "Eink off called"); | ||||
|   if (!panel_on_) | ||||
|     return; | ||||
| @@ -242,7 +238,7 @@ void Inkplate6::eink_off_() { | ||||
|   pins_z_state_(); | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::eink_on_() { | ||||
| void Inkplate::eink_on_() { | ||||
|   ESP_LOGV(TAG, "Eink on called"); | ||||
|   if (panel_on_) | ||||
|     return; | ||||
| @@ -284,7 +280,7 @@ void Inkplate6::eink_on_() { | ||||
|   this->oe_pin_->digital_write(true); | ||||
| } | ||||
| 
 | ||||
| bool Inkplate6::read_power_status_() { | ||||
| bool Inkplate::read_power_status_() { | ||||
|   uint8_t data; | ||||
|   auto err = this->read_register(0x0F, &data, 1); | ||||
|   if (err == i2c::ERROR_OK) { | ||||
| @@ -293,7 +289,7 @@ bool Inkplate6::read_power_status_() { | ||||
|   return false; | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::fill(Color color) { | ||||
| void Inkplate::fill(Color color) { | ||||
|   ESP_LOGV(TAG, "Fill called"); | ||||
|   uint32_t start_time = millis(); | ||||
| 
 | ||||
| @@ -308,7 +304,7 @@ void Inkplate6::fill(Color color) { | ||||
|   ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::display() { | ||||
| void Inkplate::display() { | ||||
|   ESP_LOGV(TAG, "Display called"); | ||||
|   uint32_t start_time = millis(); | ||||
| 
 | ||||
| @@ -324,7 +320,7 @@ void Inkplate6::display() { | ||||
|   ESP_LOGV(TAG, "Display finished (full) (%ums)", millis() - start_time); | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::display1b_() { | ||||
| void Inkplate::display1b_() { | ||||
|   ESP_LOGV(TAG, "Display1b called"); | ||||
|   uint32_t start_time = millis(); | ||||
| 
 | ||||
| @@ -334,32 +330,71 @@ void Inkplate6::display1b_() { | ||||
|   uint8_t buffer_value; | ||||
|   const uint8_t *buffer_ptr; | ||||
|   eink_on_(); | ||||
|   if (this->model_ == INKPLATE_6_PLUS) { | ||||
|     clean_fast_(0, 1); | ||||
|     clean_fast_(1, 15); | ||||
|     clean_fast_(2, 1); | ||||
|     clean_fast_(0, 5); | ||||
|     clean_fast_(2, 1); | ||||
|     clean_fast_(1, 15); | ||||
|   } else { | ||||
|     clean_fast_(0, 1); | ||||
|     clean_fast_(1, 21); | ||||
|     clean_fast_(2, 1); | ||||
|     clean_fast_(0, 12); | ||||
|     clean_fast_(2, 1); | ||||
|     clean_fast_(1, 21); | ||||
|     clean_fast_(2, 1); | ||||
|     clean_fast_(0, 12); | ||||
|     clean_fast_(2, 1); | ||||
|   uint8_t rep = 4; | ||||
|   switch (this->model_) { | ||||
|     case INKPLATE_10: | ||||
|       clean_fast_(0, 1); | ||||
|       clean_fast_(1, 10); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 10); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(1, 10); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 10); | ||||
|       rep = 5; | ||||
|       break; | ||||
|     case INKPLATE_6_PLUS: | ||||
|       clean_fast_(0, 1); | ||||
|       clean_fast_(1, 15); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 5); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(1, 15); | ||||
|       break; | ||||
|     case INKPLATE_6: | ||||
|     case INKPLATE_6_V2: | ||||
|       clean_fast_(0, 1); | ||||
|       clean_fast_(1, 18); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 18); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(1, 18); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 18); | ||||
|       clean_fast_(2, 1); | ||||
|       if (this->model_ == INKPLATE_6_V2) | ||||
|         rep = 5; | ||||
|       break; | ||||
|     case INKPLATE_5: | ||||
|       clean_fast_(0, 1); | ||||
|       clean_fast_(1, 14); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 14); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(1, 14); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 14); | ||||
|       clean_fast_(2, 1); | ||||
|       rep = 5; | ||||
|       break; | ||||
|     case INKPLATE_5_V2: | ||||
|       clean_fast_(0, 1); | ||||
|       clean_fast_(1, 11); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 11); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(1, 11); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 11); | ||||
|       rep = 3; | ||||
|       break; | ||||
|   } | ||||
| 
 | ||||
|   uint32_t clock = (1 << this->cl_pin_->get_pin()); | ||||
|   uint32_t data_mask = this->get_data_pin_mask_(); | ||||
|   ESP_LOGV(TAG, "Display1b start loops (%ums)", millis() - start_time); | ||||
| 
 | ||||
|   int rep = (this->model_ == INKPLATE_6_V2) ? 5 : 4; | ||||
| 
 | ||||
|   for (int k = 0; k < rep; k++) { | ||||
|   for (uint8_t k = 0; k < rep; k++) { | ||||
|     buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; | ||||
|     vscan_start_(); | ||||
|     for (int i = 0, im = this->get_height_internal(); i < im; i++) { | ||||
| @@ -452,28 +487,75 @@ void Inkplate6::display1b_() { | ||||
|   ESP_LOGV(TAG, "Display1b finished (%ums)", millis() - start_time); | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::display3b_() { | ||||
| void Inkplate::display3b_() { | ||||
|   ESP_LOGV(TAG, "Display3b called"); | ||||
|   uint32_t start_time = millis(); | ||||
| 
 | ||||
|   eink_on_(); | ||||
|   if (this->model_ == INKPLATE_6_PLUS) { | ||||
|     clean_fast_(0, 1); | ||||
|     clean_fast_(1, 15); | ||||
|     clean_fast_(2, 1); | ||||
|     clean_fast_(0, 5); | ||||
|     clean_fast_(2, 1); | ||||
|     clean_fast_(1, 15); | ||||
|   } else { | ||||
|     clean_fast_(0, 1); | ||||
|     clean_fast_(1, 21); | ||||
|     clean_fast_(2, 1); | ||||
|     clean_fast_(0, 12); | ||||
|     clean_fast_(2, 1); | ||||
|     clean_fast_(1, 21); | ||||
|     clean_fast_(2, 1); | ||||
|     clean_fast_(0, 12); | ||||
|     clean_fast_(2, 1); | ||||
| 
 | ||||
|   switch (this->model_) { | ||||
|     case INKPLATE_10: | ||||
|       if (this->custom_waveform_) { | ||||
|         clean_fast_(1, 1); | ||||
|         clean_fast_(0, 7); | ||||
|         clean_fast_(2, 1); | ||||
|         clean_fast_(1, 12); | ||||
|         clean_fast_(2, 1); | ||||
|         clean_fast_(0, 7); | ||||
|         clean_fast_(2, 1); | ||||
|         clean_fast_(1, 12); | ||||
|       } else { | ||||
|         clean_fast_(1, 1); | ||||
|         clean_fast_(0, 10); | ||||
|         clean_fast_(2, 1); | ||||
|         clean_fast_(1, 10); | ||||
|         clean_fast_(2, 1); | ||||
|         clean_fast_(0, 10); | ||||
|         clean_fast_(2, 1); | ||||
|         clean_fast_(1, 10); | ||||
|       } | ||||
|       break; | ||||
|     case INKPLATE_6_PLUS: | ||||
|       clean_fast_(0, 1); | ||||
|       clean_fast_(1, 15); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 5); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(1, 15); | ||||
|       break; | ||||
|     case INKPLATE_6: | ||||
|     case INKPLATE_6_V2: | ||||
|       clean_fast_(0, 1); | ||||
|       clean_fast_(1, 18); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 18); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(1, 18); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 18); | ||||
|       clean_fast_(2, 1); | ||||
|       break; | ||||
|     case INKPLATE_5: | ||||
|       clean_fast_(0, 1); | ||||
|       clean_fast_(1, 14); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 14); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(1, 14); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 14); | ||||
|       clean_fast_(2, 1); | ||||
|       break; | ||||
|     case INKPLATE_5_V2: | ||||
|       clean_fast_(0, 1); | ||||
|       clean_fast_(1, 11); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 11); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(1, 11); | ||||
|       clean_fast_(2, 1); | ||||
|       clean_fast_(0, 11); | ||||
|       break; | ||||
|   } | ||||
| 
 | ||||
|   uint32_t clock = (1 << this->cl_pin_->get_pin()); | ||||
| @@ -518,7 +600,7 @@ void Inkplate6::display3b_() { | ||||
|   ESP_LOGV(TAG, "Display3b finished (%ums)", millis() - start_time); | ||||
| } | ||||
| 
 | ||||
| bool Inkplate6::partial_update_() { | ||||
| bool Inkplate::partial_update_() { | ||||
|   ESP_LOGV(TAG, "Partial update called"); | ||||
|   uint32_t start_time = millis(); | ||||
|   if (this->greyscale_) | ||||
| @@ -560,7 +642,7 @@ bool Inkplate6::partial_update_() { | ||||
|         GPIO.out_w1ts = this->pin_lut_[data] | clock; | ||||
|         GPIO.out_w1tc = data_mask | clock; | ||||
|       } | ||||
|       // New Inkplate6 panel doesn't need last clock
 | ||||
|       // New Inkplate panel doesn't need last clock
 | ||||
|       if (this->model_ != INKPLATE_6_V2) { | ||||
|         GPIO.out_w1ts = clock; | ||||
|         GPIO.out_w1tc = data_mask | clock; | ||||
| @@ -580,7 +662,7 @@ bool Inkplate6::partial_update_() { | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::vscan_start_() { | ||||
| void Inkplate::vscan_start_() { | ||||
|   this->ckv_pin_->digital_write(true); | ||||
|   delayMicroseconds(7); | ||||
|   this->spv_pin_->digital_write(false); | ||||
| @@ -604,7 +686,7 @@ void Inkplate6::vscan_start_() { | ||||
|   this->ckv_pin_->digital_write(true); | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::hscan_start_(uint32_t d) { | ||||
| void Inkplate::hscan_start_(uint32_t d) { | ||||
|   uint8_t clock = (1 << this->cl_pin_->get_pin()); | ||||
|   this->sph_pin_->digital_write(false); | ||||
|   GPIO.out_w1ts = d | clock; | ||||
| @@ -613,14 +695,14 @@ void Inkplate6::hscan_start_(uint32_t d) { | ||||
|   this->ckv_pin_->digital_write(true); | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::vscan_end_() { | ||||
| void Inkplate::vscan_end_() { | ||||
|   this->ckv_pin_->digital_write(false); | ||||
|   this->le_pin_->digital_write(true); | ||||
|   this->le_pin_->digital_write(false); | ||||
|   delayMicroseconds(0); | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::clean() { | ||||
| void Inkplate::clean() { | ||||
|   ESP_LOGV(TAG, "Clean called"); | ||||
|   uint32_t start_time = millis(); | ||||
| 
 | ||||
| @@ -634,7 +716,7 @@ void Inkplate6::clean() { | ||||
|   ESP_LOGV(TAG, "Clean finished (%ums)", millis() - start_time); | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { | ||||
| void Inkplate::clean_fast_(uint8_t c, uint8_t rep) { | ||||
|   ESP_LOGV(TAG, "Clean fast called with: (%d, %d)", c, rep); | ||||
|   uint32_t start_time = millis(); | ||||
| 
 | ||||
| @@ -666,7 +748,7 @@ void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { | ||||
|         GPIO.out_w1ts = clock; | ||||
|         GPIO.out_w1tc = clock; | ||||
|       } | ||||
|       // New Inkplate6 panel doesn't need last clock
 | ||||
|       // New Inkplate panel doesn't need last clock
 | ||||
|       if (this->model_ != INKPLATE_6_V2) { | ||||
|         GPIO.out_w1ts = send | clock; | ||||
|         GPIO.out_w1tc = clock; | ||||
| @@ -679,7 +761,7 @@ void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { | ||||
|   ESP_LOGV(TAG, "Clean fast finished (%ums)", millis() - start_time); | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::pins_z_state_() { | ||||
| void Inkplate::pins_z_state_() { | ||||
|   this->cl_pin_->pin_mode(gpio::FLAG_INPUT); | ||||
|   this->le_pin_->pin_mode(gpio::FLAG_INPUT); | ||||
|   this->ckv_pin_->pin_mode(gpio::FLAG_INPUT); | ||||
| @@ -699,7 +781,7 @@ void Inkplate6::pins_z_state_() { | ||||
|   this->display_data_7_pin_->pin_mode(gpio::FLAG_INPUT); | ||||
| } | ||||
| 
 | ||||
| void Inkplate6::pins_as_outputs_() { | ||||
| void Inkplate::pins_as_outputs_() { | ||||
|   this->cl_pin_->pin_mode(gpio::FLAG_OUTPUT); | ||||
|   this->le_pin_->pin_mode(gpio::FLAG_OUTPUT); | ||||
|   this->ckv_pin_->pin_mode(gpio::FLAG_OUTPUT); | ||||
| @@ -719,5 +801,5 @@ void Inkplate6::pins_as_outputs_() { | ||||
|   this->display_data_7_pin_->pin_mode(gpio::FLAG_OUTPUT); | ||||
| } | ||||
| 
 | ||||
| }  // namespace inkplate6
 | ||||
| }  // namespace inkplate
 | ||||
| }  // namespace esphome
 | ||||
| @@ -5,8 +5,10 @@ | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/hal.h" | ||||
| 
 | ||||
| #include <array> | ||||
| 
 | ||||
| namespace esphome { | ||||
| namespace inkplate6 { | ||||
| namespace inkplate { | ||||
| 
 | ||||
| enum InkplateModel : uint8_t { | ||||
|   INKPLATE_6 = 0, | ||||
| @@ -17,79 +19,35 @@ enum InkplateModel : uint8_t { | ||||
|   INKPLATE_5_V2 = 5, | ||||
| }; | ||||
| 
 | ||||
| class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { | ||||
| static constexpr uint8_t GLUT_SIZE = 9; | ||||
| static constexpr uint8_t GLUT_COUNT = 8; | ||||
| 
 | ||||
| static constexpr uint8_t LUT2[16] = {0xAA, 0xA9, 0xA6, 0xA5, 0x9A, 0x99, 0x96, 0x95, | ||||
|                                      0x6A, 0x69, 0x66, 0x65, 0x5A, 0x59, 0x56, 0x55}; | ||||
| static constexpr uint8_t LUTW[16] = {0xFF, 0xFE, 0xFB, 0xFA, 0xEF, 0xEE, 0xEB, 0xEA, | ||||
|                                      0xBF, 0xBE, 0xBB, 0xBA, 0xAF, 0xAE, 0xAB, 0xAA}; | ||||
| static constexpr uint8_t LUTB[16] = {0xFF, 0xFD, 0xF7, 0xF5, 0xDF, 0xDD, 0xD7, 0xD5, | ||||
|                                      0x7F, 0x7D, 0x77, 0x75, 0x5F, 0x5D, 0x57, 0x55}; | ||||
| 
 | ||||
| static constexpr uint8_t PIXEL_MASK_LUT[8] = {0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80}; | ||||
| static constexpr uint8_t PIXEL_MASK_GLUT[2] = {0x0F, 0xF0}; | ||||
| 
 | ||||
| class Inkplate : public display::DisplayBuffer, public i2c::I2CDevice { | ||||
|  public: | ||||
|   const uint8_t LUT2[16] = {0xAA, 0xA9, 0xA6, 0xA5, 0x9A, 0x99, 0x96, 0x95, | ||||
|                             0x6A, 0x69, 0x66, 0x65, 0x5A, 0x59, 0x56, 0x55}; | ||||
|   const uint8_t LUTW[16] = {0xFF, 0xFE, 0xFB, 0xFA, 0xEF, 0xEE, 0xEB, 0xEA, | ||||
|                             0xBF, 0xBE, 0xBB, 0xBA, 0xAF, 0xAE, 0xAB, 0xAA}; | ||||
|   const uint8_t LUTB[16] = {0xFF, 0xFD, 0xF7, 0xF5, 0xDF, 0xDD, 0xD7, 0xD5, | ||||
|                             0x7F, 0x7D, 0x77, 0x75, 0x5F, 0x5D, 0x57, 0x55}; | ||||
| 
 | ||||
|   const uint8_t pixelMaskLUT[8] = {0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80}; | ||||
|   const uint8_t pixelMaskGLUT[2] = {0x0F, 0xF0}; | ||||
| 
 | ||||
|   const uint8_t waveform3BitAll[6][8][9] = {// INKPLATE_6
 | ||||
|                                             {{0, 1, 1, 0, 0, 1, 1, 0, 0}, | ||||
|                                              {0, 1, 2, 1, 1, 2, 1, 0, 0}, | ||||
|                                              {1, 1, 1, 2, 2, 1, 0, 0, 0}, | ||||
|                                              {0, 0, 0, 1, 1, 1, 2, 0, 0}, | ||||
|                                              {2, 1, 1, 1, 2, 1, 2, 0, 0}, | ||||
|                                              {2, 2, 1, 1, 2, 1, 2, 0, 0}, | ||||
|                                              {1, 1, 1, 2, 1, 2, 2, 0, 0}, | ||||
|                                              {0, 0, 0, 0, 0, 0, 2, 0, 0}}, | ||||
|                                             // INKPLATE_10
 | ||||
|                                             {{0, 0, 0, 0, 0, 0, 0, 1, 0}, | ||||
|                                              {0, 0, 0, 2, 2, 2, 1, 1, 0}, | ||||
|                                              {0, 0, 2, 1, 1, 2, 2, 1, 0}, | ||||
|                                              {0, 1, 2, 2, 1, 2, 2, 1, 0}, | ||||
|                                              {0, 0, 2, 1, 2, 2, 2, 1, 0}, | ||||
|                                              {0, 2, 2, 2, 2, 2, 2, 1, 0}, | ||||
|                                              {0, 0, 0, 0, 0, 2, 1, 2, 0}, | ||||
|                                              {0, 0, 0, 2, 2, 2, 2, 2, 0}}, | ||||
|                                             // INKPLATE_6_PLUS
 | ||||
|                                             {{0, 0, 0, 0, 0, 2, 1, 1, 0}, | ||||
|                                              {0, 0, 2, 1, 1, 1, 2, 1, 0}, | ||||
|                                              {0, 2, 2, 2, 1, 1, 2, 1, 0}, | ||||
|                                              {0, 0, 2, 2, 2, 1, 2, 1, 0}, | ||||
|                                              {0, 0, 0, 0, 2, 2, 2, 1, 0}, | ||||
|                                              {0, 0, 2, 1, 2, 1, 1, 2, 0}, | ||||
|                                              {0, 0, 2, 2, 2, 1, 1, 2, 0}, | ||||
|                                              {0, 0, 0, 0, 2, 2, 2, 2, 0}}, | ||||
|                                             // INKPLATE_6_V2
 | ||||
|                                             {{1, 0, 1, 0, 1, 1, 1, 0, 0}, | ||||
|                                              {0, 0, 0, 1, 1, 1, 1, 0, 0}, | ||||
|                                              {1, 1, 1, 1, 0, 2, 1, 0, 0}, | ||||
|                                              {1, 1, 1, 2, 2, 1, 1, 0, 0}, | ||||
|                                              {1, 1, 1, 1, 2, 2, 1, 0, 0}, | ||||
|                                              {0, 1, 1, 1, 2, 2, 1, 0, 0}, | ||||
|                                              {0, 0, 0, 0, 1, 1, 2, 0, 0}, | ||||
|                                              {0, 0, 0, 0, 0, 1, 2, 0, 0}}, | ||||
|                                             // INKPLATE_5
 | ||||
|                                             {{0, 0, 1, 1, 0, 1, 1, 1, 0}, | ||||
|                                              {0, 1, 1, 1, 1, 2, 0, 1, 0}, | ||||
|                                              {1, 2, 2, 0, 2, 1, 1, 1, 0}, | ||||
|                                              {1, 1, 1, 2, 0, 1, 1, 2, 0}, | ||||
|                                              {0, 1, 1, 1, 2, 0, 1, 2, 0}, | ||||
|                                              {0, 0, 0, 1, 1, 2, 1, 2, 0}, | ||||
|                                              {1, 1, 1, 2, 0, 2, 1, 2, 0}, | ||||
|                                              {0, 0, 0, 0, 0, 0, 0, 0, 0}}, | ||||
|                                             // INKPLATE_5_V2
 | ||||
|                                             {{0, 0, 1, 1, 2, 1, 1, 1, 0}, | ||||
|                                              {1, 1, 2, 2, 1, 2, 1, 1, 0}, | ||||
|                                              {0, 1, 2, 2, 1, 1, 2, 1, 0}, | ||||
|                                              {0, 0, 1, 1, 1, 1, 1, 2, 0}, | ||||
|                                              {1, 2, 1, 2, 1, 1, 1, 2, 0}, | ||||
|                                              {0, 1, 1, 1, 2, 0, 1, 2, 0}, | ||||
|                                              {1, 1, 1, 2, 2, 2, 1, 2, 0}, | ||||
|                                              {0, 0, 0, 0, 0, 0, 0, 0, 0}}}; | ||||
| 
 | ||||
|   void set_greyscale(bool greyscale) { | ||||
|     this->greyscale_ = greyscale; | ||||
|     this->block_partial_ = true; | ||||
|     if (this->is_ready()) | ||||
|       this->initialize_(); | ||||
|   } | ||||
| 
 | ||||
|   void set_waveform(const std::array<uint8_t, GLUT_COUNT * GLUT_SIZE> &waveform, bool is_custom) { | ||||
|     static_assert(sizeof(this->waveform_) == sizeof(uint8_t) * GLUT_COUNT * GLUT_SIZE, | ||||
|                   "waveform_ buffer size must match input waveform array size"); | ||||
|     memmove(this->waveform_, waveform.data(), sizeof(this->waveform_)); | ||||
|     this->custom_waveform_ = is_custom; | ||||
|   } | ||||
| 
 | ||||
|   void set_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; } | ||||
|   void set_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; } | ||||
| 
 | ||||
| @@ -225,6 +183,8 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { | ||||
|   bool mirror_y_{false}; | ||||
|   bool mirror_x_{false}; | ||||
|   bool partial_updating_; | ||||
|   bool custom_waveform_{false}; | ||||
|   uint8_t waveform_[GLUT_COUNT][GLUT_SIZE]; | ||||
| 
 | ||||
|   InkplateModel model_; | ||||
| 
 | ||||
| @@ -250,5 +210,5 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { | ||||
|   GPIOPin *wakeup_pin_; | ||||
| }; | ||||
| 
 | ||||
| }  // namespace inkplate6
 | ||||
| }  // namespace inkplate
 | ||||
| }  // namespace esphome
 | ||||
| @@ -1 +0,0 @@ | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
|   | ||||
| @@ -1,214 +1,5 @@ | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import display, i2c | ||||
| from esphome.components.esp32 import CONF_CPU_FREQUENCY | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_FULL_UPDATE_EVERY, | ||||
|     CONF_ID, | ||||
|     CONF_LAMBDA, | ||||
|     CONF_MIRROR_X, | ||||
|     CONF_MIRROR_Y, | ||||
|     CONF_MODEL, | ||||
|     CONF_OE_PIN, | ||||
|     CONF_PAGES, | ||||
|     CONF_TRANSFORM, | ||||
|     CONF_WAKEUP_PIN, | ||||
|     PLATFORM_ESP32, | ||||
|  | ||||
| CONFIG_SCHEMA = cv.invalid( | ||||
|     "The inkplate6 display component has been renamed to inkplate." | ||||
| ) | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| DEPENDENCIES = ["i2c", "esp32"] | ||||
| AUTO_LOAD = ["psram"] | ||||
|  | ||||
| CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" | ||||
| CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" | ||||
| CONF_DISPLAY_DATA_2_PIN = "display_data_2_pin" | ||||
| CONF_DISPLAY_DATA_3_PIN = "display_data_3_pin" | ||||
| CONF_DISPLAY_DATA_4_PIN = "display_data_4_pin" | ||||
| CONF_DISPLAY_DATA_5_PIN = "display_data_5_pin" | ||||
| CONF_DISPLAY_DATA_6_PIN = "display_data_6_pin" | ||||
| CONF_DISPLAY_DATA_7_PIN = "display_data_7_pin" | ||||
|  | ||||
| CONF_CL_PIN = "cl_pin" | ||||
| CONF_CKV_PIN = "ckv_pin" | ||||
| CONF_GREYSCALE = "greyscale" | ||||
| CONF_GMOD_PIN = "gmod_pin" | ||||
| CONF_GPIO0_ENABLE_PIN = "gpio0_enable_pin" | ||||
| CONF_LE_PIN = "le_pin" | ||||
| CONF_PARTIAL_UPDATING = "partial_updating" | ||||
| CONF_POWERUP_PIN = "powerup_pin" | ||||
| CONF_SPH_PIN = "sph_pin" | ||||
| CONF_SPV_PIN = "spv_pin" | ||||
| CONF_VCOM_PIN = "vcom_pin" | ||||
|  | ||||
| inkplate6_ns = cg.esphome_ns.namespace("inkplate6") | ||||
| Inkplate6 = inkplate6_ns.class_( | ||||
|     "Inkplate6", | ||||
|     cg.PollingComponent, | ||||
|     i2c.I2CDevice, | ||||
|     display.Display, | ||||
|     display.DisplayBuffer, | ||||
| ) | ||||
|  | ||||
| InkplateModel = inkplate6_ns.enum("InkplateModel") | ||||
|  | ||||
| MODELS = { | ||||
|     "inkplate_6": InkplateModel.INKPLATE_6, | ||||
|     "inkplate_10": InkplateModel.INKPLATE_10, | ||||
|     "inkplate_6_plus": InkplateModel.INKPLATE_6_PLUS, | ||||
|     "inkplate_6_v2": InkplateModel.INKPLATE_6_V2, | ||||
|     "inkplate_5": InkplateModel.INKPLATE_5, | ||||
|     "inkplate_5_v2": InkplateModel.INKPLATE_5_V2, | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     display.FULL_DISPLAY_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(Inkplate6), | ||||
|             cv.Optional(CONF_GREYSCALE, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_TRANSFORM): cv.Schema( | ||||
|                 { | ||||
|                     cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, | ||||
|                     cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_PARTIAL_UPDATING, default=True): cv.boolean, | ||||
|             cv.Optional(CONF_FULL_UPDATE_EVERY, default=10): cv.uint32_t, | ||||
|             cv.Optional(CONF_MODEL, default="inkplate_6"): cv.enum( | ||||
|                 MODELS, lower=True, space="_" | ||||
|             ), | ||||
|             # Control pins | ||||
|             cv.Required(CONF_CKV_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_GMOD_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_GPIO0_ENABLE_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_OE_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_POWERUP_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_SPH_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_SPV_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_VCOM_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_WAKEUP_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Optional(CONF_CL_PIN, default=0): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional(CONF_LE_PIN, default=2): pins.internal_gpio_output_pin_schema, | ||||
|             # Data pins | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_0_PIN, default=4 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_1_PIN, default=5 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_2_PIN, default=18 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_3_PIN, default=19 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_4_PIN, default=23 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_5_PIN, default=25 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_6_PIN, default=26 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Optional( | ||||
|                 CONF_DISPLAY_DATA_7_PIN, default=27 | ||||
|             ): pins.internal_gpio_output_pin_schema, | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("5s")) | ||||
|     .extend(i2c.i2c_device_schema(0x48)), | ||||
|     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _validate_cpu_frequency(config): | ||||
|     esp32_config = fv.full_config.get()[PLATFORM_ESP32] | ||||
|     if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ": | ||||
|         raise cv.Invalid( | ||||
|             "Inkplate requires 240MHz CPU frequency (set in esp32 component)" | ||||
|         ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _validate_cpu_frequency | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|  | ||||
|     await display.register_display(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     if CONF_LAMBDA in config: | ||||
|         lambda_ = await cg.process_lambda( | ||||
|             config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void | ||||
|         ) | ||||
|         cg.add(var.set_writer(lambda_)) | ||||
|  | ||||
|     cg.add(var.set_greyscale(config[CONF_GREYSCALE])) | ||||
|     if transform := config.get(CONF_TRANSFORM): | ||||
|         cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) | ||||
|         cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) | ||||
|     cg.add(var.set_partial_updating(config[CONF_PARTIAL_UPDATING])) | ||||
|     cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) | ||||
|  | ||||
|     cg.add(var.set_model(config[CONF_MODEL])) | ||||
|  | ||||
|     ckv = await cg.gpio_pin_expression(config[CONF_CKV_PIN]) | ||||
|     cg.add(var.set_ckv_pin(ckv)) | ||||
|  | ||||
|     gmod = await cg.gpio_pin_expression(config[CONF_GMOD_PIN]) | ||||
|     cg.add(var.set_gmod_pin(gmod)) | ||||
|  | ||||
|     gpio0_enable = await cg.gpio_pin_expression(config[CONF_GPIO0_ENABLE_PIN]) | ||||
|     cg.add(var.set_gpio0_enable_pin(gpio0_enable)) | ||||
|  | ||||
|     oe = await cg.gpio_pin_expression(config[CONF_OE_PIN]) | ||||
|     cg.add(var.set_oe_pin(oe)) | ||||
|  | ||||
|     powerup = await cg.gpio_pin_expression(config[CONF_POWERUP_PIN]) | ||||
|     cg.add(var.set_powerup_pin(powerup)) | ||||
|  | ||||
|     sph = await cg.gpio_pin_expression(config[CONF_SPH_PIN]) | ||||
|     cg.add(var.set_sph_pin(sph)) | ||||
|  | ||||
|     spv = await cg.gpio_pin_expression(config[CONF_SPV_PIN]) | ||||
|     cg.add(var.set_spv_pin(spv)) | ||||
|  | ||||
|     vcom = await cg.gpio_pin_expression(config[CONF_VCOM_PIN]) | ||||
|     cg.add(var.set_vcom_pin(vcom)) | ||||
|  | ||||
|     wakeup = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) | ||||
|     cg.add(var.set_wakeup_pin(wakeup)) | ||||
|  | ||||
|     cl = await cg.gpio_pin_expression(config[CONF_CL_PIN]) | ||||
|     cg.add(var.set_cl_pin(cl)) | ||||
|  | ||||
|     le = await cg.gpio_pin_expression(config[CONF_LE_PIN]) | ||||
|     cg.add(var.set_le_pin(le)) | ||||
|  | ||||
|     display_data_0 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_0_PIN]) | ||||
|     cg.add(var.set_display_data_0_pin(display_data_0)) | ||||
|  | ||||
|     display_data_1 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_1_PIN]) | ||||
|     cg.add(var.set_display_data_1_pin(display_data_1)) | ||||
|  | ||||
|     display_data_2 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_2_PIN]) | ||||
|     cg.add(var.set_display_data_2_pin(display_data_2)) | ||||
|  | ||||
|     display_data_3 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_3_PIN]) | ||||
|     cg.add(var.set_display_data_3_pin(display_data_3)) | ||||
|  | ||||
|     display_data_4 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_4_PIN]) | ||||
|     cg.add(var.set_display_data_4_pin(display_data_4)) | ||||
|  | ||||
|     display_data_5 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_5_PIN]) | ||||
|     cg.add(var.set_display_data_5_pin(display_data_5)) | ||||
|  | ||||
|     display_data_6 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_6_PIN]) | ||||
|     cg.add(var.set_display_data_6_pin(display_data_6)) | ||||
|  | ||||
|     display_data_7 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_7_PIN]) | ||||
|     cg.add(var.set_display_data_7_pin(display_data_7)) | ||||
|   | ||||
| @@ -196,8 +196,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( | ||||
|             PlatformFramework.ESP32_ARDUINO, | ||||
|             PlatformFramework.ESP32_IDF, | ||||
|         }, | ||||
|         "remote_receiver_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, | ||||
|         "remote_receiver_libretiny.cpp": { | ||||
|         "remote_receiver.cpp": { | ||||
|             PlatformFramework.ESP8266_ARDUINO, | ||||
|             PlatformFramework.BK72XX_ARDUINO, | ||||
|             PlatformFramework.RTL87XX_ARDUINO, | ||||
|             PlatformFramework.LN882X_ARDUINO, | ||||
|   | ||||
| @@ -3,12 +3,12 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| 
 | ||||
| #ifdef USE_ESP8266 | ||||
| #if defined(USE_LIBRETINY) || defined(USE_ESP8266) | ||||
| 
 | ||||
| namespace esphome { | ||||
| namespace remote_receiver { | ||||
| 
 | ||||
| static const char *const TAG = "remote_receiver.esp8266"; | ||||
| static const char *const TAG = "remote_receiver"; | ||||
| 
 | ||||
| void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) { | ||||
|   const uint32_t now = micros(); | ||||
| @@ -1,125 +0,0 @@ | ||||
| #include "remote_receiver.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_LIBRETINY | ||||
|  | ||||
| namespace esphome { | ||||
| namespace remote_receiver { | ||||
|  | ||||
| static const char *const TAG = "remote_receiver.libretiny"; | ||||
|  | ||||
| void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) { | ||||
|   const uint32_t now = micros(); | ||||
|   // If the lhs is 1 (rising edge) we should write to an uneven index and vice versa | ||||
|   const uint32_t next = (arg->buffer_write_at + 1) % arg->buffer_size; | ||||
|   const bool level = arg->pin.digital_read(); | ||||
|   if (level != next % 2) | ||||
|     return; | ||||
|  | ||||
|   // If next is buffer_read, we have hit an overflow | ||||
|   if (next == arg->buffer_read_at) | ||||
|     return; | ||||
|  | ||||
|   const uint32_t last_change = arg->buffer[arg->buffer_write_at]; | ||||
|   const uint32_t time_since_change = now - last_change; | ||||
|   if (time_since_change <= arg->filter_us) | ||||
|     return; | ||||
|  | ||||
|   arg->buffer[arg->buffer_write_at = next] = now; | ||||
| } | ||||
|  | ||||
| void RemoteReceiverComponent::setup() { | ||||
|   this->pin_->setup(); | ||||
|   auto &s = this->store_; | ||||
|   s.filter_us = this->filter_us_; | ||||
|   s.pin = this->pin_->to_isr(); | ||||
|   s.buffer_size = this->buffer_size_; | ||||
|  | ||||
|   this->high_freq_.start(); | ||||
|   if (s.buffer_size % 2 != 0) { | ||||
|     // Make sure divisible by two. This way, we know that every 0bxxx0 index is a space and every 0bxxx1 index is a mark | ||||
|     s.buffer_size++; | ||||
|   } | ||||
|  | ||||
|   s.buffer = new uint32_t[s.buffer_size]; | ||||
|   void *buf = (void *) s.buffer; | ||||
|   memset(buf, 0, s.buffer_size * sizeof(uint32_t)); | ||||
|  | ||||
|   // First index is a space. | ||||
|   if (this->pin_->digital_read()) { | ||||
|     s.buffer_write_at = s.buffer_read_at = 1; | ||||
|   } else { | ||||
|     s.buffer_write_at = s.buffer_read_at = 0; | ||||
|   } | ||||
|   this->pin_->attach_interrupt(RemoteReceiverComponentStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); | ||||
| } | ||||
| void RemoteReceiverComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "Remote Receiver:"); | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
|   if (this->pin_->digital_read()) { | ||||
|     ESP_LOGW(TAG, "Remote Receiver Signal starts with a HIGH value. Usually this means you have to " | ||||
|                   "invert the signal using 'inverted: True' in the pin schema!"); | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Buffer Size: %u\n" | ||||
|                 "  Tolerance: %u%s\n" | ||||
|                 "  Filter out pulses shorter than: %u us\n" | ||||
|                 "  Signal is done after %u us of no changes", | ||||
|                 this->buffer_size_, this->tolerance_, | ||||
|                 (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_, | ||||
|                 this->idle_us_); | ||||
| } | ||||
|  | ||||
| void RemoteReceiverComponent::loop() { | ||||
|   auto &s = this->store_; | ||||
|  | ||||
|   // copy write at to local variables, as it's volatile | ||||
|   const uint32_t write_at = s.buffer_write_at; | ||||
|   const uint32_t dist = (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size; | ||||
|   // signals must at least one rising and one leading edge | ||||
|   if (dist <= 1) | ||||
|     return; | ||||
|   const uint32_t now = micros(); | ||||
|   if (now - s.buffer[write_at] < this->idle_us_) { | ||||
|     // The last change was fewer than the configured idle time ago. | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGVV(TAG, "read_at=%u write_at=%u dist=%u now=%u end=%u", s.buffer_read_at, write_at, dist, now, | ||||
|             s.buffer[write_at]); | ||||
|  | ||||
|   // Skip first value, it's from the previous idle level | ||||
|   s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; | ||||
|   uint32_t prev = s.buffer_read_at; | ||||
|   s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; | ||||
|   const uint32_t reserve_size = 1 + (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size; | ||||
|   this->temp_.clear(); | ||||
|   this->temp_.reserve(reserve_size); | ||||
|   int32_t multiplier = s.buffer_read_at % 2 == 0 ? 1 : -1; | ||||
|  | ||||
|   for (uint32_t i = 0; prev != write_at; i++) { | ||||
|     int32_t delta = s.buffer[s.buffer_read_at] - s.buffer[prev]; | ||||
|     if (uint32_t(delta) >= this->idle_us_) { | ||||
|       // already found a space longer than idle. There must have been two pulses | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     ESP_LOGVV(TAG, "  i=%u buffer[%u]=%u - buffer[%u]=%u -> %d", i, s.buffer_read_at, s.buffer[s.buffer_read_at], prev, | ||||
|               s.buffer[prev], multiplier * delta); | ||||
|     this->temp_.push_back(multiplier * delta); | ||||
|     prev = s.buffer_read_at; | ||||
|     s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; | ||||
|     multiplier *= -1; | ||||
|   } | ||||
|   s.buffer_read_at = (s.buffer_size + s.buffer_read_at - 1) % s.buffer_size; | ||||
|   this->temp_.push_back(this->idle_us_ * multiplier); | ||||
|  | ||||
|   this->call_listeners_dumpers_(); | ||||
| } | ||||
|  | ||||
| }  // namespace remote_receiver | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -131,8 +131,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( | ||||
|             PlatformFramework.ESP32_ARDUINO, | ||||
|             PlatformFramework.ESP32_IDF, | ||||
|         }, | ||||
|         "remote_transmitter_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, | ||||
|         "remote_transmitter_libretiny.cpp": { | ||||
|         "remote_transmitter.cpp": { | ||||
|             PlatformFramework.ESP8266_ARDUINO, | ||||
|             PlatformFramework.BK72XX_ARDUINO, | ||||
|             PlatformFramework.RTL87XX_ARDUINO, | ||||
|             PlatformFramework.LN882X_ARDUINO, | ||||
|   | ||||
| @@ -2,10 +2,107 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| #if defined(USE_LIBRETINY) || defined(USE_ESP8266) | ||||
|  | ||||
| namespace esphome { | ||||
| namespace remote_transmitter { | ||||
|  | ||||
| static const char *const TAG = "remote_transmitter"; | ||||
|  | ||||
| void RemoteTransmitterComponent::setup() { | ||||
|   this->pin_->setup(); | ||||
|   this->pin_->digital_write(false); | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "Remote Transmitter:\n" | ||||
|                 "  Carrier Duty: %u%%", | ||||
|                 this->carrier_duty_percent_); | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, | ||||
|                                                         uint32_t *off_time_period) { | ||||
|   if (carrier_frequency == 0) { | ||||
|     *on_time_period = 0; | ||||
|     *off_time_period = 0; | ||||
|     return; | ||||
|   } | ||||
|   uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency;  // round(1000000/freq) | ||||
|   period = std::max(uint32_t(1), period); | ||||
|   *on_time_period = (period * this->carrier_duty_percent_) / 100; | ||||
|   *off_time_period = period - *on_time_period; | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::await_target_time_() { | ||||
|   const uint32_t current_time = micros(); | ||||
|   if (this->target_time_ == 0) { | ||||
|     this->target_time_ = current_time; | ||||
|   } else if ((int32_t) (this->target_time_ - current_time) > 0) { | ||||
|     delayMicroseconds(this->target_time_ - current_time); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { | ||||
|   this->await_target_time_(); | ||||
|   this->pin_->digital_write(true); | ||||
|  | ||||
|   const uint32_t target = this->target_time_ + usec; | ||||
|   if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { | ||||
|     while (true) {  // Modulate with carrier frequency | ||||
|       this->target_time_ += on_time; | ||||
|       if ((int32_t) (this->target_time_ - target) >= 0) | ||||
|         break; | ||||
|       this->await_target_time_(); | ||||
|       this->pin_->digital_write(false); | ||||
|  | ||||
|       this->target_time_ += off_time; | ||||
|       if ((int32_t) (this->target_time_ - target) >= 0) | ||||
|         break; | ||||
|       this->await_target_time_(); | ||||
|       this->pin_->digital_write(true); | ||||
|     } | ||||
|   } | ||||
|   this->target_time_ = target; | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::space_(uint32_t usec) { | ||||
|   this->await_target_time_(); | ||||
|   this->pin_->digital_write(false); | ||||
|   this->target_time_ += usec; | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); } | ||||
|  | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   ESP_LOGD(TAG, "Sending remote code"); | ||||
|   uint32_t on_time, off_time; | ||||
|   this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); | ||||
|   this->target_time_ = 0; | ||||
|   this->transmit_trigger_->trigger(); | ||||
|   for (uint32_t i = 0; i < send_times; i++) { | ||||
|     InterruptLock lock; | ||||
|     for (int32_t item : this->temp_.get_data()) { | ||||
|       if (item > 0) { | ||||
|         const auto length = uint32_t(item); | ||||
|         this->mark_(on_time, off_time, length); | ||||
|       } else { | ||||
|         const auto length = uint32_t(-item); | ||||
|         this->space_(length); | ||||
|       } | ||||
|       App.feed_wdt(); | ||||
|     } | ||||
|     this->await_target_time_();  // wait for duration of last pulse | ||||
|     this->pin_->digital_write(false); | ||||
|  | ||||
|     if (i + 1 < send_times) | ||||
|       this->target_time_ += send_wait; | ||||
|   } | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
|  | ||||
| }  // namespace remote_transmitter | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
|   | ||||
| @@ -1,107 +0,0 @@ | ||||
| #include "remote_transmitter.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace remote_transmitter { | ||||
|  | ||||
| static const char *const TAG = "remote_transmitter"; | ||||
|  | ||||
| void RemoteTransmitterComponent::setup() { | ||||
|   this->pin_->setup(); | ||||
|   this->pin_->digital_write(false); | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "Remote Transmitter:\n" | ||||
|                 "  Carrier Duty: %u%%", | ||||
|                 this->carrier_duty_percent_); | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, | ||||
|                                                         uint32_t *off_time_period) { | ||||
|   if (carrier_frequency == 0) { | ||||
|     *on_time_period = 0; | ||||
|     *off_time_period = 0; | ||||
|     return; | ||||
|   } | ||||
|   uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency;  // round(1000000/freq) | ||||
|   period = std::max(uint32_t(1), period); | ||||
|   *on_time_period = (period * this->carrier_duty_percent_) / 100; | ||||
|   *off_time_period = period - *on_time_period; | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::await_target_time_() { | ||||
|   const uint32_t current_time = micros(); | ||||
|   if (this->target_time_ == 0) { | ||||
|     this->target_time_ = current_time; | ||||
|   } else if ((int32_t) (this->target_time_ - current_time) > 0) { | ||||
|     delayMicroseconds(this->target_time_ - current_time); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { | ||||
|   this->await_target_time_(); | ||||
|   this->pin_->digital_write(true); | ||||
|  | ||||
|   const uint32_t target = this->target_time_ + usec; | ||||
|   if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { | ||||
|     while (true) {  // Modulate with carrier frequency | ||||
|       this->target_time_ += on_time; | ||||
|       if ((int32_t) (this->target_time_ - target) >= 0) | ||||
|         break; | ||||
|       this->await_target_time_(); | ||||
|       this->pin_->digital_write(false); | ||||
|  | ||||
|       this->target_time_ += off_time; | ||||
|       if ((int32_t) (this->target_time_ - target) >= 0) | ||||
|         break; | ||||
|       this->await_target_time_(); | ||||
|       this->pin_->digital_write(true); | ||||
|     } | ||||
|   } | ||||
|   this->target_time_ = target; | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::space_(uint32_t usec) { | ||||
|   this->await_target_time_(); | ||||
|   this->pin_->digital_write(false); | ||||
|   this->target_time_ += usec; | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); } | ||||
|  | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   ESP_LOGD(TAG, "Sending remote code"); | ||||
|   uint32_t on_time, off_time; | ||||
|   this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); | ||||
|   this->target_time_ = 0; | ||||
|   this->transmit_trigger_->trigger(); | ||||
|   for (uint32_t i = 0; i < send_times; i++) { | ||||
|     for (int32_t item : this->temp_.get_data()) { | ||||
|       if (item > 0) { | ||||
|         const auto length = uint32_t(item); | ||||
|         this->mark_(on_time, off_time, length); | ||||
|       } else { | ||||
|         const auto length = uint32_t(-item); | ||||
|         this->space_(length); | ||||
|       } | ||||
|       App.feed_wdt(); | ||||
|     } | ||||
|     this->await_target_time_();  // wait for duration of last pulse | ||||
|     this->pin_->digital_write(false); | ||||
|  | ||||
|     if (i + 1 < send_times) | ||||
|       this->target_time_ += send_wait; | ||||
|   } | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
|  | ||||
| }  // namespace remote_transmitter | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -1,110 +0,0 @@ | ||||
| #include "remote_transmitter.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| #ifdef USE_LIBRETINY | ||||
|  | ||||
| namespace esphome { | ||||
| namespace remote_transmitter { | ||||
|  | ||||
| static const char *const TAG = "remote_transmitter"; | ||||
|  | ||||
| void RemoteTransmitterComponent::setup() { | ||||
|   this->pin_->setup(); | ||||
|   this->pin_->digital_write(false); | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "Remote Transmitter:\n" | ||||
|                 "  Carrier Duty: %u%%", | ||||
|                 this->carrier_duty_percent_); | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, | ||||
|                                                         uint32_t *off_time_period) { | ||||
|   if (carrier_frequency == 0) { | ||||
|     *on_time_period = 0; | ||||
|     *off_time_period = 0; | ||||
|     return; | ||||
|   } | ||||
|   uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency;  // round(1000000/freq) | ||||
|   period = std::max(uint32_t(1), period); | ||||
|   *on_time_period = (period * this->carrier_duty_percent_) / 100; | ||||
|   *off_time_period = period - *on_time_period; | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::await_target_time_() { | ||||
|   const uint32_t current_time = micros(); | ||||
|   if (this->target_time_ == 0) { | ||||
|     this->target_time_ = current_time; | ||||
|   } else { | ||||
|     while ((int32_t) (this->target_time_ - micros()) > 0) { | ||||
|       // busy loop that ensures micros is constantly called | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { | ||||
|   this->await_target_time_(); | ||||
|   this->pin_->digital_write(true); | ||||
|  | ||||
|   const uint32_t target = this->target_time_ + usec; | ||||
|   if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { | ||||
|     while (true) {  // Modulate with carrier frequency | ||||
|       this->target_time_ += on_time; | ||||
|       if ((int32_t) (this->target_time_ - target) >= 0) | ||||
|         break; | ||||
|       this->await_target_time_(); | ||||
|       this->pin_->digital_write(false); | ||||
|  | ||||
|       this->target_time_ += off_time; | ||||
|       if ((int32_t) (this->target_time_ - target) >= 0) | ||||
|         break; | ||||
|       this->await_target_time_(); | ||||
|       this->pin_->digital_write(true); | ||||
|     } | ||||
|   } | ||||
|   this->target_time_ = target; | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::space_(uint32_t usec) { | ||||
|   this->await_target_time_(); | ||||
|   this->pin_->digital_write(false); | ||||
|   this->target_time_ += usec; | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); } | ||||
|  | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   ESP_LOGD(TAG, "Sending remote code"); | ||||
|   uint32_t on_time, off_time; | ||||
|   this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); | ||||
|   this->target_time_ = 0; | ||||
|   this->transmit_trigger_->trigger(); | ||||
|   for (uint32_t i = 0; i < send_times; i++) { | ||||
|     InterruptLock lock; | ||||
|     for (int32_t item : this->temp_.get_data()) { | ||||
|       if (item > 0) { | ||||
|         const auto length = uint32_t(item); | ||||
|         this->mark_(on_time, off_time, length); | ||||
|       } else { | ||||
|         const auto length = uint32_t(-item); | ||||
|         this->space_(length); | ||||
|       } | ||||
|       App.feed_wdt(); | ||||
|     } | ||||
|     this->await_target_time_();  // wait for duration of last pulse | ||||
|     this->pin_->digital_write(false); | ||||
|  | ||||
|     if (i + 1 < send_times) | ||||
|       this->target_time_ += send_wait; | ||||
|   } | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
|  | ||||
| }  // namespace remote_transmitter | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -32,6 +32,7 @@ from esphome.const import ( | ||||
|     CONF_FAN_WITH_COOLING, | ||||
|     CONF_FAN_WITH_HEATING, | ||||
|     CONF_HEAT_ACTION, | ||||
|     CONF_HEAT_COOL_MODE, | ||||
|     CONF_HEAT_DEADBAND, | ||||
|     CONF_HEAT_MODE, | ||||
|     CONF_HEAT_OVERRUN, | ||||
| @@ -150,7 +151,7 @@ def generate_comparable_preset(config, name): | ||||
| def validate_thermostat(config): | ||||
|     # verify corresponding action(s) exist(s) for any defined climate mode or action | ||||
|     requirements = { | ||||
|         CONF_AUTO_MODE: [ | ||||
|         CONF_HEAT_COOL_MODE: [ | ||||
|             CONF_COOL_ACTION, | ||||
|             CONF_HEAT_ACTION, | ||||
|             CONF_MIN_COOLING_OFF_TIME, | ||||
| @@ -540,6 +541,9 @@ CONFIG_SCHEMA = cv.All( | ||||
|             cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation( | ||||
|                 single=True | ||||
|             ), | ||||
|             cv.Optional(CONF_HEAT_COOL_MODE): automation.validate_automation( | ||||
|                 single=True | ||||
|             ), | ||||
|             cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True), | ||||
|             cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True), | ||||
|             cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation( | ||||
| @@ -644,7 +648,6 @@ async def to_code(config): | ||||
|     var = await climate.new_climate(config) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     heat_cool_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config | ||||
|     two_points_available = CONF_HEAT_ACTION in config and ( | ||||
|         CONF_COOL_ACTION in config | ||||
|         or (config[CONF_FAN_ONLY_COOLING] and CONF_FAN_ONLY_ACTION in config) | ||||
| @@ -739,11 +742,6 @@ async def to_code(config): | ||||
|         var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION] | ||||
|     ) | ||||
|  | ||||
|     if heat_cool_mode_available is True: | ||||
|         cg.add(var.set_supports_heat_cool(True)) | ||||
|     else: | ||||
|         cg.add(var.set_supports_heat_cool(False)) | ||||
|  | ||||
|     if CONF_COOL_ACTION in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_cool_action_trigger(), [], config[CONF_COOL_ACTION] | ||||
| @@ -780,6 +778,7 @@ async def to_code(config): | ||||
|         await automation.build_automation( | ||||
|             var.get_auto_mode_trigger(), [], config[CONF_AUTO_MODE] | ||||
|         ) | ||||
|         cg.add(var.set_supports_auto(True)) | ||||
|     if CONF_COOL_MODE in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_cool_mode_trigger(), [], config[CONF_COOL_MODE] | ||||
| @@ -800,6 +799,11 @@ async def to_code(config): | ||||
|             var.get_heat_mode_trigger(), [], config[CONF_HEAT_MODE] | ||||
|         ) | ||||
|         cg.add(var.set_supports_heat(True)) | ||||
|     if CONF_HEAT_COOL_MODE in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_heat_cool_mode_trigger(), [], config[CONF_HEAT_COOL_MODE] | ||||
|         ) | ||||
|         cg.add(var.set_supports_heat_cool(True)) | ||||
|     if CONF_OFF_MODE in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_off_mode_trigger(), [], config[CONF_OFF_MODE] | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| #include "thermostat_climate.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -9,11 +11,11 @@ static const char *const TAG = "thermostat.climate"; | ||||
| void ThermostatClimate::setup() { | ||||
|   if (this->use_startup_delay_) { | ||||
|     // start timers so that no actions are called for a moment | ||||
|     this->start_timer_(thermostat::TIMER_COOLING_OFF); | ||||
|     this->start_timer_(thermostat::TIMER_FANNING_OFF); | ||||
|     this->start_timer_(thermostat::TIMER_HEATING_OFF); | ||||
|     this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_OFF); | ||||
|     this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_OFF); | ||||
|     this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_OFF); | ||||
|     if (this->supports_fan_only_action_uses_fan_mode_timer_) | ||||
|       this->start_timer_(thermostat::TIMER_FAN_MODE); | ||||
|       this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); | ||||
|   } | ||||
|   // add a callback so that whenever the sensor state changes we can take action | ||||
|   this->sensor_->add_on_state_callback([this](float state) { | ||||
| @@ -64,7 +66,7 @@ void ThermostatClimate::setup() { | ||||
|  | ||||
| void ThermostatClimate::loop() { | ||||
|   for (auto &timer : this->timer_) { | ||||
|     if (timer.active && (timer.started + timer.time < millis())) { | ||||
|     if (timer.active && (timer.started + timer.time < App.get_loop_component_start_time())) { | ||||
|       timer.active = false; | ||||
|       timer.func(); | ||||
|     } | ||||
| @@ -127,26 +129,35 @@ bool ThermostatClimate::hysteresis_valid() { | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| bool ThermostatClimate::limit_setpoints_for_heat_cool() { | ||||
|   return this->mode == climate::CLIMATE_MODE_HEAT_COOL || | ||||
|          (this->mode == climate::CLIMATE_MODE_AUTO && this->supports_heat_cool_); | ||||
| } | ||||
|  | ||||
| void ThermostatClimate::validate_target_temperature() { | ||||
|   if (std::isnan(this->target_temperature)) { | ||||
|     // default to the midpoint between visual min and max | ||||
|     this->target_temperature = | ||||
|         ((this->get_traits().get_visual_max_temperature() - this->get_traits().get_visual_min_temperature()) / 2) + | ||||
|         this->get_traits().get_visual_min_temperature(); | ||||
|   } else { | ||||
|     // target_temperature must be between the visual minimum and the visual maximum | ||||
|     if (this->target_temperature < this->get_traits().get_visual_min_temperature()) | ||||
|       this->target_temperature = this->get_traits().get_visual_min_temperature(); | ||||
|     if (this->target_temperature > this->get_traits().get_visual_max_temperature()) | ||||
|       this->target_temperature = this->get_traits().get_visual_max_temperature(); | ||||
|     this->target_temperature = clamp(this->target_temperature, this->get_traits().get_visual_min_temperature(), | ||||
|                                      this->get_traits().get_visual_max_temperature()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ThermostatClimate::validate_target_temperatures() { | ||||
|   if (this->supports_two_points_) { | ||||
| void ThermostatClimate::validate_target_temperatures(const bool pin_target_temperature_high) { | ||||
|   if (!this->supports_two_points_) { | ||||
|     this->validate_target_temperature(); | ||||
|   } else if (pin_target_temperature_high) { | ||||
|     // if target_temperature_high is set less than target_temperature_low, move down target_temperature_low | ||||
|     this->validate_target_temperature_low(); | ||||
|     this->validate_target_temperature_high(); | ||||
|   } else { | ||||
|     this->validate_target_temperature(); | ||||
|     // if target_temperature_low is set greater than target_temperature_high, move up target_temperature_high | ||||
|     this->validate_target_temperature_high(); | ||||
|     this->validate_target_temperature_low(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -154,18 +165,13 @@ void ThermostatClimate::validate_target_temperature_low() { | ||||
|   if (std::isnan(this->target_temperature_low)) { | ||||
|     this->target_temperature_low = this->get_traits().get_visual_min_temperature(); | ||||
|   } else { | ||||
|     // target_temperature_low must not be lower than the visual minimum | ||||
|     if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) | ||||
|       this->target_temperature_low = this->get_traits().get_visual_min_temperature(); | ||||
|     // target_temperature_low must not be greater than the visual maximum minus set_point_minimum_differential_ | ||||
|     if (this->target_temperature_low > | ||||
|         this->get_traits().get_visual_max_temperature() - this->set_point_minimum_differential_) { | ||||
|       this->target_temperature_low = | ||||
|           this->get_traits().get_visual_max_temperature() - this->set_point_minimum_differential_; | ||||
|     } | ||||
|     // if target_temperature_low is set greater than target_temperature_high, move up target_temperature_high | ||||
|     if (this->target_temperature_low > this->target_temperature_high - this->set_point_minimum_differential_) | ||||
|       this->target_temperature_high = this->target_temperature_low + this->set_point_minimum_differential_; | ||||
|     float target_temperature_low_upper_limit = | ||||
|         this->limit_setpoints_for_heat_cool() | ||||
|             ? clamp(this->target_temperature_high - this->set_point_minimum_differential_, | ||||
|                     this->get_traits().get_visual_min_temperature(), this->get_traits().get_visual_max_temperature()) | ||||
|             : this->get_traits().get_visual_max_temperature(); | ||||
|     this->target_temperature_low = clamp(this->target_temperature_low, this->get_traits().get_visual_min_temperature(), | ||||
|                                          target_temperature_low_upper_limit); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -173,62 +179,64 @@ void ThermostatClimate::validate_target_temperature_high() { | ||||
|   if (std::isnan(this->target_temperature_high)) { | ||||
|     this->target_temperature_high = this->get_traits().get_visual_max_temperature(); | ||||
|   } else { | ||||
|     // target_temperature_high must not be lower than the visual maximum | ||||
|     if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) | ||||
|       this->target_temperature_high = this->get_traits().get_visual_max_temperature(); | ||||
|     // target_temperature_high must not be lower than the visual minimum plus set_point_minimum_differential_ | ||||
|     if (this->target_temperature_high < | ||||
|         this->get_traits().get_visual_min_temperature() + this->set_point_minimum_differential_) { | ||||
|       this->target_temperature_high = | ||||
|           this->get_traits().get_visual_min_temperature() + this->set_point_minimum_differential_; | ||||
|     } | ||||
|     // if target_temperature_high is set less than target_temperature_low, move down target_temperature_low | ||||
|     if (this->target_temperature_high < this->target_temperature_low + this->set_point_minimum_differential_) | ||||
|       this->target_temperature_low = this->target_temperature_high - this->set_point_minimum_differential_; | ||||
|     float target_temperature_high_lower_limit = | ||||
|         this->limit_setpoints_for_heat_cool() | ||||
|             ? clamp(this->target_temperature_low + this->set_point_minimum_differential_, | ||||
|                     this->get_traits().get_visual_min_temperature(), this->get_traits().get_visual_max_temperature()) | ||||
|             : this->get_traits().get_visual_min_temperature(); | ||||
|     this->target_temperature_high = clamp(this->target_temperature_high, target_temperature_high_lower_limit, | ||||
|                                           this->get_traits().get_visual_max_temperature()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ThermostatClimate::control(const climate::ClimateCall &call) { | ||||
|   bool target_temperature_high_changed = false; | ||||
|  | ||||
|   if (call.get_preset().has_value()) { | ||||
|     // setup_complete_ blocks modifying/resetting the temps immediately after boot | ||||
|     if (this->setup_complete_) { | ||||
|       this->change_preset_(*call.get_preset()); | ||||
|       this->change_preset_(call.get_preset().value()); | ||||
|     } else { | ||||
|       this->preset = *call.get_preset(); | ||||
|       this->preset = call.get_preset().value(); | ||||
|     } | ||||
|   } | ||||
|   if (call.get_custom_preset().has_value()) { | ||||
|     // setup_complete_ blocks modifying/resetting the temps immediately after boot | ||||
|     if (this->setup_complete_) { | ||||
|       this->change_custom_preset_(*call.get_custom_preset()); | ||||
|       this->change_custom_preset_(call.get_custom_preset().value()); | ||||
|     } else { | ||||
|       this->custom_preset = *call.get_custom_preset(); | ||||
|       this->custom_preset = call.get_custom_preset().value(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (call.get_mode().has_value()) | ||||
|     this->mode = *call.get_mode(); | ||||
|   if (call.get_fan_mode().has_value()) | ||||
|     this->fan_mode = *call.get_fan_mode(); | ||||
|   if (call.get_swing_mode().has_value()) | ||||
|     this->swing_mode = *call.get_swing_mode(); | ||||
|   if (call.get_mode().has_value()) { | ||||
|     this->mode = call.get_mode().value(); | ||||
|   } | ||||
|   if (call.get_fan_mode().has_value()) { | ||||
|     this->fan_mode = call.get_fan_mode().value(); | ||||
|   } | ||||
|   if (call.get_swing_mode().has_value()) { | ||||
|     this->swing_mode = call.get_swing_mode().value(); | ||||
|   } | ||||
|   if (this->supports_two_points_) { | ||||
|     if (call.get_target_temperature_low().has_value()) { | ||||
|       this->target_temperature_low = *call.get_target_temperature_low(); | ||||
|       validate_target_temperature_low(); | ||||
|       this->target_temperature_low = call.get_target_temperature_low().value(); | ||||
|     } | ||||
|     if (call.get_target_temperature_high().has_value()) { | ||||
|       this->target_temperature_high = *call.get_target_temperature_high(); | ||||
|       validate_target_temperature_high(); | ||||
|       target_temperature_high_changed = this->target_temperature_high != call.get_target_temperature_high().value(); | ||||
|       this->target_temperature_high = call.get_target_temperature_high().value(); | ||||
|     } | ||||
|     // ensure the two set points are valid and adjust one of them if necessary | ||||
|     this->validate_target_temperatures(target_temperature_high_changed || | ||||
|                                        (this->prev_mode_ == climate::CLIMATE_MODE_COOL)); | ||||
|   } else { | ||||
|     if (call.get_target_temperature().has_value()) { | ||||
|       this->target_temperature = *call.get_target_temperature(); | ||||
|       validate_target_temperature(); | ||||
|       this->target_temperature = call.get_target_temperature().value(); | ||||
|       this->validate_target_temperature(); | ||||
|     } | ||||
|   } | ||||
|   // make any changes happen | ||||
|   refresh(); | ||||
|   this->refresh(); | ||||
| } | ||||
|  | ||||
| climate::ClimateTraits ThermostatClimate::traits() { | ||||
| @@ -237,47 +245,47 @@ climate::ClimateTraits ThermostatClimate::traits() { | ||||
|   if (this->humidity_sensor_ != nullptr) | ||||
|     traits.set_supports_current_humidity(true); | ||||
|  | ||||
|   if (supports_auto_) | ||||
|   if (this->supports_auto_) | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_AUTO); | ||||
|   if (supports_heat_cool_) | ||||
|   if (this->supports_heat_cool_) | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); | ||||
|   if (supports_cool_) | ||||
|   if (this->supports_cool_) | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_COOL); | ||||
|   if (supports_dry_) | ||||
|   if (this->supports_dry_) | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_DRY); | ||||
|   if (supports_fan_only_) | ||||
|   if (this->supports_fan_only_) | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); | ||||
|   if (supports_heat_) | ||||
|   if (this->supports_heat_) | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); | ||||
|  | ||||
|   if (supports_fan_mode_on_) | ||||
|   if (this->supports_fan_mode_on_) | ||||
|     traits.add_supported_fan_mode(climate::CLIMATE_FAN_ON); | ||||
|   if (supports_fan_mode_off_) | ||||
|   if (this->supports_fan_mode_off_) | ||||
|     traits.add_supported_fan_mode(climate::CLIMATE_FAN_OFF); | ||||
|   if (supports_fan_mode_auto_) | ||||
|   if (this->supports_fan_mode_auto_) | ||||
|     traits.add_supported_fan_mode(climate::CLIMATE_FAN_AUTO); | ||||
|   if (supports_fan_mode_low_) | ||||
|   if (this->supports_fan_mode_low_) | ||||
|     traits.add_supported_fan_mode(climate::CLIMATE_FAN_LOW); | ||||
|   if (supports_fan_mode_medium_) | ||||
|   if (this->supports_fan_mode_medium_) | ||||
|     traits.add_supported_fan_mode(climate::CLIMATE_FAN_MEDIUM); | ||||
|   if (supports_fan_mode_high_) | ||||
|   if (this->supports_fan_mode_high_) | ||||
|     traits.add_supported_fan_mode(climate::CLIMATE_FAN_HIGH); | ||||
|   if (supports_fan_mode_middle_) | ||||
|   if (this->supports_fan_mode_middle_) | ||||
|     traits.add_supported_fan_mode(climate::CLIMATE_FAN_MIDDLE); | ||||
|   if (supports_fan_mode_focus_) | ||||
|   if (this->supports_fan_mode_focus_) | ||||
|     traits.add_supported_fan_mode(climate::CLIMATE_FAN_FOCUS); | ||||
|   if (supports_fan_mode_diffuse_) | ||||
|   if (this->supports_fan_mode_diffuse_) | ||||
|     traits.add_supported_fan_mode(climate::CLIMATE_FAN_DIFFUSE); | ||||
|   if (supports_fan_mode_quiet_) | ||||
|   if (this->supports_fan_mode_quiet_) | ||||
|     traits.add_supported_fan_mode(climate::CLIMATE_FAN_QUIET); | ||||
|  | ||||
|   if (supports_swing_mode_both_) | ||||
|   if (this->supports_swing_mode_both_) | ||||
|     traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); | ||||
|   if (supports_swing_mode_horizontal_) | ||||
|   if (this->supports_swing_mode_horizontal_) | ||||
|     traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); | ||||
|   if (supports_swing_mode_off_) | ||||
|   if (this->supports_swing_mode_off_) | ||||
|     traits.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); | ||||
|   if (supports_swing_mode_vertical_) | ||||
|   if (this->supports_swing_mode_vertical_) | ||||
|     traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); | ||||
|  | ||||
|   for (auto &it : this->preset_config_) { | ||||
| @@ -299,14 +307,15 @@ climate::ClimateAction ThermostatClimate::compute_action_(const bool ignore_time | ||||
|     return climate::CLIMATE_ACTION_OFF; | ||||
|   } | ||||
|   // do not change the action if an "ON" timer is running | ||||
|   if ((!ignore_timers) && | ||||
|       (timer_active_(thermostat::TIMER_IDLE_ON) || timer_active_(thermostat::TIMER_COOLING_ON) || | ||||
|        timer_active_(thermostat::TIMER_FANNING_ON) || timer_active_(thermostat::TIMER_HEATING_ON))) { | ||||
|   if ((!ignore_timers) && (this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || | ||||
|                            this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || | ||||
|                            this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_ON) || | ||||
|                            this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON))) { | ||||
|     return this->action; | ||||
|   } | ||||
|  | ||||
|   // ensure set point(s) is/are valid before computing the action | ||||
|   this->validate_target_temperatures(); | ||||
|   this->validate_target_temperatures(this->prev_mode_ == climate::CLIMATE_MODE_COOL); | ||||
|   // everything has been validated so we can now safely compute the action | ||||
|   switch (this->mode) { | ||||
|     // if the climate mode is OFF then the climate action must be OFF | ||||
| @@ -340,6 +349,22 @@ climate::ClimateAction ThermostatClimate::compute_action_(const bool ignore_time | ||||
|         target_action = climate::CLIMATE_ACTION_HEATING; | ||||
|       } | ||||
|       break; | ||||
|     case climate::CLIMATE_MODE_AUTO: | ||||
|       if (this->supports_two_points_) { | ||||
|         if (this->cooling_required_() && this->heating_required_()) { | ||||
|           // this is bad and should never happen, so just stop. | ||||
|           // target_action = climate::CLIMATE_ACTION_IDLE; | ||||
|         } else if (this->cooling_required_()) { | ||||
|           target_action = climate::CLIMATE_ACTION_COOLING; | ||||
|         } else if (this->heating_required_()) { | ||||
|           target_action = climate::CLIMATE_ACTION_HEATING; | ||||
|         } | ||||
|       } else if (this->supports_cool_ && this->cooling_required_()) { | ||||
|         target_action = climate::CLIMATE_ACTION_COOLING; | ||||
|       } else if (this->supports_heat_ && this->heating_required_()) { | ||||
|         target_action = climate::CLIMATE_ACTION_HEATING; | ||||
|       } | ||||
|       break; | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| @@ -362,7 +387,7 @@ climate::ClimateAction ThermostatClimate::compute_supplemental_action_() { | ||||
|   } | ||||
|  | ||||
|   // ensure set point(s) is/are valid before computing the action | ||||
|   this->validate_target_temperatures(); | ||||
|   this->validate_target_temperatures(this->prev_mode_ == climate::CLIMATE_MODE_COOL); | ||||
|   // everything has been validated so we can now safely compute the action | ||||
|   switch (this->mode) { | ||||
|     // if the climate mode is OFF then the climate action must be OFF | ||||
| @@ -420,18 +445,18 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu | ||||
|     case climate::CLIMATE_ACTION_OFF: | ||||
|     case climate::CLIMATE_ACTION_IDLE: | ||||
|       if (this->idle_action_ready_()) { | ||||
|         this->start_timer_(thermostat::TIMER_IDLE_ON); | ||||
|         this->start_timer_(thermostat::THERMOSTAT_TIMER_IDLE_ON); | ||||
|         if (this->action == climate::CLIMATE_ACTION_COOLING) | ||||
|           this->start_timer_(thermostat::TIMER_COOLING_OFF); | ||||
|           this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_OFF); | ||||
|         if (this->action == climate::CLIMATE_ACTION_FAN) { | ||||
|           if (this->supports_fan_only_action_uses_fan_mode_timer_) { | ||||
|             this->start_timer_(thermostat::TIMER_FAN_MODE); | ||||
|             this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); | ||||
|           } else { | ||||
|             this->start_timer_(thermostat::TIMER_FANNING_OFF); | ||||
|             this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_OFF); | ||||
|           } | ||||
|         } | ||||
|         if (this->action == climate::CLIMATE_ACTION_HEATING) | ||||
|           this->start_timer_(thermostat::TIMER_HEATING_OFF); | ||||
|           this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_OFF); | ||||
|         // trig = this->idle_action_trigger_; | ||||
|         ESP_LOGVV(TAG, "Switching to IDLE/OFF action"); | ||||
|         this->cooling_max_runtime_exceeded_ = false; | ||||
| @@ -441,10 +466,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu | ||||
|       break; | ||||
|     case climate::CLIMATE_ACTION_COOLING: | ||||
|       if (this->cooling_action_ready_()) { | ||||
|         this->start_timer_(thermostat::TIMER_COOLING_ON); | ||||
|         this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); | ||||
|         this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_ON); | ||||
|         this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); | ||||
|         if (this->supports_fan_with_cooling_) { | ||||
|           this->start_timer_(thermostat::TIMER_FANNING_ON); | ||||
|           this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); | ||||
|           trig_fan = this->fan_only_action_trigger_; | ||||
|         } | ||||
|         this->cooling_max_runtime_exceeded_ = false; | ||||
| @@ -455,10 +480,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu | ||||
|       break; | ||||
|     case climate::CLIMATE_ACTION_HEATING: | ||||
|       if (this->heating_action_ready_()) { | ||||
|         this->start_timer_(thermostat::TIMER_HEATING_ON); | ||||
|         this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); | ||||
|         this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_ON); | ||||
|         this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); | ||||
|         if (this->supports_fan_with_heating_) { | ||||
|           this->start_timer_(thermostat::TIMER_FANNING_ON); | ||||
|           this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); | ||||
|           trig_fan = this->fan_only_action_trigger_; | ||||
|         } | ||||
|         this->heating_max_runtime_exceeded_ = false; | ||||
| @@ -470,9 +495,9 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu | ||||
|     case climate::CLIMATE_ACTION_FAN: | ||||
|       if (this->fanning_action_ready_()) { | ||||
|         if (this->supports_fan_only_action_uses_fan_mode_timer_) { | ||||
|           this->start_timer_(thermostat::TIMER_FAN_MODE); | ||||
|           this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); | ||||
|         } else { | ||||
|           this->start_timer_(thermostat::TIMER_FANNING_ON); | ||||
|           this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); | ||||
|         } | ||||
|         trig = this->fan_only_action_trigger_; | ||||
|         ESP_LOGVV(TAG, "Switching to FAN_ONLY action"); | ||||
| @@ -481,8 +506,8 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu | ||||
|       break; | ||||
|     case climate::CLIMATE_ACTION_DRYING: | ||||
|       if (this->drying_action_ready_()) { | ||||
|         this->start_timer_(thermostat::TIMER_COOLING_ON); | ||||
|         this->start_timer_(thermostat::TIMER_FANNING_ON); | ||||
|         this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_ON); | ||||
|         this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); | ||||
|         trig = this->dry_action_trigger_; | ||||
|         ESP_LOGVV(TAG, "Switching to DRYING action"); | ||||
|         action_ready = true; | ||||
| @@ -525,14 +550,14 @@ void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction ac | ||||
|   switch (action) { | ||||
|     case climate::CLIMATE_ACTION_OFF: | ||||
|     case climate::CLIMATE_ACTION_IDLE: | ||||
|       this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); | ||||
|       this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); | ||||
|       this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); | ||||
|       this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); | ||||
|       break; | ||||
|     case climate::CLIMATE_ACTION_COOLING: | ||||
|       this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); | ||||
|       this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); | ||||
|       break; | ||||
|     case climate::CLIMATE_ACTION_HEATING: | ||||
|       this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); | ||||
|       this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); | ||||
|       break; | ||||
|     default: | ||||
|       return; | ||||
| @@ -547,15 +572,15 @@ void ThermostatClimate::trigger_supplemental_action_() { | ||||
|  | ||||
|   switch (this->supplemental_action_) { | ||||
|     case climate::CLIMATE_ACTION_COOLING: | ||||
|       if (!this->timer_active_(thermostat::TIMER_COOLING_MAX_RUN_TIME)) { | ||||
|         this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); | ||||
|       if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME)) { | ||||
|         this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); | ||||
|       } | ||||
|       trig = this->supplemental_cool_action_trigger_; | ||||
|       ESP_LOGVV(TAG, "Calling supplemental COOLING action"); | ||||
|       break; | ||||
|     case climate::CLIMATE_ACTION_HEATING: | ||||
|       if (!this->timer_active_(thermostat::TIMER_HEATING_MAX_RUN_TIME)) { | ||||
|         this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); | ||||
|       if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME)) { | ||||
|         this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); | ||||
|       } | ||||
|       trig = this->supplemental_heat_action_trigger_; | ||||
|       ESP_LOGVV(TAG, "Calling supplemental HEATING action"); | ||||
| @@ -633,7 +658,7 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bo | ||||
|       this->prev_fan_mode_trigger_->stop_action(); | ||||
|       this->prev_fan_mode_trigger_ = nullptr; | ||||
|     } | ||||
|     this->start_timer_(thermostat::TIMER_FAN_MODE); | ||||
|     this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); | ||||
|     if (trig != nullptr) { | ||||
|       trig->trigger(); | ||||
|     } | ||||
| @@ -653,13 +678,13 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ | ||||
|     this->prev_mode_trigger_->stop_action(); | ||||
|     this->prev_mode_trigger_ = nullptr; | ||||
|   } | ||||
|   Trigger<> *trig = this->auto_mode_trigger_; | ||||
|   Trigger<> *trig = this->off_mode_trigger_; | ||||
|   switch (mode) { | ||||
|     case climate::CLIMATE_MODE_OFF: | ||||
|       trig = this->off_mode_trigger_; | ||||
|     case climate::CLIMATE_MODE_AUTO: | ||||
|       trig = this->auto_mode_trigger_; | ||||
|       break; | ||||
|     case climate::CLIMATE_MODE_HEAT_COOL: | ||||
|       // trig = this->auto_mode_trigger_; | ||||
|       trig = this->heat_cool_mode_trigger_; | ||||
|       break; | ||||
|     case climate::CLIMATE_MODE_COOL: | ||||
|       trig = this->cool_mode_trigger_; | ||||
| @@ -673,11 +698,12 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ | ||||
|     case climate::CLIMATE_MODE_DRY: | ||||
|       trig = this->dry_mode_trigger_; | ||||
|       break; | ||||
|     case climate::CLIMATE_MODE_OFF: | ||||
|     default: | ||||
|       // we cannot report an invalid mode back to HA (even if it asked for one) | ||||
|       //  and must assume some valid value | ||||
|       mode = climate::CLIMATE_MODE_HEAT_COOL; | ||||
|       // trig = this->auto_mode_trigger_; | ||||
|       mode = climate::CLIMATE_MODE_OFF; | ||||
|       // trig = this->off_mode_trigger_; | ||||
|   } | ||||
|   if (trig != nullptr) { | ||||
|     trig->trigger(); | ||||
| @@ -685,8 +711,9 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ | ||||
|   this->mode = mode; | ||||
|   this->prev_mode_ = mode; | ||||
|   this->prev_mode_trigger_ = trig; | ||||
|   if (publish_state) | ||||
|   if (publish_state) { | ||||
|     this->publish_state(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode, bool publish_state) { | ||||
| @@ -732,35 +759,44 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo | ||||
|  | ||||
| bool ThermostatClimate::idle_action_ready_() { | ||||
|   if (this->supports_fan_only_action_uses_fan_mode_timer_) { | ||||
|     return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FAN_MODE) || | ||||
|              this->timer_active_(thermostat::TIMER_HEATING_ON)); | ||||
|     return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || | ||||
|              this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE) || | ||||
|              this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); | ||||
|   } | ||||
|   return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FANNING_ON) || | ||||
|            this->timer_active_(thermostat::TIMER_HEATING_ON)); | ||||
|   return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || | ||||
|            this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_ON) || | ||||
|            this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); | ||||
| } | ||||
|  | ||||
| bool ThermostatClimate::cooling_action_ready_() { | ||||
|   return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || | ||||
|            this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); | ||||
|   return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || | ||||
|            this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || | ||||
|            this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) || | ||||
|            this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); | ||||
| } | ||||
|  | ||||
| bool ThermostatClimate::drying_action_ready_() { | ||||
|   return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || | ||||
|            this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); | ||||
|   return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || | ||||
|            this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || | ||||
|            this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) || | ||||
|            this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); | ||||
| } | ||||
|  | ||||
| bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); } | ||||
| bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE)); } | ||||
|  | ||||
| bool ThermostatClimate::fanning_action_ready_() { | ||||
|   if (this->supports_fan_only_action_uses_fan_mode_timer_) { | ||||
|     return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); | ||||
|     return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE)); | ||||
|   } | ||||
|   return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF)); | ||||
|   return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || | ||||
|            this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF)); | ||||
| } | ||||
|  | ||||
| bool ThermostatClimate::heating_action_ready_() { | ||||
|   return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_COOLING_ON) || | ||||
|            this->timer_active_(thermostat::TIMER_FANNING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_OFF)); | ||||
|   return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || | ||||
|            this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || | ||||
|            this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || | ||||
|            this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_OFF)); | ||||
| } | ||||
|  | ||||
| void ThermostatClimate::start_timer_(const ThermostatClimateTimerIndex timer_index) { | ||||
| @@ -958,37 +994,25 @@ bool ThermostatClimate::supplemental_heating_required_() { | ||||
|           (this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING)); | ||||
| } | ||||
|  | ||||
| void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, | ||||
|                                             bool is_default_preset) { | ||||
|   ESP_LOGCONFIG(TAG, "      %s Is Default: %s", preset_name, YESNO(is_default_preset)); | ||||
|  | ||||
| void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config) { | ||||
|   if (this->supports_heat_) { | ||||
|     if (this->supports_two_points_) { | ||||
|       ESP_LOGCONFIG(TAG, "      %s Default Target Temperature Low: %.1f°C", preset_name, | ||||
|                     config.default_temperature_low); | ||||
|     } else { | ||||
|       ESP_LOGCONFIG(TAG, "      %s Default Target Temperature Low: %.1f°C", preset_name, config.default_temperature); | ||||
|     } | ||||
|     ESP_LOGCONFIG(TAG, "      Default Target Temperature Low: %.1f°C", | ||||
|                   this->supports_two_points_ ? config.default_temperature_low : config.default_temperature); | ||||
|   } | ||||
|   if ((this->supports_cool_) || (this->supports_fan_only_)) { | ||||
|     if (this->supports_two_points_) { | ||||
|       ESP_LOGCONFIG(TAG, "      %s Default Target Temperature High: %.1f°C", preset_name, | ||||
|                     config.default_temperature_high); | ||||
|     } else { | ||||
|       ESP_LOGCONFIG(TAG, "      %s Default Target Temperature High: %.1f°C", preset_name, config.default_temperature); | ||||
|     } | ||||
|     ESP_LOGCONFIG(TAG, "      Default Target Temperature High: %.1f°C", | ||||
|                   this->supports_two_points_ ? config.default_temperature_high : config.default_temperature); | ||||
|   } | ||||
|  | ||||
|   if (config.mode_.has_value()) { | ||||
|     ESP_LOGCONFIG(TAG, "      %s Default Mode: %s", preset_name, | ||||
|                   LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); | ||||
|     ESP_LOGCONFIG(TAG, "      Default Mode: %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); | ||||
|   } | ||||
|   if (config.fan_mode_.has_value()) { | ||||
|     ESP_LOGCONFIG(TAG, "      %s Default Fan Mode: %s", preset_name, | ||||
|     ESP_LOGCONFIG(TAG, "      Default Fan Mode: %s", | ||||
|                   LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); | ||||
|   } | ||||
|   if (config.swing_mode_.has_value()) { | ||||
|     ESP_LOGCONFIG(TAG, "      %s Default Swing Mode: %s", preset_name, | ||||
|     ESP_LOGCONFIG(TAG, "      Default Swing Mode: %s", | ||||
|                   LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_))); | ||||
|   } | ||||
| } | ||||
| @@ -1106,6 +1130,7 @@ ThermostatClimate::ThermostatClimate() | ||||
|       heat_action_trigger_(new Trigger<>()), | ||||
|       supplemental_heat_action_trigger_(new Trigger<>()), | ||||
|       heat_mode_trigger_(new Trigger<>()), | ||||
|       heat_cool_mode_trigger_(new Trigger<>()), | ||||
|       auto_mode_trigger_(new Trigger<>()), | ||||
|       idle_action_trigger_(new Trigger<>()), | ||||
|       off_mode_trigger_(new Trigger<>()), | ||||
| @@ -1147,43 +1172,43 @@ void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ | ||||
| void ThermostatClimate::set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; } | ||||
| void ThermostatClimate::set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; } | ||||
| void ThermostatClimate::set_cooling_maximum_run_time_in_sec(uint32_t time) { | ||||
|   this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].time = | ||||
|   this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME].time = | ||||
|       1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); | ||||
| } | ||||
| void ThermostatClimate::set_cooling_minimum_off_time_in_sec(uint32_t time) { | ||||
|   this->timer_[thermostat::TIMER_COOLING_OFF].time = | ||||
|   this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_OFF].time = | ||||
|       1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); | ||||
| } | ||||
| void ThermostatClimate::set_cooling_minimum_run_time_in_sec(uint32_t time) { | ||||
|   this->timer_[thermostat::TIMER_COOLING_ON].time = | ||||
|   this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_ON].time = | ||||
|       1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); | ||||
| } | ||||
| void ThermostatClimate::set_fan_mode_minimum_switching_time_in_sec(uint32_t time) { | ||||
|   this->timer_[thermostat::TIMER_FAN_MODE].time = | ||||
|   this->timer_[thermostat::THERMOSTAT_TIMER_FAN_MODE].time = | ||||
|       1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); | ||||
| } | ||||
| void ThermostatClimate::set_fanning_minimum_off_time_in_sec(uint32_t time) { | ||||
|   this->timer_[thermostat::TIMER_FANNING_OFF].time = | ||||
|   this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_OFF].time = | ||||
|       1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); | ||||
| } | ||||
| void ThermostatClimate::set_fanning_minimum_run_time_in_sec(uint32_t time) { | ||||
|   this->timer_[thermostat::TIMER_FANNING_ON].time = | ||||
|   this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_ON].time = | ||||
|       1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); | ||||
| } | ||||
| void ThermostatClimate::set_heating_maximum_run_time_in_sec(uint32_t time) { | ||||
|   this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].time = | ||||
|   this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME].time = | ||||
|       1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); | ||||
| } | ||||
| void ThermostatClimate::set_heating_minimum_off_time_in_sec(uint32_t time) { | ||||
|   this->timer_[thermostat::TIMER_HEATING_OFF].time = | ||||
|   this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_OFF].time = | ||||
|       1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); | ||||
| } | ||||
| void ThermostatClimate::set_heating_minimum_run_time_in_sec(uint32_t time) { | ||||
|   this->timer_[thermostat::TIMER_HEATING_ON].time = | ||||
|   this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_ON].time = | ||||
|       1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); | ||||
| } | ||||
| void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { | ||||
|   this->timer_[thermostat::TIMER_IDLE_ON].time = | ||||
|   this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time = | ||||
|       1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); | ||||
| } | ||||
| void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } | ||||
| @@ -1274,6 +1299,7 @@ Trigger<> *ThermostatClimate::get_cool_mode_trigger() const { return this->cool_ | ||||
| Trigger<> *ThermostatClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } | ||||
| Trigger<> *ThermostatClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } | ||||
| Trigger<> *ThermostatClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } | ||||
| Trigger<> *ThermostatClimate::get_heat_cool_mode_trigger() const { return this->heat_cool_mode_trigger_; } | ||||
| Trigger<> *ThermostatClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } | ||||
| Trigger<> *ThermostatClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } | ||||
| Trigger<> *ThermostatClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } | ||||
| @@ -1295,64 +1321,69 @@ Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->p | ||||
| void ThermostatClimate::dump_config() { | ||||
|   LOG_CLIMATE("", "Thermostat", this); | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  On boot, restore from: %s\n" | ||||
|                 "  Use Start-up Delay: %s", | ||||
|                 this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? "DEFAULT_PRESET" : "MEMORY", | ||||
|                 YESNO(this->use_startup_delay_)); | ||||
|   if (this->supports_two_points_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "  Use Start-up Delay: %s", YESNO(this->use_startup_delay_)); | ||||
|   if (this->supports_cool_) { | ||||
|     ESP_LOGCONFIG(TAG, | ||||
|                   "  Cooling Parameters:\n" | ||||
|                   "    Deadband: %.1f°C\n" | ||||
|                   "    Overrun: %.1f°C", | ||||
|                   this->cooling_deadband_, this->cooling_overrun_); | ||||
|     if ((this->supplemental_cool_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) > 0)) { | ||||
|       ESP_LOGCONFIG(TAG, | ||||
|                     "    Supplemental Delta: %.1f°C\n" | ||||
|                     "    Maximum Run Time: %" PRIu32 "s", | ||||
|                     this->supplemental_cool_delta_, | ||||
|                     this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) / 1000); | ||||
|     } | ||||
|     ESP_LOGCONFIG(TAG, | ||||
|                   "    Overrun: %.1f°C\n" | ||||
|                   "    Minimum Off Time: %" PRIu32 "s\n" | ||||
|                   "    Minimum Run Time: %" PRIu32 "s", | ||||
|                   this->timer_duration_(thermostat::TIMER_COOLING_OFF) / 1000, | ||||
|                   this->timer_duration_(thermostat::TIMER_COOLING_ON) / 1000); | ||||
|                   this->cooling_deadband_, this->cooling_overrun_, | ||||
|                   this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) / 1000, | ||||
|                   this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_ON) / 1000); | ||||
|     if ((this->supplemental_cool_delta_ > 0) || | ||||
|         (this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME) > 0)) { | ||||
|       ESP_LOGCONFIG(TAG, | ||||
|                     "    Maximum Run Time: %" PRIu32 "s\n" | ||||
|                     "    Supplemental Delta: %.1f°C", | ||||
|                     this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME) / 1000, | ||||
|                     this->supplemental_cool_delta_); | ||||
|     } | ||||
|   } | ||||
|   if (this->supports_heat_) { | ||||
|     ESP_LOGCONFIG(TAG, | ||||
|                   "  Heating Parameters:\n" | ||||
|                   "    Deadband: %.1f°C\n" | ||||
|                   "    Overrun: %.1f°C", | ||||
|                   this->heating_deadband_, this->heating_overrun_); | ||||
|     if ((this->supplemental_heat_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) > 0)) { | ||||
|       ESP_LOGCONFIG(TAG, | ||||
|                     "    Supplemental Delta: %.1f°C\n" | ||||
|                     "    Maximum Run Time: %" PRIu32 "s", | ||||
|                     this->supplemental_heat_delta_, | ||||
|                     this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) / 1000); | ||||
|     } | ||||
|     ESP_LOGCONFIG(TAG, | ||||
|                   "    Overrun: %.1f°C\n" | ||||
|                   "    Minimum Off Time: %" PRIu32 "s\n" | ||||
|                   "    Minimum Run Time: %" PRIu32 "s", | ||||
|                   this->timer_duration_(thermostat::TIMER_HEATING_OFF) / 1000, | ||||
|                   this->timer_duration_(thermostat::TIMER_HEATING_ON) / 1000); | ||||
|                   this->heating_deadband_, this->heating_overrun_, | ||||
|                   this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_OFF) / 1000, | ||||
|                   this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_ON) / 1000); | ||||
|     if ((this->supplemental_heat_delta_ > 0) || | ||||
|         (this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME) > 0)) { | ||||
|       ESP_LOGCONFIG(TAG, | ||||
|                     "    Maximum Run Time: %" PRIu32 "s\n" | ||||
|                     "    Supplemental Delta: %.1f°C", | ||||
|                     this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME) / 1000, | ||||
|                     this->supplemental_heat_delta_); | ||||
|     } | ||||
|   } | ||||
|   if (this->supports_fan_only_) { | ||||
|     ESP_LOGCONFIG(TAG, | ||||
|                   "  Fanning Minimum Off Time: %" PRIu32 "s\n" | ||||
|                   "  Fanning Minimum Run Time: %" PRIu32 "s", | ||||
|                   this->timer_duration_(thermostat::TIMER_FANNING_OFF) / 1000, | ||||
|                   this->timer_duration_(thermostat::TIMER_FANNING_ON) / 1000); | ||||
|                   "  Fan Parameters:\n" | ||||
|                   "    Minimum Off Time: %" PRIu32 "s\n" | ||||
|                   "    Minimum Run Time: %" PRIu32 "s", | ||||
|                   this->timer_duration_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) / 1000, | ||||
|                   this->timer_duration_(thermostat::THERMOSTAT_TIMER_FANNING_ON) / 1000); | ||||
|   } | ||||
|   if (this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || | ||||
|       this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ || | ||||
|       this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_ || | ||||
|       this->supports_fan_mode_quiet_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Minimum Fan Mode Switching Time: %" PRIu32 "s", | ||||
|                   this->timer_duration_(thermostat::TIMER_FAN_MODE) / 1000); | ||||
|                   this->timer_duration_(thermostat::THERMOSTAT_TIMER_FAN_MODE) / 1000); | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "  Minimum Idle Time: %" PRIu32 "s", this->timer_[thermostat::TIMER_IDLE_ON].time / 1000); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Minimum Idle Time: %" PRIu32 "s\n" | ||||
|                 "  Supported MODES:\n" | ||||
|                 "    AUTO: %s\n" | ||||
|                 "    HEAT/COOL: %s\n" | ||||
| @@ -1362,8 +1393,9 @@ void ThermostatClimate::dump_config() { | ||||
|                 "    FAN_ONLY: %s\n" | ||||
|                 "    FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" | ||||
|                 "    FAN_ONLY_COOLING: %s", | ||||
|                 YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), | ||||
|                 YESNO(this->supports_cool_), YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), | ||||
|                 this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time / 1000, YESNO(this->supports_auto_), | ||||
|                 YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), YESNO(this->supports_cool_), | ||||
|                 YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), | ||||
|                 YESNO(this->supports_fan_only_action_uses_fan_mode_timer_), YESNO(this->supports_fan_only_cooling_)); | ||||
|   if (this->supports_cool_) { | ||||
|     ESP_LOGCONFIG(TAG, "    FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); | ||||
| @@ -1382,40 +1414,39 @@ void ThermostatClimate::dump_config() { | ||||
|                 "    MIDDLE: %s\n" | ||||
|                 "    FOCUS: %s\n" | ||||
|                 "    DIFFUSE: %s\n" | ||||
|                 "    QUIET: %s", | ||||
|                 YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_), | ||||
|                 YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_), | ||||
|                 YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_), | ||||
|                 YESNO(this->supports_fan_mode_middle_), YESNO(this->supports_fan_mode_focus_), | ||||
|                 YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_)); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "    QUIET: %s\n" | ||||
|                 "  Supported SWING MODES:\n" | ||||
|                 "    BOTH: %s\n" | ||||
|                 "    OFF: %s\n" | ||||
|                 "    HORIZONTAL: %s\n" | ||||
|                 "    VERTICAL: %s\n" | ||||
|                 "  Supports TWO SET POINTS: %s", | ||||
|                 YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_), | ||||
|                 YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_), | ||||
|                 YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_), | ||||
|                 YESNO(this->supports_fan_mode_middle_), YESNO(this->supports_fan_mode_focus_), | ||||
|                 YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_), | ||||
|                 YESNO(this->supports_swing_mode_both_), YESNO(this->supports_swing_mode_off_), | ||||
|                 YESNO(this->supports_swing_mode_horizontal_), YESNO(this->supports_swing_mode_vertical_), | ||||
|                 YESNO(this->supports_two_points_)); | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Supported PRESETS: "); | ||||
|   for (auto &it : this->preset_config_) { | ||||
|     const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); | ||||
|  | ||||
|     ESP_LOGCONFIG(TAG, "    Supports %s: %s", preset_name, YESNO(true)); | ||||
|     this->dump_preset_config_(preset_name, it.second, it.first == this->default_preset_); | ||||
|   if (!this->preset_config_.empty()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Supported PRESETS:"); | ||||
|     for (auto &it : this->preset_config_) { | ||||
|       const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); | ||||
|       ESP_LOGCONFIG(TAG, "    %s:%s", preset_name, it.first == this->default_preset_ ? " (default)" : ""); | ||||
|       this->dump_preset_config_(preset_name, it.second); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Supported CUSTOM PRESETS: "); | ||||
|   for (auto &it : this->custom_preset_config_) { | ||||
|     const auto *preset_name = it.first.c_str(); | ||||
|  | ||||
|     ESP_LOGCONFIG(TAG, "    Supports %s: %s", preset_name, YESNO(true)); | ||||
|     this->dump_preset_config_(preset_name, it.second, it.first == this->default_custom_preset_); | ||||
|   if (!this->custom_preset_config_.empty()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Supported CUSTOM PRESETS:"); | ||||
|     for (auto &it : this->custom_preset_config_) { | ||||
|       const auto *preset_name = it.first.c_str(); | ||||
|       ESP_LOGCONFIG(TAG, "    %s:%s", preset_name, it.first == this->default_custom_preset_ ? " (default)" : ""); | ||||
|       this->dump_preset_config_(preset_name, it.second); | ||||
|     } | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "  On boot, restore from: %s", | ||||
|                 this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? "DEFAULT_PRESET" : "MEMORY"); | ||||
| } | ||||
|  | ||||
| ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; | ||||
|   | ||||
| @@ -6,24 +6,25 @@ | ||||
| #include "esphome/components/climate/climate.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
|  | ||||
| #include <array> | ||||
| #include <cinttypes> | ||||
| #include <map> | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace thermostat { | ||||
|  | ||||
| enum ThermostatClimateTimerIndex : uint8_t { | ||||
|   TIMER_COOLING_MAX_RUN_TIME = 0, | ||||
|   TIMER_COOLING_OFF = 1, | ||||
|   TIMER_COOLING_ON = 2, | ||||
|   TIMER_FAN_MODE = 3, | ||||
|   TIMER_FANNING_OFF = 4, | ||||
|   TIMER_FANNING_ON = 5, | ||||
|   TIMER_HEATING_MAX_RUN_TIME = 6, | ||||
|   TIMER_HEATING_OFF = 7, | ||||
|   TIMER_HEATING_ON = 8, | ||||
|   TIMER_IDLE_ON = 9, | ||||
|   THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME = 0, | ||||
|   THERMOSTAT_TIMER_COOLING_OFF = 1, | ||||
|   THERMOSTAT_TIMER_COOLING_ON = 2, | ||||
|   THERMOSTAT_TIMER_FAN_MODE = 3, | ||||
|   THERMOSTAT_TIMER_FANNING_OFF = 4, | ||||
|   THERMOSTAT_TIMER_FANNING_ON = 5, | ||||
|   THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME = 6, | ||||
|   THERMOSTAT_TIMER_HEATING_OFF = 7, | ||||
|   THERMOSTAT_TIMER_HEATING_ON = 8, | ||||
|   THERMOSTAT_TIMER_IDLE_ON = 9, | ||||
|   THERMOSTAT_TIMER_COUNT = 10, | ||||
| }; | ||||
|  | ||||
| enum OnBootRestoreFrom : uint8_t { | ||||
| @@ -131,6 +132,7 @@ class ThermostatClimate : public climate::Climate, public Component { | ||||
|   Trigger<> *get_dry_mode_trigger() const; | ||||
|   Trigger<> *get_fan_only_mode_trigger() const; | ||||
|   Trigger<> *get_heat_mode_trigger() const; | ||||
|   Trigger<> *get_heat_cool_mode_trigger() const; | ||||
|   Trigger<> *get_off_mode_trigger() const; | ||||
|   Trigger<> *get_fan_mode_on_trigger() const; | ||||
|   Trigger<> *get_fan_mode_off_trigger() const; | ||||
| @@ -163,9 +165,10 @@ class ThermostatClimate : public climate::Climate, public Component { | ||||
|   /// Returns the fan mode that is locked in (check fan_mode_change_delayed(), first!) | ||||
|   climate::ClimateFanMode locked_fan_mode(); | ||||
|   /// Set point and hysteresis validation | ||||
|   bool hysteresis_valid();  // returns true if valid | ||||
|   bool hysteresis_valid();               // returns true if valid | ||||
|   bool limit_setpoints_for_heat_cool();  // returns true if set points should be further limited within visual range | ||||
|   void validate_target_temperature(); | ||||
|   void validate_target_temperatures(); | ||||
|   void validate_target_temperatures(bool pin_target_temperature_high); | ||||
|   void validate_target_temperature_low(); | ||||
|   void validate_target_temperature_high(); | ||||
|  | ||||
| @@ -241,12 +244,28 @@ class ThermostatClimate : public climate::Climate, public Component { | ||||
|   bool supplemental_cooling_required_(); | ||||
|   bool supplemental_heating_required_(); | ||||
|  | ||||
|   void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, | ||||
|                            bool is_default_preset); | ||||
|   void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config); | ||||
|  | ||||
|   /// Minimum allowable duration in seconds for action timers | ||||
|   const uint8_t min_timer_duration_{1}; | ||||
|  | ||||
|   /// Store previously-known states | ||||
|   /// | ||||
|   /// These are used to determine when a trigger/action needs to be called | ||||
|   climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; | ||||
|   climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; | ||||
|   climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; | ||||
|  | ||||
|   /// The current supplemental action | ||||
|   climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; | ||||
|  | ||||
|   /// Default standard preset to use on start up | ||||
|   climate::ClimatePreset default_preset_{}; | ||||
|  | ||||
|   /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior | ||||
|   /// state will attempt to be restored if possible | ||||
|   OnBootRestoreFrom on_boot_restore_from_{OnBootRestoreFrom::MEMORY}; | ||||
|  | ||||
|   /// Whether the controller supports auto/cooling/drying/fanning/heating. | ||||
|   /// | ||||
|   /// A false value for any given attribute means that the controller has no such action | ||||
| @@ -362,9 +381,15 @@ class ThermostatClimate : public climate::Climate, public Component { | ||||
|   Trigger<> *supplemental_heat_action_trigger_{nullptr}; | ||||
|   Trigger<> *heat_mode_trigger_{nullptr}; | ||||
|  | ||||
|   /// The trigger to call when the controller should switch to heat/cool mode. | ||||
|   /// | ||||
|   /// In heat/cool mode, the controller will enable heating/cooling as necessary and switch | ||||
|   /// to idle when the temperature is within the thresholds/set points. | ||||
|   Trigger<> *heat_cool_mode_trigger_{nullptr}; | ||||
|  | ||||
|   /// The trigger to call when the controller should switch to auto mode. | ||||
|   /// | ||||
|   /// In auto mode, the controller will enable heating/cooling as necessary and switch | ||||
|   /// In auto mode, the controller will enable heating/cooling as supported/necessary and switch | ||||
|   /// to idle when the temperature is within the thresholds/set points. | ||||
|   Trigger<> *auto_mode_trigger_{nullptr}; | ||||
|  | ||||
| @@ -438,35 +463,21 @@ class ThermostatClimate : public climate::Climate, public Component { | ||||
|   Trigger<> *prev_mode_trigger_{nullptr}; | ||||
|   Trigger<> *prev_swing_mode_trigger_{nullptr}; | ||||
|  | ||||
|   /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior | ||||
|   /// state will attempt to be restored if possible | ||||
|   OnBootRestoreFrom on_boot_restore_from_{OnBootRestoreFrom::MEMORY}; | ||||
|  | ||||
|   /// Store previously-known states | ||||
|   /// | ||||
|   /// These are used to determine when a trigger/action needs to be called | ||||
|   climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; | ||||
|   climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; | ||||
|   climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; | ||||
|   climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; | ||||
|  | ||||
|   /// Default standard preset to use on start up | ||||
|   climate::ClimatePreset default_preset_{}; | ||||
|   /// Default custom preset to use on start up | ||||
|   std::string default_custom_preset_{}; | ||||
|  | ||||
|   /// Climate action timers | ||||
|   std::vector<ThermostatClimateTimer> timer_{ | ||||
|       {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)}, | ||||
|   std::array<ThermostatClimateTimer, THERMOSTAT_TIMER_COUNT> timer_{ | ||||
|       ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)), | ||||
|       ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)), | ||||
|       ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)), | ||||
|       ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)), | ||||
|       ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)), | ||||
|       ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)), | ||||
|       ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)), | ||||
|       ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)), | ||||
|       ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)), | ||||
|       ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)), | ||||
|   }; | ||||
|  | ||||
|   /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) | ||||
|   | ||||
| @@ -494,155 +494,155 @@ const uint8_t INDEX_GZ[] PROGMEM = { | ||||
|     0x1c, 0x40, 0xc8, 0x12, 0x7c, 0xa6, 0xc1, 0x29, 0x21, 0xa4, 0xd5, 0x9f, 0x05, 0x5f, 0xe2, 0x9b, 0x98, 0xa6, 0xc1, | ||||
|     0xbc, 0xe8, 0x96, 0x04, 0xa0, 0x22, 0xa6, 0x6f, 0x45, 0x79, 0x6f, 0x9c, 0xa4, 0x8a, 0xea, 0xb5, 0x82, 0xb3, 0x59, | ||||
|     0x52, 0xcf, 0x96, 0x58, 0x9a, 0xe5, 0x93, 0x19, 0x25, 0xfc, 0xa6, 0x79, 0xeb, 0xf6, 0x36, 0xc7, 0xd7, 0x60, 0x76, | ||||
|     0x65, 0x7c, 0x4d, 0x02, 0x5b, 0x3e, 0xbd, 0x0f, 0xc7, 0xe5, 0xef, 0x57, 0x34, 0xcf, 0xc3, 0xb1, 0xae, 0xb9, 0x3d, | ||||
|     0x9e, 0x26, 0x41, 0xb4, 0x63, 0x69, 0x06, 0x08, 0x88, 0x89, 0x01, 0x46, 0xc0, 0xa7, 0xa1, 0x43, 0x64, 0x30, 0xf5, | ||||
|     0x7a, 0x74, 0x4d, 0x0e, 0x5f, 0x2f, 0x12, 0xe1, 0xb8, 0x2a, 0x38, 0x99, 0x66, 0x54, 0x96, 0x2a, 0x34, 0x16, 0x27, | ||||
|     0xfb, 0x50, 0xa0, 0x5e, 0x6f, 0x89, 0xa2, 0x19, 0x07, 0xca, 0xf6, 0x58, 0x9a, 0x63, 0xa2, 0x68, 0x76, 0xa2, 0x52, | ||||
|     0x99, 0xa5, 0xb4, 0x1e, 0xbb, 0xf9, 0xbc, 0x3d, 0x84, 0x3f, 0x3a, 0x32, 0xf4, 0xf9, 0x68, 0x34, 0xba, 0x37, 0xaa, | ||||
|     0xf6, 0x79, 0x34, 0xa2, 0x1d, 0x7a, 0xd4, 0x85, 0x24, 0x96, 0xa6, 0x8e, 0xc5, 0xb4, 0x0b, 0x89, 0xbb, 0xc5, 0xc3, | ||||
|     0x2a, 0x43, 0xd8, 0x46, 0xc4, 0x8b, 0x87, 0x47, 0xd8, 0x8a, 0x69, 0x46, 0x17, 0x93, 0x30, 0x1b, 0xb3, 0x34, 0x68, | ||||
|     0x15, 0xfe, 0x5c, 0x87, 0xa4, 0x3e, 0x3f, 0x3e, 0x3e, 0x2e, 0xfc, 0xc8, 0x3c, 0xb5, 0xa2, 0xa8, 0xf0, 0x87, 0x8b, | ||||
|     0x72, 0x1a, 0xad, 0xd6, 0x68, 0x54, 0xf8, 0xcc, 0x14, 0x1c, 0x74, 0x86, 0xd1, 0x41, 0xa7, 0xf0, 0x6f, 0xac, 0x1a, | ||||
|     0x85, 0x4f, 0xf5, 0x53, 0x46, 0xa3, 0x5a, 0x26, 0xcc, 0xe3, 0x56, 0xab, 0xf0, 0x15, 0xa1, 0x2d, 0xc0, 0x2c, 0x55, | ||||
|     0x3f, 0x83, 0x70, 0x26, 0x38, 0x30, 0xf7, 0x6e, 0x22, 0xbc, 0xc1, 0xa5, 0xbe, 0x65, 0x44, 0x7d, 0x93, 0xa3, 0x40, | ||||
|     0x17, 0xf8, 0x67, 0x3b, 0x78, 0x04, 0xc4, 0x2c, 0x83, 0x46, 0x89, 0x89, 0x2d, 0xd5, 0x5e, 0x03, 0x65, 0xc9, 0xd7, | ||||
|     0x3f, 0x93, 0xa4, 0x8a, 0x29, 0x01, 0x27, 0x83, 0x9a, 0xea, 0x32, 0x3c, 0x4a, 0xb7, 0xc8, 0x0f, 0xf6, 0x69, 0xf9, | ||||
|     0x71, 0xf7, 0x10, 0xf1, 0xc1, 0xfe, 0x70, 0xf1, 0x41, 0xa9, 0x25, 0x3e, 0x14, 0xf3, 0xb8, 0x13, 0xc4, 0x1d, 0xc6, | ||||
|     0x74, 0xf8, 0xf1, 0x9a, 0xdf, 0x36, 0x61, 0x4b, 0x64, 0xae, 0x14, 0x2c, 0xbb, 0xbf, 0x35, 0x6b, 0xc6, 0x74, 0x66, | ||||
|     0x7d, 0xd1, 0x43, 0xaa, 0x0f, 0x6f, 0x52, 0xe2, 0xbe, 0x31, 0xb6, 0xad, 0x2a, 0x19, 0x8d, 0x88, 0xfb, 0x66, 0x34, | ||||
|     0x72, 0xcd, 0x59, 0xc9, 0x50, 0x50, 0x59, 0xeb, 0x75, 0xad, 0x44, 0xd6, 0xfa, 0xf2, 0x4b, 0xbb, 0xcc, 0x2e, 0xd0, | ||||
|     0xa1, 0x27, 0x3b, 0xcc, 0xa4, 0xdf, 0x44, 0x2c, 0x87, 0xad, 0x06, 0x1f, 0x1a, 0xa9, 0xdf, 0xd5, 0x98, 0xd6, 0xae, | ||||
|     0xd5, 0x2e, 0x01, 0xde, 0x70, 0x17, 0xf8, 0xea, 0x45, 0x01, 0x63, 0x6a, 0xf2, 0x16, 0x9f, 0xde, 0x7d, 0x15, 0x79, | ||||
|     0x77, 0x02, 0x15, 0x2c, 0x7f, 0x93, 0xae, 0x1c, 0x02, 0x52, 0x30, 0x12, 0x62, 0x4f, 0xab, 0x10, 0x7c, 0x3c, 0x4e, | ||||
|     0xe0, 0x5b, 0x2f, 0x8b, 0xda, 0xfd, 0xb1, 0xaa, 0x79, 0xbf, 0x36, 0xdf, 0xc0, 0x6e, 0xa8, 0x6f, 0x5b, 0x95, 0x9f, | ||||
|     0x9e, 0x52, 0xc9, 0xe3, 0x73, 0xfd, 0x0d, 0x22, 0x69, 0x16, 0x2f, 0x34, 0x93, 0x5f, 0xa8, 0x94, 0x63, 0x01, 0xe9, | ||||
|     0x36, 0xaa, 0xe3, 0xa8, 0x28, 0xf4, 0x61, 0x8d, 0x88, 0xe5, 0x53, 0xb8, 0xd7, 0x54, 0xb5, 0xa4, 0x9f, 0x62, 0xe1, | ||||
|     0xf9, 0x8d, 0x15, 0xdf, 0xa9, 0x2d, 0x57, 0x61, 0x02, 0x3c, 0xca, 0x61, 0x7e, 0x27, 0x0a, 0x57, 0xfb, 0xdd, 0x0d, | ||||
|     0x12, 0x5d, 0x47, 0xe1, 0x53, 0x45, 0x9e, 0xac, 0x19, 0x82, 0xf3, 0xbb, 0x5c, 0x10, 0xf3, 0xca, 0x14, 0x14, 0x76, | ||||
|     0xfc, 0x52, 0xbe, 0x51, 0xd8, 0x92, 0xd1, 0x92, 0x7c, 0x1a, 0xa6, 0x8a, 0x8d, 0x12, 0x57, 0xf1, 0x83, 0xdd, 0x45, | ||||
|     0xb5, 0xf2, 0x85, 0x6b, 0xc0, 0x56, 0xc4, 0xdb, 0x3b, 0xd9, 0x87, 0x06, 0x3d, 0xa7, 0x06, 0x7a, 0xba, 0x16, 0x64, | ||||
|     0xf9, 0x44, 0xba, 0xc3, 0x95, 0x9f, 0xdf, 0x60, 0x3f, 0xbf, 0x71, 0xfe, 0xbc, 0x68, 0xde, 0xd0, 0xeb, 0x8f, 0x4c, | ||||
|     0x34, 0x45, 0x38, 0x6d, 0x82, 0xe1, 0x23, 0x9d, 0xa3, 0x9a, 0x3d, 0xcb, 0x2c, 0x3f, 0x75, 0xd5, 0x41, 0x77, 0x96, | ||||
|     0x43, 0x56, 0x84, 0x54, 0xdf, 0x83, 0x94, 0xa7, 0xb4, 0x5b, 0xcf, 0xe6, 0xb4, 0x83, 0xec, 0x06, 0x5b, 0x17, 0x0b, | ||||
|     0x0e, 0x59, 0x14, 0xe2, 0x2e, 0x68, 0x69, 0xb6, 0xde, 0x32, 0x11, 0xf4, 0xd6, 0xc6, 0xfa, 0x81, 0x46, 0x6e, 0x43, | ||||
|     0x4a, 0xaf, 0x6c, 0x3d, 0x93, 0x60, 0x5b, 0x26, 0xc0, 0xa7, 0x72, 0x1b, 0xc1, 0xa5, 0x6a, 0xfe, 0x5a, 0x49, 0xa1, | ||||
|     0xab, 0xc5, 0x32, 0xb7, 0xf1, 0x21, 0x90, 0x05, 0xe1, 0x48, 0xd0, 0x0c, 0x3f, 0xa4, 0xe6, 0xb5, 0x3c, 0x86, 0xb4, | ||||
|     0x00, 0x31, 0x13, 0xb4, 0x8f, 0xa7, 0xb7, 0x0f, 0xef, 0xfe, 0xfe, 0xe9, 0x17, 0x1a, 0x47, 0xe6, 0x5a, 0x1e, 0xd7, | ||||
|     0xed, 0xc2, 0x46, 0x48, 0xc2, 0xbb, 0x80, 0xa5, 0x52, 0xe6, 0x5d, 0x83, 0x5f, 0xb4, 0x3b, 0xe5, 0x3a, 0x49, 0x37, | ||||
|     0xa3, 0x89, 0xfc, 0x0a, 0x9f, 0x5e, 0x8a, 0x83, 0x47, 0xd3, 0x5b, 0xb3, 0x1a, 0xed, 0x95, 0xe4, 0xdb, 0x3f, 0x34, | ||||
|     0xc7, 0x76, 0x7b, 0x52, 0x6f, 0x3d, 0x4f, 0xf4, 0x68, 0x7a, 0xdb, 0x55, 0x82, 0xb6, 0x99, 0x29, 0xa8, 0x5a, 0xd3, | ||||
|     0x5b, 0x3b, 0xcb, 0xb8, 0xea, 0xc8, 0xf1, 0x0f, 0x72, 0x87, 0x86, 0x39, 0xed, 0xc2, 0xbd, 0xe3, 0x6c, 0x18, 0x26, | ||||
|     0x5a, 0x98, 0x4f, 0x58, 0x14, 0x25, 0xb4, 0x6b, 0xe4, 0xb5, 0xd3, 0x7e, 0x04, 0x49, 0xba, 0xf6, 0x92, 0xd5, 0x57, | ||||
|     0xc5, 0x42, 0x5e, 0x89, 0xa7, 0xf0, 0x3a, 0xe7, 0x09, 0x7c, 0xf4, 0x63, 0x23, 0x3a, 0x75, 0xf6, 0x6a, 0xab, 0x42, | ||||
|     0x9e, 0xfc, 0x5d, 0x9f, 0xcb, 0x51, 0xeb, 0x4f, 0x5d, 0xb9, 0xe0, 0xad, 0xae, 0xe0, 0xd3, 0xa0, 0x79, 0x50, 0x9f, | ||||
|     0x08, 0xbc, 0x2a, 0xa7, 0x80, 0x37, 0x4c, 0x0b, 0x83, 0xb4, 0x52, 0x7c, 0xda, 0xf1, 0xdb, 0xba, 0x4c, 0x76, 0x00, | ||||
|     0x79, 0x61, 0x65, 0x51, 0x51, 0x9f, 0xcc, 0xbf, 0xcd, 0x6e, 0x79, 0xb2, 0x79, 0xb7, 0x3c, 0x31, 0xbb, 0xe5, 0x7e, | ||||
|     0x8a, 0xfd, 0x7c, 0xd4, 0x86, 0x3f, 0xdd, 0x6a, 0x42, 0x41, 0xcb, 0x39, 0x98, 0xde, 0x3a, 0xa0, 0xa7, 0x35, 0x3b, | ||||
|     0xd3, 0x5b, 0x95, 0x63, 0x0d, 0xb1, 0x9b, 0x16, 0x64, 0x1d, 0xe3, 0x96, 0x03, 0x85, 0xf0, 0xb7, 0x55, 0x7b, 0xd5, | ||||
|     0x3e, 0x84, 0x77, 0xd0, 0xea, 0x68, 0xfd, 0x5d, 0xe7, 0xfe, 0x4d, 0x1b, 0xa4, 0x5c, 0x78, 0x81, 0xe1, 0xc6, 0xc8, | ||||
|     0x17, 0xe1, 0xf5, 0x35, 0x8d, 0x82, 0x11, 0x1f, 0xce, 0xf2, 0x7f, 0xd2, 0xf0, 0x6b, 0x24, 0xde, 0xbb, 0xa5, 0x57, | ||||
|     0xfa, 0x31, 0x4d, 0x55, 0xc6, 0xb7, 0xe9, 0x61, 0x51, 0xae, 0x53, 0x90, 0x0f, 0xc3, 0x84, 0x7a, 0x1d, 0xff, 0x70, | ||||
|     0xc3, 0x26, 0xf8, 0x77, 0x59, 0x9b, 0x8d, 0x93, 0xf9, 0xbd, 0xc8, 0xb8, 0x17, 0x09, 0xbf, 0x0a, 0x07, 0xf6, 0x1a, | ||||
|     0xb6, 0x8e, 0x37, 0x83, 0x3b, 0x30, 0x23, 0x5d, 0x18, 0xa1, 0xa0, 0xe5, 0x4e, 0x44, 0x47, 0xe1, 0x2c, 0x11, 0xf7, | ||||
|     0xf7, 0xba, 0x8d, 0x32, 0xd6, 0x7a, 0xbd, 0x87, 0xa1, 0x57, 0x75, 0x1f, 0xc8, 0xa5, 0x3f, 0x7f, 0x72, 0x08, 0x7f, | ||||
|     0x54, 0xfe, 0xd7, 0x5d, 0xa5, 0xab, 0x2b, 0xbb, 0x17, 0x74, 0xf5, 0xdd, 0x9a, 0x32, 0xae, 0x44, 0xb8, 0xd4, 0xc7, | ||||
|     0x1f, 0x5a, 0x1b, 0xb4, 0xca, 0x07, 0x55, 0xd7, 0x5a, 0xd6, 0xaf, 0xaa, 0xfd, 0xeb, 0x3a, 0x7f, 0x60, 0xdd, 0xa1, | ||||
|     0xd2, 0x5c, 0xeb, 0x75, 0xf5, 0x67, 0x08, 0xd7, 0x2a, 0x1b, 0x8c, 0xcb, 0xfa, 0xbb, 0xe4, 0xae, 0x34, 0x51, 0x54, | ||||
|     0x34, 0x16, 0xac, 0x94, 0x5d, 0x65, 0xa5, 0xe4, 0x94, 0x5c, 0x9d, 0xf4, 0x6f, 0x27, 0x89, 0x33, 0x57, 0xc7, 0x25, | ||||
|     0x89, 0xdb, 0xf6, 0x5b, 0xae, 0x23, 0xf3, 0x00, 0xe0, 0xd6, 0x76, 0x57, 0x7e, 0xde, 0xd6, 0xed, 0x83, 0xa6, 0x35, | ||||
|     0x1f, 0x4b, 0xcd, 0xee, 0x65, 0x78, 0x47, 0xb3, 0xcb, 0x8e, 0xeb, 0x80, 0x9f, 0xa6, 0xa9, 0x52, 0x26, 0x64, 0x99, | ||||
|     0xd3, 0x71, 0x9d, 0xdb, 0x49, 0x92, 0xe6, 0xc4, 0x8d, 0x85, 0x98, 0x06, 0xea, 0xfb, 0xb7, 0x37, 0x07, 0x3e, 0xcf, | ||||
|     0xc6, 0xfb, 0x9d, 0x56, 0xab, 0x05, 0x17, 0xc0, 0xba, 0xce, 0x9c, 0xd1, 0x9b, 0xa7, 0xfc, 0x96, 0xb8, 0x2d, 0xa7, | ||||
|     0xe5, 0xb4, 0x3b, 0xc7, 0x4e, 0xbb, 0x73, 0xe8, 0x3f, 0x3a, 0x76, 0x7b, 0x9f, 0x39, 0xce, 0x49, 0x44, 0x47, 0x39, | ||||
|     0xfc, 0x70, 0x9c, 0x13, 0xa9, 0x78, 0xa9, 0xdf, 0x8e, 0xe3, 0x0f, 0x93, 0xbc, 0xd9, 0x76, 0x16, 0xfa, 0xd1, 0x71, | ||||
|     0xe0, 0x50, 0x69, 0xe0, 0x7c, 0x3e, 0xea, 0x8c, 0x0e, 0x47, 0x4f, 0xba, 0xba, 0xb8, 0xf8, 0xac, 0x56, 0x1d, 0xab, | ||||
|     0xff, 0x3b, 0x56, 0xb3, 0x5c, 0x64, 0xfc, 0x23, 0xd5, 0x39, 0x89, 0x0e, 0x88, 0x9e, 0x8d, 0x4d, 0x3b, 0xeb, 0x23, | ||||
|     0xb5, 0x8f, 0xaf, 0x87, 0xa3, 0x4e, 0x55, 0x5d, 0xc2, 0xb8, 0x5f, 0x02, 0x79, 0xb2, 0x6f, 0x40, 0x3f, 0xb1, 0xd1, | ||||
|     0xd4, 0x6e, 0x6e, 0x42, 0x54, 0xdb, 0xd5, 0x73, 0x1c, 0x9b, 0xf9, 0x9d, 0xc0, 0x19, 0x06, 0xa3, 0xab, 0x4a, 0x08, | ||||
|     0x5c, 0x27, 0x22, 0xee, 0xab, 0x76, 0xe7, 0x18, 0xb7, 0xdb, 0x8f, 0xfc, 0x47, 0xc7, 0xc3, 0x16, 0x3e, 0xf4, 0x0f, | ||||
|     0x9b, 0x07, 0xfe, 0x23, 0x7c, 0xdc, 0x3c, 0xc6, 0xc7, 0x2f, 0x8e, 0x87, 0xcd, 0x43, 0xff, 0x10, 0xb7, 0x9a, 0xc7, | ||||
|     0x50, 0xd8, 0x3c, 0x6e, 0x1e, 0xcf, 0x9b, 0x87, 0xc7, 0xc3, 0x96, 0x2c, 0xed, 0xf8, 0x47, 0x47, 0xcd, 0x76, 0xcb, | ||||
|     0x3f, 0x3a, 0xc2, 0x47, 0xfe, 0xa3, 0x47, 0xcd, 0xf6, 0x81, 0xff, 0xe8, 0xd1, 0xcb, 0xa3, 0x63, 0xff, 0x00, 0xde, | ||||
|     0x1d, 0x1c, 0x0c, 0x0f, 0xfc, 0x76, 0xbb, 0x09, 0xff, 0xe0, 0x63, 0xbf, 0xa3, 0x7e, 0xb4, 0xdb, 0xfe, 0x41, 0x1b, | ||||
|     0xb7, 0x92, 0xa3, 0x8e, 0xff, 0xe8, 0x09, 0x96, 0xff, 0xca, 0x6a, 0x58, 0xfe, 0x03, 0xdd, 0xe0, 0x27, 0x7e, 0xe7, | ||||
|     0x91, 0xfa, 0x25, 0x3b, 0x9c, 0x1f, 0x1e, 0xff, 0xe0, 0xee, 0x6f, 0x9d, 0x43, 0x5b, 0xcd, 0xe1, 0xf8, 0xc8, 0x3f, | ||||
|     0x38, 0xc0, 0x87, 0x6d, 0xff, 0xf8, 0x20, 0x6e, 0x1e, 0x76, 0xfc, 0x47, 0x8f, 0x87, 0xcd, 0xb6, 0xff, 0xf8, 0x31, | ||||
|     0x6e, 0x35, 0x0f, 0xfc, 0x0e, 0x6e, 0xfb, 0x87, 0x07, 0xf2, 0xc7, 0x81, 0xdf, 0x99, 0x3f, 0x7e, 0xe2, 0x3f, 0x3a, | ||||
|     0x8a, 0x1f, 0xf9, 0x87, 0xdf, 0x1e, 0x1e, 0xfb, 0x9d, 0x83, 0xf8, 0xe0, 0x91, 0xdf, 0x79, 0x3c, 0x7f, 0xe4, 0x1f, | ||||
|     0xc6, 0xcd, 0xce, 0xa3, 0x7b, 0x5b, 0xb6, 0x3b, 0x3e, 0xe0, 0x48, 0xbe, 0x86, 0x17, 0x58, 0xbf, 0x80, 0xbf, 0xb1, | ||||
|     0x6c, 0xfb, 0xef, 0xd8, 0x4d, 0xbe, 0xde, 0xf4, 0x89, 0x7f, 0xfc, 0x78, 0xa8, 0xaa, 0x43, 0x41, 0xd3, 0xd4, 0x80, | ||||
|     0x26, 0xf3, 0xa6, 0x1a, 0x56, 0x76, 0xd7, 0x34, 0x1d, 0x99, 0xbf, 0x7a, 0xb0, 0x79, 0x13, 0x06, 0x56, 0xe3, 0xfe, | ||||
|     0x87, 0xf6, 0x53, 0x2e, 0xf9, 0xc9, 0xfe, 0x58, 0x91, 0xfe, 0xb8, 0xf7, 0x99, 0xba, 0xdd, 0xf9, 0xb3, 0x2b, 0x9c, | ||||
|     0x6e, 0x73, 0x7c, 0x64, 0x9f, 0x76, 0x7c, 0x70, 0xfa, 0x10, 0xcf, 0x47, 0xf6, 0x87, 0x7b, 0x3e, 0x52, 0xba, 0xe2, | ||||
|     0x38, 0xbf, 0x16, 0x6b, 0x0e, 0x8e, 0x55, 0xab, 0xf8, 0xa9, 0xf0, 0x06, 0x39, 0x7c, 0x47, 0xac, 0xe8, 0x5e, 0x0b, | ||||
|     0xc2, 0xa9, 0xed, 0x07, 0xe2, 0xc0, 0x62, 0xaf, 0x85, 0xe2, 0xb1, 0xc9, 0x36, 0x84, 0x84, 0x9f, 0x46, 0xc8, 0xb7, | ||||
|     0x0f, 0xc1, 0x47, 0xf8, 0x87, 0xe3, 0x23, 0xb1, 0xf1, 0x51, 0xf3, 0xe5, 0x4b, 0x4f, 0x83, 0xf4, 0x14, 0x9c, 0xcb, | ||||
|     0x67, 0x0f, 0x0e, 0x51, 0x35, 0xdc, 0x7d, 0x0a, 0x45, 0xb9, 0xab, 0x22, 0x5f, 0xef, 0x7e, 0x4d, 0xd8, 0x41, 0x9d, | ||||
|     0x98, 0x24, 0xae, 0x76, 0xcb, 0x4c, 0xa5, 0xd4, 0xd1, 0x0f, 0xa5, 0x50, 0xea, 0xf8, 0x2d, 0xbf, 0x55, 0xba, 0x74, | ||||
|     0xe0, 0x94, 0x2c, 0x59, 0x70, 0x11, 0xc2, 0x17, 0x6b, 0x13, 0x3e, 0x96, 0xdf, 0xb6, 0x85, 0xaf, 0x09, 0x40, 0xd2, | ||||
|     0xcf, 0x50, 0x7d, 0xc8, 0x21, 0x70, 0x5d, 0x7d, 0xb7, 0x06, 0x9c, 0xc2, 0xfc, 0x06, 0x4e, 0xaa, 0x9a, 0xa8, 0xc4, | ||||
|     0x04, 0xbc, 0x1d, 0xaf, 0x68, 0xc4, 0x42, 0xcf, 0xf5, 0xa6, 0x19, 0x1d, 0xd1, 0x2c, 0x6f, 0xd6, 0x8e, 0x6f, 0xca, | ||||
|     0x93, 0x9b, 0xc8, 0x35, 0x9f, 0x46, 0xcd, 0xe0, 0x76, 0x6c, 0x32, 0xd0, 0xfe, 0x46, 0x57, 0x1b, 0x60, 0x6e, 0x81, | ||||
|     0x4d, 0x49, 0x06, 0xb2, 0xb6, 0x52, 0xda, 0x5c, 0xa5, 0xb5, 0xb5, 0xfd, 0xce, 0x11, 0x72, 0x64, 0x31, 0xdc, 0x3b, | ||||
|     0xfc, 0xbd, 0xd7, 0x3c, 0x68, 0xfd, 0x09, 0x59, 0xcd, 0xca, 0x8e, 0x2e, 0xb4, 0xbb, 0x2d, 0xad, 0xbe, 0x29, 0x5d, | ||||
|     0x3f, 0x5b, 0xeb, 0x2a, 0x8a, 0xf8, 0x5c, 0xcd, 0xdd, 0x45, 0xdd, 0x54, 0x47, 0xb8, 0xd5, 0x0d, 0x11, 0x23, 0x36, | ||||
|     0xf6, 0xec, 0x2f, 0x06, 0xab, 0x7b, 0x8d, 0xe5, 0x87, 0xc6, 0x51, 0x51, 0x55, 0x49, 0xd1, 0x42, 0xc6, 0x5b, 0x58, | ||||
|     0xea, 0xa4, 0xcb, 0xa5, 0x97, 0x82, 0x8b, 0x9c, 0x58, 0x38, 0x85, 0x67, 0x54, 0x43, 0x72, 0x8a, 0x4b, 0x80, 0x24, | ||||
|     0x82, 0x49, 0xaa, 0xfe, 0xaf, 0x8a, 0xcd, 0x0f, 0xed, 0xf8, 0xf2, 0x93, 0x30, 0x1d, 0x03, 0x15, 0x86, 0xe9, 0x78, | ||||
|     0xcd, 0xad, 0xa6, 0x42, 0x46, 0x2b, 0xa5, 0x55, 0x57, 0x95, 0xfb, 0x2c, 0x7f, 0x7a, 0xf7, 0x5e, 0x5f, 0x80, 0xe6, | ||||
|     0x82, 0x77, 0x5a, 0x46, 0x38, 0xaa, 0xcb, 0x9a, 0x1b, 0xe4, 0x8b, 0x93, 0x09, 0x15, 0xa1, 0xca, 0xd7, 0x04, 0x7d, | ||||
|     0x02, 0x4e, 0xcd, 0x3a, 0xda, 0x1a, 0x25, 0xae, 0x94, 0xee, 0x24, 0xa2, 0x73, 0x36, 0xd4, 0xa2, 0x1e, 0x3b, 0xfa, | ||||
|     0xe6, 0x80, 0xa6, 0x5c, 0x1a, 0xd2, 0xc6, 0xca, 0x1f, 0x33, 0x0c, 0x65, 0x46, 0x3e, 0x49, 0xb9, 0xdb, 0xfb, 0xa2, | ||||
|     0xfc, 0xfa, 0xe9, 0xb6, 0x45, 0x48, 0x58, 0xfa, 0x71, 0x90, 0xd1, 0xe4, 0x9f, 0xc8, 0x17, 0x6c, 0xc8, 0xd3, 0x2f, | ||||
|     0x2e, 0xe0, 0xab, 0xf4, 0x7e, 0x9c, 0xd1, 0x11, 0xf9, 0x02, 0x64, 0x7c, 0x20, 0xad, 0x0f, 0x60, 0x84, 0x8d, 0xdb, | ||||
|     0x49, 0x82, 0xa5, 0xc6, 0xf4, 0x00, 0x85, 0x48, 0x81, 0xeb, 0x76, 0x8e, 0x5c, 0x47, 0xd9, 0xc4, 0xf2, 0x77, 0x4f, | ||||
|     0x89, 0x53, 0xa9, 0x04, 0x38, 0xed, 0x8e, 0x7f, 0x14, 0x77, 0xfc, 0x27, 0xf3, 0xc7, 0xfe, 0x71, 0xdc, 0x7e, 0x3c, | ||||
|     0x6f, 0xc2, 0xff, 0x1d, 0xff, 0x49, 0xd2, 0xec, 0xf8, 0x4f, 0xe0, 0xef, 0xb7, 0x87, 0xfe, 0x51, 0xdc, 0x6c, 0xfb, | ||||
|     0xc7, 0xf3, 0x03, 0xff, 0xe0, 0x65, 0xbb, 0xe3, 0x1f, 0x38, 0x6d, 0x47, 0xb5, 0x03, 0x76, 0xad, 0xb8, 0xf3, 0x17, | ||||
|     0x2b, 0x1b, 0x62, 0x43, 0x38, 0x4e, 0xe5, 0x9c, 0xba, 0xd8, 0x2b, 0xbf, 0xb1, 0xa8, 0xf7, 0xa7, 0x76, 0xd6, 0x3d, | ||||
|     0x0b, 0x33, 0xf8, 0xd0, 0x4d, 0x7d, 0xef, 0xd6, 0xde, 0xe1, 0x1a, 0xbf, 0xd8, 0x30, 0x04, 0xec, 0x70, 0x17, 0xdb, | ||||
|     0x47, 0xef, 0xe1, 0xdc, 0xba, 0xbc, 0x17, 0xdc, 0x5c, 0x8f, 0xb8, 0x9d, 0xb4, 0x55, 0x45, 0x73, 0x05, 0xa3, 0x64, | ||||
|     0x16, 0x4c, 0x7e, 0x81, 0x41, 0x0e, 0xf2, 0x55, 0x54, 0xac, 0x8e, 0x0f, 0xa9, 0xaf, 0x19, 0xb7, 0x6e, 0x1f, 0xa0, | ||||
|     0xd5, 0x81, 0x8d, 0x88, 0xc1, 0x7d, 0x11, 0x45, 0x61, 0x40, 0xaf, 0xb9, 0x69, 0x2b, 0x2c, 0x49, 0x7e, 0x41, 0xf3, | ||||
|     0xbe, 0x0b, 0x45, 0x6e, 0xe0, 0x4a, 0x17, 0x9f, 0x5b, 0x7e, 0xec, 0xa7, 0x24, 0xec, 0xaa, 0x00, 0xcb, 0x43, 0x57, | ||||
|     0xb0, 0x6b, 0x01, 0x3f, 0x2e, 0xda, 0xdb, 0xdb, 0xba, 0x5f, 0xa4, 0x02, 0x09, 0x73, 0xad, 0xbe, 0x11, 0x62, 0xb3, | ||||
|     0x22, 0xd7, 0x46, 0x74, 0xd9, 0xaf, 0x44, 0x21, 0xd2, 0x78, 0xba, 0xa6, 0xa1, 0xf0, 0xc3, 0x54, 0x25, 0xd1, 0x58, | ||||
|     0x0c, 0x0b, 0xb7, 0xe9, 0x01, 0x2a, 0xb8, 0x08, 0xad, 0xef, 0x00, 0xeb, 0x7d, 0xce, 0x45, 0x68, 0xce, 0xd2, 0x5a, | ||||
|     0xd7, 0x06, 0x81, 0xa3, 0x37, 0xee, 0xf4, 0xde, 0xbc, 0x3f, 0x75, 0xd4, 0xf6, 0x3c, 0xd9, 0x8f, 0x3b, 0xbd, 0x13, | ||||
|     0xe9, 0x33, 0x51, 0x27, 0xf1, 0x88, 0x3a, 0x89, 0xe7, 0xe8, 0x53, 0x99, 0x10, 0x49, 0x2b, 0xf6, 0xd5, 0xb4, 0xa5, | ||||
|     0xcd, 0xa0, 0xbc, 0xbd, 0x93, 0x59, 0x22, 0x18, 0xdc, 0x71, 0xbd, 0x2f, 0x8f, 0xe1, 0xc1, 0x82, 0x95, 0x79, 0xd8, | ||||
|     0x5a, 0x3b, 0xbc, 0x16, 0xa9, 0xf1, 0x0d, 0x8f, 0x58, 0x42, 0x4d, 0xe6, 0xb5, 0xee, 0xaa, 0x3c, 0x29, 0xb0, 0x5e, | ||||
|     0x3b, 0x9f, 0x5d, 0x4f, 0x98, 0x70, 0xcd, 0x79, 0x86, 0x0f, 0xba, 0xc1, 0x89, 0x1c, 0xaa, 0x77, 0x55, 0x68, 0xe7, | ||||
|     0xb5, 0xf9, 0x9a, 0x4f, 0x7d, 0x49, 0xf5, 0xec, 0xb5, 0x84, 0x80, 0x13, 0x72, 0xf1, 0x41, 0xaf, 0x74, 0x17, 0xdb, | ||||
|     0xef, 0x8a, 0x93, 0xfd, 0xf8, 0xa0, 0x77, 0x15, 0x4c, 0x75, 0x7f, 0x2f, 0xf9, 0x78, 0x73, 0x5f, 0x09, 0x1f, 0xf7, | ||||
|     0xe5, 0x51, 0x10, 0x75, 0x48, 0xd9, 0x28, 0xbf, 0x3c, 0x71, 0x7b, 0x27, 0x5a, 0x19, 0x70, 0x64, 0x60, 0xdd, 0x3d, | ||||
|     0x6a, 0x99, 0xd3, 0x25, 0x09, 0x1f, 0xc3, 0x86, 0x54, 0x4d, 0xac, 0x41, 0x6a, 0x1e, 0xf7, 0xb8, 0xdd, 0x3b, 0x09, | ||||
|     0x1d, 0xc9, 0x5b, 0x24, 0xf3, 0xc8, 0x83, 0x7d, 0x68, 0x1c, 0xf3, 0x09, 0xf5, 0x19, 0xdf, 0xbf, 0xa1, 0xd7, 0xcd, | ||||
|     0x70, 0xca, 0x2a, 0xf7, 0x36, 0x28, 0x1d, 0xe5, 0x90, 0xdc, 0x78, 0xc4, 0xf5, 0xd9, 0xab, 0x4e, 0xe5, 0x6e, 0x3b, | ||||
|     0x04, 0x9b, 0xc7, 0xb8, 0xe6, 0xa4, 0x4f, 0xce, 0x02, 0x8b, 0xf7, 0x4e, 0xf6, 0xc3, 0x15, 0x8c, 0x48, 0x7e, 0x5f, | ||||
|     0x68, 0x47, 0x3b, 0x18, 0x36, 0x40, 0x6f, 0xae, 0xa3, 0xc4, 0x81, 0x71, 0xc8, 0x6b, 0x41, 0x5d, 0xb8, 0xbd, 0x7f, | ||||
|     0xfd, 0x1f, 0xff, 0x4b, 0xfb, 0xd8, 0x4f, 0xf6, 0xe3, 0xb6, 0xe9, 0x6b, 0x65, 0x55, 0x8a, 0x13, 0x38, 0xee, 0x59, | ||||
|     0x05, 0x85, 0xe9, 0x6d, 0x73, 0x9c, 0xb1, 0xa8, 0x19, 0x87, 0xc9, 0xc8, 0xed, 0x6d, 0xc7, 0xa6, 0x7d, 0x6c, 0x4b, | ||||
|     0x43, 0x5d, 0x2f, 0x02, 0x7a, 0xfd, 0x4d, 0x07, 0x8f, 0xcc, 0xf9, 0x15, 0xb9, 0xb5, 0xed, 0x63, 0x48, 0xd5, 0xee, | ||||
|     0xab, 0x1d, 0x45, 0x4a, 0xf5, 0x27, 0xc2, 0x34, 0x07, 0x4c, 0x6b, 0x27, 0x90, 0x0a, 0xd7, 0x29, 0x83, 0x5a, 0xff, | ||||
|     0xf7, 0x7f, 0xfe, 0x97, 0xff, 0x66, 0x1e, 0x21, 0x56, 0xf5, 0xaf, 0xff, 0xfd, 0x3f, 0xff, 0x9f, 0xff, 0xfd, 0x5f, | ||||
|     0xe1, 0xd4, 0x8a, 0x8e, 0x67, 0x49, 0xa6, 0xe2, 0x54, 0xc1, 0x2c, 0xc5, 0x5d, 0x1c, 0x48, 0xec, 0x9c, 0xb0, 0x5c, | ||||
|     0xb0, 0x61, 0xfd, 0x4c, 0xd2, 0xb9, 0x1c, 0x50, 0xee, 0x4c, 0x0d, 0x9d, 0xdc, 0xe1, 0x45, 0x45, 0x50, 0x35, 0x94, | ||||
|     0x4b, 0xc2, 0x2d, 0x4e, 0xf6, 0x01, 0xdf, 0x0f, 0x3b, 0xc6, 0xe9, 0x97, 0xcb, 0xb1, 0x30, 0x64, 0x02, 0x25, 0x45, | ||||
|     0x55, 0xee, 0x40, 0x6c, 0x65, 0x01, 0x8f, 0x41, 0xc7, 0x2a, 0x96, 0xab, 0x57, 0x6b, 0xd3, 0xfd, 0x69, 0x96, 0x0b, | ||||
|     0x36, 0x02, 0x94, 0x2b, 0x3f, 0xb1, 0x0c, 0x63, 0x37, 0x41, 0x57, 0x4c, 0xee, 0x0a, 0xd9, 0x8b, 0x22, 0xd0, 0xc3, | ||||
|     0xe3, 0x3f, 0x15, 0x7f, 0x99, 0x80, 0x46, 0xe6, 0x78, 0x93, 0xf0, 0x56, 0x9b, 0xe7, 0x8f, 0x5a, 0xad, 0xe9, 0x2d, | ||||
|     0x5a, 0x54, 0x23, 0xe0, 0x6d, 0x83, 0x49, 0x3a, 0xb6, 0x3b, 0x94, 0xf1, 0xef, 0xd2, 0x8d, 0xdd, 0x72, 0xc0, 0x17, | ||||
|     0xee, 0xb4, 0x8a, 0xe2, 0xcf, 0x0b, 0xe9, 0x49, 0x65, 0xbf, 0x40, 0x9c, 0x5a, 0x3b, 0x9d, 0xaf, 0xb9, 0x3d, 0xb9, | ||||
|     0x85, 0xd5, 0xaa, 0xa3, 0x5a, 0xc5, 0xed, 0xf5, 0xd3, 0x89, 0x76, 0x9c, 0xdd, 0x8e, 0x90, 0x1f, 0x42, 0xcc, 0x3b, | ||||
|     0x6e, 0xe3, 0xb8, 0xb3, 0x28, 0xbb, 0x17, 0x82, 0x4f, 0xec, 0xc0, 0x3a, 0x0d, 0xe9, 0x90, 0x8e, 0x8c, 0xb3, 0x5e, | ||||
|     0xbf, 0x57, 0x41, 0xf3, 0x22, 0x3e, 0xd8, 0x30, 0x96, 0x06, 0x49, 0x06, 0xd4, 0x9d, 0x56, 0xf1, 0x39, 0xec, 0xc0, | ||||
|     0xc5, 0x28, 0xe1, 0xa1, 0x08, 0x24, 0xc1, 0x76, 0xed, 0xf0, 0x7c, 0x08, 0x3c, 0x89, 0x2f, 0x2c, 0x78, 0xba, 0xaa, | ||||
|     0x2a, 0xb8, 0xcd, 0xeb, 0x67, 0x48, 0x0b, 0x5f, 0x36, 0xb7, 0xbb, 0x52, 0x5e, 0xb7, 0x6f, 0x75, 0xd4, 0xfb, 0x5d, | ||||
|     0xcd, 0x5d, 0xa5, 0x05, 0x52, 0x07, 0x6d, 0x7e, 0xaf, 0xe4, 0xba, 0x7a, 0xfb, 0xb5, 0xf0, 0x5c, 0x09, 0xa6, 0xbb, | ||||
|     0x5a, 0x4b, 0x16, 0x42, 0xad, 0x77, 0xe4, 0xdb, 0xd2, 0x64, 0x0a, 0xa7, 0x53, 0x59, 0x11, 0x75, 0x4f, 0xf6, 0x95, | ||||
|     0xa6, 0x0b, 0xdc, 0x43, 0xa6, 0x74, 0xa8, 0x0c, 0x0a, 0x5d, 0x49, 0x6f, 0x05, 0xf5, 0x4b, 0xe7, 0x56, 0xc0, 0xa7, | ||||
|     0xe3, 0x7a, 0xff, 0x0f, 0x82, 0x7a, 0x0b, 0xa7, 0xcf, 0x89, 0x00, 0x00}; | ||||
|     0x65, 0x7c, 0xed, 0x25, 0x00, 0x5b, 0x3e, 0xbd, 0x0f, 0xc7, 0xe5, 0xef, 0x57, 0x34, 0xcf, 0xc3, 0xb1, 0xae, 0xb9, | ||||
|     0x3d, 0x9e, 0x26, 0x41, 0xb4, 0x63, 0x69, 0x06, 0x08, 0x88, 0x89, 0x01, 0x46, 0xc0, 0xa7, 0xa1, 0x43, 0x64, 0x30, | ||||
|     0xf5, 0x7a, 0x74, 0x4d, 0xe2, 0xaa, 0x5e, 0x24, 0xc2, 0x71, 0x55, 0x70, 0x32, 0xcd, 0xa8, 0x2c, 0x55, 0x68, 0x2c, | ||||
|     0x4e, 0xf6, 0xa1, 0x40, 0xbd, 0xde, 0x12, 0x45, 0x33, 0x0e, 0x94, 0xed, 0xb1, 0x34, 0xc7, 0x44, 0xd1, 0xec, 0x44, | ||||
|     0xa5, 0x32, 0x4b, 0x69, 0x3d, 0x76, 0xf3, 0x79, 0x7b, 0x08, 0x7f, 0x74, 0x64, 0xe8, 0xf3, 0xd1, 0x68, 0x74, 0x6f, | ||||
|     0x54, 0xed, 0xf3, 0x68, 0x44, 0x3b, 0xf4, 0xa8, 0x0b, 0x49, 0x2c, 0x4d, 0x1d, 0x8b, 0x69, 0x17, 0x12, 0x77, 0x8b, | ||||
|     0x87, 0x55, 0x86, 0xb0, 0x8d, 0x88, 0x17, 0x0f, 0x8f, 0xb0, 0x15, 0xd3, 0x8c, 0x2e, 0x26, 0x61, 0x36, 0x66, 0x69, | ||||
|     0xd0, 0x2a, 0xfc, 0xb9, 0x0e, 0x49, 0x7d, 0x7e, 0x7c, 0x7c, 0x5c, 0xf8, 0x91, 0x79, 0x6a, 0x45, 0x51, 0xe1, 0x0f, | ||||
|     0x17, 0xe5, 0x34, 0x5a, 0xad, 0xd1, 0xa8, 0xf0, 0x99, 0x29, 0x38, 0xe8, 0x0c, 0xa3, 0x83, 0x4e, 0xe1, 0xdf, 0x58, | ||||
|     0x35, 0x0a, 0x9f, 0xea, 0xa7, 0x8c, 0x46, 0xb5, 0x4c, 0x98, 0xc7, 0xad, 0x56, 0xe1, 0x2b, 0x42, 0x5b, 0x80, 0x59, | ||||
|     0xaa, 0x7e, 0x06, 0xe1, 0x4c, 0x70, 0x60, 0xee, 0xdd, 0x44, 0x78, 0x83, 0x4b, 0x7d, 0xcb, 0x88, 0xfa, 0x26, 0x47, | ||||
|     0x81, 0x2e, 0xf0, 0xcf, 0x76, 0xf0, 0x08, 0x88, 0x59, 0x06, 0x8d, 0x12, 0x13, 0x5b, 0xaa, 0xbd, 0x06, 0xca, 0x92, | ||||
|     0xaf, 0x7f, 0x26, 0x49, 0x15, 0x53, 0x02, 0x4e, 0x06, 0x35, 0xd5, 0x65, 0x78, 0x94, 0x6e, 0x91, 0x1f, 0xec, 0xd3, | ||||
|     0xf2, 0xe3, 0xee, 0x21, 0xe2, 0x83, 0xfd, 0xe1, 0xe2, 0x83, 0x52, 0x4b, 0x7c, 0x28, 0xe6, 0x71, 0x27, 0x88, 0x3b, | ||||
|     0x8c, 0xe9, 0xf0, 0xe3, 0x35, 0xbf, 0x6d, 0xc2, 0x96, 0xc8, 0x5c, 0x29, 0x58, 0x76, 0x7f, 0x6b, 0xd6, 0x8c, 0xe9, | ||||
|     0xcc, 0xfa, 0xa2, 0x87, 0x54, 0x1f, 0xde, 0xa4, 0xc4, 0x7d, 0x63, 0x6c, 0x5b, 0x55, 0x32, 0x1a, 0x11, 0xf7, 0xcd, | ||||
|     0x68, 0xe4, 0x9a, 0xb3, 0x92, 0xa1, 0xa0, 0xb2, 0xd6, 0xeb, 0x5a, 0x89, 0xac, 0xf5, 0xe5, 0x97, 0x76, 0x99, 0x5d, | ||||
|     0xa0, 0x43, 0x4f, 0x76, 0x98, 0x49, 0xbf, 0x89, 0x58, 0x0e, 0x5b, 0x0d, 0x3e, 0x34, 0x52, 0xbf, 0xab, 0x31, 0xad, | ||||
|     0x5d, 0xab, 0x5d, 0x02, 0xbc, 0xe1, 0x2e, 0xf0, 0xd5, 0x8b, 0x02, 0xc6, 0xd4, 0xe4, 0x2d, 0x3e, 0xbd, 0xfb, 0x2a, | ||||
|     0xf2, 0xee, 0x04, 0x2a, 0x58, 0xfe, 0x26, 0x5d, 0x39, 0x04, 0xa4, 0x60, 0x24, 0xc4, 0x9e, 0x56, 0x21, 0xf8, 0x78, | ||||
|     0x9c, 0xc0, 0xb7, 0x5e, 0x16, 0xb5, 0xfb, 0x63, 0x55, 0xf3, 0x7e, 0x6d, 0xbe, 0x81, 0xdd, 0x50, 0xdf, 0xb6, 0x2a, | ||||
|     0x3f, 0x3d, 0xa5, 0x92, 0xc7, 0xe7, 0xfa, 0x1b, 0x44, 0xd2, 0x2c, 0x5e, 0x68, 0x26, 0xbf, 0x50, 0x29, 0xc7, 0x02, | ||||
|     0xd2, 0x6d, 0x54, 0xc7, 0x51, 0x51, 0xe8, 0xc3, 0x1a, 0x11, 0xcb, 0xa7, 0x70, 0xaf, 0xa9, 0x6a, 0x49, 0x3f, 0xc5, | ||||
|     0xc2, 0xf3, 0x1b, 0x2b, 0xbe, 0x53, 0x5b, 0xae, 0xc2, 0x04, 0x78, 0x94, 0xc3, 0xfc, 0x4e, 0x14, 0xae, 0xf6, 0xbb, | ||||
|     0x1b, 0x24, 0xba, 0x8e, 0xc2, 0xa7, 0x8a, 0x3c, 0x59, 0x33, 0x04, 0xe7, 0x77, 0xb9, 0x20, 0xe6, 0x95, 0x29, 0x28, | ||||
|     0xec, 0xf8, 0xa5, 0x7c, 0xa3, 0xb0, 0x25, 0xa3, 0x25, 0xf9, 0x34, 0x4c, 0x15, 0x1b, 0x25, 0xae, 0xe2, 0x07, 0xbb, | ||||
|     0x8b, 0x6a, 0xe5, 0x0b, 0xd7, 0x80, 0xad, 0x88, 0xb7, 0x77, 0xb2, 0x0f, 0x0d, 0x7a, 0x4e, 0x0d, 0xf4, 0x74, 0x2d, | ||||
|     0xc8, 0xf2, 0x89, 0x74, 0x87, 0x2b, 0x3f, 0xbf, 0xc1, 0x7e, 0x7e, 0xe3, 0xfc, 0x79, 0xd1, 0xbc, 0xa1, 0xd7, 0x1f, | ||||
|     0x99, 0x68, 0x8a, 0x70, 0xda, 0x04, 0xc3, 0x47, 0x3a, 0x47, 0x35, 0x7b, 0x96, 0x59, 0x7e, 0xea, 0xaa, 0x83, 0xee, | ||||
|     0x2c, 0x87, 0xac, 0x08, 0xa9, 0xbe, 0x07, 0x29, 0x4f, 0x69, 0xb7, 0x9e, 0xcd, 0x69, 0x07, 0xd9, 0x0d, 0xb6, 0x2e, | ||||
|     0x16, 0x1c, 0xb2, 0x28, 0xc4, 0x5d, 0xd0, 0xd2, 0x6c, 0xbd, 0x65, 0x22, 0xe8, 0xad, 0x8d, 0xf5, 0x03, 0x8d, 0xdc, | ||||
|     0x86, 0x94, 0x5e, 0xd9, 0x7a, 0x26, 0xc1, 0xb6, 0x4c, 0x80, 0x4f, 0xe5, 0x36, 0x82, 0x4b, 0xd5, 0xfc, 0xb5, 0x92, | ||||
|     0x42, 0x57, 0x8b, 0x65, 0x6e, 0xe3, 0x43, 0x20, 0x0b, 0xc2, 0x91, 0xa0, 0x19, 0x7e, 0x48, 0xcd, 0x6b, 0x79, 0x0c, | ||||
|     0x69, 0x01, 0x62, 0x26, 0x68, 0x1f, 0x4f, 0x6f, 0x1f, 0xde, 0xfd, 0xfd, 0xd3, 0x2f, 0x34, 0x8e, 0xcc, 0xb5, 0x3c, | ||||
|     0xae, 0xdb, 0x85, 0x8d, 0x90, 0x84, 0x77, 0x01, 0x4b, 0xa5, 0xcc, 0xbb, 0x06, 0xbf, 0x68, 0x77, 0xca, 0x75, 0x92, | ||||
|     0x6e, 0x46, 0x13, 0xf9, 0x15, 0x3e, 0xbd, 0x14, 0x07, 0x8f, 0xa6, 0xb7, 0x66, 0x35, 0xda, 0x2b, 0xc9, 0xb7, 0x7f, | ||||
|     0x68, 0x8e, 0xed, 0xf6, 0xa4, 0xde, 0x7a, 0x9e, 0xe8, 0xd1, 0xf4, 0xb6, 0xab, 0x04, 0x6d, 0x33, 0x53, 0x50, 0xb5, | ||||
|     0xa6, 0xb7, 0x76, 0x96, 0x71, 0xd5, 0x91, 0xe3, 0x1f, 0xe4, 0x0e, 0x0d, 0x73, 0xda, 0x85, 0x7b, 0xc7, 0xd9, 0x30, | ||||
|     0x4c, 0xb4, 0x30, 0x9f, 0xb0, 0x28, 0x4a, 0x68, 0xd7, 0xc8, 0x6b, 0xa7, 0xfd, 0x08, 0x92, 0x74, 0xed, 0x25, 0xab, | ||||
|     0xaf, 0x8a, 0x85, 0xbc, 0x12, 0x4f, 0xe1, 0x75, 0xce, 0x13, 0xf8, 0xe8, 0xc7, 0x46, 0x74, 0xea, 0xec, 0xd5, 0x56, | ||||
|     0x85, 0x3c, 0xf9, 0xbb, 0x3e, 0x97, 0xa3, 0xd6, 0x9f, 0xba, 0x72, 0xc1, 0x5b, 0x5d, 0xc1, 0xa7, 0x41, 0xf3, 0xa0, | ||||
|     0x3e, 0x11, 0x78, 0x55, 0x4e, 0x01, 0x6f, 0x98, 0x16, 0x06, 0x69, 0xa5, 0xf8, 0xb4, 0xe3, 0xb7, 0x75, 0x99, 0xec, | ||||
|     0x00, 0xf2, 0xc2, 0xca, 0xa2, 0xa2, 0x3e, 0x99, 0x7f, 0x9b, 0xdd, 0xf2, 0x64, 0xf3, 0x6e, 0x79, 0x62, 0x76, 0xcb, | ||||
|     0xfd, 0x14, 0xfb, 0xf9, 0xa8, 0x0d, 0x7f, 0xba, 0xd5, 0x84, 0x82, 0x96, 0x73, 0x30, 0xbd, 0x75, 0x40, 0x4f, 0x6b, | ||||
|     0x76, 0xa6, 0xb7, 0x2a, 0xc7, 0x1a, 0x62, 0x37, 0x2d, 0xc8, 0x3a, 0xc6, 0x2d, 0x07, 0x0a, 0xe1, 0x6f, 0xab, 0xf6, | ||||
|     0xaa, 0x7d, 0x08, 0xef, 0xa0, 0xd5, 0xd1, 0xfa, 0xbb, 0xce, 0xfd, 0x9b, 0x36, 0x48, 0xb9, 0xf0, 0x02, 0xc3, 0x8d, | ||||
|     0x91, 0x2f, 0xc2, 0xeb, 0x6b, 0x1a, 0x05, 0x23, 0x3e, 0x9c, 0xe5, 0xff, 0xa4, 0xe1, 0xd7, 0x48, 0xbc, 0x77, 0x4b, | ||||
|     0xaf, 0xf4, 0x63, 0x9a, 0xaa, 0x8c, 0x6f, 0xd3, 0xc3, 0xa2, 0x5c, 0xa7, 0x20, 0x1f, 0x86, 0x09, 0xf5, 0x3a, 0xfe, | ||||
|     0xe1, 0x86, 0x4d, 0xf0, 0xef, 0xb2, 0x36, 0x1b, 0x27, 0xf3, 0x7b, 0x91, 0x71, 0x2f, 0x12, 0x7e, 0x15, 0x0e, 0xec, | ||||
|     0x35, 0x6c, 0x1d, 0x6f, 0x06, 0x77, 0x60, 0x46, 0xba, 0x30, 0x42, 0x41, 0xcb, 0x9d, 0x88, 0x8e, 0xc2, 0x59, 0x22, | ||||
|     0xee, 0xef, 0x75, 0x1b, 0x65, 0xac, 0xf5, 0x7a, 0x0f, 0x43, 0xaf, 0xea, 0x3e, 0x90, 0x4b, 0x7f, 0xfe, 0xe4, 0x10, | ||||
|     0xfe, 0xa8, 0xfc, 0xaf, 0xbb, 0x4a, 0x57, 0x57, 0x76, 0x2f, 0xe8, 0xea, 0xbb, 0x35, 0x65, 0x5c, 0x89, 0x70, 0xa9, | ||||
|     0x8f, 0x3f, 0xb4, 0x36, 0x68, 0x95, 0x0f, 0xaa, 0xae, 0xb5, 0xac, 0x5f, 0x55, 0xfb, 0xd7, 0x75, 0xfe, 0xc0, 0xba, | ||||
|     0x43, 0xa5, 0xb9, 0xd6, 0xeb, 0xea, 0xcf, 0x10, 0xae, 0x55, 0x36, 0x18, 0x97, 0xf5, 0x77, 0xc9, 0x5d, 0x69, 0xa2, | ||||
|     0xa8, 0x68, 0x2c, 0x58, 0x29, 0xbb, 0xca, 0x4a, 0xc9, 0x29, 0xb9, 0x3a, 0xe9, 0xdf, 0x4e, 0x12, 0x67, 0xae, 0x8e, | ||||
|     0x4b, 0x12, 0xb7, 0xed, 0xb7, 0x5c, 0x47, 0xe6, 0x01, 0xc0, 0xad, 0xed, 0xae, 0xfc, 0xbc, 0xad, 0xdb, 0x07, 0x4d, | ||||
|     0x6b, 0x3e, 0x96, 0x9a, 0xdd, 0xcb, 0xf0, 0x8e, 0x66, 0x97, 0x1d, 0xd7, 0x01, 0x3f, 0x4d, 0x53, 0xa5, 0x4c, 0xc8, | ||||
|     0x32, 0xa7, 0xe3, 0x3a, 0xb7, 0x93, 0x24, 0xcd, 0x89, 0x1b, 0x0b, 0x31, 0x0d, 0xd4, 0xf7, 0x6f, 0x6f, 0x0e, 0x7c, | ||||
|     0x9e, 0x8d, 0xf7, 0x3b, 0xad, 0x56, 0x0b, 0x2e, 0x80, 0x75, 0x9d, 0x39, 0xa3, 0x37, 0x4f, 0xf9, 0x2d, 0x71, 0x5b, | ||||
|     0x4e, 0xcb, 0x69, 0x77, 0x8e, 0x9d, 0x76, 0xe7, 0xd0, 0x7f, 0x74, 0xec, 0xf6, 0x3e, 0x73, 0x9c, 0x93, 0x88, 0x8e, | ||||
|     0x72, 0xf8, 0xe1, 0x38, 0x27, 0x52, 0xf1, 0x52, 0xbf, 0x1d, 0xc7, 0x1f, 0x26, 0x79, 0xb3, 0xed, 0x2c, 0xf4, 0xa3, | ||||
|     0xe3, 0xc0, 0xa1, 0xd2, 0xc0, 0xf9, 0x7c, 0xd4, 0x19, 0x1d, 0x8e, 0x9e, 0x74, 0x75, 0x71, 0xf1, 0x59, 0xad, 0x3a, | ||||
|     0x56, 0xff, 0x77, 0xac, 0x66, 0xb9, 0xc8, 0xf8, 0x47, 0xaa, 0x73, 0x12, 0x1d, 0x10, 0x3d, 0x1b, 0x9b, 0x76, 0xd6, | ||||
|     0x47, 0x6a, 0x1f, 0x5f, 0x0f, 0x47, 0x9d, 0xaa, 0xba, 0x84, 0x71, 0xbf, 0x04, 0xf2, 0x64, 0xdf, 0x80, 0x7e, 0x62, | ||||
|     0xa3, 0xa9, 0xdd, 0xdc, 0x84, 0xa8, 0xb6, 0xab, 0xe7, 0x38, 0x36, 0xf3, 0x3b, 0x81, 0x33, 0x0c, 0x46, 0x57, 0x95, | ||||
|     0x10, 0xb8, 0x4e, 0x44, 0xdc, 0x57, 0xed, 0xce, 0x31, 0x6e, 0xb7, 0x1f, 0xf9, 0x8f, 0x8e, 0x87, 0x2d, 0x7c, 0xe8, | ||||
|     0x1f, 0x36, 0x0f, 0xfc, 0x47, 0xf8, 0xb8, 0x79, 0x8c, 0x8f, 0x5f, 0x1c, 0x0f, 0x9b, 0x87, 0xfe, 0x21, 0x6e, 0x35, | ||||
|     0x8f, 0xa1, 0xb0, 0x79, 0xdc, 0x3c, 0x9e, 0x37, 0x0f, 0x8f, 0x87, 0x2d, 0x59, 0xda, 0xf1, 0x8f, 0x8e, 0x9a, 0xed, | ||||
|     0x96, 0x7f, 0x74, 0x84, 0x8f, 0xfc, 0x47, 0x8f, 0x9a, 0xed, 0x03, 0xff, 0xd1, 0xa3, 0x97, 0x47, 0xc7, 0xfe, 0x01, | ||||
|     0xbc, 0x3b, 0x38, 0x18, 0x1e, 0xf8, 0xed, 0x76, 0x13, 0xfe, 0xc1, 0xc7, 0x7e, 0x47, 0xfd, 0x68, 0xb7, 0xfd, 0x83, | ||||
|     0x36, 0x6e, 0x25, 0x47, 0x1d, 0xff, 0xd1, 0x13, 0x2c, 0xff, 0x95, 0xd5, 0xb0, 0xfc, 0x07, 0xba, 0xc1, 0x4f, 0xfc, | ||||
|     0xce, 0x23, 0xf5, 0x4b, 0x76, 0x38, 0x3f, 0x3c, 0xfe, 0xc1, 0xdd, 0xdf, 0x3a, 0x87, 0xb6, 0x9a, 0xc3, 0xf1, 0x91, | ||||
|     0x7f, 0x70, 0x80, 0x0f, 0xdb, 0xfe, 0xf1, 0x41, 0xdc, 0x3c, 0xec, 0xf8, 0x8f, 0x1e, 0x0f, 0x9b, 0x6d, 0xff, 0xf1, | ||||
|     0x63, 0xdc, 0x6a, 0x1e, 0xf8, 0x1d, 0xdc, 0xf6, 0x0f, 0x0f, 0xe4, 0x8f, 0x03, 0xbf, 0x33, 0x7f, 0xfc, 0xc4, 0x7f, | ||||
|     0x74, 0x14, 0x3f, 0xf2, 0x0f, 0xbf, 0x3d, 0x3c, 0xf6, 0x3b, 0x07, 0xf1, 0xc1, 0x23, 0xbf, 0xf3, 0x78, 0xfe, 0xc8, | ||||
|     0x3f, 0x8c, 0x9b, 0x9d, 0x47, 0xf7, 0xb6, 0x6c, 0x77, 0x7c, 0xc0, 0x91, 0x7c, 0x0d, 0x2f, 0xb0, 0x7e, 0x01, 0x7f, | ||||
|     0x63, 0xd9, 0xf6, 0xdf, 0xb1, 0x9b, 0x7c, 0xbd, 0xe9, 0x13, 0xff, 0xf8, 0xf1, 0x50, 0x55, 0x87, 0x82, 0xa6, 0xa9, | ||||
|     0x01, 0x4d, 0xe6, 0x4d, 0x35, 0xac, 0xec, 0xae, 0x69, 0x3a, 0x32, 0x7f, 0xf5, 0x60, 0xf3, 0x26, 0x0c, 0xac, 0xc6, | ||||
|     0xfd, 0x0f, 0xed, 0xa7, 0x5c, 0xf2, 0x93, 0xfd, 0xb1, 0x22, 0xfd, 0x71, 0xef, 0x33, 0x75, 0xbb, 0xf3, 0x67, 0x57, | ||||
|     0x38, 0xdd, 0xe6, 0xf8, 0xc8, 0x3e, 0xed, 0xf8, 0xe0, 0xf4, 0x21, 0x9e, 0x8f, 0xec, 0x0f, 0xf7, 0x7c, 0xa4, 0x74, | ||||
|     0xc5, 0x71, 0x7e, 0x2d, 0xd6, 0x1c, 0x1c, 0xab, 0x56, 0xf1, 0x53, 0xe1, 0x0d, 0x72, 0xf8, 0x8e, 0x58, 0xd1, 0xbd, | ||||
|     0x16, 0x84, 0x53, 0xdb, 0x0f, 0xc4, 0x81, 0xc5, 0x5e, 0x0b, 0xc5, 0x63, 0x93, 0x6d, 0x08, 0x09, 0x3f, 0x8d, 0x90, | ||||
|     0x6f, 0x1f, 0x82, 0x8f, 0xf0, 0x0f, 0xc7, 0x47, 0x62, 0xe3, 0xa3, 0xe6, 0xcb, 0x97, 0x9e, 0x06, 0xe9, 0x29, 0x38, | ||||
|     0x97, 0xcf, 0x1e, 0x1c, 0xa2, 0x6a, 0xb8, 0xfb, 0x14, 0x8a, 0x72, 0x57, 0x45, 0xbe, 0xde, 0xfd, 0x9a, 0xb0, 0x83, | ||||
|     0x3a, 0x31, 0x49, 0x5c, 0xed, 0x96, 0x99, 0x4a, 0xa9, 0xa3, 0x1f, 0x4a, 0xa1, 0xd4, 0xf1, 0x5b, 0x7e, 0xab, 0x74, | ||||
|     0xe9, 0xc0, 0x29, 0x59, 0xb2, 0xe0, 0x22, 0x84, 0x2f, 0xd6, 0x26, 0x7c, 0x2c, 0xbf, 0x6d, 0x0b, 0x5f, 0x13, 0x80, | ||||
|     0xa4, 0x9f, 0xa1, 0xfa, 0x90, 0x43, 0xe0, 0xba, 0xfa, 0x6e, 0x0d, 0x38, 0x85, 0xf9, 0x0d, 0x9c, 0x54, 0x35, 0x51, | ||||
|     0x89, 0x09, 0x78, 0x3b, 0x5e, 0xd1, 0x88, 0x85, 0x9e, 0xeb, 0x4d, 0x33, 0x3a, 0xa2, 0x59, 0xde, 0xac, 0x1d, 0xdf, | ||||
|     0x94, 0x27, 0x37, 0x91, 0x6b, 0x3e, 0x8d, 0x9a, 0xc1, 0xed, 0xd8, 0x64, 0xa0, 0xfd, 0x8d, 0xae, 0x36, 0xc0, 0xdc, | ||||
|     0x02, 0x9b, 0x92, 0x0c, 0x64, 0x6d, 0xa5, 0xb4, 0xb9, 0x4a, 0x6b, 0x6b, 0xfb, 0x9d, 0x23, 0xe4, 0xc8, 0x62, 0xb8, | ||||
|     0x77, 0xf8, 0x7b, 0xaf, 0x79, 0xd0, 0xfa, 0x13, 0xb2, 0x9a, 0x95, 0x1d, 0x5d, 0x68, 0x77, 0x5b, 0x5a, 0x7d, 0x53, | ||||
|     0xba, 0x7e, 0xb6, 0xd6, 0x55, 0x14, 0xf1, 0xb9, 0x9a, 0xbb, 0x8b, 0xba, 0xa9, 0x8e, 0x70, 0xab, 0x1b, 0x22, 0x46, | ||||
|     0x6c, 0xec, 0xd9, 0x5f, 0x0c, 0x56, 0xf7, 0x1a, 0xcb, 0x0f, 0x8d, 0xa3, 0xa2, 0xaa, 0x92, 0xa2, 0x85, 0x8c, 0xb7, | ||||
|     0xb0, 0xd4, 0x49, 0x97, 0x4b, 0x2f, 0x05, 0x17, 0x39, 0xb1, 0x70, 0x0a, 0xcf, 0xa8, 0x86, 0xe4, 0x14, 0x97, 0x00, | ||||
|     0x49, 0x04, 0x93, 0x54, 0xfd, 0x5f, 0x15, 0x9b, 0x1f, 0xda, 0xf1, 0xe5, 0x27, 0x61, 0x3a, 0x06, 0x2a, 0x0c, 0xd3, | ||||
|     0xf1, 0x9a, 0x5b, 0x4d, 0x85, 0x8c, 0x56, 0x4a, 0xab, 0xae, 0x2a, 0xf7, 0x59, 0xfe, 0xf4, 0xee, 0xbd, 0xbe, 0x00, | ||||
|     0xcd, 0x05, 0xef, 0xb4, 0x8c, 0x70, 0x54, 0x97, 0x35, 0x37, 0xc8, 0x17, 0x27, 0x13, 0x2a, 0x42, 0x95, 0xaf, 0x09, | ||||
|     0xfa, 0x04, 0x9c, 0x9a, 0x75, 0xb4, 0x35, 0x4a, 0x5c, 0x29, 0xdd, 0x49, 0x44, 0xe7, 0x6c, 0xa8, 0x45, 0x3d, 0x76, | ||||
|     0xf4, 0xcd, 0x01, 0x4d, 0xb9, 0x34, 0xa4, 0x8d, 0x95, 0x3f, 0x66, 0x18, 0xca, 0x8c, 0x7c, 0x92, 0x72, 0xb7, 0xf7, | ||||
|     0x45, 0xf9, 0xf5, 0xd3, 0x6d, 0x8b, 0x90, 0xb0, 0xf4, 0xe3, 0x20, 0xa3, 0xc9, 0x3f, 0x91, 0x2f, 0xd8, 0x90, 0xa7, | ||||
|     0x5f, 0x5c, 0xc0, 0x57, 0xe9, 0xfd, 0x38, 0xa3, 0x23, 0xf2, 0x05, 0xc8, 0xf8, 0x40, 0x5a, 0x1f, 0xc0, 0x08, 0x1b, | ||||
|     0xb7, 0x93, 0x04, 0x4b, 0x8d, 0xe9, 0x01, 0x0a, 0x91, 0x02, 0xd7, 0xed, 0x1c, 0xb9, 0x8e, 0xb2, 0x89, 0xe5, 0xef, | ||||
|     0x9e, 0x12, 0xa7, 0x52, 0x09, 0x70, 0xda, 0x1d, 0xff, 0x28, 0xee, 0xf8, 0x4f, 0xe6, 0x8f, 0xfd, 0xe3, 0xb8, 0xfd, | ||||
|     0x78, 0xde, 0x84, 0xff, 0x3b, 0xfe, 0x93, 0xa4, 0xd9, 0xf1, 0x9f, 0xc0, 0xdf, 0x6f, 0x0f, 0xfd, 0xa3, 0xb8, 0xd9, | ||||
|     0xf6, 0x8f, 0xe7, 0x07, 0xfe, 0xc1, 0xcb, 0x76, 0xc7, 0x3f, 0x70, 0xda, 0x8e, 0x6a, 0x07, 0xec, 0x5a, 0x71, 0xe7, | ||||
|     0x2f, 0x56, 0x36, 0xc4, 0x86, 0x70, 0x9c, 0xca, 0x39, 0x75, 0xb1, 0x57, 0x7e, 0x63, 0x51, 0xef, 0x4f, 0xed, 0xac, | ||||
|     0x7b, 0x16, 0x66, 0xf0, 0xa1, 0x9b, 0xfa, 0xde, 0xad, 0xbd, 0xc3, 0x35, 0x7e, 0xb1, 0x61, 0x08, 0xd8, 0xe1, 0x2e, | ||||
|     0xb6, 0x8f, 0xde, 0xc3, 0xb9, 0x75, 0x79, 0x2f, 0xb8, 0xb9, 0x1e, 0x71, 0x3b, 0x69, 0xab, 0x8a, 0xe6, 0x0a, 0x46, | ||||
|     0xc9, 0x2c, 0x98, 0xfc, 0x02, 0x83, 0x1c, 0xe4, 0xab, 0xa8, 0x58, 0x1d, 0x1f, 0x52, 0x5f, 0x33, 0x6e, 0xdd, 0x3e, | ||||
|     0x40, 0xab, 0x03, 0x1b, 0x11, 0x83, 0xfb, 0x22, 0x8a, 0xc2, 0x80, 0x5e, 0x73, 0xd3, 0x56, 0x58, 0x92, 0xfc, 0x82, | ||||
|     0xe6, 0x7d, 0x17, 0x8a, 0xdc, 0xc0, 0x95, 0x2e, 0x3e, 0xb7, 0xfc, 0xd8, 0x4f, 0x49, 0xd8, 0x55, 0x01, 0x96, 0x87, | ||||
|     0xae, 0x60, 0xd7, 0x02, 0x7e, 0x5c, 0xb4, 0xb7, 0xb7, 0x75, 0xbf, 0x48, 0x05, 0x12, 0xe6, 0x5a, 0x7d, 0x23, 0xc4, | ||||
|     0x66, 0x45, 0xae, 0x8d, 0xe8, 0xb2, 0x5f, 0x89, 0x42, 0xa4, 0xf1, 0x74, 0x4d, 0x43, 0xe1, 0x87, 0xa9, 0x4a, 0xa2, | ||||
|     0xb1, 0x18, 0x16, 0x6e, 0xd3, 0x03, 0x54, 0x70, 0x11, 0x5a, 0xdf, 0x01, 0xd6, 0xfb, 0x9c, 0x8b, 0xd0, 0x9c, 0xa5, | ||||
|     0xb5, 0xae, 0x0d, 0x02, 0x47, 0x6f, 0xdc, 0xe9, 0xbd, 0x79, 0x7f, 0xea, 0xa8, 0xed, 0x79, 0xb2, 0x1f, 0x77, 0x7a, | ||||
|     0x27, 0xd2, 0x67, 0xa2, 0x4e, 0xe2, 0x11, 0x75, 0x12, 0xcf, 0xd1, 0xa7, 0x32, 0x21, 0x92, 0x56, 0xec, 0xab, 0x69, | ||||
|     0x4b, 0x9b, 0x41, 0x79, 0x7b, 0x27, 0xb3, 0x44, 0x30, 0xb8, 0xe3, 0x7a, 0x5f, 0x1e, 0xc3, 0x83, 0x05, 0x2b, 0xf3, | ||||
|     0xb0, 0xb5, 0x76, 0x78, 0x2d, 0x52, 0xe3, 0x1b, 0x1e, 0xb1, 0x84, 0x9a, 0xcc, 0x6b, 0xdd, 0x55, 0x79, 0x52, 0x60, | ||||
|     0xbd, 0x76, 0x3e, 0xbb, 0x9e, 0x30, 0xe1, 0x9a, 0xf3, 0x0c, 0x1f, 0x74, 0x83, 0x13, 0x39, 0x54, 0xef, 0xaa, 0xd0, | ||||
|     0xce, 0x6b, 0xf3, 0x35, 0x9f, 0xfa, 0x92, 0xea, 0xd9, 0x6b, 0x09, 0x01, 0x27, 0xe4, 0xe2, 0x83, 0x5e, 0xe9, 0x2e, | ||||
|     0xb6, 0xdf, 0x15, 0x27, 0xfb, 0xf1, 0x41, 0xef, 0x2a, 0x98, 0xea, 0xfe, 0x5e, 0xf2, 0xf1, 0xe6, 0xbe, 0x12, 0x3e, | ||||
|     0xee, 0xcb, 0xa3, 0x20, 0xea, 0x90, 0xb2, 0x51, 0x7e, 0x79, 0xe2, 0xf6, 0x4e, 0xb4, 0x32, 0xe0, 0xc8, 0xc0, 0xba, | ||||
|     0x7b, 0xd4, 0x32, 0xa7, 0x4b, 0x12, 0x3e, 0x86, 0x0d, 0xa9, 0x9a, 0x58, 0x83, 0xd4, 0x3c, 0xee, 0x71, 0xbb, 0x77, | ||||
|     0x12, 0x3a, 0x92, 0xb7, 0x48, 0xe6, 0x91, 0x07, 0xfb, 0xd0, 0x38, 0xe6, 0x13, 0xea, 0x33, 0xbe, 0x7f, 0x43, 0xaf, | ||||
|     0x9b, 0xe1, 0x94, 0x55, 0xee, 0x6d, 0x50, 0x3a, 0xca, 0x21, 0xb9, 0xf1, 0x88, 0xeb, 0xb3, 0x57, 0x9d, 0xca, 0xdd, | ||||
|     0x76, 0x08, 0x36, 0x8f, 0x71, 0xcd, 0x49, 0x9f, 0x9c, 0x05, 0x16, 0xef, 0x9d, 0xec, 0x87, 0x2b, 0x18, 0x91, 0xfc, | ||||
|     0xbe, 0xd0, 0x8e, 0x76, 0x30, 0x6c, 0x80, 0xde, 0x5c, 0x47, 0x89, 0x03, 0xe3, 0x90, 0xd7, 0x82, 0xba, 0x70, 0x7b, | ||||
|     0xff, 0xfa, 0x3f, 0xfe, 0x97, 0xf6, 0xb1, 0x9f, 0xec, 0xc7, 0x6d, 0xd3, 0xd7, 0xca, 0xaa, 0x14, 0x27, 0x70, 0xdc, | ||||
|     0xb3, 0x0a, 0x0a, 0xd3, 0xdb, 0xe6, 0x38, 0x63, 0x51, 0x33, 0x0e, 0x93, 0x91, 0xdb, 0xdb, 0x8e, 0x4d, 0xfb, 0xd8, | ||||
|     0x96, 0x86, 0xba, 0x5e, 0x04, 0xf4, 0xfa, 0x9b, 0x0e, 0x1e, 0x99, 0xf3, 0x2b, 0x72, 0x6b, 0xdb, 0xc7, 0x90, 0xaa, | ||||
|     0xdd, 0x57, 0x3b, 0x8a, 0x94, 0xea, 0x4f, 0x84, 0x69, 0x0e, 0x98, 0xd6, 0x4e, 0x20, 0x15, 0xae, 0x53, 0x06, 0xb5, | ||||
|     0xfe, 0xef, 0xff, 0xfc, 0x2f, 0xff, 0xcd, 0x3c, 0x42, 0xac, 0xea, 0x5f, 0xff, 0xfb, 0x7f, 0xfe, 0x3f, 0xff, 0xfb, | ||||
|     0xbf, 0xc2, 0xa9, 0x15, 0x1d, 0xcf, 0x92, 0x4c, 0xc5, 0xa9, 0x82, 0x59, 0x8a, 0xbb, 0x38, 0x90, 0xd8, 0x39, 0x61, | ||||
|     0xb9, 0x60, 0xc3, 0xfa, 0x99, 0xa4, 0x73, 0x39, 0xa0, 0xdc, 0x99, 0x1a, 0x3a, 0xb9, 0xc3, 0x8b, 0x8a, 0xa0, 0x6a, | ||||
|     0x28, 0x97, 0x84, 0x5b, 0x9c, 0xec, 0x03, 0xbe, 0x1f, 0x76, 0x8c, 0xd3, 0x2f, 0x97, 0x63, 0x61, 0xc8, 0x04, 0x4a, | ||||
|     0x8a, 0xaa, 0xdc, 0x81, 0xd8, 0xca, 0x02, 0x1e, 0x83, 0x8e, 0x55, 0x2c, 0x57, 0xaf, 0xd6, 0xa6, 0xfb, 0xd3, 0x2c, | ||||
|     0x17, 0x6c, 0x04, 0x28, 0x57, 0x7e, 0x62, 0x19, 0xc6, 0x6e, 0x82, 0xae, 0x98, 0xdc, 0x15, 0xb2, 0x17, 0x45, 0xa0, | ||||
|     0x87, 0xc7, 0x7f, 0x2a, 0xfe, 0x32, 0x01, 0x8d, 0xcc, 0xf1, 0x26, 0xe1, 0xad, 0x36, 0xcf, 0x1f, 0xb5, 0x5a, 0xd3, | ||||
|     0x5b, 0xb4, 0xa8, 0x46, 0xc0, 0xdb, 0x06, 0x93, 0x74, 0x6c, 0x77, 0x28, 0xe3, 0xdf, 0xa5, 0x1b, 0xbb, 0xe5, 0x80, | ||||
|     0x2f, 0xdc, 0x69, 0x15, 0xc5, 0x9f, 0x17, 0xd2, 0x93, 0xca, 0x7e, 0x81, 0x38, 0xb5, 0x76, 0x3a, 0x5f, 0x73, 0x7b, | ||||
|     0x72, 0x0b, 0xab, 0x55, 0x47, 0xb5, 0x8a, 0xdb, 0xeb, 0xa7, 0x13, 0xed, 0x38, 0xbb, 0x1d, 0x21, 0x3f, 0x84, 0x98, | ||||
|     0x77, 0xdc, 0xc6, 0x71, 0x67, 0x51, 0x76, 0x2f, 0x04, 0x9f, 0xd8, 0x81, 0x75, 0x1a, 0xd2, 0x21, 0x1d, 0x19, 0x67, | ||||
|     0xbd, 0x7e, 0xaf, 0x82, 0xe6, 0x45, 0x7c, 0xb0, 0x61, 0x2c, 0x0d, 0x92, 0x0c, 0xa8, 0x3b, 0xad, 0xe2, 0x73, 0xd8, | ||||
|     0x81, 0x8b, 0x51, 0xc2, 0x43, 0x11, 0x48, 0x82, 0xed, 0xda, 0xe1, 0xf9, 0x10, 0x78, 0x12, 0x5f, 0x58, 0xf0, 0x74, | ||||
|     0x55, 0x55, 0x70, 0x9b, 0xd7, 0xcf, 0x90, 0x16, 0xbe, 0x6c, 0x6e, 0x77, 0xa5, 0xbc, 0x6e, 0xdf, 0xea, 0xa8, 0xf7, | ||||
|     0xbb, 0x9a, 0xbb, 0x4a, 0x0b, 0xa4, 0x0e, 0xda, 0xfc, 0x5e, 0xc9, 0x75, 0xf5, 0xf6, 0x6b, 0xe1, 0xb9, 0x12, 0x4c, | ||||
|     0x77, 0xb5, 0x96, 0x2c, 0x84, 0x5a, 0xef, 0xc8, 0xb7, 0xa5, 0xc9, 0x14, 0x4e, 0xa7, 0xb2, 0x22, 0xea, 0x9e, 0xec, | ||||
|     0x2b, 0x4d, 0x17, 0xb8, 0x87, 0x4c, 0xe9, 0x50, 0x19, 0x14, 0xba, 0x92, 0xde, 0x0a, 0xea, 0x97, 0xce, 0xad, 0x80, | ||||
|     0x4f, 0xc7, 0xf5, 0xfe, 0x1f, 0xe7, 0xe0, 0x1c, 0x12, 0xcf, 0x89, 0x00, 0x00}; | ||||
|  | ||||
| }  // namespace web_server | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -4,7 +4,7 @@ from enum import Enum | ||||
|  | ||||
| from esphome.enum import StrEnum | ||||
|  | ||||
| __version__ = "2025.9.0-dev" | ||||
| __version__ = "2025.10.0-dev" | ||||
|  | ||||
| ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" | ||||
| VALID_SUBSTITUTIONS_CHARACTERS = ( | ||||
| @@ -424,6 +424,7 @@ CONF_HEAD = "head" | ||||
| CONF_HEADING = "heading" | ||||
| CONF_HEARTBEAT = "heartbeat" | ||||
| CONF_HEAT_ACTION = "heat_action" | ||||
| CONF_HEAT_COOL_MODE = "heat_cool_mode" | ||||
| CONF_HEAT_DEADBAND = "heat_deadband" | ||||
| CONF_HEAT_MODE = "heat_mode" | ||||
| CONF_HEAT_OVERRUN = "heat_overrun" | ||||
|   | ||||
| @@ -272,12 +272,15 @@ class OrderedDict(collections.OrderedDict): | ||||
|         return dict(self).__repr__() | ||||
|  | ||||
|  | ||||
| def list_yaml_files(folders: list[str]) -> list[str]: | ||||
|     files = filter_yaml_files( | ||||
|         [os.path.join(folder, p) for folder in folders for p in os.listdir(folder)] | ||||
|     ) | ||||
|     files.sort() | ||||
|     return files | ||||
| def list_yaml_files(configs: list[str]) -> list[str]: | ||||
|     files: list[str] = [] | ||||
|     for config in configs: | ||||
|         if os.path.isfile(config): | ||||
|             files.append(config) | ||||
|         else: | ||||
|             files.extend(os.path.join(config, p) for p in os.listdir(config)) | ||||
|     files = filter_yaml_files(files) | ||||
|     return sorted(files) | ||||
|  | ||||
|  | ||||
| def filter_yaml_files(files: list[str]) -> list[str]: | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| pylint==3.3.8 | ||||
| flake8==7.3.0  # also change in .pre-commit-config.yaml when updating | ||||
| ruff==0.12.12  # also change in .pre-commit-config.yaml when updating | ||||
| ruff==0.13.0  # also change in .pre-commit-config.yaml when updating | ||||
| pyupgrade==3.20.0  # also change in .pre-commit-config.yaml when updating | ||||
| pre-commit | ||||
|  | ||||
|   | ||||
| @@ -4,8 +4,14 @@ uart: | ||||
|     rx_pin: ${rx_pin} | ||||
|     baud_rate: 9600 | ||||
|  | ||||
| button: | ||||
|   - platform: bl0940 | ||||
|     bl0940_id: test_id | ||||
|     name: Cal Reset | ||||
|  | ||||
| sensor: | ||||
|   - platform: bl0940 | ||||
|     id: test_id | ||||
|     voltage: | ||||
|       name: BL0940 Voltage | ||||
|     current: | ||||
| @@ -18,3 +24,18 @@ sensor: | ||||
|       name: BL0940 Internal temperature | ||||
|     external_temperature: | ||||
|       name: BL0940 External temperature | ||||
|  | ||||
| number: | ||||
|   - platform: bl0940 | ||||
|     id: bl0940_number_id | ||||
|     bl0940_id: test_id | ||||
|     current_calibration: | ||||
|       name: Cal Current | ||||
|       min_value: -5 | ||||
|       max_value: 5 | ||||
|     voltage_calibration: | ||||
|       name: Cal Voltage | ||||
|       step: 0.01 | ||||
|     power_calibration: | ||||
|       name: Cal Power | ||||
|       disabled_by_default: true | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| i2c: | ||||
|   - id: i2c_inkplate6 | ||||
|   - id: i2c_inkplate | ||||
|     scl: 16 | ||||
|     sda: 17 | ||||
| 
 | ||||
| @@ -7,7 +7,7 @@ esp32: | ||||
|   cpu_frequency: 240MHz | ||||
| 
 | ||||
| display: | ||||
|   - platform: inkplate6 | ||||
|   - platform: inkplate | ||||
|     id: inkplate_display | ||||
|     greyscale: false | ||||
|     partial_updating: false | ||||
| @@ -36,3 +36,10 @@ def fixture_path() -> Path: | ||||
|     Location of all fixture files. | ||||
|     """ | ||||
|     return here / "fixtures" | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def setup_core(tmp_path: Path) -> Path: | ||||
|     """Set up CORE with test paths.""" | ||||
|     CORE.config_path = str(tmp_path / "test.yaml") | ||||
|     return tmp_path | ||||
|   | ||||
							
								
								
									
										187
									
								
								tests/unit_tests/test_config_validation_paths.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								tests/unit_tests/test_config_validation_paths.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | ||||
| """Tests for config_validation.py path-related functions.""" | ||||
|  | ||||
| from pathlib import Path | ||||
|  | ||||
| import pytest | ||||
| import voluptuous as vol | ||||
|  | ||||
| from esphome import config_validation as cv | ||||
|  | ||||
|  | ||||
| def test_directory_valid_path(setup_core: Path) -> None: | ||||
|     """Test directory validator with valid directory.""" | ||||
|     test_dir = setup_core / "test_directory" | ||||
|     test_dir.mkdir() | ||||
|  | ||||
|     result = cv.directory("test_directory") | ||||
|  | ||||
|     assert result == "test_directory" | ||||
|  | ||||
|  | ||||
| def test_directory_absolute_path(setup_core: Path) -> None: | ||||
|     """Test directory validator with absolute path.""" | ||||
|     test_dir = setup_core / "test_directory" | ||||
|     test_dir.mkdir() | ||||
|  | ||||
|     result = cv.directory(str(test_dir)) | ||||
|  | ||||
|     assert result == str(test_dir) | ||||
|  | ||||
|  | ||||
| def test_directory_nonexistent_path(setup_core: Path) -> None: | ||||
|     """Test directory validator raises error for non-existent directory.""" | ||||
|     with pytest.raises( | ||||
|         vol.Invalid, match="Could not find directory.*nonexistent_directory" | ||||
|     ): | ||||
|         cv.directory("nonexistent_directory") | ||||
|  | ||||
|  | ||||
| def test_directory_file_instead_of_directory(setup_core: Path) -> None: | ||||
|     """Test directory validator raises error when path is a file.""" | ||||
|     test_file = setup_core / "test_file.txt" | ||||
|     test_file.write_text("content") | ||||
|  | ||||
|     with pytest.raises(vol.Invalid, match="is not a directory"): | ||||
|         cv.directory("test_file.txt") | ||||
|  | ||||
|  | ||||
| def test_directory_with_parent_directory(setup_core: Path) -> None: | ||||
|     """Test directory validator with nested directory structure.""" | ||||
|     nested_dir = setup_core / "parent" / "child" / "grandchild" | ||||
|     nested_dir.mkdir(parents=True) | ||||
|  | ||||
|     result = cv.directory("parent/child/grandchild") | ||||
|  | ||||
|     assert result == "parent/child/grandchild" | ||||
|  | ||||
|  | ||||
| def test_file_valid_path(setup_core: Path) -> None: | ||||
|     """Test file_ validator with valid file.""" | ||||
|     test_file = setup_core / "test_file.yaml" | ||||
|     test_file.write_text("test content") | ||||
|  | ||||
|     result = cv.file_("test_file.yaml") | ||||
|  | ||||
|     assert result == "test_file.yaml" | ||||
|  | ||||
|  | ||||
| def test_file_absolute_path(setup_core: Path) -> None: | ||||
|     """Test file_ validator with absolute path.""" | ||||
|     test_file = setup_core / "test_file.yaml" | ||||
|     test_file.write_text("test content") | ||||
|  | ||||
|     result = cv.file_(str(test_file)) | ||||
|  | ||||
|     assert result == str(test_file) | ||||
|  | ||||
|  | ||||
| def test_file_nonexistent_path(setup_core: Path) -> None: | ||||
|     """Test file_ validator raises error for non-existent file.""" | ||||
|     with pytest.raises(vol.Invalid, match="Could not find file.*nonexistent_file.yaml"): | ||||
|         cv.file_("nonexistent_file.yaml") | ||||
|  | ||||
|  | ||||
| def test_file_directory_instead_of_file(setup_core: Path) -> None: | ||||
|     """Test file_ validator raises error when path is a directory.""" | ||||
|     test_dir = setup_core / "test_directory" | ||||
|     test_dir.mkdir() | ||||
|  | ||||
|     with pytest.raises(vol.Invalid, match="is not a file"): | ||||
|         cv.file_("test_directory") | ||||
|  | ||||
|  | ||||
| def test_file_with_parent_directory(setup_core: Path) -> None: | ||||
|     """Test file_ validator with file in nested directory.""" | ||||
|     nested_dir = setup_core / "configs" / "sensors" | ||||
|     nested_dir.mkdir(parents=True) | ||||
|     test_file = nested_dir / "temperature.yaml" | ||||
|     test_file.write_text("sensor config") | ||||
|  | ||||
|     result = cv.file_("configs/sensors/temperature.yaml") | ||||
|  | ||||
|     assert result == "configs/sensors/temperature.yaml" | ||||
|  | ||||
|  | ||||
| def test_directory_handles_trailing_slash(setup_core: Path) -> None: | ||||
|     """Test directory validator handles trailing slashes correctly.""" | ||||
|     test_dir = setup_core / "test_dir" | ||||
|     test_dir.mkdir() | ||||
|  | ||||
|     result = cv.directory("test_dir/") | ||||
|     assert result == "test_dir/" | ||||
|  | ||||
|     result = cv.directory("test_dir") | ||||
|     assert result == "test_dir" | ||||
|  | ||||
|  | ||||
| def test_file_handles_various_extensions(setup_core: Path) -> None: | ||||
|     """Test file_ validator works with different file extensions.""" | ||||
|     yaml_file = setup_core / "config.yaml" | ||||
|     yaml_file.write_text("yaml content") | ||||
|     assert cv.file_("config.yaml") == "config.yaml" | ||||
|  | ||||
|     yml_file = setup_core / "config.yml" | ||||
|     yml_file.write_text("yml content") | ||||
|     assert cv.file_("config.yml") == "config.yml" | ||||
|  | ||||
|     txt_file = setup_core / "readme.txt" | ||||
|     txt_file.write_text("text content") | ||||
|     assert cv.file_("readme.txt") == "readme.txt" | ||||
|  | ||||
|     no_ext_file = setup_core / "LICENSE" | ||||
|     no_ext_file.write_text("license content") | ||||
|     assert cv.file_("LICENSE") == "LICENSE" | ||||
|  | ||||
|  | ||||
| def test_directory_with_symlink(setup_core: Path) -> None: | ||||
|     """Test directory validator follows symlinks.""" | ||||
|     actual_dir = setup_core / "actual_directory" | ||||
|     actual_dir.mkdir() | ||||
|  | ||||
|     symlink_dir = setup_core / "symlink_directory" | ||||
|     symlink_dir.symlink_to(actual_dir) | ||||
|  | ||||
|     result = cv.directory("symlink_directory") | ||||
|     assert result == "symlink_directory" | ||||
|  | ||||
|  | ||||
| def test_file_with_symlink(setup_core: Path) -> None: | ||||
|     """Test file_ validator follows symlinks.""" | ||||
|     actual_file = setup_core / "actual_file.txt" | ||||
|     actual_file.write_text("content") | ||||
|  | ||||
|     symlink_file = setup_core / "symlink_file.txt" | ||||
|     symlink_file.symlink_to(actual_file) | ||||
|  | ||||
|     result = cv.file_("symlink_file.txt") | ||||
|     assert result == "symlink_file.txt" | ||||
|  | ||||
|  | ||||
| def test_directory_error_shows_full_path(setup_core: Path) -> None: | ||||
|     """Test directory validator error message includes full path.""" | ||||
|     with pytest.raises(vol.Invalid, match=".*missing_dir.*full path:.*"): | ||||
|         cv.directory("missing_dir") | ||||
|  | ||||
|  | ||||
| def test_file_error_shows_full_path(setup_core: Path) -> None: | ||||
|     """Test file_ validator error message includes full path.""" | ||||
|     with pytest.raises(vol.Invalid, match=".*missing_file.yaml.*full path:.*"): | ||||
|         cv.file_("missing_file.yaml") | ||||
|  | ||||
|  | ||||
| def test_directory_with_spaces_in_name(setup_core: Path) -> None: | ||||
|     """Test directory validator handles spaces in directory names.""" | ||||
|     dir_with_spaces = setup_core / "my test directory" | ||||
|     dir_with_spaces.mkdir() | ||||
|  | ||||
|     result = cv.directory("my test directory") | ||||
|     assert result == "my test directory" | ||||
|  | ||||
|  | ||||
| def test_file_with_spaces_in_name(setup_core: Path) -> None: | ||||
|     """Test file_ validator handles spaces in file names.""" | ||||
|     file_with_spaces = setup_core / "my test file.yaml" | ||||
|     file_with_spaces.write_text("content") | ||||
|  | ||||
|     result = cv.file_("my test file.yaml") | ||||
|     assert result == "my test file.yaml" | ||||
							
								
								
									
										196
									
								
								tests/unit_tests/test_external_files.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								tests/unit_tests/test_external_files.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | ||||
| """Tests for external_files.py functions.""" | ||||
|  | ||||
| from pathlib import Path | ||||
| import time | ||||
| from unittest.mock import MagicMock, patch | ||||
|  | ||||
| import pytest | ||||
| import requests | ||||
|  | ||||
| from esphome import external_files | ||||
| from esphome.config_validation import Invalid | ||||
| from esphome.core import CORE, TimePeriod | ||||
|  | ||||
|  | ||||
| def test_compute_local_file_dir(setup_core: Path) -> None: | ||||
|     """Test compute_local_file_dir creates and returns correct path.""" | ||||
|     domain = "font" | ||||
|  | ||||
|     result = external_files.compute_local_file_dir(domain) | ||||
|  | ||||
|     assert isinstance(result, Path) | ||||
|     assert result == Path(CORE.data_dir) / domain | ||||
|     assert result.exists() | ||||
|     assert result.is_dir() | ||||
|  | ||||
|  | ||||
| def test_compute_local_file_dir_nested(setup_core: Path) -> None: | ||||
|     """Test compute_local_file_dir works with nested domains.""" | ||||
|     domain = "images/icons" | ||||
|  | ||||
|     result = external_files.compute_local_file_dir(domain) | ||||
|  | ||||
|     assert result == Path(CORE.data_dir) / "images" / "icons" | ||||
|     assert result.exists() | ||||
|     assert result.is_dir() | ||||
|  | ||||
|  | ||||
| def test_is_file_recent_with_recent_file(setup_core: Path) -> None: | ||||
|     """Test is_file_recent returns True for recently created file.""" | ||||
|     test_file = setup_core / "recent.txt" | ||||
|     test_file.write_text("content") | ||||
|  | ||||
|     refresh = TimePeriod(seconds=3600) | ||||
|  | ||||
|     result = external_files.is_file_recent(str(test_file), refresh) | ||||
|  | ||||
|     assert result is True | ||||
|  | ||||
|  | ||||
| def test_is_file_recent_with_old_file(setup_core: Path) -> None: | ||||
|     """Test is_file_recent returns False for old file.""" | ||||
|     test_file = setup_core / "old.txt" | ||||
|     test_file.write_text("content") | ||||
|  | ||||
|     old_time = time.time() - 7200 | ||||
|  | ||||
|     with patch("os.path.getctime", return_value=old_time): | ||||
|         refresh = TimePeriod(seconds=3600) | ||||
|  | ||||
|         result = external_files.is_file_recent(str(test_file), refresh) | ||||
|  | ||||
|         assert result is False | ||||
|  | ||||
|  | ||||
| def test_is_file_recent_nonexistent_file(setup_core: Path) -> None: | ||||
|     """Test is_file_recent returns False for non-existent file.""" | ||||
|     test_file = setup_core / "nonexistent.txt" | ||||
|     refresh = TimePeriod(seconds=3600) | ||||
|  | ||||
|     result = external_files.is_file_recent(str(test_file), refresh) | ||||
|  | ||||
|     assert result is False | ||||
|  | ||||
|  | ||||
| def test_is_file_recent_with_zero_refresh(setup_core: Path) -> None: | ||||
|     """Test is_file_recent with zero refresh period returns False.""" | ||||
|     test_file = setup_core / "test.txt" | ||||
|     test_file.write_text("content") | ||||
|  | ||||
|     # Mock getctime to return a time 10 seconds ago | ||||
|     with patch("os.path.getctime", return_value=time.time() - 10): | ||||
|         refresh = TimePeriod(seconds=0) | ||||
|         result = external_files.is_file_recent(str(test_file), refresh) | ||||
|         assert result is False | ||||
|  | ||||
|  | ||||
| @patch("esphome.external_files.requests.head") | ||||
| def test_has_remote_file_changed_not_modified( | ||||
|     mock_head: MagicMock, setup_core: Path | ||||
| ) -> None: | ||||
|     """Test has_remote_file_changed returns False when file not modified.""" | ||||
|     test_file = setup_core / "cached.txt" | ||||
|     test_file.write_text("cached content") | ||||
|  | ||||
|     mock_response = MagicMock() | ||||
|     mock_response.status_code = 304 | ||||
|     mock_head.return_value = mock_response | ||||
|  | ||||
|     url = "https://example.com/file.txt" | ||||
|     result = external_files.has_remote_file_changed(url, str(test_file)) | ||||
|  | ||||
|     assert result is False | ||||
|     mock_head.assert_called_once() | ||||
|  | ||||
|     call_args = mock_head.call_args | ||||
|     headers = call_args[1]["headers"] | ||||
|     assert external_files.IF_MODIFIED_SINCE in headers | ||||
|     assert external_files.CACHE_CONTROL in headers | ||||
|  | ||||
|  | ||||
| @patch("esphome.external_files.requests.head") | ||||
| def test_has_remote_file_changed_modified( | ||||
|     mock_head: MagicMock, setup_core: Path | ||||
| ) -> None: | ||||
|     """Test has_remote_file_changed returns True when file modified.""" | ||||
|     test_file = setup_core / "cached.txt" | ||||
|     test_file.write_text("cached content") | ||||
|  | ||||
|     mock_response = MagicMock() | ||||
|     mock_response.status_code = 200 | ||||
|     mock_head.return_value = mock_response | ||||
|  | ||||
|     url = "https://example.com/file.txt" | ||||
|     result = external_files.has_remote_file_changed(url, str(test_file)) | ||||
|  | ||||
|     assert result is True | ||||
|  | ||||
|  | ||||
| def test_has_remote_file_changed_no_local_file(setup_core: Path) -> None: | ||||
|     """Test has_remote_file_changed returns True when local file doesn't exist.""" | ||||
|     test_file = setup_core / "nonexistent.txt" | ||||
|  | ||||
|     url = "https://example.com/file.txt" | ||||
|     result = external_files.has_remote_file_changed(url, str(test_file)) | ||||
|  | ||||
|     assert result is True | ||||
|  | ||||
|  | ||||
| @patch("esphome.external_files.requests.head") | ||||
| def test_has_remote_file_changed_network_error( | ||||
|     mock_head: MagicMock, setup_core: Path | ||||
| ) -> None: | ||||
|     """Test has_remote_file_changed handles network errors gracefully.""" | ||||
|     test_file = setup_core / "cached.txt" | ||||
|     test_file.write_text("cached content") | ||||
|  | ||||
|     mock_head.side_effect = requests.exceptions.RequestException("Network error") | ||||
|  | ||||
|     url = "https://example.com/file.txt" | ||||
|  | ||||
|     with pytest.raises(Invalid, match="Could not check if.*Network error"): | ||||
|         external_files.has_remote_file_changed(url, str(test_file)) | ||||
|  | ||||
|  | ||||
| @patch("esphome.external_files.requests.head") | ||||
| def test_has_remote_file_changed_timeout( | ||||
|     mock_head: MagicMock, setup_core: Path | ||||
| ) -> None: | ||||
|     """Test has_remote_file_changed respects timeout.""" | ||||
|     test_file = setup_core / "cached.txt" | ||||
|     test_file.write_text("cached content") | ||||
|  | ||||
|     mock_response = MagicMock() | ||||
|     mock_response.status_code = 304 | ||||
|     mock_head.return_value = mock_response | ||||
|  | ||||
|     url = "https://example.com/file.txt" | ||||
|     external_files.has_remote_file_changed(url, str(test_file)) | ||||
|  | ||||
|     call_args = mock_head.call_args | ||||
|     assert call_args[1]["timeout"] == external_files.NETWORK_TIMEOUT | ||||
|  | ||||
|  | ||||
| def test_compute_local_file_dir_creates_parent_dirs(setup_core: Path) -> None: | ||||
|     """Test compute_local_file_dir creates parent directories.""" | ||||
|     domain = "level1/level2/level3/level4" | ||||
|  | ||||
|     result = external_files.compute_local_file_dir(domain) | ||||
|  | ||||
|     assert result.exists() | ||||
|     assert result.is_dir() | ||||
|     assert result.parent.name == "level3" | ||||
|     assert result.parent.parent.name == "level2" | ||||
|     assert result.parent.parent.parent.name == "level1" | ||||
|  | ||||
|  | ||||
| def test_is_file_recent_handles_float_seconds(setup_core: Path) -> None: | ||||
|     """Test is_file_recent works with float seconds in TimePeriod.""" | ||||
|     test_file = setup_core / "test.txt" | ||||
|     test_file.write_text("content") | ||||
|  | ||||
|     refresh = TimePeriod(seconds=3600.5) | ||||
|  | ||||
|     result = external_files.is_file_recent(str(test_file), refresh) | ||||
|  | ||||
|     assert result is True | ||||
							
								
								
									
										129
									
								
								tests/unit_tests/test_platformio_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								tests/unit_tests/test_platformio_api.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| """Tests for platformio_api.py path functions.""" | ||||
|  | ||||
| from pathlib import Path | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from esphome import platformio_api | ||||
| from esphome.core import CORE | ||||
|  | ||||
|  | ||||
| def test_idedata_firmware_elf_path(setup_core: Path) -> None: | ||||
|     """Test IDEData.firmware_elf_path returns correct path.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|     raw_data = {"prog_path": "/path/to/firmware.elf"} | ||||
|     idedata = platformio_api.IDEData(raw_data) | ||||
|  | ||||
|     assert idedata.firmware_elf_path == "/path/to/firmware.elf" | ||||
|  | ||||
|  | ||||
| def test_idedata_firmware_bin_path(setup_core: Path) -> None: | ||||
|     """Test IDEData.firmware_bin_path returns Path with .bin extension.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|     prog_path = str(Path("/path/to/firmware.elf")) | ||||
|     raw_data = {"prog_path": prog_path} | ||||
|     idedata = platformio_api.IDEData(raw_data) | ||||
|  | ||||
|     result = idedata.firmware_bin_path | ||||
|     assert isinstance(result, str) | ||||
|     expected = str(Path("/path/to/firmware.bin")) | ||||
|     assert result == expected | ||||
|     assert result.endswith(".bin") | ||||
|  | ||||
|  | ||||
| def test_idedata_firmware_bin_path_preserves_directory(setup_core: Path) -> None: | ||||
|     """Test firmware_bin_path preserves the directory structure.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|     prog_path = str(Path("/complex/path/to/build/firmware.elf")) | ||||
|     raw_data = {"prog_path": prog_path} | ||||
|     idedata = platformio_api.IDEData(raw_data) | ||||
|  | ||||
|     result = idedata.firmware_bin_path | ||||
|     expected = str(Path("/complex/path/to/build/firmware.bin")) | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_idedata_extra_flash_images(setup_core: Path) -> None: | ||||
|     """Test IDEData.extra_flash_images returns list of FlashImage objects.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|     raw_data = { | ||||
|         "prog_path": "/path/to/firmware.elf", | ||||
|         "extra": { | ||||
|             "flash_images": [ | ||||
|                 {"path": "/path/to/bootloader.bin", "offset": "0x1000"}, | ||||
|                 {"path": "/path/to/partition.bin", "offset": "0x8000"}, | ||||
|             ] | ||||
|         }, | ||||
|     } | ||||
|     idedata = platformio_api.IDEData(raw_data) | ||||
|  | ||||
|     images = idedata.extra_flash_images | ||||
|     assert len(images) == 2 | ||||
|     assert all(isinstance(img, platformio_api.FlashImage) for img in images) | ||||
|     assert images[0].path == "/path/to/bootloader.bin" | ||||
|     assert images[0].offset == "0x1000" | ||||
|     assert images[1].path == "/path/to/partition.bin" | ||||
|     assert images[1].offset == "0x8000" | ||||
|  | ||||
|  | ||||
| def test_idedata_extra_flash_images_empty(setup_core: Path) -> None: | ||||
|     """Test extra_flash_images returns empty list when no extra images.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|     raw_data = {"prog_path": "/path/to/firmware.elf", "extra": {"flash_images": []}} | ||||
|     idedata = platformio_api.IDEData(raw_data) | ||||
|  | ||||
|     images = idedata.extra_flash_images | ||||
|     assert images == [] | ||||
|  | ||||
|  | ||||
| def test_idedata_cc_path(setup_core: Path) -> None: | ||||
|     """Test IDEData.cc_path returns compiler path.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|     raw_data = { | ||||
|         "prog_path": "/path/to/firmware.elf", | ||||
|         "cc_path": "/Users/test/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc", | ||||
|     } | ||||
|     idedata = platformio_api.IDEData(raw_data) | ||||
|  | ||||
|     assert ( | ||||
|         idedata.cc_path | ||||
|         == "/Users/test/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc" | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_flash_image_dataclass() -> None: | ||||
|     """Test FlashImage dataclass stores path and offset correctly.""" | ||||
|     image = platformio_api.FlashImage(path="/path/to/image.bin", offset="0x10000") | ||||
|  | ||||
|     assert image.path == "/path/to/image.bin" | ||||
|     assert image.offset == "0x10000" | ||||
|  | ||||
|  | ||||
| def test_load_idedata_returns_dict(setup_core: Path) -> None: | ||||
|     """Test _load_idedata returns parsed idedata dict when successful.""" | ||||
|     CORE.build_path = str(setup_core / "build" / "test") | ||||
|     CORE.name = "test" | ||||
|  | ||||
|     # Create required files | ||||
|     platformio_ini = setup_core / "build" / "test" / "platformio.ini" | ||||
|     platformio_ini.parent.mkdir(parents=True, exist_ok=True) | ||||
|     platformio_ini.touch() | ||||
|  | ||||
|     idedata_path = setup_core / ".esphome" / "idedata" / "test.json" | ||||
|     idedata_path.parent.mkdir(parents=True, exist_ok=True) | ||||
|     idedata_path.write_text('{"prog_path": "/test/firmware.elf"}') | ||||
|  | ||||
|     with patch("esphome.platformio_api.run_platformio_cli_run") as mock_run: | ||||
|         mock_run.return_value = '{"prog_path": "/test/firmware.elf"}' | ||||
|  | ||||
|         config = {"name": "test"} | ||||
|         result = platformio_api._load_idedata(config) | ||||
|  | ||||
|     assert result is not None | ||||
|     assert isinstance(result, dict) | ||||
|     assert result["prog_path"] == "/test/firmware.elf" | ||||
							
								
								
									
										182
									
								
								tests/unit_tests/test_storage_json.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								tests/unit_tests/test_storage_json.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| """Tests for storage_json.py path functions.""" | ||||
|  | ||||
| from pathlib import Path | ||||
| import sys | ||||
| from unittest.mock import patch | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from esphome import storage_json | ||||
| from esphome.core import CORE | ||||
|  | ||||
|  | ||||
| def test_storage_path(setup_core: Path) -> None: | ||||
|     """Test storage_path returns correct path for current config.""" | ||||
|     CORE.config_path = str(setup_core / "my_device.yaml") | ||||
|  | ||||
|     result = storage_json.storage_path() | ||||
|  | ||||
|     data_dir = Path(CORE.data_dir) | ||||
|     expected = str(data_dir / "storage" / "my_device.yaml.json") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_ext_storage_path(setup_core: Path) -> None: | ||||
|     """Test ext_storage_path returns correct path for given filename.""" | ||||
|     result = storage_json.ext_storage_path("other_device.yaml") | ||||
|  | ||||
|     data_dir = Path(CORE.data_dir) | ||||
|     expected = str(data_dir / "storage" / "other_device.yaml.json") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_ext_storage_path_handles_various_extensions(setup_core: Path) -> None: | ||||
|     """Test ext_storage_path works with different file extensions.""" | ||||
|     result_yml = storage_json.ext_storage_path("device.yml") | ||||
|     assert result_yml.endswith("device.yml.json") | ||||
|  | ||||
|     result_no_ext = storage_json.ext_storage_path("device") | ||||
|     assert result_no_ext.endswith("device.json") | ||||
|  | ||||
|     result_path = storage_json.ext_storage_path("my/device.yaml") | ||||
|     assert result_path.endswith("device.yaml.json") | ||||
|  | ||||
|  | ||||
| def test_esphome_storage_path(setup_core: Path) -> None: | ||||
|     """Test esphome_storage_path returns correct path.""" | ||||
|     result = storage_json.esphome_storage_path() | ||||
|  | ||||
|     data_dir = Path(CORE.data_dir) | ||||
|     expected = str(data_dir / "esphome.json") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_ignored_devices_storage_path(setup_core: Path) -> None: | ||||
|     """Test ignored_devices_storage_path returns correct path.""" | ||||
|     result = storage_json.ignored_devices_storage_path() | ||||
|  | ||||
|     data_dir = Path(CORE.data_dir) | ||||
|     expected = str(data_dir / "ignored-devices.json") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_trash_storage_path(setup_core: Path) -> None: | ||||
|     """Test trash_storage_path returns correct path.""" | ||||
|     CORE.config_path = str(setup_core / "configs" / "device.yaml") | ||||
|  | ||||
|     result = storage_json.trash_storage_path() | ||||
|  | ||||
|     expected = str(setup_core / "configs" / "trash") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_archive_storage_path(setup_core: Path) -> None: | ||||
|     """Test archive_storage_path returns correct path.""" | ||||
|     CORE.config_path = str(setup_core / "configs" / "device.yaml") | ||||
|  | ||||
|     result = storage_json.archive_storage_path() | ||||
|  | ||||
|     expected = str(setup_core / "configs" / "archive") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_storage_path_with_subdirectory(setup_core: Path) -> None: | ||||
|     """Test storage paths work correctly when config is in subdirectory.""" | ||||
|     subdir = setup_core / "configs" / "basement" | ||||
|     subdir.mkdir(parents=True, exist_ok=True) | ||||
|     CORE.config_path = str(subdir / "sensor.yaml") | ||||
|  | ||||
|     result = storage_json.storage_path() | ||||
|  | ||||
|     data_dir = Path(CORE.data_dir) | ||||
|     expected = str(data_dir / "storage" / "sensor.yaml.json") | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| def test_storage_json_firmware_bin_path_property(setup_core: Path) -> None: | ||||
|     """Test StorageJSON firmware_bin_path property.""" | ||||
|     storage = storage_json.StorageJSON( | ||||
|         storage_version=1, | ||||
|         name="test_device", | ||||
|         friendly_name="Test Device", | ||||
|         comment=None, | ||||
|         esphome_version="2024.1.0", | ||||
|         src_version=None, | ||||
|         address="192.168.1.100", | ||||
|         web_port=80, | ||||
|         target_platform="ESP32", | ||||
|         build_path="build/test_device", | ||||
|         firmware_bin_path="/path/to/firmware.bin", | ||||
|         loaded_integrations={"wifi", "api"}, | ||||
|         loaded_platforms=set(), | ||||
|         no_mdns=False, | ||||
|     ) | ||||
|  | ||||
|     assert storage.firmware_bin_path == "/path/to/firmware.bin" | ||||
|  | ||||
|  | ||||
| def test_storage_json_save_creates_directory(setup_core: Path, tmp_path: Path) -> None: | ||||
|     """Test StorageJSON.save creates storage directory if it doesn't exist.""" | ||||
|     storage_dir = tmp_path / "new_data" / "storage" | ||||
|     storage_file = storage_dir / "test.json" | ||||
|  | ||||
|     assert not storage_dir.exists() | ||||
|  | ||||
|     storage = storage_json.StorageJSON( | ||||
|         storage_version=1, | ||||
|         name="test", | ||||
|         friendly_name="Test", | ||||
|         comment=None, | ||||
|         esphome_version="2024.1.0", | ||||
|         src_version=None, | ||||
|         address="test.local", | ||||
|         web_port=None, | ||||
|         target_platform="ESP8266", | ||||
|         build_path=None, | ||||
|         firmware_bin_path=None, | ||||
|         loaded_integrations=set(), | ||||
|         loaded_platforms=set(), | ||||
|         no_mdns=False, | ||||
|     ) | ||||
|  | ||||
|     with patch("esphome.storage_json.write_file_if_changed") as mock_write: | ||||
|         storage.save(str(storage_file)) | ||||
|         mock_write.assert_called_once() | ||||
|         call_args = mock_write.call_args[0] | ||||
|         assert call_args[0] == str(storage_file) | ||||
|  | ||||
|  | ||||
| def test_storage_json_from_wizard(setup_core: Path) -> None: | ||||
|     """Test StorageJSON.from_wizard creates correct storage object.""" | ||||
|     storage = storage_json.StorageJSON.from_wizard( | ||||
|         name="my_device", | ||||
|         friendly_name="My Device", | ||||
|         address="my_device.local", | ||||
|         platform="ESP32", | ||||
|     ) | ||||
|  | ||||
|     assert storage.name == "my_device" | ||||
|     assert storage.friendly_name == "My Device" | ||||
|     assert storage.address == "my_device.local" | ||||
|     assert storage.target_platform == "ESP32" | ||||
|     assert storage.build_path is None | ||||
|     assert storage.firmware_bin_path is None | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif(sys.platform == "win32", reason="HA addons don't run on Windows") | ||||
| @patch("esphome.core.is_ha_addon") | ||||
| def test_storage_paths_with_ha_addon(mock_is_ha_addon: bool, tmp_path: Path) -> None: | ||||
|     """Test storage paths when running as Home Assistant addon.""" | ||||
|     mock_is_ha_addon.return_value = True | ||||
|  | ||||
|     CORE.config_path = str(tmp_path / "test.yaml") | ||||
|  | ||||
|     result = storage_json.storage_path() | ||||
|     # When is_ha_addon is True, CORE.data_dir returns "/data" | ||||
|     # This is the standard mount point for HA addon containers | ||||
|     expected = str(Path("/data") / "storage" / "test.yaml.json") | ||||
|     assert result == expected | ||||
|  | ||||
|     result = storage_json.esphome_storage_path() | ||||
|     expected = str(Path("/data") / "esphome.json") | ||||
|     assert result == expected | ||||
							
								
								
									
										143
									
								
								tests/unit_tests/test_util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								tests/unit_tests/test_util.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| """Tests for esphome.util module.""" | ||||
|  | ||||
| from pathlib import Path | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from esphome import util | ||||
|  | ||||
|  | ||||
| def test_list_yaml_files_with_files_and_directories(tmp_path: Path) -> None: | ||||
|     """Test that list_yaml_files handles both files and directories.""" | ||||
|     # Create directory structure | ||||
|     dir1 = tmp_path / "configs" | ||||
|     dir1.mkdir() | ||||
|     dir2 = tmp_path / "more_configs" | ||||
|     dir2.mkdir() | ||||
|  | ||||
|     # Create YAML files in directories | ||||
|     (dir1 / "config1.yaml").write_text("test: 1") | ||||
|     (dir1 / "config2.yml").write_text("test: 2") | ||||
|     (dir1 / "not_yaml.txt").write_text("not yaml") | ||||
|  | ||||
|     (dir2 / "config3.yaml").write_text("test: 3") | ||||
|  | ||||
|     # Create standalone YAML files | ||||
|     standalone1 = tmp_path / "standalone.yaml" | ||||
|     standalone1.write_text("test: 4") | ||||
|     standalone2 = tmp_path / "another.yml" | ||||
|     standalone2.write_text("test: 5") | ||||
|  | ||||
|     # Test with mixed input (directories and files) | ||||
|     configs = [ | ||||
|         str(dir1), | ||||
|         str(standalone1), | ||||
|         str(dir2), | ||||
|         str(standalone2), | ||||
|     ] | ||||
|  | ||||
|     result = util.list_yaml_files(configs) | ||||
|  | ||||
|     # Should include all YAML files but not the .txt file | ||||
|     assert set(result) == { | ||||
|         str(dir1 / "config1.yaml"), | ||||
|         str(dir1 / "config2.yml"), | ||||
|         str(dir2 / "config3.yaml"), | ||||
|         str(standalone1), | ||||
|         str(standalone2), | ||||
|     } | ||||
|     # Check that results are sorted | ||||
|     assert result == sorted(result) | ||||
|  | ||||
|  | ||||
| def test_list_yaml_files_only_directories(tmp_path: Path) -> None: | ||||
|     """Test list_yaml_files with only directories.""" | ||||
|     dir1 = tmp_path / "dir1" | ||||
|     dir1.mkdir() | ||||
|     dir2 = tmp_path / "dir2" | ||||
|     dir2.mkdir() | ||||
|  | ||||
|     (dir1 / "a.yaml").write_text("test: a") | ||||
|     (dir1 / "b.yml").write_text("test: b") | ||||
|     (dir2 / "c.yaml").write_text("test: c") | ||||
|  | ||||
|     result = util.list_yaml_files([str(dir1), str(dir2)]) | ||||
|  | ||||
|     assert set(result) == { | ||||
|         str(dir1 / "a.yaml"), | ||||
|         str(dir1 / "b.yml"), | ||||
|         str(dir2 / "c.yaml"), | ||||
|     } | ||||
|     assert result == sorted(result) | ||||
|  | ||||
|  | ||||
| def test_list_yaml_files_only_files(tmp_path: Path) -> None: | ||||
|     """Test list_yaml_files with only files.""" | ||||
|     file1 = tmp_path / "file1.yaml" | ||||
|     file2 = tmp_path / "file2.yml" | ||||
|     file3 = tmp_path / "file3.yaml" | ||||
|     non_yaml = tmp_path / "not_yaml.json" | ||||
|  | ||||
|     file1.write_text("test: 1") | ||||
|     file2.write_text("test: 2") | ||||
|     file3.write_text("test: 3") | ||||
|     non_yaml.write_text("{}") | ||||
|  | ||||
|     # Include a non-YAML file to test filtering | ||||
|     result = util.list_yaml_files( | ||||
|         [ | ||||
|             str(file1), | ||||
|             str(file2), | ||||
|             str(file3), | ||||
|             str(non_yaml), | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
|     assert set(result) == { | ||||
|         str(file1), | ||||
|         str(file2), | ||||
|         str(file3), | ||||
|     } | ||||
|     assert result == sorted(result) | ||||
|  | ||||
|  | ||||
| def test_list_yaml_files_empty_directory(tmp_path: Path) -> None: | ||||
|     """Test list_yaml_files with an empty directory.""" | ||||
|     empty_dir = tmp_path / "empty" | ||||
|     empty_dir.mkdir() | ||||
|  | ||||
|     result = util.list_yaml_files([str(empty_dir)]) | ||||
|  | ||||
|     assert result == [] | ||||
|  | ||||
|  | ||||
| def test_list_yaml_files_nonexistent_path(tmp_path: Path) -> None: | ||||
|     """Test list_yaml_files with a nonexistent path raises an error.""" | ||||
|     nonexistent = tmp_path / "nonexistent" | ||||
|     existing = tmp_path / "existing.yaml" | ||||
|     existing.write_text("test: 1") | ||||
|  | ||||
|     # Should raise an error for non-existent directory | ||||
|     with pytest.raises(FileNotFoundError): | ||||
|         util.list_yaml_files([str(nonexistent), str(existing)]) | ||||
|  | ||||
|  | ||||
| def test_list_yaml_files_mixed_extensions(tmp_path: Path) -> None: | ||||
|     """Test that both .yaml and .yml extensions are recognized.""" | ||||
|     dir1 = tmp_path / "configs" | ||||
|     dir1.mkdir() | ||||
|  | ||||
|     yaml_file = dir1 / "config.yaml" | ||||
|     yml_file = dir1 / "config.yml" | ||||
|     other_file = dir1 / "config.txt" | ||||
|  | ||||
|     yaml_file.write_text("test: yaml") | ||||
|     yml_file.write_text("test: yml") | ||||
|     other_file.write_text("test: txt") | ||||
|  | ||||
|     result = util.list_yaml_files([str(dir1)]) | ||||
|  | ||||
|     assert set(result) == { | ||||
|         str(yaml_file), | ||||
|         str(yml_file), | ||||
|     } | ||||
		Reference in New Issue
	
	Block a user