Merge branch 'dev' into bump-arduino-esp32

This commit is contained in:
Jimmy Hedman 2025-01-13 20:42:23 +01:00 committed by GitHub
commit 093512b150
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
223 changed files with 4727 additions and 2264 deletions

View File

@ -46,7 +46,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@v6.10.0
uses: docker/build-push-action@v6.11.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@ -72,7 +72,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@v6.10.0
uses: docker/build-push-action@v6.11.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false

View File

@ -46,9 +46,9 @@ jobs:
with:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.7.1
uses: docker/setup-buildx-action@v3.8.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
uses: docker/setup-qemu-action@v3.3.0
- name: Set TAG
run: |

View File

@ -13,6 +13,7 @@ on:
- ".github/workflows/ci.yml"
- "!.yamllint"
- "!.github/dependabot.yml"
- "!docker/**"
merge_group:
permissions:

View File

@ -65,7 +65,7 @@ jobs:
pip3 install build
python3 -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@v1.12.2
uses: pypa/gh-action-pypi-publish@v1.12.3
deploy-docker:
name: Build ESPHome ${{ matrix.platform }}
@ -90,10 +90,10 @@ jobs:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.7.1
uses: docker/setup-buildx-action@v3.8.0
- name: Set up QEMU
if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v3.2.0
uses: docker/setup-qemu-action@v3.3.0
- name: Log in to docker hub
uses: docker/login-action@v3.3.0
@ -141,7 +141,7 @@ jobs:
echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
- name: Upload digests
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.6.0
with:
name: digests-${{ steps.sanitize.outputs.name }}
path: /tmp/digests
@ -184,7 +184,7 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.7.1
uses: docker/setup-buildx-action@v3.8.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'

View File

@ -36,7 +36,7 @@ jobs:
python ./script/sync-device_class.py
- name: Commit changes
uses: peter-evans/create-pull-request@v7.0.5
uses: peter-evans/create-pull-request@v7.0.6
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@nabucasa.com>

View File

@ -302,7 +302,7 @@ esphome/components/noblex/* @AGalfra
esphome/components/npi19/* @bakerkj
esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @guillempages
esphome/components/online_image/* @clydebarrow @guillempages
esphome/components/opentherm/* @olegtarasov
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
@ -355,6 +355,7 @@ esphome/components/sdl/* @clydebarrow
esphome/components/sdm_meter/* @jesserockz @polyfaces
esphome/components/sdp3x/* @Azimath
esphome/components/seeed_mr24hpc1/* @limengdu
esphome/components/seeed_mr60bha2/* @limengdu
esphome/components/seeed_mr60fda2/* @limengdu
esphome/components/selec_meter/* @sourabhjaiswal
esphome/components/select/* @esphome/core

View File

@ -1,6 +1,11 @@
# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/)
[![ESPHome Logo](https://esphome.io/_images/logo-text.png)](https://esphome.io/)
<a href="https://esphome.io/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://esphome.io/_static/logo-text-on-dark.svg", alt="ESPHome Logo">
<img src="https://esphome.io/_static/logo-text-on-light.svg" alt="ESPHome Logo">
</picture>
</a>
**Documentation:** https://esphome.io/

View File

@ -29,7 +29,7 @@ RUN \
# Use pinned versions so that we get updates with build caching
&& apt-get install -y --no-install-recommends \
python3-pip=23.0.1+dfsg-1 \
python3-setuptools=66.1.1-1 \
python3-setuptools=66.1.1-1+deb12u1 \
python3-venv=3.11.2-1+b1 \
python3-wheel=0.38.4-2 \
iputils-ping=3:20221126-1+deb12u1 \

View File

@ -758,6 +758,14 @@ def parse_args(argv):
options_parser.add_argument(
"-q", "--quiet", help="Disable all ESPHome logs.", action="store_true"
)
options_parser.add_argument(
"-l",
"--log-level",
help="Set the log level.",
default=os.getenv("ESPHOME_LOG_LEVEL", "INFO"),
action="store",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
)
options_parser.add_argument(
"--dashboard", help=argparse.SUPPRESS, action="store_true"
)
@ -987,11 +995,16 @@ def run_esphome(argv):
args = parse_args(argv)
CORE.dashboard = args.dashboard
# Override log level if verbose is set
if args.verbose:
args.log_level = "DEBUG"
elif args.quiet:
args.log_level = "CRITICAL"
setup_log(
args.verbose,
args.quiet,
log_level=args.log_level,
# Show timestamp for dashboard access logs
args.command == "dashboard",
include_timestamp=args.command == "dashboard",
)
if args.command in PRE_CONFIG_ACTIONS:

View File

@ -1,11 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER
from esphome.core import CORE
import esphome.codegen as cg
from esphome.components.esp32 import get_esp32_variant
from esphome.const import PLATFORM_ESP8266
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
@ -15,6 +10,9 @@ from esphome.components.esp32.const import (
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
import esphome.config_validation as cv
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
from esphome.core import CORE
CODEOWNERS = ["@esphome/core"]
@ -102,11 +100,11 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
6: adc1_channel_t.ADC1_CHANNEL_6,
},
VARIANT_ESP32H2: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4,
},
}

View File

@ -3,13 +3,12 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#ifdef USE_ESP32
#include <esp_adc_cal.h>
#include "driver/adc.h"
#endif
#endif // USE_ESP32
namespace esphome {
namespace adc {
@ -43,7 +42,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
this->channel1_ = ADC1_CHANNEL_MAX;
}
void set_autorange(bool autorange) { this->autorange_ = autorange; }
#endif
#endif // USE_ESP32
/// Update ADC values
void update() override;
@ -59,11 +58,11 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
#ifdef USE_ESP8266
std::string unique_id() override;
#endif
#endif // USE_ESP8266
#ifdef USE_RP2040
void set_is_temperature() { this->is_temperature_ = true; }
#endif
#endif // USE_RP2040
protected:
InternalGPIOPin *pin_;
@ -72,7 +71,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
#ifdef USE_RP2040
bool is_temperature_{false};
#endif
#endif // USE_RP2040
#ifdef USE_ESP32
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
@ -83,8 +82,8 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {};
#else
esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {};
#endif
#endif
#endif // ESP_IDF_VERSION_MAJOR
#endif // USE_ESP32
};
} // namespace adc

View File

@ -0,0 +1,24 @@
#include "adc_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace adc {
static const char *const TAG = "adc.common";
void ADCSensor::update() {
float value_v = this->sample();
ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v);
this->publish_state(value_v);
}
void ADCSensor::set_sample_count(uint8_t sample_count) {
if (sample_count != 0) {
this->sample_count_ = sample_count;
}
}
float ADCSensor::get_setup_priority() const { return setup_priority::DATA; }
} // namespace adc
} // namespace esphome

View File

@ -1,30 +1,13 @@
#ifdef USE_ESP32
#include "adc_sensor.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_ESP8266
#ifdef USE_ADC_SENSOR_VCC
#include <Esp.h>
ADC_MODE(ADC_VCC)
#else
#include <Arduino.h>
#endif
#endif
#ifdef USE_RP2040
#ifdef CYW43_USES_VSYS_PIN
#include "pico/cyw43_arch.h"
#endif
#include <hardware/adc.h>
#endif
namespace esphome {
namespace adc {
static const char *const TAG = "adc";
static const char *const TAG = "adc.esp32";
// 13-bit for S2, 12-bit for all other ESP32 variants
#ifdef USE_ESP32
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
#ifndef SOC_ADC_RTC_MAX_BITWIDTH
@ -32,24 +15,15 @@ static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_widt
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13;
#else
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12;
#endif
#endif
#endif // USE_ESP32_VARIANT_ESP32S2
#endif // SOC_ADC_RTC_MAX_BITWIDTH
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit)
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit)
#endif
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;
#ifdef USE_RP2040
extern "C"
#endif
void
ADCSensor::setup() {
void ADCSensor::setup() {
ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
#if !defined(USE_ADC_SENSOR_VCC) && !defined(USE_RP2040)
this->pin_->setup();
#endif
#ifdef USE_ESP32
if (this->channel1_ != ADC1_CHANNEL_MAX) {
adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
if (!this->autorange_) {
@ -61,7 +35,6 @@ extern "C"
}
}
// load characteristics for each attenuation
for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) {
auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2;
auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS,
@ -79,31 +52,10 @@ extern "C"
break;
}
}
#endif // USE_ESP32
#ifdef USE_RP2040
static bool initialized = false;
if (!initialized) {
adc_init();
initialized = true;
}
#endif
ESP_LOGCONFIG(TAG, "ADC '%s' setup finished!", this->get_name().c_str());
}
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
#ifdef USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Pin: VCC");
#else
LOG_PIN(" Pin: ", this->pin_);
#endif
#endif // USE_ESP8266 || USE_LIBRETINY
#ifdef USE_ESP32
LOG_PIN(" Pin: ", this->pin_);
if (this->autorange_) {
ESP_LOGCONFIG(TAG, " Attenuation: auto");
@ -125,55 +77,10 @@ void ADCSensor::dump_config() {
break;
}
}
#endif // USE_ESP32
#ifdef USE_RP2040
if (this->is_temperature_) {
ESP_LOGCONFIG(TAG, " Pin: Temperature");
} else {
#ifdef USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Pin: VCC");
#else
LOG_PIN(" Pin: ", this->pin_);
#endif // USE_ADC_SENSOR_VCC
}
#endif // USE_RP2040
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
LOG_UPDATE_INTERVAL(this);
}
float ADCSensor::get_setup_priority() const { return setup_priority::DATA; }
void ADCSensor::update() {
float value_v = this->sample();
ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v);
this->publish_state(value_v);
}
void ADCSensor::set_sample_count(uint8_t sample_count) {
if (sample_count != 0) {
this->sample_count_ = sample_count;
}
}
#ifdef USE_ESP8266
float ADCSensor::sample() {
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
#ifdef USE_ADC_SENSOR_VCC
raw += ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance)
#else
raw += analogRead(this->pin_->get_pin()); // NOLINT
#endif
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
if (this->output_raw_) {
return raw;
}
return raw / 1024.0f;
}
#endif
#ifdef USE_ESP32
float ADCSensor::sample() {
if (!this->autorange_) {
uint32_t sum = 0;
@ -240,93 +147,17 @@ float ADCSensor::sample() {
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]);
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]);
// Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC)
uint32_t c12 = std::min(raw12, ADC_HALF);
uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF);
uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF);
uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF);
// max theoretical csum value is 4096*4 = 16384
uint32_t csum = c12 + c6 + c2 + c0;
// each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32
uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0);
return mv_scaled / (float) (csum * 1000U);
}
#endif // USE_ESP32
#ifdef USE_RP2040
float ADCSensor::sample() {
if (this->is_temperature_) {
adc_set_temp_sensor_enabled(true);
delay(1);
adc_select_input(4);
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += adc_read();
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
adc_set_temp_sensor_enabled(false);
if (this->output_raw_) {
return raw;
}
return raw * 3.3f / 4096.0f;
} else {
uint8_t pin = this->pin_->get_pin();
#ifdef CYW43_USES_VSYS_PIN
if (pin == PICO_VSYS_PIN) {
// Measuring VSYS on Raspberry Pico W needs to be wrapped with
// `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in
// https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and
// VSYS ADC both share GPIO29
cyw43_thread_enter();
}
#endif // CYW43_USES_VSYS_PIN
adc_gpio_init(pin);
adc_select_input(pin - 26);
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += adc_read();
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
#ifdef CYW43_USES_VSYS_PIN
if (pin == PICO_VSYS_PIN) {
cyw43_thread_exit();
}
#endif // CYW43_USES_VSYS_PIN
if (this->output_raw_) {
return raw;
}
float coeff = pin == PICO_VSYS_PIN ? 3.0 : 1.0;
return raw * 3.3f / 4096.0f * coeff;
}
}
#endif
#ifdef USE_LIBRETINY
float ADCSensor::sample() {
uint32_t raw = 0;
if (this->output_raw_) {
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += analogRead(this->pin_->get_pin()); // NOLINT
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
return raw;
}
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += analogReadVoltage(this->pin_->get_pin()); // NOLINT
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
return raw / 1000.0f;
}
#endif // USE_LIBRETINY
#ifdef USE_ESP8266
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
#endif
} // namespace adc
} // namespace esphome
#endif // USE_ESP32

View File

@ -0,0 +1,58 @@
#ifdef USE_ESP8266
#include "adc_sensor.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_ADC_SENSOR_VCC
#include <Esp.h>
ADC_MODE(ADC_VCC)
#else
#include <Arduino.h>
#endif // USE_ADC_SENSOR_VCC
namespace esphome {
namespace adc {
static const char *const TAG = "adc.esp8266";
void ADCSensor::setup() {
ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
#ifndef USE_ADC_SENSOR_VCC
this->pin_->setup();
#endif
}
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
#ifdef USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Pin: VCC");
#else
LOG_PIN(" Pin: ", this->pin_);
#endif // USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
LOG_UPDATE_INTERVAL(this);
}
float ADCSensor::sample() {
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
#ifdef USE_ADC_SENSOR_VCC
raw += ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance)
#else
raw += analogRead(this->pin_->get_pin()); // NOLINT
#endif // USE_ADC_SENSOR_VCC
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
if (this->output_raw_) {
return raw;
}
return raw / 1024.0f;
}
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
} // namespace adc
} // namespace esphome
#endif // USE_ESP8266

View File

@ -0,0 +1,48 @@
#ifdef USE_LIBRETINY
#include "adc_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace adc {
static const char *const TAG = "adc.libretiny";
void ADCSensor::setup() {
ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
#ifndef USE_ADC_SENSOR_VCC
this->pin_->setup();
#endif // !USE_ADC_SENSOR_VCC
}
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
#ifdef USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Pin: VCC");
#else // USE_ADC_SENSOR_VCC
LOG_PIN(" Pin: ", this->pin_);
#endif // USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
LOG_UPDATE_INTERVAL(this);
}
float ADCSensor::sample() {
uint32_t raw = 0;
if (this->output_raw_) {
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += analogRead(this->pin_->get_pin()); // NOLINT
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
return raw;
}
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += analogReadVoltage(this->pin_->get_pin()); // NOLINT
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
return raw / 1000.0f;
}
} // namespace adc
} // namespace esphome
#endif // USE_LIBRETINY

View File

@ -0,0 +1,93 @@
#ifdef USE_RP2040
#include "adc_sensor.h"
#include "esphome/core/log.h"
#ifdef CYW43_USES_VSYS_PIN
#include "pico/cyw43_arch.h"
#endif // CYW43_USES_VSYS_PIN
#include <hardware/adc.h>
namespace esphome {
namespace adc {
static const char *const TAG = "adc.rp2040";
void ADCSensor::setup() {
ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
static bool initialized = false;
if (!initialized) {
adc_init();
initialized = true;
}
}
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
if (this->is_temperature_) {
ESP_LOGCONFIG(TAG, " Pin: Temperature");
} else {
#ifdef USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Pin: VCC");
#else
LOG_PIN(" Pin: ", this->pin_);
#endif // USE_ADC_SENSOR_VCC
}
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
LOG_UPDATE_INTERVAL(this);
}
float ADCSensor::sample() {
if (this->is_temperature_) {
adc_set_temp_sensor_enabled(true);
delay(1);
adc_select_input(4);
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += adc_read();
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
adc_set_temp_sensor_enabled(false);
if (this->output_raw_) {
return raw;
}
return raw * 3.3f / 4096.0f;
}
uint8_t pin = this->pin_->get_pin();
#ifdef CYW43_USES_VSYS_PIN
if (pin == PICO_VSYS_PIN) {
// Measuring VSYS on Raspberry Pico W needs to be wrapped with
// `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in
// https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and
// VSYS ADC both share GPIO29
cyw43_thread_enter();
}
#endif // CYW43_USES_VSYS_PIN
adc_gpio_init(pin);
adc_select_input(pin - 26);
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += adc_read();
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
#ifdef CYW43_USES_VSYS_PIN
if (pin == PICO_VSYS_PIN) {
cyw43_thread_exit();
}
#endif // CYW43_USES_VSYS_PIN
if (this->output_raw_) {
return raw;
}
float coeff = pin == PICO_VSYS_PIN ? 3.0f : 1.0f;
return raw * 3.3f / 4096.0f * coeff;
}
} // namespace adc
} // namespace esphome
#endif // USE_RP2040

View File

@ -1,28 +1,10 @@
import logging
from esphome import automation, core
from esphome import automation
import esphome.codegen as cg
import esphome.components.image as espImage
from esphome.components.image import (
CONF_USE_TRANSPARENCY,
LOCAL_SCHEMA,
SOURCE_LOCAL,
SOURCE_WEB,
WEB_SCHEMA,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_FILE,
CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_REPEAT,
CONF_RESIZE,
CONF_SOURCE,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.const import CONF_ID, CONF_REPEAT
_LOGGER = logging.getLogger(__name__)
@ -30,6 +12,7 @@ AUTO_LOAD = ["image"]
CODEOWNERS = ["@syndlex"]
DEPENDENCIES = ["display"]
MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
CONF_LOOP = "loop"
CONF_START_FRAME = "start_frame"
@ -51,72 +34,9 @@ SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
SOURCE_LOCAL: LOCAL_SCHEMA,
SOURCE_WEB: WEB_SCHEMA,
},
key=CONF_SOURCE,
)
def _file_schema(value):
if isinstance(value, str):
return validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(value)
FILE_SCHEMA = cv.Schema(_file_schema)
def validate_file_shorthand(value):
value = cv.string_strict(value)
if value.startswith("http://") or value.startswith("https://"):
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_WEB,
CONF_URL: value,
}
)
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_LOCAL,
CONF_PATH: value,
}
)
def validate_cross_dependencies(config):
"""
Validate fields whose possible values depend on other fields.
For example, validate that explicitly transparent image types
have "use_transparency" set to True.
Also set the default value for those kind of dependent fields.
"""
image_type = config[CONF_TYPE]
is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"]
# If the use_transparency option was not specified, set the default depending on the image type
if CONF_USE_TRANSPARENCY not in config:
config[CONF_USE_TRANSPARENCY] = is_transparent_type
if is_transparent_type and not config[CONF_USE_TRANSPARENCY]:
raise cv.Invalid(f"Image type {image_type} must always be transparent.")
return config
ANIMATION_SCHEMA = cv.Schema(
cv.All(
CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Required(CONF_FILE): FILE_SCHEMA,
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
espImage.IMAGE_TYPE, upper=True
),
# Not setting default here on purpose; the default depends on the image type,
# and thus will be set in the "validate_cross_dependencies" validator.
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
cv.Optional(CONF_LOOP): cv.All(
{
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
@ -124,13 +44,9 @@ ANIMATION_SCHEMA = cv.Schema(
cv.Optional(CONF_REPEAT): cv.positive_int,
}
),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
},
validate_cross_dependencies,
)
)
CONFIG_SCHEMA = ANIMATION_SCHEMA
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
{
@ -164,180 +80,26 @@ async def animation_action_to_code(config, action_id, template_arg, args):
async def to_code(config):
from PIL import Image
(
prog_arr,
width,
height,
image_type,
trans_value,
frame_count,
) = await espImage.write_image(config, all_frames=True)
conf_file = config[CONF_FILE]
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
path = CORE.relative_config_path(conf_file[CONF_PATH])
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
path = espImage.compute_local_image_path(conf_file).as_posix()
else:
raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}")
try:
image = Image.open(path)
except Exception as e:
raise core.EsphomeError(f"Could not load image file {path}: {e}")
width, height = image.size
frames = image.n_frames
if CONF_RESIZE in config:
new_width_max, new_height_max = config[CONF_RESIZE]
ratio = min(new_width_max / width, new_height_max / height)
width, height = int(width * ratio), int(height * ratio)
elif width > 500 or height > 500:
_LOGGER.warning(
'The image "%s" you requested is very big. Please consider'
" using the resize parameter.",
path,
)
transparent = config[CONF_USE_TRANSPARENCY]
if config[CONF_TYPE] == "GRAYSCALE":
data = [0 for _ in range(height * width * frames)]
pos = 0
for frameIndex in range(frames):
image.seek(frameIndex)
frame = image.convert("LA", dither=Image.Dither.NONE)
if CONF_RESIZE in config:
frame = frame.resize([width, height])
pixels = list(frame.getdata())
if len(pixels) != height * width:
raise core.EsphomeError(
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
)
for pix, a in pixels:
if transparent:
if pix == 1:
pix = 0
if a < 0x80:
pix = 1
data[pos] = pix
pos += 1
elif config[CONF_TYPE] == "RGBA":
data = [0 for _ in range(height * width * 4 * frames)]
pos = 0
for frameIndex in range(frames):
image.seek(frameIndex)
frame = image.convert("RGBA")
if CONF_RESIZE in config:
frame = frame.resize([width, height])
pixels = list(frame.getdata())
if len(pixels) != height * width:
raise core.EsphomeError(
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
)
for pix in pixels:
data[pos] = pix[0]
pos += 1
data[pos] = pix[1]
pos += 1
data[pos] = pix[2]
pos += 1
data[pos] = pix[3]
pos += 1
elif config[CONF_TYPE] == "RGB24":
data = [0 for _ in range(height * width * 3 * frames)]
pos = 0
for frameIndex in range(frames):
image.seek(frameIndex)
frame = image.convert("RGBA")
if CONF_RESIZE in config:
frame = frame.resize([width, height])
pixels = list(frame.getdata())
if len(pixels) != height * width:
raise core.EsphomeError(
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
)
for r, g, b, a in pixels:
if transparent:
if r == 0 and g == 0 and b == 1:
b = 0
if a < 0x80:
r = 0
g = 0
b = 1
data[pos] = r
pos += 1
data[pos] = g
pos += 1
data[pos] = b
pos += 1
elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]:
bytes_per_pixel = 3 if transparent else 2
data = [0 for _ in range(height * width * bytes_per_pixel * frames)]
pos = 0
for frameIndex in range(frames):
image.seek(frameIndex)
frame = image.convert("RGBA")
if CONF_RESIZE in config:
frame = frame.resize([width, height])
pixels = list(frame.getdata())
if len(pixels) != height * width:
raise core.EsphomeError(
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
)
for r, g, b, a in pixels:
R = r >> 3
G = g >> 2
B = b >> 3
rgb = (R << 11) | (G << 5) | B
data[pos] = rgb >> 8
pos += 1
data[pos] = rgb & 0xFF
pos += 1
if transparent:
data[pos] = a
pos += 1
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
width8 = ((width + 7) // 8) * 8
data = [0 for _ in range((height * width8 // 8) * frames)]
for frameIndex in range(frames):
image.seek(frameIndex)
if transparent:
alpha = image.split()[-1]
has_alpha = alpha.getextrema()[0] < 0xFF
else:
has_alpha = False
frame = image.convert("1", dither=Image.Dither.NONE)
if CONF_RESIZE in config:
frame = frame.resize([width, height])
if transparent:
alpha = alpha.resize([width, height])
for x, y in [(i, j) for i in range(width) for j in range(height)]:
if transparent and has_alpha:
if not alpha.getpixel((x, y)):
continue
elif frame.getpixel((x, y)):
continue
pos = x + y * width8 + (height * width8 * frameIndex)
data[pos // 8] |= 0x80 >> (pos % 8)
else:
raise core.EsphomeError(
f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}."
)
rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
var = cg.new_Pvariable(
config[CONF_ID],
prog_arr,
width,
height,
frames,
espImage.IMAGE_TYPE[config[CONF_TYPE]],
frame_count,
image_type,
trans_value,
)
cg.add(var.set_transparency(transparent))
if loop_config := config.get(CONF_LOOP):
start = loop_config[CONF_START_FRAME]
end = loop_config.get(CONF_END_FRAME, frames)
end = loop_config.get(CONF_END_FRAME, frame_count)
count = loop_config.get(CONF_REPEAT, -1)
cg.add(var.set_loop(start, end, count))

View File

@ -6,8 +6,8 @@ namespace esphome {
namespace animation {
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
image::ImageType type)
: Image(data_start, width, height, type),
image::ImageType type, image::Transparency transparent)
: Image(data_start, width, height, type, transparent),
animation_data_start_(data_start),
current_frame_(0),
animation_frame_count_(animation_frame_count),

View File

@ -8,7 +8,8 @@ namespace animation {
class Animation : public image::Image {
public:
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type);
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type,
image::Transparency transparent);
uint32_t get_animation_frame_count() const;
int get_current_frame() const;

View File

@ -25,8 +25,7 @@ void BLEClient::loop() {
void BLEClient::dump_config() {
ESP_LOGCONFIG(TAG, "BLE Client:");
ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str());
ESP_LOGCONFIG(TAG, " Auto-Connect: %s", TRUEFALSE(this->auto_connect_));
BLEClientBase::dump_config();
}
bool BLEClient::parse_device(const espbt::ESPBTDevice &device) {

View File

@ -13,6 +13,11 @@ namespace bluetooth_proxy {
static const char *const TAG = "bluetooth_proxy.connection";
void BluetoothConnection::dump_config() {
ESP_LOGCONFIG(TAG, "BLE Connection:");
BLEClientBase::dump_config();
}
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))

View File

@ -11,6 +11,7 @@ class BluetoothProxy;
class BluetoothConnection : public esp32_ble_client::BLEClientBase {
public:
void dump_config() override;
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;

View File

@ -37,8 +37,9 @@ void ClimateIR::setup() {
this->publish_state();
});
this->current_temperature = this->sensor_->state;
} else
} else {
this->current_temperature = NAN;
}
// restore set points
auto restore = this->restore_state_();
if (restore.has_value()) {

View File

@ -131,8 +131,9 @@ bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteRecei
} else {
parent->mode = climate::CLIMATE_MODE_FAN_ONLY;
}
} else
} else {
parent->mode = climate::CLIMATE_MODE_COOL;
}
// Fan Speed
if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || parent->mode == climate::CLIMATE_MODE_HEAT_COOL ||

View File

@ -50,6 +50,10 @@ void DebugComponent::dump_config() {
this->reset_reason_->publish_state(get_reset_reason_());
}
#endif // USE_TEXT_SENSOR
#ifdef USE_ESP32
this->log_partition_info_(); // Log partition information for ESP32
#endif // USE_ESP32
}
void DebugComponent::loop() {

View File

@ -55,6 +55,20 @@ class DebugComponent : public PollingComponent {
#endif // USE_ESP32
#endif // USE_SENSOR
#ifdef USE_ESP32
/**
* @brief Logs information about the device's partition table.
*
* This function iterates through the ESP32's partition table and logs details
* about each partition, including its name, type, subtype, starting address,
* and size. The information is useful for diagnosing issues related to flash
* memory or verifying the partition configuration dynamically at runtime.
*
* Only available when compiled for ESP32 platforms.
*/
void log_partition_info_();
#endif // USE_ESP32
#ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *device_info_{nullptr};
text_sensor::TextSensor *reset_reason_{nullptr};

View File

@ -5,6 +5,7 @@
#include <esp_heap_caps.h>
#include <esp_system.h>
#include <esp_chip_info.h>
#include <esp_partition.h>
#if defined(USE_ESP32_VARIANT_ESP32)
#include <esp32/rom/rtc.h>
@ -28,8 +29,72 @@ namespace debug {
static const char *const TAG = "debug";
void DebugComponent::log_partition_info_() {
ESP_LOGCONFIG(TAG, "Partition table:");
ESP_LOGCONFIG(TAG, " %-12s %-4s %-8s %-10s %-10s", "Name", "Type", "Subtype", "Address", "Size");
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
while (it != NULL) {
const esp_partition_t *partition = esp_partition_get(it);
ESP_LOGCONFIG(TAG, " %-12s %-4d %-8d 0x%08X 0x%08X", partition->label, partition->type, partition->subtype,
partition->address, partition->size);
it = esp_partition_next(it);
}
esp_partition_iterator_release(it);
}
std::string DebugComponent::get_reset_reason_() {
std::string reset_reason;
switch (esp_reset_reason()) {
case ESP_RST_POWERON:
reset_reason = "Reset due to power-on event";
break;
case ESP_RST_EXT:
reset_reason = "Reset by external pin";
break;
case ESP_RST_SW:
reset_reason = "Software reset via esp_restart";
break;
case ESP_RST_PANIC:
reset_reason = "Software reset due to exception/panic";
break;
case ESP_RST_INT_WDT:
reset_reason = "Reset (software or hardware) due to interrupt watchdog";
break;
case ESP_RST_TASK_WDT:
reset_reason = "Reset due to task watchdog";
break;
case ESP_RST_WDT:
reset_reason = "Reset due to other watchdogs";
break;
case ESP_RST_DEEPSLEEP:
reset_reason = "Reset after exiting deep sleep mode";
break;
case ESP_RST_BROWNOUT:
reset_reason = "Brownout reset (software or hardware)";
break;
case ESP_RST_SDIO:
reset_reason = "Reset over SDIO";
break;
#ifdef USE_ESP32_VARIANT_ESP32
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 4))
case ESP_RST_USB:
reset_reason = "Reset by USB peripheral";
break;
case ESP_RST_JTAG:
reset_reason = "Reset by JTAG";
break;
case ESP_RST_EFUSE:
reset_reason = "Reset due to efuse error";
break;
case ESP_RST_PWR_GLITCH:
reset_reason = "Reset due to power glitch detected";
break;
case ESP_RST_CPU_LOCKUP:
reset_reason = "Reset due to CPU lock up (double exception)";
break;
#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 4)
#endif // USE_ESP32_VARIANT_ESP32
default: // Includes ESP_RST_UNKNOWN
switch (rtc_get_reset_reason(0)) {
case POWERON_RESET:
reset_reason = "Power On Reset";
@ -134,6 +199,8 @@ std::string DebugComponent::get_reset_reason_() {
default:
reset_reason = "Unknown Reset Reason";
}
break;
}
ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
return reset_reason;
}
@ -223,6 +290,19 @@ void DebugComponent::get_device_info_(std::string &device_info) {
device_info += " Cores:" + to_string(info.cores);
device_info += " Revision:" + to_string(info.revision);
// Framework detection
device_info += "|Framework: ";
#ifdef USE_ARDUINO
ESP_LOGD(TAG, "Framework: Arduino");
device_info += "Arduino";
#elif defined(USE_ESP_IDF)
ESP_LOGD(TAG, "Framework: ESP-IDF");
device_info += "ESP-IDF";
#else
ESP_LOGW(TAG, "Framework: UNKNOWN");
device_info += "UNKNOWN";
#endif
ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version());
device_info += "|ESP-IDF: ";
device_info += esp_get_idf_version();
@ -294,4 +374,4 @@ void DebugComponent::update_platform_() {
} // namespace debug
} // namespace esphome
#endif
#endif // USE_ESP32

View File

@ -159,6 +159,15 @@ void DFPlayer::loop() {
}
break;
case 9: // End byte
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char byte_sequence[100];
byte_sequence[0] = '\0';
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
this->read_buffer_[i]);
}
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
#endif
if (byte != 0xEF) {
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
this->read_pos_ = 0;
@ -238,13 +247,17 @@ void DFPlayer::loop() {
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
break;
case 0x3C:
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
case 0x3D:
ESP_LOGV(TAG, "Playback finished");
ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
default:
ESP_LOGV(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
}
this->sent_cmd_ = 0;
this->read_pos_ = 0;

View File

@ -118,8 +118,9 @@ std::unique_ptr<Command> CircularCommandQueue::dequeue() {
if (front_ == rear_) {
front_ = -1;
rear_ = -1;
} else
} else {
front_ = (front_ + 1) % COMMAND_QUEUE_SIZE;
}
return dequeued_cmd;
}

View File

@ -157,10 +157,11 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
if (bit == 0) {
bit = 7;
byte++;
} else
} else {
bit--;
}
}
}
if (!report_errors && error_code != 0)
return false;

View File

@ -39,6 +39,7 @@ DisplayOnPageChangeTrigger = display_ns.class_(
CONF_ON_PAGE_CHANGE = "on_page_change"
CONF_SHOW_TEST_CARD = "show_test_card"
CONF_UNSPECIFIED = "unspecified"
DISPLAY_ROTATIONS = {
0: display_ns.DISPLAY_ROTATION_0_DEGREES,
@ -55,16 +56,22 @@ def validate_rotation(value):
return cv.enum(DISPLAY_ROTATIONS, int=True)(value)
def validate_auto_clear(value):
if value == CONF_UNSPECIFIED:
return value
return cv.boolean(value)
BASIC_DISPLAY_SCHEMA = cv.Schema(
{
cv.Optional(CONF_LAMBDA): cv.lambda_,
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema("1s"))
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Optional(CONF_PAGES): cv.All(
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(): cv.declare_id(DisplayPage),
@ -82,7 +89,9 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
}
),
cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean,
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
@ -92,8 +101,12 @@ async def setup_display_core_(var, config):
if CONF_ROTATION in config:
cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
if CONF_AUTO_CLEAR_ENABLED in config:
cg.add(var.set_auto_clear(config[CONF_AUTO_CLEAR_ENABLED]))
if auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED):
# Default to true if pages or lambda is specified. Ideally this would be done during validation, but
# the possible schemas are too complex to do this easily.
if auto_clear == CONF_UNSPECIFIED:
auto_clear = CONF_LAMBDA in config or CONF_PAGES in config
cg.add(var.set_auto_clear(auto_clear))
if CONF_PAGES in config:
pages = []

View File

@ -1,6 +1,6 @@
#include "display.h"
#include "display_color_utils.h"
#include <utility>
#include "display_color_utils.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@ -266,9 +266,10 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
if (dymax < float(-dxmax) * tan_a) {
upd_dxmax = ceil(float(dymax) / tan_a);
hline_width = -dxmax - upd_dxmax + 1;
} else
} else {
hline_width = 0;
}
}
if (hline_width > 0)
this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color);
}
@ -670,7 +671,7 @@ void Display::strftime(int x, int y, BaseFont *font, Color color, Color backgrou
this->print(x, y, font, color, align, buffer, background);
}
void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) {
this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time);
this->strftime(x, y, font, color, COLOR_OFF, align, format, time);
}
void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) {
this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time);

View File

@ -90,9 +90,10 @@ void Rect::info(const std::string &prefix) {
if (this->is_set()) {
ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
this->y2());
} else
} else {
ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
}
}
} // namespace display
} // namespace esphome

View File

@ -602,6 +602,9 @@ async def to_code(config):
cg.add_platformio_option(
"platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"]
)
add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
)
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False)
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True)
add_idf_sdkconfig_option(

View File

@ -1,4 +1,12 @@
from .const import VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3
from .const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
ESP32_BASE_PINS = {
"TX": 1,
@ -1344,6 +1352,26 @@ done | sort
"""
BOARDS = {
"4d_systems_esp32s3_gen4_r8n16": {
"name": "4D Systems GEN4-ESP32 16MB (ESP32S3-R8N16)",
"variant": VARIANT_ESP32S3,
},
"adafruit_camera_esp32s3": {
"name": "Adafruit pyCamera S3",
"variant": VARIANT_ESP32S3,
},
"adafruit_feather_esp32c6": {
"name": "Adafruit Feather ESP32-C6",
"variant": VARIANT_ESP32C6,
},
"adafruit_feather_esp32s2": {
"name": "Adafruit Feather ESP32-S2",
"variant": VARIANT_ESP32S2,
},
"adafruit_feather_esp32s2_reversetft": {
"name": "Adafruit Feather ESP32-S2 Reverse TFT",
"variant": VARIANT_ESP32S2,
},
"adafruit_feather_esp32s2_tft": {
"name": "Adafruit Feather ESP32-S2 TFT",
"variant": VARIANT_ESP32S2,
@ -1356,6 +1384,10 @@ BOARDS = {
"name": "Adafruit Feather ESP32-S3 No PSRAM",
"variant": VARIANT_ESP32S3,
},
"adafruit_feather_esp32s3_reversetft": {
"name": "Adafruit Feather ESP32-S3 Reverse TFT",
"variant": VARIANT_ESP32S3,
},
"adafruit_feather_esp32s3_tft": {
"name": "Adafruit Feather ESP32-S3 TFT",
"variant": VARIANT_ESP32S3,
@ -1376,10 +1408,18 @@ BOARDS = {
"name": "Adafruit MagTag 2.9",
"variant": VARIANT_ESP32S2,
},
"adafruit_matrixportal_esp32s3": {
"name": "Adafruit MatrixPortal ESP32-S3",
"variant": VARIANT_ESP32S3,
},
"adafruit_metro_esp32s2": {
"name": "Adafruit Metro ESP32-S2",
"variant": VARIANT_ESP32S2,
},
"adafruit_metro_esp32s3": {
"name": "Adafruit Metro ESP32-S3",
"variant": VARIANT_ESP32S3,
},
"adafruit_qtpy_esp32c3": {
"name": "Adafruit QT Py ESP32-C3",
"variant": VARIANT_ESP32C3,
@ -1392,10 +1432,18 @@ BOARDS = {
"name": "Adafruit QT Py ESP32-S2",
"variant": VARIANT_ESP32S2,
},
"adafruit_qtpy_esp32s3_n4r2": {
"name": "Adafruit QT Py ESP32-S3 (4M Flash 2M PSRAM)",
"variant": VARIANT_ESP32S3,
},
"adafruit_qtpy_esp32s3_nopsram": {
"name": "Adafruit QT Py ESP32-S3 No PSRAM",
"variant": VARIANT_ESP32S3,
},
"adafruit_qualia_s3_rgb666": {
"name": "Adafruit Qualia ESP32-S3 RGB666",
"variant": VARIANT_ESP32S3,
},
"airm2m_core_esp32c3": {
"name": "AirM2M CORE ESP32C3",
"variant": VARIANT_ESP32C3,
@ -1404,14 +1452,30 @@ BOARDS = {
"name": "ALKS ESP32",
"variant": VARIANT_ESP32,
},
"arduino_nano_esp32": {
"name": "Arduino Nano ESP32",
"variant": VARIANT_ESP32S3,
},
"atd147_s3": {
"name": "ArtronShop ATD1.47-S3",
"variant": VARIANT_ESP32S3,
},
"atmegazero_esp32s2": {
"name": "EspinalLab ATMegaZero ESP32-S2",
"variant": VARIANT_ESP32S2,
},
"aventen_s3_sync": {
"name": "Aventen S3 Sync",
"variant": VARIANT_ESP32S3,
},
"az-delivery-devkit-v4": {
"name": "AZ-Delivery ESP-32 Dev Kit C V4",
"variant": VARIANT_ESP32,
},
"bee_data_logger": {
"name": "Smart Bee Data Logger",
"variant": VARIANT_ESP32S3,
},
"bee_motion_mini": {
"name": "Smart Bee Motion Mini",
"variant": VARIANT_ESP32C3,
@ -1436,14 +1500,6 @@ BOARDS = {
"name": "BPI-Leaf-S3",
"variant": VARIANT_ESP32S3,
},
"briki_abc_esp32": {
"name": "Briki ABC (MBC-WB) - ESP32",
"variant": VARIANT_ESP32,
},
"briki_mbc-wb_esp32": {
"name": "Briki MBC-WB - ESP32",
"variant": VARIANT_ESP32,
},
"cnrs_aw2eth": {
"name": "CNRS AW2ETH",
"variant": VARIANT_ESP32,
@ -1496,18 +1552,38 @@ BOARDS = {
"name": "DFRobot Beetle ESP32-C3",
"variant": VARIANT_ESP32C3,
},
"dfrobot_firebeetle2_esp32e": {
"name": "DFRobot Firebeetle 2 ESP32-E",
"variant": VARIANT_ESP32,
},
"dfrobot_firebeetle2_esp32s3": {
"name": "DFRobot Firebeetle 2 ESP32-S3",
"variant": VARIANT_ESP32S3,
},
"dfrobot_romeo_esp32s3": {
"name": "DFRobot Romeo ESP32-S3",
"variant": VARIANT_ESP32S3,
},
"dpu_esp32": {
"name": "TAMC DPU ESP32",
"variant": VARIANT_ESP32,
},
"edgebox-esp-100": {
"name": "Seeed Studio Edgebox-ESP-100",
"variant": VARIANT_ESP32S3,
},
"esp320": {
"name": "Electronic SweetPeas ESP320",
"variant": VARIANT_ESP32,
},
"esp32-c2-devkitm-1": {
"name": "Espressif ESP32-C2-DevKitM-1",
"variant": VARIANT_ESP32C2,
},
"esp32-c3-devkitc-02": {
"name": "Espressif ESP32-C3-DevKitC-02",
"variant": VARIANT_ESP32C3,
},
"esp32-c3-devkitm-1": {
"name": "Espressif ESP32-C3-DevKitM-1",
"variant": VARIANT_ESP32C3,
@ -1516,6 +1592,14 @@ BOARDS = {
"name": "Ai-Thinker ESP-C3-M1-I-Kit",
"variant": VARIANT_ESP32C3,
},
"esp32-c6-devkitc-1": {
"name": "Espressif ESP32-C6-DevKitC-1",
"variant": VARIANT_ESP32C6,
},
"esp32-c6-devkitm-1": {
"name": "Espressif ESP32-C6-DevKitM-1",
"variant": VARIANT_ESP32C6,
},
"esp32cam": {
"name": "AI Thinker ESP32-CAM",
"variant": VARIANT_ESP32,
@ -1544,6 +1628,14 @@ BOARDS = {
"name": "OLIMEX ESP32-GATEWAY",
"variant": VARIANT_ESP32,
},
"esp32-h2-devkitm-1": {
"name": "Espressif ESP32-H2-DevKit",
"variant": VARIANT_ESP32H2,
},
"esp32-pico-devkitm-2": {
"name": "Espressif ESP32-PICO-DevKitM-2",
"variant": VARIANT_ESP32,
},
"esp32-poe-iso": {
"name": "OLIMEX ESP32-PoE-ISO",
"variant": VARIANT_ESP32,
@ -1580,10 +1672,22 @@ BOARDS = {
"name": "Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)",
"variant": VARIANT_ESP32S3,
},
"esp32-s3-korvo-2": {
"name": "Espressif ESP32-S3-Korvo-2",
"esp32-s3-devkitm-1": {
"name": "Espressif ESP32-S3-DevKitM-1",
"variant": VARIANT_ESP32S3,
},
"esp32s3_powerfeather": {
"name": "ESP32-S3 PowerFeather",
"variant": VARIANT_ESP32S3,
},
"esp32s3usbotg": {
"name": "Espressif ESP32-S3-USB-OTG",
"variant": VARIANT_ESP32S3,
},
"esp32-solo1": {
"name": "Espressif Generic ESP32-solo1 4M Flash",
"variant": VARIANT_ESP32,
},
"esp32thing": {
"name": "SparkFun ESP32 Thing",
"variant": VARIANT_ESP32,
@ -1652,9 +1756,9 @@ BOARDS = {
"name": "Heltec WiFi Kit 32",
"variant": VARIANT_ESP32,
},
"heltec_wifi_kit_32_v2": {
"name": "Heltec WiFi Kit 32 (V2)",
"variant": VARIANT_ESP32,
"heltec_wifi_kit_32_V3": {
"name": "Heltec WiFi Kit 32 (V3)",
"variant": VARIANT_ESP32S3,
},
"heltec_wifi_lora_32": {
"name": "Heltec WiFi LoRa 32",
@ -1664,6 +1768,10 @@ BOARDS = {
"name": "Heltec WiFi LoRa 32 (V2)",
"variant": VARIANT_ESP32,
},
"heltec_wifi_lora_32_V3": {
"name": "Heltec WiFi LoRa 32 (V3)",
"variant": VARIANT_ESP32S3,
},
"heltec_wireless_stick_lite": {
"name": "Heltec Wireless Stick Lite",
"variant": VARIANT_ESP32,
@ -1708,6 +1816,14 @@ BOARDS = {
"name": "oddWires IoT-Bus Proteus",
"variant": VARIANT_ESP32,
},
"ioxesp32": {
"name": "ArtronShop IOXESP32",
"variant": VARIANT_ESP32,
},
"ioxesp32ps": {
"name": "ArtronShop IOXESP32PS",
"variant": VARIANT_ESP32,
},
"kb32-ft": {
"name": "MakerAsia KB32-FT",
"variant": VARIANT_ESP32,
@ -1720,10 +1836,26 @@ BOARDS = {
"name": "Labplus mPython",
"variant": VARIANT_ESP32,
},
"lilka_v2": {
"name": "Lilka v2",
"variant": VARIANT_ESP32S3,
},
"lilygo-t-display": {
"name": "LilyGo T-Display",
"variant": VARIANT_ESP32,
},
"lilygo-t-display-s3": {
"name": "LilyGo T-Display-S3",
"variant": VARIANT_ESP32S3,
},
"lionbit": {
"name": "Lion:Bit Dev Board",
"variant": VARIANT_ESP32,
},
"lionbits3": {
"name": "Lion:Bit S3 STEM Dev Board",
"variant": VARIANT_ESP32S3,
},
"lolin32_lite": {
"name": "WEMOS LOLIN32 Lite",
"variant": VARIANT_ESP32,
@ -1752,10 +1884,18 @@ BOARDS = {
"name": "WEMOS LOLIN S2 PICO",
"variant": VARIANT_ESP32S2,
},
"lolin_s3_mini": {
"name": "WEMOS LOLIN S3 Mini",
"variant": VARIANT_ESP32S3,
},
"lolin_s3": {
"name": "WEMOS LOLIN S3",
"variant": VARIANT_ESP32S3,
},
"lolin_s3_pro": {
"name": "WEMOS LOLIN S3 PRO",
"variant": VARIANT_ESP32S3,
},
"lopy4": {
"name": "Pycom LoPy4",
"variant": VARIANT_ESP32,
@ -1768,10 +1908,18 @@ BOARDS = {
"name": "M5Stack-ATOM",
"variant": VARIANT_ESP32,
},
"m5stack-atoms3": {
"name": "M5Stack AtomS3",
"variant": VARIANT_ESP32S3,
},
"m5stack-core2": {
"name": "M5Stack Core2",
"variant": VARIANT_ESP32,
},
"m5stack-core-esp32-16M": {
"name": "M5Stack Core ESP32 16M",
"variant": VARIANT_ESP32,
},
"m5stack-core-esp32": {
"name": "M5Stack Core ESP32",
"variant": VARIANT_ESP32,
@ -1780,6 +1928,10 @@ BOARDS = {
"name": "M5Stack-Core Ink",
"variant": VARIANT_ESP32,
},
"m5stack-cores3": {
"name": "M5Stack CoreS3",
"variant": VARIANT_ESP32S3,
},
"m5stack-fire": {
"name": "M5Stack FIRE",
"variant": VARIANT_ESP32,
@ -1788,6 +1940,14 @@ BOARDS = {
"name": "M5Stack GREY ESP32",
"variant": VARIANT_ESP32,
},
"m5stack_paper": {
"name": "M5Stack Paper",
"variant": VARIANT_ESP32,
},
"m5stack-stamps3": {
"name": "M5Stack StampS3",
"variant": VARIANT_ESP32S3,
},
"m5stack-station": {
"name": "M5Stack Station",
"variant": VARIANT_ESP32,
@ -1796,6 +1956,10 @@ BOARDS = {
"name": "M5Stack Timer CAM",
"variant": VARIANT_ESP32,
},
"m5stamp-pico": {
"name": "M5Stamp-Pico",
"variant": VARIANT_ESP32,
},
"m5stick-c": {
"name": "M5Stick-C",
"variant": VARIANT_ESP32,
@ -1832,10 +1996,26 @@ BOARDS = {
"name": "Deparment of Alchemy MiniMain ESP32-S2",
"variant": VARIANT_ESP32S2,
},
"motorgo_mini_1": {
"name": "MotorGo Mini 1 (ESP32-S3)",
"variant": VARIANT_ESP32S3,
},
"namino_arancio": {
"name": "Namino Arancio",
"variant": VARIANT_ESP32S3,
},
"namino_rosso": {
"name": "Namino Rosso",
"variant": VARIANT_ESP32S3,
},
"nano32": {
"name": "MakerAsia Nano32",
"variant": VARIANT_ESP32,
},
"nebulas3": {
"name": "Kinetic Dynamics Nebula S3",
"variant": VARIANT_ESP32S3,
},
"nina_w10": {
"name": "u-blox NINA-W10 series",
"variant": VARIANT_ESP32,
@ -1896,10 +2076,22 @@ BOARDS = {
"name": "Munich Labs RedPill ESP32-S3",
"variant": VARIANT_ESP32S3,
},
"roboheart_hercules": {
"name": "RoboHeart Hercules",
"variant": VARIANT_ESP32,
},
"seeed_xiao_esp32c3": {
"name": "Seeed Studio XIAO ESP32C3",
"variant": VARIANT_ESP32C3,
},
"seeed_xiao_esp32s3": {
"name": "Seeed Studio XIAO ESP32S3",
"variant": VARIANT_ESP32S3,
},
"sensebox_mcu_esp32s2": {
"name": "senseBox MCU-S2 ESP32-S2",
"variant": VARIANT_ESP32S2,
},
"sensesiot_weizen": {
"name": "LOGISENSES Senses Weizen",
"variant": VARIANT_ESP32,
@ -1912,6 +2104,10 @@ BOARDS = {
"name": "S.ODI Ultra v1",
"variant": VARIANT_ESP32,
},
"sparkfun_esp32c6_thing_plus": {
"name": "Sparkfun ESP32-C6 Thing Plus",
"variant": VARIANT_ESP32C6,
},
"sparkfun_esp32_iot_redboard": {
"name": "SparkFun ESP32 IoT RedBoard",
"variant": VARIANT_ESP32,
@ -2004,6 +2200,10 @@ BOARDS = {
"name": "Unexpected Maker FeatherS3",
"variant": VARIANT_ESP32S3,
},
"um_nanos3": {
"name": "Unexpected Maker NanoS3",
"variant": VARIANT_ESP32S3,
},
"um_pros3": {
"name": "Unexpected Maker PROS3",
"variant": VARIANT_ESP32S3,
@ -2040,6 +2240,14 @@ BOARDS = {
"name": "uPesy ESP32 Wrover DevKit",
"variant": VARIANT_ESP32,
},
"valtrack_v4_mfw_esp32_c3": {
"name": "Valetron Systems VALTRACK-V4MVF",
"variant": VARIANT_ESP32C3,
},
"valtrack_v4_vts_esp32_c3": {
"name": "Valetron Systems VALTRACK-V4VTS",
"variant": VARIANT_ESP32C3,
},
"vintlabs-devkit-v1": {
"name": "VintLabs ESP32 Devkit",
"variant": VARIANT_ESP32,

View File

@ -58,7 +58,11 @@ uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
#else
uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
#endif
uint32_t arch_get_cpu_freq_hz() { return rtc_clk_apb_freq_get(); }
uint32_t arch_get_cpu_freq_hz() {
rtc_cpu_freq_config_t config;
rtc_clk_cpu_freq_get_config(&config);
return config.freq_mhz * 1000000U;
}
#ifdef USE_ESP_IDF
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@ -27,6 +27,9 @@ namespace esp32_ble {
static const char *const TAG = "esp32_ble";
static RAMAllocator<BLEEvent> EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
void ESP32BLE::setup() {
global_ble = this;
ESP_LOGCONFIG(TAG, "Setting up BLE...");
@ -322,7 +325,8 @@ void ESP32BLE::loop() {
default:
break;
}
delete ble_event; // NOLINT(cppcoreguidelines-owning-memory)
ble_event->~BLEEvent();
EVENT_ALLOCATOR.deallocate(ble_event, 1);
ble_event = this->ble_events_.pop();
}
if (this->advertising_ != nullptr) {
@ -331,9 +335,14 @@ void ESP32BLE::loop() {
}
void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
BLEEvent *new_event = new BLEEvent(event, param); // NOLINT(cppcoreguidelines-owning-memory)
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
if (new_event == nullptr) {
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
return;
}
new (new_event) BLEEvent(event, param);
global_ble->ble_events_.push(new_event);
} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
} // NOLINT(clang-analyzer-unix.Malloc)
void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event);
@ -344,9 +353,14 @@ void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) {
BLEEvent *new_event = new BLEEvent(event, gatts_if, param); // NOLINT(cppcoreguidelines-owning-memory)
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
if (new_event == nullptr) {
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
return;
}
new (new_event) BLEEvent(event, gatts_if, param);
global_ble->ble_events_.push(new_event);
} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
} // NOLINT(clang-analyzer-unix.Malloc)
void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) {
@ -358,9 +372,14 @@ void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
BLEEvent *new_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory)
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
if (new_event == nullptr) {
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
return;
}
new (new_event) BLEEvent(event, gattc_if, param);
global_ble->ble_events_.push(new_event);
} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
} // NOLINT(clang-analyzer-unix.Malloc)
void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {

View File

@ -83,7 +83,7 @@ esp_err_t BLEAdvertising::services_advertisement_() {
esp_err_t err;
this->advertising_data_.set_scan_rsp = false;
this->advertising_data_.include_name = true;
this->advertising_data_.include_name = !this->scan_response_;
this->advertising_data_.include_txpower = !this->scan_response_;
err = esp_ble_gap_config_adv_data(&this->advertising_data_);
if (err != ESP_OK) {

View File

@ -26,11 +26,11 @@ template<class T> class Queue {
void push(T *element) {
if (element == nullptr)
return;
if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) {
// It is not called from main loop. Thus it won't block main thread.
xSemaphoreTake(m_, portMAX_DELAY);
q_.push(element);
xSemaphoreGive(m_);
}
}
T *pop() {
T *element = nullptr;

View File

@ -44,6 +44,50 @@ void BLEClientBase::loop() {
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void BLEClientBase::dump_config() {
ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str());
ESP_LOGCONFIG(TAG, " Auto-Connect: %s", TRUEFALSE(this->auto_connect_));
std::string state_name;
switch (this->state()) {
case espbt::ClientState::INIT:
state_name = "INIT";
break;
case espbt::ClientState::DISCONNECTING:
state_name = "DISCONNECTING";
break;
case espbt::ClientState::IDLE:
state_name = "IDLE";
break;
case espbt::ClientState::SEARCHING:
state_name = "SEARCHING";
break;
case espbt::ClientState::DISCOVERED:
state_name = "DISCOVERED";
break;
case espbt::ClientState::READY_TO_CONNECT:
state_name = "READY_TO_CONNECT";
break;
case espbt::ClientState::CONNECTING:
state_name = "CONNECTING";
break;
case espbt::ClientState::CONNECTED:
state_name = "CONNECTED";
break;
case espbt::ClientState::ESTABLISHED:
state_name = "ESTABLISHED";
break;
default:
state_name = "UNKNOWN_STATE";
break;
}
ESP_LOGCONFIG(TAG, " State: %s", state_name.c_str());
if (this->status_ == ESP_GATT_NO_RESOURCES) {
ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config.");
} else if (this->status_ != ESP_GATT_OK) {
ESP_LOGW(TAG, " Failed due to error code %d", this->status_);
}
}
bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
if (!this->auto_connect_)
return false;
@ -129,6 +173,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
} else {
ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_,
this->address_str_.c_str(), param->reg.app_id, param->reg.status);
this->status_ = param->reg.status;
this->mark_failed();
}
break;
}

View File

@ -26,6 +26,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void setup() override;
void loop() override;
float get_setup_priority() const override;
void dump_config() override;
void run_later(std::function<void()> &&f); // NOLINT
bool parse_device(const espbt::ESPBTDevice &device) override;
@ -103,6 +104,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
bool paired_{false};
espbt::ConnectionType connection_type_{espbt::ConnectionType::V1};
std::vector<BLEService *> services_;
esp_gatt_status_t status_{ESP_GATT_OK};
void log_event_(const char *name);
};

View File

@ -58,7 +58,6 @@ void ESP32BLETracker::setup() {
global_esp32_ble_tracker = this;
this->scan_result_lock_ = xSemaphoreCreateMutex();
this->scan_end_lock_ = xSemaphoreCreateMutex();
this->scanner_idle_ = true;
#ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback(
@ -107,6 +106,15 @@ void ESP32BLETracker::loop() {
break;
}
}
if (connecting != connecting_ || discovered != discovered_ || searching != searching_ ||
disconnecting != disconnecting_) {
connecting_ = connecting;
discovered_ = discovered;
searching_ = searching;
disconnecting_ = disconnecting;
ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
searching_, disconnecting_);
}
bool promote_to_connecting = discovered && !searching && !connecting;
if (!this->scanner_idle_) {
@ -183,8 +191,9 @@ void ESP32BLETracker::loop() {
}
if (this->scan_start_failed_ || this->scan_set_param_failed_) {
if (this->scan_start_fail_count_ == 255) {
ESP_LOGE(TAG, "ESP-IDF BLE scan could not restart after 255 attempts, rebooting to restore BLE stack...");
if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
ESP_LOGE(TAG, "ESP-IDF BLE scan could not restart after %d attempts, rebooting to restore BLE stack...",
std::numeric_limits<uint8_t>::max());
App.reboot();
}
if (xSemaphoreTake(this->scan_end_lock_, 0L)) {
@ -282,6 +291,12 @@ void ESP32BLETracker::start_scan_(bool first) {
this->scan_params_.scan_interval = this->scan_interval_;
this->scan_params_.scan_window = this->scan_window_;
// Start timeout before scan is started. Otherwise scan never starts if any error.
this->set_timeout("scan", this->scan_duration_ * 2000, []() {
ESP_LOGE(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack...");
App.reboot();
});
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_set_scan_params failed: %d", err);
@ -293,11 +308,6 @@ void ESP32BLETracker::start_scan_(bool first) {
return;
}
this->scanner_idle_ = false;
this->set_timeout("scan", this->scan_duration_ * 2000, []() {
ESP_LOGE(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack...");
App.reboot();
});
}
void ESP32BLETracker::end_of_scan_() {
@ -371,6 +381,7 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
}
void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param &param) {
ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
if (param.status == ESP_BT_STATUS_DONE) {
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
} else {
@ -379,20 +390,25 @@ void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t:
}
void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param) {
ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status);
this->scan_start_failed_ = param.status;
if (param.status == ESP_BT_STATUS_SUCCESS) {
this->scan_start_fail_count_ = 0;
} else {
if (this->scan_start_fail_count_ != std::numeric_limits<uint8_t>::max()) {
this->scan_start_fail_count_++;
}
xSemaphoreGive(this->scan_end_lock_);
}
}
void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param) {
ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status);
xSemaphoreGive(this->scan_end_lock_);
}
void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt);
if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
if (xSemaphoreTake(this->scan_result_lock_, 0L)) {
if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
@ -663,7 +679,14 @@ void ESP32BLETracker::dump_config() {
ESP_LOGCONFIG(TAG, " Scan Interval: %.1f ms", this->scan_interval_ * 0.625f);
ESP_LOGCONFIG(TAG, " Scan Window: %.1f ms", this->scan_window_ * 0.625f);
ESP_LOGCONFIG(TAG, " Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE");
ESP_LOGCONFIG(TAG, " Continuous Scanning: %s", this->scan_continuous_ ? "True" : "False");
ESP_LOGCONFIG(TAG, " Continuous Scanning: %s", YESNO(this->scan_continuous_));
ESP_LOGCONFIG(TAG, " Scanner Idle: %s", YESNO(this->scanner_idle_));
ESP_LOGCONFIG(TAG, " Scan End: %s", YESNO(xSemaphoreGetMutexHolder(this->scan_end_lock_) == nullptr));
ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
searching_, disconnecting_);
if (this->scan_start_fail_count_) {
ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
}
}
void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) {

View File

@ -178,7 +178,7 @@ class ESPBTClient : public ESPBTDeviceListener {
int app_id;
protected:
ClientState state_;
ClientState state_{ClientState::INIT};
};
class ESP32BLETracker : public Component,
@ -229,7 +229,7 @@ class ESP32BLETracker : public Component,
/// Called when a `ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT` event is received.
void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param);
int app_id_;
int app_id_{0};
/// Vector of addresses that have already been printed in print_bt_device_info
std::vector<uint64_t> already_discovered_;
@ -242,10 +242,10 @@ class ESP32BLETracker : public Component,
uint32_t scan_duration_;
uint32_t scan_interval_;
uint32_t scan_window_;
uint8_t scan_start_fail_count_;
uint8_t scan_start_fail_count_{0};
bool scan_continuous_;
bool scan_active_;
bool scanner_idle_;
bool scanner_idle_{true};
bool ble_was_disabled_{true};
bool raw_advertisements_{false};
bool parse_advertisements_{false};
@ -260,6 +260,10 @@ class ESP32BLETracker : public Component,
esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_;
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
int connecting_{0};
int discovered_{0};
int searching_{0};
int disconnecting_{0};
};
// NOLINTNEXTLINE

View File

@ -112,7 +112,7 @@ void ESP32ImprovComponent::loop() {
this->set_state_(improv::STATE_AUTHORIZED);
} else
#else
this->set_state_(improv::STATE_AUTHORIZED);
{ this->set_state_(improv::STATE_AUTHORIZED); }
#endif
{
if (!this->check_identify_())

View File

@ -1,7 +1,8 @@
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.components import esp32
import esphome.config_validation as cv
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
from esphome.core import CORE
CODEOWNERS = ["@jesserockz"]
@ -36,8 +37,32 @@ RMT_CHANNEL_ENUMS = {
}
def validate_rmt_channel(*, tx: bool):
def use_new_rmt_driver():
framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
if CORE.using_esp_idf and framework_version >= cv.Version(5, 0, 0):
return True
return False
def validate_clock_resolution():
def _validator(value):
cv.only_on_esp32(value)
value = cv.int_(value)
variant = esp32.get_esp32_variant()
if variant == esp32.const.VARIANT_ESP32H2 and value > 32000000:
raise cv.Invalid(
f"ESP32 variant {variant} has a max clock_resolution of 32000000."
)
if value > 80000000:
raise cv.Invalid(
f"ESP32 variant {variant} has a max clock_resolution of 80000000."
)
return value
return _validator
def validate_rmt_channel(*, tx: bool):
rmt_channels = RMT_TX_CHANNELS if tx else RMT_RX_CHANNELS
def _validator(value):

View File

@ -1,5 +1,5 @@
#include <cinttypes>
#include "led_strip.h"
#include <cinttypes>
#ifdef USE_ESP32
@ -13,9 +13,13 @@ namespace esp32_rmt_led_strip {
static const char *const TAG = "esp32_rmt_led_strip";
#ifdef USE_ESP32_VARIANT_ESP32H2
static const uint32_t RMT_CLK_FREQ = 32000000;
static const uint8_t RMT_CLK_DIV = 1;
#else
static const uint32_t RMT_CLK_FREQ = 80000000;
static const uint8_t RMT_CLK_DIV = 2;
#endif
void ESP32RMTLEDStripLightOutput::setup() {
ESP_LOGCONFIG(TAG, "Setting up ESP32 LED Strip...");
@ -29,6 +33,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
this->mark_failed();
return;
}
memset(this->buf_, 0, buffer_size);
this->effect_data_ = allocator.allocate(this->num_leds_);
if (this->effect_data_ == nullptr) {
@ -37,9 +42,48 @@ void ESP32RMTLEDStripLightOutput::setup() {
return;
}
#if ESP_IDF_VERSION_MAJOR >= 5
RAMAllocator<rmt_symbol_word_t> rmt_allocator(this->use_psram_ ? 0 : RAMAllocator<rmt_symbol_word_t>::ALLOC_INTERNAL);
// 8 bits per byte, 1 rmt_symbol_word_t per bit + 1 rmt_symbol_word_t for reset
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1);
rmt_tx_channel_config_t channel;
memset(&channel, 0, sizeof(channel));
channel.clk_src = RMT_CLK_SRC_DEFAULT;
channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV;
channel.gpio_num = gpio_num_t(this->pin_);
channel.mem_block_symbols = this->rmt_symbols_;
channel.trans_queue_depth = 1;
channel.flags.io_loop_back = 0;
channel.flags.io_od_mode = 0;
channel.flags.invert_out = 0;
channel.flags.with_dma = 0;
channel.intr_priority = 0;
if (rmt_new_tx_channel(&channel, &this->channel_) != ESP_OK) {
ESP_LOGE(TAG, "Channel creation failed");
this->mark_failed();
return;
}
rmt_copy_encoder_config_t encoder;
memset(&encoder, 0, sizeof(encoder));
if (rmt_new_copy_encoder(&encoder, &this->encoder_) != ESP_OK) {
ESP_LOGE(TAG, "Encoder creation failed");
this->mark_failed();
return;
}
if (rmt_enable(this->channel_) != ESP_OK) {
ESP_LOGE(TAG, "Enabling channel failed");
this->mark_failed();
return;
}
#else
RAMAllocator<rmt_item32_t> rmt_allocator(this->use_psram_ ? 0 : RAMAllocator<rmt_item32_t>::ALLOC_INTERNAL);
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 +
1); // 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset
// 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1);
rmt_config_t config;
memset(&config, 0, sizeof(config));
@ -64,6 +108,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
this->mark_failed();
return;
}
#endif
}
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
@ -100,7 +145,12 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
ESP_LOGVV(TAG, "Writing RGB values to bus...");
if (rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000)) != ESP_OK) {
#if ESP_IDF_VERSION_MAJOR >= 5
esp_err_t error = rmt_tx_wait_all_done(this->channel_, 1000);
#else
esp_err_t error = rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000));
#endif
if (error != ESP_OK) {
ESP_LOGE(TAG, "RMT TX timeout");
this->status_set_warning();
return;
@ -112,7 +162,11 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
size_t size = 0;
size_t len = 0;
uint8_t *psrc = this->buf_;
#if ESP_IDF_VERSION_MAJOR >= 5
rmt_symbol_word_t *pdest = this->rmt_buf_;
#else
rmt_item32_t *pdest = this->rmt_buf_;
#endif
while (size < buffer_size) {
uint8_t b = *psrc;
for (int i = 0; i < 8; i++) {
@ -130,7 +184,16 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
len++;
}
if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) {
#if ESP_IDF_VERSION_MAJOR >= 5
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.loop_count = 0;
config.flags.eot_level = 0;
error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config);
#else
error = rmt_write_items(this->channel_, this->rmt_buf_, len, false);
#endif
if (error != ESP_OK) {
ESP_LOGE(TAG, "RMT TX error");
this->status_set_warning();
return;
@ -186,7 +249,11 @@ light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index
void ESP32RMTLEDStripLightOutput::dump_config() {
ESP_LOGCONFIG(TAG, "ESP32 RMT LED Strip:");
ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_);
#if ESP_IDF_VERSION_MAJOR >= 5
ESP_LOGCONFIG(TAG, " RMT Symbols: %" PRIu32, this->rmt_symbols_);
#else
ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_);
#endif
const char *rgb_order;
switch (this->rgb_order_) {
case ORDER_RGB:

View File

@ -9,8 +9,14 @@
#include "esphome/core/helpers.h"
#include <driver/gpio.h>
#include <driver/rmt.h>
#include <esp_err.h>
#include <esp_idf_version.h>
#if ESP_IDF_VERSION_MAJOR >= 5
#include <driver/rmt_tx.h>
#else
#include <driver/rmt.h>
#endif
namespace esphome {
namespace esp32_rmt_led_strip {
@ -54,7 +60,11 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
uint32_t reset_time_high, uint32_t reset_time_low);
void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; }
#if ESP_IDF_VERSION_MAJOR >= 5
void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; }
#else
void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; }
#endif
void clear_effect_data() override {
for (int i = 0; i < this->size(); i++)
@ -70,7 +80,17 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
uint8_t *buf_{nullptr};
uint8_t *effect_data_{nullptr};
#if ESP_IDF_VERSION_MAJOR >= 5
rmt_channel_handle_t channel_{nullptr};
rmt_encoder_handle_t encoder_{nullptr};
rmt_symbol_word_t *rmt_buf_{nullptr};
rmt_symbol_word_t bit0_, bit1_, reset_;
uint32_t rmt_symbols_;
#else
rmt_item32_t *rmt_buf_{nullptr};
rmt_item32_t bit0_, bit1_, reset_;
rmt_channel_t channel_{RMT_CHANNEL_0};
#endif
uint8_t pin_;
uint16_t num_leds_;
@ -78,9 +98,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
bool is_wrgb_;
bool use_psram_;
rmt_item32_t bit0_, bit1_, reset_;
RGBOrder rgb_order_;
rmt_channel_t channel_;
uint32_t last_refresh_{0};
optional<uint32_t> max_refresh_rate_{};

View File

@ -1,4 +1,5 @@
from dataclasses import dataclass
import logging
from esphome import pins
import esphome.codegen as cg
@ -13,7 +14,11 @@ from esphome.const import (
CONF_PIN,
CONF_RGB_ORDER,
CONF_RMT_CHANNEL,
CONF_RMT_SYMBOLS,
)
from esphome.core import CORE
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"]
@ -23,8 +28,6 @@ ESP32RMTLEDStripLightOutput = esp32_rmt_led_strip_ns.class_(
"ESP32RMTLEDStripLightOutput", light.AddressableLight
)
rmt_channel_t = cg.global_ns.enum("rmt_channel_t")
RGBOrder = esp32_rmt_led_strip_ns.enum("RGBOrder")
RGB_ORDERS = {
@ -65,6 +68,53 @@ CONF_RESET_HIGH = "reset_high"
CONF_RESET_LOW = "reset_low"
class OptionalForIDF5(cv.SplitDefault):
@property
def default(self):
if not esp32_rmt.use_new_rmt_driver():
return cv.UNDEFINED
return super().default
@default.setter
def default(self, value):
# Ignore default set from vol.Optional
pass
def only_with_new_rmt_driver(obj):
if not esp32_rmt.use_new_rmt_driver():
raise cv.Invalid(
"This feature is only available for the IDF framework version 5."
)
return obj
def not_with_new_rmt_driver(obj):
if esp32_rmt.use_new_rmt_driver():
raise cv.Invalid(
"This feature is not available for the IDF framework version 5."
)
return obj
def final_validation(config):
if not esp32_rmt.use_new_rmt_driver():
if CONF_RMT_CHANNEL not in config:
if CORE.using_esp_idf:
raise cv.Invalid(
"rmt_channel is a required option for IDF version < 5."
)
raise cv.Invalid(
"rmt_channel is a required option for the Arduino framework."
)
_LOGGER.warning(
"RMT_LED_STRIP support for IDF version < 5 is deprecated and will be removed soon."
)
FINAL_VALIDATE_SCHEMA = final_validation
CONFIG_SCHEMA = cv.All(
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
{
@ -72,7 +122,18 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
cv.Required(CONF_RMT_CHANNEL): esp32_rmt.validate_rmt_channel(tx=True),
cv.Optional(CONF_RMT_CHANNEL): cv.All(
not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True)
),
OptionalForIDF5(
CONF_RMT_SYMBOLS,
esp32_idf=64,
esp32_s2_idf=64,
esp32_s3_idf=48,
esp32_c3_idf=48,
esp32_c6_idf=48,
esp32_h2_idf=48,
): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)),
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
@ -148,6 +209,10 @@ async def to_code(config):
cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
cg.add(var.set_use_psram(config[CONF_USE_PSRAM]))
if esp32_rmt.use_new_rmt_driver():
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
else:
rmt_channel_t = cg.global_ns.enum("rmt_channel_t")
cg.add(
var.set_rmt_channel(
getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}")

View File

@ -94,11 +94,11 @@ CLK_MODES = {
MANUAL_IP_SCHEMA = cv.Schema(
{
cv.Required(CONF_STATIC_IP): cv.ipv4,
cv.Required(CONF_GATEWAY): cv.ipv4,
cv.Required(CONF_SUBNET): cv.ipv4,
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4,
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4,
cv.Required(CONF_STATIC_IP): cv.ipv4address,
cv.Required(CONF_GATEWAY): cv.ipv4address,
cv.Required(CONF_SUBNET): cv.ipv4address,
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4address,
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4address,
}
)
@ -255,11 +255,11 @@ FINAL_VALIDATE_SCHEMA = _final_validate
def manual_ip(config):
return cg.StructInitializer(
ManualIP,
("static_ip", IPAddress(*config[CONF_STATIC_IP].args)),
("gateway", IPAddress(*config[CONF_GATEWAY].args)),
("subnet", IPAddress(*config[CONF_SUBNET].args)),
("dns1", IPAddress(*config[CONF_DNS1].args)),
("dns2", IPAddress(*config[CONF_DNS2].args)),
("static_ip", IPAddress(str(config[CONF_STATIC_IP]))),
("gateway", IPAddress(str(config[CONF_GATEWAY]))),
("subnet", IPAddress(str(config[CONF_SUBNET]))),
("dns1", IPAddress(str(config[CONF_DNS1]))),
("dns2", IPAddress(str(config[CONF_DNS2]))),
)

View File

@ -51,8 +51,11 @@ CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs"
# Cache loaded freetype fonts
class FontCache(dict):
def __missing__(self, key):
try:
res = self[key] = freetype.Face(key)
return res
except freetype.FT_Exception as e:
raise cv.Invalid(f"Could not load Font file {key}: {e}") from e
FONT_CACHE = FontCache()

View File

@ -133,9 +133,11 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
auto diff_r = (float) color.r - (float) background.r;
auto diff_g = (float) color.g - (float) background.g;
auto diff_b = (float) color.b - (float) background.b;
auto diff_w = (float) color.w - (float) background.w;
auto b_r = (float) background.r;
auto b_g = (float) background.g;
auto b_b = (float) background.g;
auto b_b = (float) background.b;
auto b_w = (float) background.w;
for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) {
for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) {
uint8_t pixel = 0;
@ -153,8 +155,8 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
display->draw_pixel_at(glyph_x, glyph_y, color);
} else if (pixel != 0) {
auto on = (float) pixel / (float) bpp_max;
auto blended =
Color((uint8_t) (diff_r * on + b_r), (uint8_t) (diff_g * on + b_g), (uint8_t) (diff_b * on + b_b));
auto blended = Color((uint8_t) (diff_r * on + b_r), (uint8_t) (diff_g * on + b_g),
(uint8_t) (diff_b * on + b_b), (uint8_t) (diff_w * on + b_w));
display->draw_pixel_at(glyph_x, glyph_y, blended);
}
}

View File

@ -97,8 +97,9 @@ void GCJA5Component::parse_data_() {
if (this->rx_message_[0] != 0x02 || this->rx_message_[31] != 0x03 || !this->calculate_checksum_()) {
ESP_LOGVV(TAG, "Discarding bad packet - failed checks.");
return;
} else
} else {
ESP_LOGVV(TAG, "Good packet found.");
}
this->have_good_data_ = true;
uint8_t status = this->rx_message_[29];

View File

@ -342,9 +342,10 @@ bool HaierClimateBase::prepare_pending_action() {
this->action_request_.reset();
return false;
}
} else
} else {
return false;
}
}
ClimateTraits HaierClimateBase::traits() { return traits_; }

View File

@ -710,9 +710,10 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo
alarm_code++;
}
active_alarms_[i] = packet[2 + i];
} else
} else {
alarm_code += 8;
}
}
} else {
float alarm_count = 0.0f;
static uint8_t nibble_bits_count[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};

View File

@ -87,9 +87,10 @@ void HeatpumpIRClimate::setup() {
this->publish_state();
});
this->current_temperature = this->sensor_->state;
} else
} else {
this->current_temperature = NAN;
}
}
void HeatpumpIRClimate::transmit_state() {
uint8_t power_mode_cmd;

View File

@ -17,14 +17,14 @@ void IDFI2CBus::setup() {
ESP_LOGCONFIG(TAG, "Setting up I2C bus...");
static i2c_port_t next_port = I2C_NUM_0;
port_ = next_port;
#if I2C_NUM_MAX > 1
#if SOC_I2C_NUM > 1
next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX;
#else
next_port = I2C_NUM_MAX;
#endif
if (port_ == I2C_NUM_MAX) {
ESP_LOGE(TAG, "Too many I2C buses configured");
ESP_LOGE(TAG, "Too many I2C buses configured. Max %u supported.", SOC_I2C_NUM);
this->mark_failed();
return;
}

View File

@ -25,6 +25,7 @@ void I2SAudioMicrophone::setup() {
}
} else
#endif
{
if (this->pdm_) {
if (this->parent_->get_port() != I2S_NUM_0) {
ESP_LOGE(TAG, "PDM only works on I2S0!");
@ -33,6 +34,7 @@ void I2SAudioMicrophone::setup() {
}
}
}
}
void I2SAudioMicrophone::start() {
if (this->is_failed())

View File

@ -247,7 +247,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
// Ensure ring buffer is at least as large as the total size of the DMA buffers
const size_t ring_buffer_size =
std::min((uint32_t) dma_buffers_size, this_speaker->buffer_duration_ms_ * bytes_per_ms);
std::max((uint32_t) dma_buffers_size, this_speaker->buffer_duration_ms_ * bytes_per_ms);
if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(dma_buffers_size, ring_buffer_size))) {
// Failed to allocate buffers

View File

@ -6,7 +6,7 @@ import logging
from pathlib import Path
import re
import puremagic
from PIL import Image, UnidentifiedImageError
from esphome import core, external_files
import esphome.codegen as cg
@ -29,21 +29,236 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "image"
DEPENDENCIES = ["display"]
MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
image_ns = cg.esphome_ns.namespace("image")
ImageType = image_ns.enum("ImageType")
CONF_OPAQUE = "opaque"
CONF_CHROMA_KEY = "chroma_key"
CONF_ALPHA_CHANNEL = "alpha_channel"
CONF_INVERT_ALPHA = "invert_alpha"
TRANSPARENCY_TYPES = (
CONF_OPAQUE,
CONF_CHROMA_KEY,
CONF_ALPHA_CHANNEL,
)
def get_image_type_enum(type):
return getattr(ImageType, f"IMAGE_TYPE_{type.upper()}")
def get_transparency_enum(transparency):
return getattr(TransparencyType, f"TRANSPARENCY_{transparency.upper()}")
class ImageEncoder:
"""
Superclass of image type encoders
"""
# Control which transparency options are available for a given type
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE}
# All imageencoder types are valid
@staticmethod
def validate(value):
return value
def __init__(self, width, height, transparency, dither, invert_alpha):
"""
:param width: The image width in pixels
:param height: The image height in pixels
:param transparency: Transparency type
:param dither: Dither method
:param invert_alpha: True if the alpha channel should be inverted; for monochrome formats inverts the colours.
"""
self.transparency = transparency
self.width = width
self.height = height
self.data = [0 for _ in range(width * height)]
self.dither = dither
self.index = 0
self.invert_alpha = invert_alpha
def convert(self, image):
"""
Convert the image format
:param image: Input image
:return: converted image
"""
return image
def encode(self, pixel):
"""
Encode a single pixel
"""
def end_row(self):
"""
Marks the end of a pixel row
:return:
"""
class ImageBinary(ImageEncoder):
allow_config = {CONF_OPAQUE, CONF_INVERT_ALPHA, CONF_CHROMA_KEY}
def __init__(self, width, height, transparency, dither, invert_alpha):
self.width8 = (width + 7) // 8
super().__init__(self.width8, height, transparency, dither, invert_alpha)
self.bitno = 0
def convert(self, image):
return image.convert("1", dither=self.dither)
def encode(self, pixel):
if self.invert_alpha:
pixel = not pixel
if pixel:
self.data[self.index] |= 0x80 >> (self.bitno % 8)
self.bitno += 1
if self.bitno == 8:
self.bitno = 0
self.index += 1
def end_row(self):
"""
Pad rows to a byte boundary
"""
if self.bitno != 0:
self.bitno = 0
self.index += 1
class ImageGrayscale(ImageEncoder):
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_INVERT_ALPHA, CONF_OPAQUE}
def convert(self, image):
return image.convert("LA")
def encode(self, pixel):
b, a = pixel
if self.transparency == CONF_CHROMA_KEY:
if b == 1:
b = 0
if a != 0xFF:
b = 1
if self.invert_alpha:
b ^= 0xFF
if self.transparency == CONF_ALPHA_CHANNEL:
if a != 0xFF:
b = a
self.data[self.index] = b
self.index += 1
class ImageRGB565(ImageEncoder):
def __init__(self, width, height, transparency, dither, invert_alpha):
stride = 3 if transparency == CONF_ALPHA_CHANNEL else 2
super().__init__(
width * stride,
height,
transparency,
dither,
invert_alpha,
)
def convert(self, image):
return image.convert("RGBA")
def encode(self, pixel):
r, g, b, a = pixel
r = r >> 3
g = g >> 2
b = b >> 3
if self.transparency == CONF_CHROMA_KEY:
if r == 0 and g == 1 and b == 0:
g = 0
elif a < 128:
r = 0
g = 1
b = 0
rgb = (r << 11) | (g << 5) | b
self.data[self.index] = rgb >> 8
self.index += 1
self.data[self.index] = rgb & 0xFF
self.index += 1
if self.transparency == CONF_ALPHA_CHANNEL:
if self.invert_alpha:
a ^= 0xFF
self.data[self.index] = a
self.index += 1
class ImageRGB(ImageEncoder):
def __init__(self, width, height, transparency, dither, invert_alpha):
stride = 4 if transparency == CONF_ALPHA_CHANNEL else 3
super().__init__(
width * stride,
height,
transparency,
dither,
invert_alpha,
)
def convert(self, image):
return image.convert("RGBA")
def encode(self, pixel):
r, g, b, a = pixel
if self.transparency == CONF_CHROMA_KEY:
if r == 0 and g == 1 and b == 0:
g = 0
elif a < 128:
r = 0
g = 1
b = 0
self.data[self.index] = r
self.index += 1
self.data[self.index] = g
self.index += 1
self.data[self.index] = b
self.index += 1
if self.transparency == CONF_ALPHA_CHANNEL:
if self.invert_alpha:
a ^= 0xFF
self.data[self.index] = a
self.index += 1
class ReplaceWith:
"""
Placeholder class to provide feedback on deprecated features
"""
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE}
def __init__(self, replace_with):
self.replace_with = replace_with
def validate(self, value):
raise cv.Invalid(
f"Image type {value} is removed; replace with {self.replace_with}"
)
IMAGE_TYPE = {
"BINARY": ImageType.IMAGE_TYPE_BINARY,
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY,
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
"RGB565": ImageType.IMAGE_TYPE_RGB565,
"RGB24": ImageType.IMAGE_TYPE_RGB24,
"RGBA": ImageType.IMAGE_TYPE_RGBA,
"BINARY": ImageBinary,
"GRAYSCALE": ImageGrayscale,
"RGB565": ImageRGB565,
"RGB": ImageRGB,
"TRANSPARENT_BINARY": ReplaceWith(
"'type: BINARY' and 'use_transparency: chroma_key'"
),
"RGB24": ReplaceWith("'type: RGB'"),
"RGBA": ReplaceWith("'type: RGB' and 'use_transparency: alpha_channel'"),
}
TransparencyType = image_ns.enum("TransparencyType")
CONF_USE_TRANSPARENCY = "use_transparency"
# If the MDI file cannot be downloaded within this time, abort.
@ -53,17 +268,11 @@ SOURCE_LOCAL = "local"
SOURCE_MDI = "mdi"
SOURCE_WEB = "web"
Image_ = image_ns.class_("Image")
def _compute_local_icon_path(value: dict) -> Path:
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi"
return base_dir / f"{value[CONF_ICON]}.svg"
def compute_local_image_path(value: dict) -> Path:
url = value[CONF_URL]
def compute_local_image_path(value) -> Path:
url = value[CONF_URL] if isinstance(value, dict) else value
h = hashlib.new("sha256")
h.update(url.encode())
key = h.hexdigest()[:8]
@ -71,30 +280,38 @@ def compute_local_image_path(value: dict) -> Path:
return base_dir / key
def download_mdi(value):
validate_cairosvg_installed(value)
def local_path(value):
value = value[CONF_PATH] if isinstance(value, dict) else value
return str(CORE.relative_config_path(value))
mdi_id = value[CONF_ICON]
path = _compute_local_icon_path(value)
def download_file(url, path):
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return str(path)
def download_mdi(value):
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi"
path = base_dir / f"{mdi_id}.svg"
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return value
return download_file(url, path)
def download_image(value):
url = value[CONF_URL]
path = compute_local_image_path(value)
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return value
value = value[CONF_URL] if isinstance(value, dict) else value
return download_file(value, compute_local_image_path(value))
def validate_cairosvg_installed(value):
"""Validate that cairosvg is installed"""
def is_svg_file(file):
if not file:
return False
with open(file, "rb") as f:
return "<svg " in str(f.read(1024))
def validate_cairosvg_installed():
try:
import cairosvg
except ImportError as err:
@ -110,73 +327,28 @@ def validate_cairosvg_installed(value):
"(pip install -U cairosvg)"
)
return value
def validate_cross_dependencies(config):
"""
Validate fields whose possible values depend on other fields.
For example, validate that explicitly transparent image types
have "use_transparency" set to True.
Also set the default value for those kind of dependent fields.
"""
is_mdi = CONF_FILE in config and config[CONF_FILE][CONF_SOURCE] == SOURCE_MDI
if CONF_TYPE not in config:
if is_mdi:
config[CONF_TYPE] = "TRANSPARENT_BINARY"
else:
config[CONF_TYPE] = "BINARY"
image_type = config[CONF_TYPE]
is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"]
# If the use_transparency option was not specified, set the default depending on the image type
if CONF_USE_TRANSPARENCY not in config:
config[CONF_USE_TRANSPARENCY] = is_transparent_type
if is_transparent_type and not config[CONF_USE_TRANSPARENCY]:
raise cv.Invalid(f"Image type {image_type} must always be transparent.")
if is_mdi and config[CONF_TYPE] not in ["BINARY", "TRANSPARENT_BINARY"]:
raise cv.Invalid("MDI images must be binary images.")
return config
def validate_file_shorthand(value):
value = cv.string_strict(value)
if value.startswith("mdi:"):
validate_cairosvg_installed(value)
match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value)
if match is None:
raise cv.Invalid("Could not parse mdi icon name.")
icon = match.group(1)
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_MDI,
CONF_ICON: icon,
}
)
return download_mdi(icon)
if value.startswith("http://") or value.startswith("https://"):
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_WEB,
CONF_URL: value,
}
)
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_LOCAL,
CONF_PATH: value,
}
)
return download_image(value)
value = cv.file_(value)
return local_path(value)
LOCAL_SCHEMA = cv.Schema(
LOCAL_SCHEMA = cv.All(
{
cv.Required(CONF_PATH): cv.file_,
}
},
local_path,
)
MDI_SCHEMA = cv.All(
@ -203,205 +375,202 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
)
def _file_schema(value):
if isinstance(value, str):
return validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(value)
def validate_transparency(choices=TRANSPARENCY_TYPES):
def validate(value):
if isinstance(value, bool):
value = str(value)
return cv.one_of(*choices, lower=True)(value)
return validate
FILE_SCHEMA = cv.Schema(_file_schema)
def validate_type(image_types):
def validate(value):
value = cv.one_of(*image_types, upper=True)(value)
return IMAGE_TYPE[value].validate(value)
IMAGE_SCHEMA = cv.Schema(
cv.All(
return validate
def validate_settings(value):
type = value[CONF_TYPE]
transparency = value[CONF_USE_TRANSPARENCY].lower()
allow_config = IMAGE_TYPE[type].allow_config
if transparency not in allow_config:
raise cv.Invalid(
f"Image format '{type}' cannot have transparency: {transparency}"
)
invert_alpha = value.get(CONF_INVERT_ALPHA, False)
if (
invert_alpha
and transparency != CONF_ALPHA_CHANNEL
and CONF_INVERT_ALPHA not in allow_config
):
raise cv.Invalid("No alpha channel to invert")
if file := value.get(CONF_FILE):
file = Path(file)
if is_svg_file(file):
validate_cairosvg_installed()
else:
try:
Image.open(file)
except UnidentifiedImageError as exc:
raise cv.Invalid(f"File can't be opened as image: {file}") from exc
return value
BASE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(Image_),
cv.Required(CONF_FILE): FILE_SCHEMA,
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
cv.Optional(CONF_RESIZE): cv.dimensions,
# Not setting default here on purpose; the default depends on the source type
# (file or mdi), and will be set in the "validate_cross_dependencies" validator.
cv.Optional(CONF_TYPE): cv.enum(IMAGE_TYPE, upper=True),
# Not setting default here on purpose; the default depends on the image type,
# and thus will be set in the "validate_cross_dependencies" validator.
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
"NONE", "FLOYDSTEINBERG", upper=True
),
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
},
validate_cross_dependencies,
)
}
).add_extra(validate_settings)
IMAGE_SCHEMA = BASE_SCHEMA.extend(
{
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
cv.Optional(
CONF_USE_TRANSPARENCY, default=CONF_OPAQUE
): validate_transparency(),
}
)
CONFIG_SCHEMA = IMAGE_SCHEMA
def load_svg_image(file: bytes, resize: tuple[int, int]):
# Local imports only to allow "validate_pillow_installed" to run *before* importing it
# cairosvg is only needed in case of SVG images; adding it
# to the top would force configurations not using SVG to also have it
# installed for no reason.
from cairosvg import svg2png
from PIL import Image
if resize:
req_width, req_height = resize
svg_image = svg2png(
file,
output_width=req_width,
output_height=req_height,
def typed_image_schema(image_type):
"""
Construct a schema for a specific image type, allowing transparency options
"""
return cv.Any(
cv.Schema(
{
cv.Optional(t.lower()): cv.ensure_list(
BASE_SCHEMA.extend(
{
cv.Optional(
CONF_USE_TRANSPARENCY, default=t
): validate_transparency((t,)),
cv.Optional(CONF_TYPE, default=image_type): validate_type(
(image_type,)
),
}
)
)
for t in IMAGE_TYPE[image_type].allow_config.intersection(
TRANSPARENCY_TYPES
)
}
),
# Allow a default configuration with no transparency preselected
cv.ensure_list(
BASE_SCHEMA.extend(
{
cv.Optional(
CONF_USE_TRANSPARENCY, default=CONF_OPAQUE
): validate_transparency(),
cv.Optional(CONF_TYPE, default=image_type): validate_type(
(image_type,)
),
}
)
),
)
else:
svg_image = svg2png(file)
return Image.open(io.BytesIO(svg_image))
async def to_code(config):
# Local import only to allow "validate_pillow_installed" to run *before* importing it
from PIL import Image
# The config schema can be a (possibly empty) single list of images,
# or a dictionary of image types each with a list of images
CONFIG_SCHEMA = cv.Any(
cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
cv.ensure_list(IMAGE_SCHEMA),
)
conf_file = config[CONF_FILE]
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
path = CORE.relative_config_path(conf_file[CONF_PATH])
elif conf_file[CONF_SOURCE] == SOURCE_MDI:
path = _compute_local_icon_path(conf_file).as_posix()
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
path = compute_local_image_path(conf_file).as_posix()
else:
raise core.EsphomeError(f"Unknown image source: {conf_file[CONF_SOURCE]}")
try:
with open(path, "rb") as f:
file_contents = f.read()
except Exception as e:
raise core.EsphomeError(f"Could not load image file {path}: {e}")
file_type = puremagic.from_string(file_contents, mime=True)
async def write_image(config, all_frames=False):
path = Path(config[CONF_FILE])
if not path.is_file():
raise core.EsphomeError(f"Could not load image file {path}")
resize = config.get(CONF_RESIZE)
if "svg" in file_type:
image = load_svg_image(file_contents, resize)
else:
image = Image.open(io.BytesIO(file_contents))
if resize:
image.thumbnail(resize)
if is_svg_file(path):
# Local import so use of non-SVG files needn't require cairosvg installed
from cairosvg import svg2png
if not resize:
resize = (None, None)
with open(path, "rb") as file:
image = svg2png(
file_obj=file,
output_width=resize[0],
output_height=resize[1],
)
image = Image.open(io.BytesIO(image))
width, height = image.size
else:
image = Image.open(path)
width, height = image.size
if resize:
# Preserve aspect ratio
new_width_max = min(width, resize[0])
new_height_max = min(height, resize[1])
ratio = min(new_width_max / width, new_height_max / height)
width, height = int(width * ratio), int(height * ratio)
if CONF_RESIZE not in config and (width > 500 or height > 500):
if not resize and (width > 500 or height > 500):
_LOGGER.warning(
'The image "%s" you requested is very big. Please consider'
" using the resize parameter.",
path,
)
transparent = config[CONF_USE_TRANSPARENCY]
dither = (
Image.Dither.NONE
if config[CONF_DITHER] == "NONE"
else Image.Dither.FLOYDSTEINBERG
)
if config[CONF_TYPE] == "GRAYSCALE":
image = image.convert("LA", dither=dither)
pixels = list(image.getdata())
data = [0 for _ in range(height * width)]
pos = 0
for g, a in pixels:
if transparent:
if g == 1:
g = 0
if a < 0x80:
g = 1
type = config[CONF_TYPE]
transparency = config[CONF_USE_TRANSPARENCY]
invert_alpha = config[CONF_INVERT_ALPHA]
frame_count = 1
if all_frames:
try:
frame_count = image.n_frames
except AttributeError:
pass
if frame_count <= 1:
_LOGGER.warning("Image file %s has no animation frames", path)
data[pos] = g
pos += 1
total_rows = height * frame_count
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
for frame_index in range(frame_count):
image.seek(frame_index)
pixels = encoder.convert(image.resize((width, height))).getdata()
for row in range(height):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
elif config[CONF_TYPE] == "RGBA":
image = image.convert("RGBA")
pixels = list(image.getdata())
data = [0 for _ in range(height * width * 4)]
pos = 0
for r, g, b, a in pixels:
data[pos] = r
pos += 1
data[pos] = g
pos += 1
data[pos] = b
pos += 1
data[pos] = a
pos += 1
elif config[CONF_TYPE] == "RGB24":
image = image.convert("RGBA")
pixels = list(image.getdata())
data = [0 for _ in range(height * width * 3)]
pos = 0
for r, g, b, a in pixels:
if transparent:
if r == 0 and g == 0 and b == 1:
b = 0
if a < 0x80:
r = 0
g = 0
b = 1
data[pos] = r
pos += 1
data[pos] = g
pos += 1
data[pos] = b
pos += 1
elif config[CONF_TYPE] in ["RGB565"]:
image = image.convert("RGBA")
pixels = list(image.getdata())
bytes_per_pixel = 3 if transparent else 2
data = [0 for _ in range(height * width * bytes_per_pixel)]
pos = 0
for r, g, b, a in pixels:
R = r >> 3
G = g >> 2
B = b >> 3
rgb = (R << 11) | (G << 5) | B
data[pos] = rgb >> 8
pos += 1
data[pos] = rgb & 0xFF
pos += 1
if transparent:
data[pos] = a
pos += 1
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
if transparent:
alpha = image.split()[-1]
has_alpha = alpha.getextrema()[0] < 0xFF
_LOGGER.debug("%s Has alpha: %s", config[CONF_ID], has_alpha)
image = image.convert("1", dither=dither)
width8 = ((width + 7) // 8) * 8
data = [0 for _ in range(height * width8 // 8)]
for y in range(height):
for x in range(width):
if transparent and has_alpha:
a = alpha.getpixel((x, y))
if not a:
continue
elif image.getpixel((x, y)):
continue
pos = x + y * width8
data[pos // 8] |= 0x80 >> (pos % 8)
else:
raise core.EsphomeError(
f"Image f{config[CONF_ID]} has an unsupported type: {config[CONF_TYPE]}."
)
rhs = [HexInt(x) for x in data]
rhs = [HexInt(x) for x in encoder.data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
var = cg.new_Pvariable(
config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]]
image_type = get_image_type_enum(type)
trans_value = get_transparency_enum(transparency)
return prog_arr, width, height, image_type, trans_value, frame_count
async def to_code(config):
if isinstance(config, list):
for entry in config:
await to_code(entry)
elif CONF_ID not in config:
for entry in config.values():
await to_code(entry)
else:
prog_arr, width, height, image_type, trans_value, _ = await write_image(config)
cg.new_Pvariable(
config[CONF_ID], prog_arr, width, height, image_type, trans_value
)
cg.add(var.set_transparency(transparent))

View File

@ -12,7 +12,7 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
for (int img_y = 0; img_y < height_; img_y++) {
if (this->get_binary_pixel_(img_x, img_y)) {
display->draw_pixel_at(x + img_x, y + img_y, color_on);
} else if (!this->transparent_) {
} else if (!this->transparency_) {
display->draw_pixel_at(x + img_x, y + img_y, color_off);
}
}
@ -39,20 +39,10 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
}
}
break;
case IMAGE_TYPE_RGB24:
case IMAGE_TYPE_RGB:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgb24_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
case IMAGE_TYPE_RGBA:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgba_pixel_(img_x, img_y);
auto color = this->get_rgb_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
@ -61,20 +51,20 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
break;
}
}
Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const {
Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return color_off;
switch (this->type_) {
case IMAGE_TYPE_BINARY:
return this->get_binary_pixel_(x, y) ? color_on : color_off;
if (this->get_binary_pixel_(x, y))
return color_on;
return color_off;
case IMAGE_TYPE_GRAYSCALE:
return this->get_grayscale_pixel_(x, y);
case IMAGE_TYPE_RGB565:
return this->get_rgb565_pixel_(x, y);
case IMAGE_TYPE_RGB24:
return this->get_rgb24_pixel_(x, y);
case IMAGE_TYPE_RGBA:
return this->get_rgba_pixel_(x, y);
case IMAGE_TYPE_RGB:
return this->get_rgb_pixel_(x, y);
default:
return color_off;
}
@ -98,23 +88,40 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT;
break;
case IMAGE_TYPE_RGB24:
this->dsc_.header.cf = LV_IMG_CF_RGB888;
case IMAGE_TYPE_RGB:
#if LV_COLOR_DEPTH == 32
switch (this->transparent_) {
case TRANSPARENCY_ALPHA_CHANNEL:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
break;
case TRANSPARENCY_CHROMA_KEY:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED;
break;
default:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
break;
}
#else
this->dsc_.header.cf =
this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGBA8888 : LV_IMG_CF_RGB888;
#endif
break;
case IMAGE_TYPE_RGB565:
#if LV_COLOR_DEPTH == 16
this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_ALPHA : LV_IMG_CF_TRUE_COLOR;
#else
this->dsc_.header.cf = LV_IMG_CF_RGB565;
#endif
break;
case IMAGE_TYPE_RGBA:
#if LV_COLOR_DEPTH == 32
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
#else
switch (this->transparency_) {
case TRANSPARENCY_ALPHA_CHANNEL:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
break;
case TRANSPARENCY_CHROMA_KEY:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED;
break;
default:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
break;
}
#else
this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
#endif
break;
}
@ -128,51 +135,73 @@ bool Image::get_binary_pixel_(int x, int y) const {
const uint32_t pos = x + y * width_8;
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
}
Color Image::get_rgba_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 4;
return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3));
}
Color Image::get_rgb24_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 3;
Color Image::get_rgb_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
progmem_read_byte(this->data_start_ + pos + 2));
if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
// (0, 0, 1) has been defined as transparent color for non-alpha images.
// putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if)
progmem_read_byte(this->data_start_ + pos + 2), 0xFF);
switch (this->transparency_) {
case TRANSPARENCY_CHROMA_KEY:
if (color.g == 1 && color.r == 0 && color.b == 0) {
// (0, 1, 0) has been defined as transparent color for non-alpha images.
color.w = 0;
} else {
color.w = 0xFF;
}
break;
case TRANSPARENCY_ALPHA_CHANNEL:
color.w = progmem_read_byte(this->data_start_ + (pos + 3));
break;
default:
break;
}
return color;
}
Color Image::get_rgb565_pixel_(int x, int y) const {
const uint8_t *pos = this->data_start_;
if (this->transparent_) {
pos += (x + y * this->width_) * 3;
} else {
pos += (x + y * this->width_) * 2;
}
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;
auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF;
Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a);
return color;
auto a = 0xFF;
switch (this->transparency_) {
case TRANSPARENCY_ALPHA_CHANNEL:
a = progmem_read_byte(pos + 2);
break;
case TRANSPARENCY_CHROMA_KEY:
if (rgb565 == 0x0020)
a = 0;
break;
default:
break;
}
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a);
}
Color Image::get_grayscale_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF;
uint8_t alpha = (gray == 1 && this->transparency_ == TRANSPARENCY_CHROMA_KEY) ? 0 : 0xFF;
return Color(gray, gray, gray, alpha);
}
int Image::get_width() const { return this->width_; }
int Image::get_height() const { return this->height_; }
ImageType Image::get_type() const { return this->type_; }
Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
: width_(width), height_(height), type_(type), data_start_(data_start) {}
Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency)
: width_(width), height_(height), type_(type), data_start_(data_start), transparency_(transparency) {
switch (this->type_) {
case IMAGE_TYPE_BINARY:
this->bpp_ = 1;
break;
case IMAGE_TYPE_GRAYSCALE:
this->bpp_ = 8;
break;
case IMAGE_TYPE_RGB565:
this->bpp_ = transparency == TRANSPARENCY_ALPHA_CHANNEL ? 24 : 16;
break;
case IMAGE_TYPE_RGB:
this->bpp_ = this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? 32 : 24;
break;
}
}
} // namespace image
} // namespace esphome

View File

@ -12,51 +12,40 @@ namespace image {
enum ImageType {
IMAGE_TYPE_BINARY = 0,
IMAGE_TYPE_GRAYSCALE = 1,
IMAGE_TYPE_RGB24 = 2,
IMAGE_TYPE_RGB = 2,
IMAGE_TYPE_RGB565 = 3,
IMAGE_TYPE_RGBA = 4,
};
enum Transparency {
TRANSPARENCY_OPAQUE = 0,
TRANSPARENCY_CHROMA_KEY = 1,
TRANSPARENCY_ALPHA_CHANNEL = 2,
};
class Image : public display::BaseImage {
public:
Image(const uint8_t *data_start, int width, int height, ImageType type);
Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency);
Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const;
int get_width() const override;
int get_height() const override;
const uint8_t *get_data_start() const { return this->data_start_; }
ImageType get_type() const;
int get_bpp() const {
switch (this->type_) {
case IMAGE_TYPE_BINARY:
return 1;
case IMAGE_TYPE_GRAYSCALE:
return 8;
case IMAGE_TYPE_RGB565:
return this->transparent_ ? 24 : 16;
case IMAGE_TYPE_RGB24:
return 24;
case IMAGE_TYPE_RGBA:
return 32;
}
return 0;
}
int get_bpp() const { return this->bpp_; }
/// Return the stride of the image in bytes, that is, the distance in bytes
/// between two consecutive rows of pixels.
uint32_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; }
size_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; }
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
void set_transparency(bool transparent) { transparent_ = transparent; }
bool has_transparency() const { return transparent_; }
bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; }
#ifdef USE_LVGL
lv_img_dsc_t *get_lv_img_dsc();
#endif
protected:
bool get_binary_pixel_(int x, int y) const;
Color get_rgb24_pixel_(int x, int y) const;
Color get_rgba_pixel_(int x, int y) const;
Color get_rgb_pixel_(int x, int y) const;
Color get_rgb565_pixel_(int x, int y) const;
Color get_grayscale_pixel_(int x, int y) const;
@ -64,7 +53,9 @@ class Image : public display::BaseImage {
int height_;
ImageType type_;
const uint8_t *data_start_;
bool transparent_;
Transparency transparency_;
size_t bpp_{};
size_t stride_{};
#ifdef USE_LVGL
lv_img_dsc_t dsc_{};
#endif

View File

@ -1,45 +1,27 @@
#include "json_util.h"
#include "esphome/core/log.h"
#ifdef USE_ESP8266
#include <Esp.h>
#endif
#ifdef USE_ESP32
#include <esp_heap_caps.h>
#endif
#ifdef USE_RP2040
#include <Arduino.h>
#endif
namespace esphome {
namespace json {
static const char *const TAG = "json";
static std::vector<char> global_json_build_buffer; // NOLINT
static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
std::string build_json(const json_build_t &f) {
// Here we are allocating up to 5kb of memory,
// with the heap size minus 2kb to be safe if less than 5kb
// as we can not have a true dynamic sized document.
// The excess memory is freed below with `shrinkToFit()`
#ifdef USE_ESP8266
const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance)
#elif defined(USE_ESP32)
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
#elif defined(USE_RP2040)
const size_t free_heap = rp2040.getFreeHeap();
#elif defined(USE_LIBRETINY)
const size_t free_heap = lt_heap_get_free();
#endif
auto free_heap = ALLOCATOR.get_max_free_block_size();
size_t request_size = std::min(free_heap, (size_t) 512);
while (true) {
ESP_LOGV(TAG, "Attempting to allocate %u bytes for JSON serialization", request_size);
ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size);
DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) {
ESP_LOGE(TAG,
"Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
"Could not allocate memory for JSON document! Requested %zu bytes, largest free heap block: %zu bytes",
request_size, free_heap);
return "{}";
}
@ -47,7 +29,7 @@ std::string build_json(const json_build_t &f) {
f(root);
if (json_document.overflowed()) {
if (request_size == free_heap) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %u bytes",
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %zu bytes",
free_heap);
return "{}";
}
@ -55,7 +37,7 @@ std::string build_json(const json_build_t &f) {
continue;
}
json_document.shrinkToFit();
ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity());
std::string output;
serializeJson(json_document, output);
return output;
@ -67,20 +49,12 @@ bool parse_json(const std::string &data, const json_parse_t &f) {
// with the heap size minus 2kb to be safe if less than that
// as we can not have a true dynamic sized document.
// The excess memory is freed below with `shrinkToFit()`
#ifdef USE_ESP8266
const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance)
#elif defined(USE_ESP32)
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
#elif defined(USE_RP2040)
const size_t free_heap = rp2040.getFreeHeap();
#elif defined(USE_LIBRETINY)
const size_t free_heap = lt_heap_get_free();
#endif
auto free_heap = ALLOCATOR.get_max_free_block_size();
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));
while (true) {
DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size,
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %zu bytes, free heap: %zu", request_size,
free_heap);
return false;
}

View File

@ -147,7 +147,7 @@ class LibreTinyPreferences : public ESPPreferences {
ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str());
return true;
}
stored_data.data.reserve(kv.value_len);
stored_data.data.resize(kv.value_len);
fdb_blob_make(&blob, stored_data.data.data(), kv.value_len);
size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob);
if (actual_len != kv.value_len) {

View File

@ -36,7 +36,7 @@ inline static uint8_t to_uint8_scale(float x) { return static_cast<uint8_t>(roun
* range as set in the traits, so the output needs to do this.
*
* For COLD_WARM_WHITE capability:
* - cold_white, warm_white: The brightness of the cald and warm white channels of the light.
* - cold_white, warm_white: The brightness of the light's cold and warm white channels.
*
* All values (except color temperature) are represented using floats in the range 0.0 (off) to 1.0 (on), and are
* automatically clamped to this range. Properties not used in the current color mode can still have (invalid) values

View File

@ -23,7 +23,7 @@ from esphome.helpers import write_file_if_changed
from . import defines as df, helpers, lv_validation as lvalid
from .automation import disp_update, focused_widgets, update_to_code
from .defines import add_define
from .defines import CONF_DRAW_ROUNDING, add_define
from .encoders import (
ENCODERS_CONFIG,
encoders_to_code,
@ -197,14 +197,18 @@ def final_validation(configs):
for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1]
display = global_config.get_config_for_path(path)
if CONF_LAMBDA in display:
if CONF_LAMBDA in display or CONF_PAGES in display:
raise cv.Invalid(
"Using lambda: in display config not compatible with LVGL"
"Using lambda: or pages: in display config is not compatible with LVGL"
)
if display[CONF_AUTO_CLEAR_ENABLED]:
if display.get(CONF_AUTO_CLEAR_ENABLED) is True:
raise cv.Invalid(
"Using auto_clear_enabled: true in display config not compatible with LVGL"
)
if draw_rounding := display.get(CONF_DRAW_ROUNDING):
config[CONF_DRAW_ROUNDING] = max(
draw_rounding, config[CONF_DRAW_ROUNDING]
)
buffer_frac = config[CONF_BUFFER_SIZE]
if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config:
LOGGER.warning("buffer_size: may need to be reduced without PSRAM")

View File

@ -4,24 +4,26 @@ from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT
from esphome.cpp_generator import get_variable
from esphome.cpp_generator import TemplateArguments, get_variable
from esphome.cpp_types import nullptr
from .defines import (
CONF_DISP_BG_COLOR,
CONF_DISP_BG_IMAGE,
CONF_DISP_BG_OPA,
CONF_EDITING,
CONF_FREEZE,
CONF_LVGL_ID,
CONF_SHOW_SNOW,
literal,
)
from .lv_validation import lv_bool, lv_color, lv_image
from .lv_validation import lv_bool, lv_color, lv_image, opacity
from .lvcode import (
LVGL_COMP_ARG,
UPDATE_EVENT,
LambdaContext,
LocalVariable,
LvglComponent,
ReturnStatement,
add_line_marks,
lv,
@ -92,7 +94,11 @@ async def lvgl_is_paused(config, condition_id, template_arg, args):
lvgl = config[CONF_LVGL_ID]
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
lv_add(ReturnStatement(lvgl_comp.is_paused()))
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
var = cg.new_Pvariable(
condition_id,
TemplateArguments(LvglComponent, *template_arg),
await context.get_lambda(),
)
await cg.register_parented(var, lvgl)
return var
@ -113,19 +119,32 @@ async def lvgl_is_idle(config, condition_id, template_arg, args):
timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32)
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
lv_add(ReturnStatement(lvgl_comp.is_idle(timeout)))
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
var = cg.new_Pvariable(
condition_id,
TemplateArguments(LvglComponent, *template_arg),
await context.get_lambda(),
)
await cg.register_parented(var, lvgl)
return var
async def disp_update(disp, config: dict):
if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config:
if (
CONF_DISP_BG_COLOR not in config
and CONF_DISP_BG_IMAGE not in config
and CONF_DISP_BG_OPA not in config
):
return
with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp:
if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None:
lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
if bg_image := config.get(CONF_DISP_BG_IMAGE):
if bg_image == "none":
lv.disp_set_bg_image(disp_temp, static_cast("void *", "nullptr"))
else:
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))
if (bg_opa := config.get(CONF_DISP_BG_OPA)) is not None:
lv.disp_set_bg_opa(disp_temp, await opacity.process(bg_opa))
@automation.register_action(

View File

@ -168,6 +168,7 @@ LV_EVENT_MAP = {
"READY": "READY",
"CANCEL": "CANCEL",
"ALL_EVENTS": "ALL",
"CHANGE": "VALUE_CHANGED",
}
LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP)
@ -214,7 +215,7 @@ LV_LONG_MODES = LvConstant(
)
STATES = (
"default",
# default state not included here
"checked",
"focused",
"focus_key",
@ -402,6 +403,7 @@ CONF_COLUMN = "column"
CONF_DIGITS = "digits"
CONF_DISP_BG_COLOR = "disp_bg_color"
CONF_DISP_BG_IMAGE = "disp_bg_image"
CONF_DISP_BG_OPA = "disp_bg_opa"
CONF_BODY = "body"
CONF_BUTTONS = "buttons"
CONF_BYTE_ORDER = "byte_order"

View File

@ -119,6 +119,7 @@ void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_ev
}
void LvglComponent::add_page(LvPageType *page) {
this->pages_.push_back(page);
page->set_parent(this);
page->setup(this->pages_.size() - 1);
}
void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) {
@ -143,6 +144,8 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
this->show_page(this->current_page_, anim, time);
}
size_t LvglComponent::get_current_page() const { return this->current_page_; }
bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; }
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
auto width = lv_area_get_width(area);
auto height = lv_area_get_height(area);

View File

@ -59,6 +59,16 @@ inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) {
inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image) {
lv_disp_set_bg_image(disp, image->get_lv_img_dsc());
}
inline void lv_obj_set_style_bg_img_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) {
lv_obj_set_style_bg_img_src(obj, image->get_lv_img_dsc(), selector);
}
#ifdef USE_LVGL_METER
inline lv_meter_indicator_t *lv_meter_add_needle_img(lv_obj_t *obj, lv_meter_scale_t *scale, esphome::image::Image *src,
lv_coord_t pivot_x, lv_coord_t pivot_y) {
return lv_meter_add_needle_img(obj, scale, src->get_lv_img_dsc(), pivot_x, pivot_y);
}
#endif // USE_LVGL_METER
#endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ANIMIMG
inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) {
@ -84,7 +94,9 @@ class LvCompound {
lv_obj_t *obj{};
};
class LvPageType {
class LvglComponent;
class LvPageType : public Parented<LvglComponent> {
public:
LvPageType(bool skip) : skip(skip) {}
@ -92,6 +104,9 @@ class LvPageType {
this->index = index;
this->obj = lv_obj_create(nullptr);
}
bool is_showing() const;
lv_obj_t *obj{};
size_t index{};
bool skip;
@ -178,6 +193,7 @@ class LvglComponent : public PollingComponent {
void show_next_page(lv_scr_load_anim_t anim, uint32_t time);
void show_prev_page(lv_scr_load_anim_t anim, uint32_t time);
void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; }
size_t get_current_page() const;
void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); }
void restore_focus_mark(lv_group_t *group) {
auto *mark = this->focus_marks_[group];
@ -241,14 +257,13 @@ template<typename... Ts> class LvglAction : public Action<Ts...>, public Parente
std::function<void(LvglComponent *)> action_{};
};
template<typename... Ts> class LvglCondition : public Condition<Ts...>, public Parented<LvglComponent> {
template<typename Tc, typename... Ts> class LvglCondition : public Condition<Ts...>, public Parented<Tc> {
public:
LvglCondition(std::function<bool(LvglComponent *)> &&condition_lambda)
: condition_lambda_(std::move(condition_lambda)) {}
LvglCondition(std::function<bool(Tc *)> &&condition_lambda) : condition_lambda_(std::move(condition_lambda)) {}
bool check(Ts... x) override { return this->condition_lambda_(this->parent_); }
protected:
std::function<bool(LvglComponent *)> condition_lambda_{};
std::function<bool(Tc *)> condition_lambda_{};
};
#ifdef USE_LVGL_TOUCHSCREEN

View File

@ -19,7 +19,7 @@ from esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
from .helpers import add_lv_use, requires_component, validate_printf
from .lv_validation import lv_color, lv_font, lv_gradient, lv_image
from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity
from .lvcode import LvglComponent, lv_event_t_ptr
from .types import (
LVEncoderListener,
@ -344,8 +344,11 @@ FLEX_OBJ_SCHEMA = {
DISP_BG_SCHEMA = cv.Schema(
{
cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image,
cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any(
cv.one_of("none", lower=True), lv_image
),
cv.Optional(df.CONF_DISP_BG_COLOR): lv_color,
cv.Optional(df.CONF_DISP_BG_OPA): opacity,
}
)

View File

@ -79,7 +79,7 @@ class ImgType(WidgetType):
if CONF_ANTIALIAS in config:
lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS])
if mode := config.get(CONF_MODE):
lv.img_set_mode(w.obj, mode)
await w.set_property("size_mode", mode)
img_spec = ImgType()

View File

@ -27,7 +27,7 @@ from ..defines import (
CONF_START_VALUE,
CONF_TICKS,
)
from ..helpers import add_lv_use
from ..helpers import add_lv_use, lvgl_components_required
from ..lv_validation import (
angle,
get_end_value,
@ -182,6 +182,7 @@ class MeterType(WidgetType):
async def to_code(self, w: Widget, config):
"""For a meter object, create and set parameters"""
lvgl_components_required.add(CONF_METER)
var = w.obj
for scale_conf in config.get(CONF_SCALES, ()):
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2

View File

@ -2,6 +2,7 @@ from esphome import automation, codegen as cg
from esphome.automation import Trigger
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME, CONF_TRIGGER_ID
from esphome.cpp_generator import MockObj, TemplateArguments
from ..defines import (
CONF_ANIMATION,
@ -17,18 +18,28 @@ from ..lvcode import (
EVENT_ARG,
LVGL_COMP_ARG,
LambdaContext,
ReturnStatement,
add_line_marks,
lv_add,
lvgl_comp,
lvgl_static,
)
from ..schemas import LVGL_SCHEMA
from ..types import LvglAction, lv_page_t
from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
from ..types import LvglAction, LvglCondition, lv_page_t
from . import (
Widget,
WidgetType,
add_widgets,
get_widgets,
set_obj_properties,
wait_for_widgets,
)
CONF_ON_LOAD = "on_load"
CONF_ON_UNLOAD = "on_unload"
PAGE_ARG = "_page"
PAGE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_SKIP, default=False): lv_bool,
@ -86,6 +97,30 @@ async def page_next_to_code(config, action_id, template_arg, args):
return var
@automation.register_condition(
"lvgl.page.is_showing",
LvglCondition,
cv.maybe_simple_value(
cv.Schema({cv.Required(CONF_ID): cv.use_id(lv_page_t)}),
key=CONF_ID,
),
)
async def page_is_showing_to_code(config, condition_id, template_arg, args):
await wait_for_widgets()
page = await cg.get_variable(config[CONF_ID])
async with LambdaContext(
[(lv_page_t.operator("ptr"), PAGE_ARG)], return_type=cg.bool_
) as context:
lv_add(ReturnStatement(MockObj(PAGE_ARG, "->").is_showing()))
var = cg.new_Pvariable(
condition_id,
TemplateArguments(lv_page_t, *template_arg),
await context.get_lambda(),
)
await cg.register_parented(var, page)
return var
@automation.register_action(
"lvgl.page.previous",
LvglAction,

View File

@ -11,16 +11,18 @@ void MicroNovaSwitch::write_state(bool state) {
if (this->micronova_->get_current_stove_state() == 0) {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_);
this->publish_state(true);
} else
} else {
ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", micronova_->get_current_stove_state());
}
} else {
// don't send power-off when status is Off or Final cleaning
if (this->micronova_->get_current_stove_state() != 0 && micronova_->get_current_stove_state() != 6) {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_);
this->publish_state(false);
} else
} else {
ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", micronova_->get_current_stove_state());
}
}
this->micronova_->update();
break;

View File

@ -19,10 +19,12 @@ template<typename... Ts> class MideaActionBase : public Action<Ts...> {
template<typename... Ts> class FollowMeAction : public MideaActionBase<Ts...> {
TEMPLATABLE_VALUE(float, temperature)
TEMPLATABLE_VALUE(bool, use_fahrenheit)
TEMPLATABLE_VALUE(bool, beeper)
void play(Ts... x) override {
this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...));
this->parent_->do_follow_me(this->temperature_.value(x...), this->use_fahrenheit_.value(x...),
this->beeper_.value(x...));
}
};

View File

@ -1,6 +1,7 @@
#ifdef USE_ARDUINO
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "air_conditioner.h"
#include "ac_adapter.h"
#include <cmath>
@ -121,7 +122,7 @@ void AirConditioner::dump_config() {
/* ACTIONS */
void AirConditioner::do_follow_me(float temperature, bool beeper) {
void AirConditioner::do_follow_me(float temperature, bool use_fahrenheit, bool beeper) {
#ifdef USE_REMOTE_TRANSMITTER
// Check if temperature is finite (not NaN or infinite)
if (!std::isfinite(temperature)) {
@ -131,13 +132,14 @@ void AirConditioner::do_follow_me(float temperature, bool beeper) {
// Round and convert temperature to long, then clamp and convert it to uint8_t
uint8_t temp_uint8 =
static_cast<uint8_t>(std::max(0L, std::min(static_cast<long>(UINT8_MAX), std::lroundf(temperature))));
static_cast<uint8_t>(esphome::clamp<long>(std::lroundf(temperature), 0L, static_cast<long>(UINT8_MAX)));
ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %f °C, rounded to: %u °C", temperature,
temp_uint8);
char temp_symbol = use_fahrenheit ? 'F' : 'C';
ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %.5f °%c, rounded to: %u °%c", temperature,
temp_symbol, temp_uint8, temp_symbol);
// Create and transmit the data
IrFollowMeData data(temp_uint8, beeper);
IrFollowMeData data(temp_uint8, use_fahrenheit, beeper);
this->transmitter_.transmit(data);
#else
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");

View File

@ -32,7 +32,7 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
/* ### ACTIONS ### */
/* ############### */
void do_follow_me(float temperature, bool beeper = false);
void do_follow_me(float temperature, bool use_fahrenheit, bool beeper = false);
void do_display_toggle();
void do_swing_step();
void do_beeper_on() { this->set_beeper_feedback(true); }

View File

@ -18,6 +18,7 @@ from esphome.const import (
CONF_SUPPORTED_SWING_MODES,
CONF_TIMEOUT,
CONF_TEMPERATURE,
CONF_USE_FAHRENHEIT,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
@ -172,11 +173,10 @@ MIDEA_ACTION_BASE_SCHEMA = cv.Schema(
)
# FollowMe action
MIDEA_FOLLOW_ME_MIN = 0
MIDEA_FOLLOW_ME_MAX = 37
MIDEA_FOLLOW_ME_SCHEMA = cv.Schema(
{
cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature),
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.templatable(cv.boolean),
cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean),
}
)
@ -186,6 +186,8 @@ MIDEA_FOLLOW_ME_SCHEMA = cv.Schema(
async def follow_me_to_code(var, config, args):
template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_)
cg.add(var.set_beeper(template_))
template_ = await cg.templatable(config[CONF_USE_FAHRENHEIT], args, cg.bool_)
cg.add(var.set_use_fahrenheit(template_))
template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_)
cg.add(var.set_temperature(template_))

View File

@ -16,22 +16,53 @@ class IrFollowMeData : public IrData {
IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {}
// Copy from Base
IrFollowMeData(const IrData &data) : IrData(data) {}
// Direct from temperature and beeper values
// Direct from temperature in celsius and beeper values
IrFollowMeData(uint8_t temp, bool beeper = false) : IrFollowMeData() {
this->set_temp(temp);
this->set_temp(temp, false);
this->set_beeper(beeper);
}
// Direct from temperature, fahrenheit and beeper values
IrFollowMeData(uint8_t temp, bool fahrenheit, bool beeper) : IrFollowMeData() {
this->set_temp(temp, fahrenheit);
this->set_beeper(beeper);
}
/* TEMPERATURE */
uint8_t temp() const { return this->get_value_(4) - 1; }
void set_temp(uint8_t val) { this->set_value_(4, std::min(MAX_TEMP, val) + 1); }
uint8_t temp() const {
if (this->fahrenheit()) {
return this->get_value_(4) + 31;
}
return this->get_value_(4) - 1;
}
void set_temp(uint8_t val, bool fahrenheit = false) {
this->set_fahrenheit(fahrenheit);
if (this->fahrenheit()) {
// see https://github.com/esphome/feature-requests/issues/1627#issuecomment-1365639966
val = esphome::clamp<uint8_t>(val, MIN_TEMP_F, MAX_TEMP_F) - 31;
} else {
val = esphome::clamp<uint8_t>(val, MIN_TEMP_C, MAX_TEMP_C) + 1;
}
this->set_value_(4, val);
}
/* BEEPER */
bool beeper() const { return this->get_value_(3, 128); }
void set_beeper(bool val) { this->set_mask_(3, val, 128); }
/* FAHRENHEIT */
bool fahrenheit() const { return this->get_value_(2, 32); }
void set_fahrenheit(bool val) { this->set_mask_(2, val, 32); }
protected:
static const uint8_t MAX_TEMP = 37;
static const uint8_t MIN_TEMP_C = 0;
static const uint8_t MAX_TEMP_C = 37;
// see
// https://github.com/crankyoldgit/IRremoteESP8266/blob/9bdf8abcb465268c5409db99dc83a26df64c7445/src/ir_Midea.h#L116
static const uint8_t MIN_TEMP_F = 32;
// see
// https://github.com/crankyoldgit/IRremoteESP8266/blob/9bdf8abcb465268c5409db99dc83a26df64c7445/src/ir_Midea.h#L117
static const uint8_t MAX_TEMP_F = 99;
};
class IrSpecialData : public IrData {

View File

@ -373,7 +373,7 @@ async def to_code(config):
)
)
cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX]))
cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX], CORE.name))
if config[CONF_USE_ABBREVIATIONS]:
cg.add_define("USE_MQTT_ABBREVIATIONS")

View File

@ -606,7 +606,13 @@ void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; }
void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); }
void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); }
const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix) { this->topic_prefix_ = topic_prefix; }
void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix) {
if (App.is_name_add_mac_suffix_enabled() && (topic_prefix == check_topic_prefix)) {
this->topic_prefix_ = str_sanitize(App.get_name());
} else {
this->topic_prefix_ = topic_prefix;
}
}
const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) {
this->publish_nan_as_none_ = publish_nan_as_none;

View File

@ -165,7 +165,7 @@ class MQTTClientComponent : public Component {
*
* @param topic_prefix The topic prefix. The last "/" is appended automatically.
*/
void set_topic_prefix(const std::string &topic_prefix);
void set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix);
/// Get the topic prefix of this device, using default if necessary
const std::string &get_topic_prefix() const;

View File

@ -49,6 +49,23 @@ class TouchTrigger : public Trigger<uint8_t, uint8_t, bool> {
}
};
template<typename... Ts> class NextionSetBrightnessAction : public Action<Ts...> {
public:
explicit NextionSetBrightnessAction(Nextion *component) : component_(component) {}
TEMPLATABLE_VALUE(float, brightness)
void play(Ts... x) override {
this->component_->set_brightness(this->brightness_.value(x...));
this->component_->set_backlight_brightness(this->brightness_.value(x...));
}
void set_brightness(std::function<void(Ts..., float)> brightness) { this->brightness_ = brightness; }
protected:
Nextion *component_;
};
template<typename... Ts> class NextionPublishFloatAction : public Action<Ts...> {
public:
explicit NextionPublishFloatAction(NextionComponent *component) : component_(component) {}

View File

@ -1,30 +1,30 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import display, uart
from esphome.components import esp32
import esphome.codegen as cg
from esphome.components import display, esp32, uart
import esphome.config_validation as cv
from esphome.const import (
CONF_BRIGHTNESS,
CONF_ID,
CONF_LAMBDA,
CONF_BRIGHTNESS,
CONF_TRIGGER_ID,
CONF_ON_TOUCH,
CONF_TRIGGER_ID,
)
from esphome.core import CORE
from . import Nextion, nextion_ns, nextion_ref
from .base_component import (
CONF_AUTO_WAKE_ON_TOUCH,
CONF_EXIT_REPARSE_ON_START,
CONF_ON_BUFFER_OVERFLOW,
CONF_ON_PAGE,
CONF_ON_SETUP,
CONF_ON_SLEEP,
CONF_ON_WAKE,
CONF_ON_SETUP,
CONF_ON_PAGE,
CONF_SKIP_CONNECTION_HANDSHAKE,
CONF_START_UP_PAGE,
CONF_TFT_URL,
CONF_TOUCH_SLEEP_TIMEOUT,
CONF_WAKE_UP_PAGE,
CONF_START_UP_PAGE,
CONF_AUTO_WAKE_ON_TOUCH,
CONF_EXIT_REPARSE_ON_START,
CONF_SKIP_CONNECTION_HANDSHAKE,
)
CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"]
@ -32,6 +32,9 @@ CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"]
DEPENDENCIES = ["uart"]
AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"]
NextionSetBrightnessAction = nextion_ns.class_(
"NextionSetBrightnessAction", automation.Action
)
SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template())
SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template())
WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template())
@ -46,7 +49,7 @@ CONFIG_SCHEMA = (
{
cv.GenerateID(): cv.declare_id(Nextion),
cv.Optional(CONF_TFT_URL): cv.url,
cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage,
cv.Optional(CONF_BRIGHTNESS): cv.percentage,
cv.Optional(CONF_ON_SETUP): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger),
@ -92,12 +95,34 @@ CONFIG_SCHEMA = (
)
@automation.register_action(
"display.nextion.set_brightness",
NextionSetBrightnessAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(Nextion),
cv.Required(CONF_BRIGHTNESS): cv.templatable(cv.percentage),
},
key=CONF_BRIGHTNESS,
),
)
async def nextion_set_brightness_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float)
cg.add(var.set_brightness(template_))
return var
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await uart.register_uart_device(var, config)
if CONF_BRIGHTNESS in config:
cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(
config[CONF_LAMBDA], [(nextion_ref, "it")], return_type=cg.void
@ -119,17 +144,17 @@ async def to_code(config):
cg.add_library("ESP8266HTTPClient", None)
if CONF_TOUCH_SLEEP_TIMEOUT in config:
cg.add(var.set_touch_sleep_timeout_internal(config[CONF_TOUCH_SLEEP_TIMEOUT]))
cg.add(var.set_touch_sleep_timeout(config[CONF_TOUCH_SLEEP_TIMEOUT]))
if CONF_WAKE_UP_PAGE in config:
cg.add(var.set_wake_up_page_internal(config[CONF_WAKE_UP_PAGE]))
cg.add(var.set_wake_up_page(config[CONF_WAKE_UP_PAGE]))
if CONF_START_UP_PAGE in config:
cg.add(var.set_start_up_page_internal(config[CONF_START_UP_PAGE]))
cg.add(var.set_start_up_page(config[CONF_START_UP_PAGE]))
cg.add(var.set_auto_wake_on_touch_internal(config[CONF_AUTO_WAKE_ON_TOUCH]))
cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH]))
cg.add(var.set_exit_reparse_on_start_internal(config[CONF_EXIT_REPARSE_ON_START]))
cg.add(var.set_exit_reparse_on_start(config[CONF_EXIT_REPARSE_ON_START]))
cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE]))

View File

@ -40,7 +40,7 @@ bool Nextion::send_command_(const std::string &command) {
}
bool Nextion::check_connect_() {
if (this->get_is_connected_())
if (this->is_connected_)
return true;
// Check if the handshake should be skipped for the Nextion connection
@ -273,21 +273,15 @@ void Nextion::loop() {
this->sent_setup_commands_ = true;
this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command.
this->set_backlight_brightness(this->brightness_);
if (this->brightness_.has_value()) {
this->set_backlight_brightness(this->brightness_.value());
}
// Check if a startup page has been set and send the command
if (this->start_up_page_ != -1) {
this->goto_page(this->start_up_page_);
}
// This could probably be removed from the loop area, as those are redundant.
this->set_auto_wake_on_touch(this->auto_wake_on_touch_);
this->set_exit_reparse_on_start(this->exit_reparse_on_start_);
if (this->touch_sleep_timeout_ != 0) {
this->set_touch_sleep_timeout(this->touch_sleep_timeout_);
}
if (this->wake_up_page_ != -1) {
this->set_wake_up_page(this->wake_up_page_);
}

View File

@ -856,76 +856,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/
void set_backlight_brightness(float brightness);
/**
* Set the touch sleep timeout of the display.
* @param timeout Timeout in seconds.
*
* Example:
* ```cpp
* it.set_touch_sleep_timeout(30);
* ```
*
* After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up
* `thup`.
*/
void set_touch_sleep_timeout(uint16_t timeout);
/**
* Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode.
* @param page_id The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to
* wakes up to current page.
*
* Example:
* ```cpp
* it.set_wake_up_page(2);
* ```
*
* The display will wake up to page 2.
*/
void set_wake_up_page(uint8_t page_id = 255);
/**
* Sets which page Nextion loads when connecting to ESPHome.
* @param page_id The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to
* wakes up to current page.
*
* Example:
* ```cpp
* it.set_start_up_page(2);
* ```
*
* The display will go to page 2 when it establishes a connection to ESPHome.
*/
void set_start_up_page(uint8_t page_id = 255);
/**
* Sets if Nextion should auto-wake from sleep when touch press occurs.
* @param auto_wake True or false. When auto_wake is true and Nextion is in sleep mode,
* the first touch will only trigger the auto wake mode and not trigger a Touch Event.
*
* Example:
* ```cpp
* it.set_auto_wake_on_touch(true);
* ```
*
* The display will wake up by touch.
*/
void set_auto_wake_on_touch(bool auto_wake);
/**
* Sets if Nextion should exit the active reparse mode before the "connect" command is sent
* @param exit_reparse True or false. When exit_reparse is true, the exit reparse command
* will be sent before requesting the connection from Nextion.
*
* Example:
* ```cpp
* it.set_exit_reparse_on_start(true);
* ```
*
* The display will be requested to leave active reparse mode before setup.
*/
void set_exit_reparse_on_start(bool exit_reparse);
/**
* Sets whether the Nextion display should skip the connection handshake process.
* @param skip_handshake True or false. When skip_connection_handshake is true,
@ -1172,15 +1102,75 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
void update_components_by_prefix(const std::string &prefix);
void set_touch_sleep_timeout_internal(uint32_t touch_sleep_timeout) {
this->touch_sleep_timeout_ = touch_sleep_timeout;
}
void set_wake_up_page_internal(uint8_t wake_up_page) { this->wake_up_page_ = wake_up_page; }
void set_start_up_page_internal(uint8_t start_up_page) { this->start_up_page_ = start_up_page; }
void set_auto_wake_on_touch_internal(bool auto_wake_on_touch) { this->auto_wake_on_touch_ = auto_wake_on_touch; }
void set_exit_reparse_on_start_internal(bool exit_reparse_on_start) {
this->exit_reparse_on_start_ = exit_reparse_on_start;
}
/**
* Set the touch sleep timeout of the display.
* @param timeout Timeout in seconds.
*
* Example:
* ```cpp
* it.set_touch_sleep_timeout(30);
* ```
*
* After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up
* `thup`.
*/
void set_touch_sleep_timeout(uint32_t touch_sleep_timeout);
/**
* Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode.
* @param wake_up_page The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to
* wakes up to current page.
*
* Example:
* ```cpp
* it.set_wake_up_page(2);
* ```
*
* The display will wake up to page 2.
*/
void set_wake_up_page(uint8_t wake_up_page = 255);
/**
* Sets which page Nextion loads when connecting to ESPHome.
* @param start_up_page The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to
* wakes up to current page.
*
* Example:
* ```cpp
* it.set_start_up_page(2);
* ```
*
* The display will go to page 2 when it establishes a connection to ESPHome.
*/
void set_start_up_page(uint8_t start_up_page = 255) { this->start_up_page_ = start_up_page; }
/**
* Sets if Nextion should auto-wake from sleep when touch press occurs.
* @param auto_wake_on_touch True or false. When auto_wake is true and Nextion is in sleep mode,
* the first touch will only trigger the auto wake mode and not trigger a Touch Event.
*
* Example:
* ```cpp
* it.set_auto_wake_on_touch(true);
* ```
*
* The display will wake up by touch.
*/
void set_auto_wake_on_touch(bool auto_wake_on_touch);
/**
* Sets if Nextion should exit the active reparse mode before the "connect" command is sent
* @param exit_reparse_on_start True or false. When exit_reparse_on_start is true, the exit reparse command
* will be sent before requesting the connection from Nextion.
*
* Example:
* ```cpp
* it.set_exit_reparse_on_start(true);
* ```
*
* The display will be requested to leave active reparse mode before setup.
*/
void set_exit_reparse_on_start(bool exit_reparse_on_start) { this->exit_reparse_on_start_ = exit_reparse_on_start; }
/**
* @brief Retrieves the number of commands pending in the Nextion command queue.
@ -1217,6 +1207,25 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/
bool is_updating() override;
/**
* @brief Check if the Nextion display is successfully connected.
*
* This method returns whether a successful connection has been established with
* the Nextion display. A connection is considered established when:
*
* - The initial handshake with the display is completed successfully, or
* - The handshake is skipped via skip_connection_handshake_ flag
*
* The connection status is particularly useful when:
* - Troubleshooting communication issues
* - Ensuring the display is ready before sending commands
* - Implementing connection-dependent behaviors
*
* @return true if the Nextion display is connected and ready to receive commands
* @return false if the display is not yet connected or connection was lost
*/
bool is_connected() { return this->is_connected_; }
protected:
std::deque<NextionQueue *> nextion_queue_;
std::deque<NextionQueue *> waveform_queue_;
@ -1315,8 +1324,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
#endif // USE_NEXTION_TFT_UPLOAD
bool get_is_connected_() { return this->is_connected_; }
bool check_connect_();
std::vector<NextionComponentBase *> touch_;
@ -1332,7 +1339,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
CallbackManager<void()> buffer_overflow_callback_{};
optional<nextion_writer_t> writer_;
float brightness_{1.0};
optional<float> brightness_;
std::string device_model_;
std::string firmware_version_;

View File

@ -10,19 +10,19 @@ static const char *const TAG = "nextion";
// Sleep safe commands
void Nextion::soft_reset() { this->send_command_("rest"); }
void Nextion::set_wake_up_page(uint8_t page_id) {
this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", page_id, true);
void Nextion::set_wake_up_page(uint8_t wake_up_page) {
this->wake_up_page_ = wake_up_page;
this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true);
}
void Nextion::set_start_up_page(uint8_t page_id) { this->start_up_page_ = page_id; }
void Nextion::set_touch_sleep_timeout(uint16_t timeout) {
if (timeout < 3) {
void Nextion::set_touch_sleep_timeout(uint32_t touch_sleep_timeout) {
if (touch_sleep_timeout < 3) {
ESP_LOGD(TAG, "Sleep timeout out of bounds, range 3-65535");
return;
}
this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", timeout, true);
this->touch_sleep_timeout_ = touch_sleep_timeout;
this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", touch_sleep_timeout, true);
}
void Nextion::sleep(bool sleep) {
@ -54,7 +54,6 @@ bool Nextion::set_protocol_reparse_mode(bool active_mode) {
this->ignore_is_setup_ = false;
return all_commands_sent;
}
void Nextion::set_exit_reparse_on_start(bool exit_reparse) { this->exit_reparse_on_start_ = exit_reparse; }
// Set Colors - Background
void Nextion::set_component_background_color(const char *component, uint16_t color) {
@ -191,8 +190,9 @@ void Nextion::set_backlight_brightness(float brightness) {
this->add_no_result_to_queue_with_printf_("backlight_brightness", "dim=%d", static_cast<int>(brightness * 100));
}
void Nextion::set_auto_wake_on_touch(bool auto_wake) {
this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake ? 1 : 0);
void Nextion::set_auto_wake_on_touch(bool auto_wake_on_touch) {
this->auto_wake_on_touch_ = auto_wake_on_touch;
this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake_on_touch ? 1 : 0);
}
// General Component

View File

@ -4,14 +4,18 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import (
CONF_INVERT_ALPHA,
CONF_USE_TRANSPARENCY,
IMAGE_TYPE,
IMAGE_SCHEMA,
Image_,
validate_cross_dependencies,
get_image_type_enum,
get_transparency_enum,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
CONF_DITHER,
CONF_FILE,
CONF_FORMAT,
CONF_ID,
CONF_ON_ERROR,
@ -23,7 +27,7 @@ from esphome.const import (
AUTO_LOAD = ["image"]
DEPENDENCIES = ["display", "http_request"]
CODEOWNERS = ["@guillempages"]
CODEOWNERS = ["@guillempages", "@clydebarrow"]
MULTI_CONF = True
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
@ -35,9 +39,30 @@ online_image_ns = cg.esphome_ns.namespace("online_image")
ImageFormat = online_image_ns.enum("ImageFormat")
FORMAT_PNG = "PNG"
IMAGE_FORMAT = {FORMAT_PNG: ImageFormat.PNG} # Add new supported formats here
class Format:
def __init__(self, image_type):
self.image_type = image_type
@property
def enum(self):
return getattr(ImageFormat, self.image_type)
def actions(self):
pass
class PNGFormat(Format):
def __init__(self):
super().__init__("PNG")
def actions(self):
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
cg.add_library("pngle", "1.0.2")
# New formats can be added here.
IMAGE_FORMATS = {x.image_type: x for x in (PNGFormat(),)}
OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_)
@ -57,28 +82,32 @@ DownloadErrorTrigger = online_image_ns.class_(
"DownloadErrorTrigger", automation.Trigger.template()
)
ONLINE_IMAGE_SCHEMA = cv.Schema(
def remove_options(*options):
return {
cv.Optional(option): cv.invalid(
f"{option} is an invalid option for online_image"
)
for option in options
}
ONLINE_IMAGE_SCHEMA = (
IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER))
.extend(
{
cv.Required(CONF_ID): cv.declare_id(OnlineImage),
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
#
# Common image options
#
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True),
# Not setting default here on purpose; the default depends on the image type,
# and thus will be set in the "validate_cross_dependencies" validator.
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
#
# Online Image specific options
#
cv.Required(CONF_URL): cv.url,
cv.Required(CONF_FORMAT): cv.enum(IMAGE_FORMAT, upper=True),
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True),
cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_),
cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadFinishedTrigger),
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DownloadFinishedTrigger
),
}
),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(
@ -87,18 +116,20 @@ ONLINE_IMAGE_SCHEMA = cv.Schema(
}
),
}
).extend(cv.polling_component_schema("never"))
)
.extend(cv.polling_component_schema("never"))
)
CONFIG_SCHEMA = cv.Schema(
cv.All(
ONLINE_IMAGE_SCHEMA,
validate_cross_dependencies,
cv.require_framework_version(
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
# esp8266_arduino=cv.Version(2, 7, 0),
esp32_arduino=cv.Version(0, 0, 0),
esp_idf=cv.Version(4, 0, 0),
rp2040_arduino=cv.Version(0, 0, 0),
host=cv.Version(0, 0, 0),
),
)
)
@ -132,29 +163,26 @@ async def online_image_action_to_code(config, action_id, template_arg, args):
async def to_code(config):
format = config[CONF_FORMAT]
if format in [FORMAT_PNG]:
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
cg.add_library("pngle", "1.0.2")
image_format = IMAGE_FORMATS[config[CONF_FORMAT]]
image_format.actions()
url = config[CONF_URL]
width, height = config.get(CONF_RESIZE, (0, 0))
transparent = config[CONF_USE_TRANSPARENCY]
transparent = get_transparency_enum(config[CONF_USE_TRANSPARENCY])
var = cg.new_Pvariable(
config[CONF_ID],
url,
width,
height,
format,
config[CONF_TYPE],
image_format.enum,
get_image_type_enum(config[CONF_TYPE]),
transparent,
config[CONF_BUFFER_SIZE],
)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
cg.add(var.set_transparency(transparent))
if placeholder_id := config.get(CONF_PLACEHOLDER):
placeholder = await cg.get_variable(placeholder_id)
cg.add(var.set_placeholder(placeholder))

View File

@ -1,5 +1,4 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/color.h"
namespace esphome {
@ -23,7 +22,7 @@ class ImageDecoder {
/**
* @brief Initialize the decoder.
*
* @param download_size The total number of bytes that need to be download for the image.
* @param download_size The total number of bytes that need to be downloaded for the image.
*/
virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; }
@ -38,7 +37,7 @@ class ImageDecoder {
* @return int The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully
* decode anything, or negative in case of a decoding error.
*/
virtual int decode(uint8_t *buffer, size_t size);
virtual int decode(uint8_t *buffer, size_t size) = 0;
/**
* @brief Request the image to be resized once the actual dimensions are known.
@ -50,7 +49,7 @@ class ImageDecoder {
void set_size(int width, int height);
/**
* @brief Draw a rectangle on the display_buffer using the defined color.
* @brief Fill a rectangle on the display_buffer using the defined color.
* Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly.
* In case of binary displays, the color will be converted to binary as well.
* Called by the callback functions, to be able to access the parent Image class.
@ -59,7 +58,7 @@ class ImageDecoder {
* @param y The top-most coordinate of the rectangle.
* @param w The width of the rectangle.
* @param h The height of the rectangle.
* @param color The color to draw the rectangle with.
* @param color The fill color
*/
void draw(int x, int y, int w, int h, const Color &color);
@ -67,7 +66,7 @@ class ImageDecoder {
protected:
OnlineImage *image_;
// Initializing to 1, to ensure it is different than initial "decoded_bytes_".
// Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_".
// Will be overwritten anyway once the download size is known.
uint32_t download_size_ = 1;
uint32_t decoded_bytes_ = 0;

View File

@ -25,8 +25,8 @@ inline bool is_color_on(const Color &color) {
}
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
uint32_t download_buffer_size)
: Image(nullptr, 0, 0, type),
image::Transparency transparency, uint32_t download_buffer_size)
: Image(nullptr, 0, 0, type, transparency),
buffer_(nullptr),
download_buffer_(download_buffer_size),
format_(format),
@ -45,7 +45,7 @@ void OnlineImage::draw(int x, int y, display::Display *display, Color color_on,
void OnlineImage::release() {
if (this->buffer_) {
ESP_LOGD(TAG, "Deallocating old buffer...");
ESP_LOGV(TAG, "Deallocating old buffer...");
this->allocator_.deallocate(this->buffer_, this->get_buffer_size_());
this->data_start_ = nullptr;
this->buffer_ = nullptr;
@ -70,28 +70,19 @@ bool OnlineImage::resize_(int width_in, int height_in) {
if (this->buffer_) {
return false;
}
auto new_size = this->get_buffer_size_(width, height);
ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size);
delay_microseconds_safe(2000);
size_t new_size = this->get_buffer_size_(width, height);
ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size);
this->buffer_ = this->allocator_.allocate(new_size);
if (this->buffer_) {
this->buffer_width_ = width;
this->buffer_height_ = height;
this->width_ = width;
ESP_LOGD(TAG, "New size: (%d, %d)", width, height);
} else {
#if defined(USE_ESP8266)
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
int max_block = ESP.getMaxFreeBlockSize();
#elif defined(USE_ESP32)
int max_block = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
#else
int max_block = -1;
#endif
ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %d Bytes", max_block);
if (this->buffer_ == nullptr) {
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size,
this->allocator_.get_max_free_block_size());
this->end_connection_();
return false;
}
this->buffer_width_ = width;
this->buffer_height_ = height;
this->width_ = width;
ESP_LOGV(TAG, "New size: (%d, %d)", width, height);
return true;
}
@ -99,9 +90,8 @@ void OnlineImage::update() {
if (this->decoder_) {
ESP_LOGW(TAG, "Image already being updated.");
return;
} else {
ESP_LOGI(TAG, "Updating image");
}
ESP_LOGI(TAG, "Updating image %s", this->url_.c_str());
this->downloader_ = this->parent_->get(this->url_);
@ -150,10 +140,11 @@ void OnlineImage::loop() {
return;
}
if (!this->downloader_ || this->decoder_->is_finished()) {
ESP_LOGD(TAG, "Image fully downloaded");
this->data_start_ = buffer_;
this->width_ = buffer_width_;
this->height_ = buffer_height_;
ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(),
this->width_, this->height_);
this->end_connection_();
this->download_finished_callback_.call();
return;
@ -179,6 +170,19 @@ void OnlineImage::loop() {
}
}
void OnlineImage::map_chroma_key(Color &color) {
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
if (color.g == 1 && color.r == 0 && color.b == 0) {
color.g = 0;
}
if (color.w < 0x80) {
color.r = 0;
color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1;
color.b = 0;
}
}
}
void OnlineImage::draw_pixel_(int x, int y, Color color) {
if (!this->buffer_) {
ESP_LOGE(TAG, "Buffer not allocated!");
@ -192,57 +196,53 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
switch (this->type_) {
case ImageType::IMAGE_TYPE_BINARY: {
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
const uint32_t pos = x + y * width_8;
if ((this->has_transparency() && color.w > 127) || is_color_on(color)) {
this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u));
pos = x + y * width_8;
auto bitno = 0x80 >> (pos % 8u);
pos /= 8u;
auto on = is_color_on(color);
if (this->has_transparency() && color.w < 0x80)
on = false;
if (on) {
this->buffer_[pos] |= bitno;
} else {
this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u));
this->buffer_[pos] &= ~bitno;
}
break;
}
case ImageType::IMAGE_TYPE_GRAYSCALE: {
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
if (this->has_transparency()) {
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
if (gray == 1) {
gray = 0;
}
if (color.w < 0x80) {
gray = 1;
}
} else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
if (color.w != 0xFF)
gray = color.w;
}
this->buffer_[pos] = gray;
break;
}
case ImageType::IMAGE_TYPE_RGB565: {
this->map_chroma_key(color);
uint16_t col565 = display::ColorUtil::color_to_565(color);
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
if (this->has_transparency())
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 2] = color.w;
}
break;
}
case ImageType::IMAGE_TYPE_RGBA: {
case ImageType::IMAGE_TYPE_RGB: {
this->map_chroma_key(color);
this->buffer_[pos + 0] = color.r;
this->buffer_[pos + 1] = color.g;
this->buffer_[pos + 2] = color.b;
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 3] = color.w;
break;
}
case ImageType::IMAGE_TYPE_RGB24:
default: {
if (this->has_transparency()) {
if (color.b == 1 && color.r == 0 && color.g == 0) {
color.b = 0;
}
if (color.w < 0x80) {
color.r = 0;
color.g = 0;
color.b = 1;
}
}
this->buffer_[pos + 0] = color.r;
this->buffer_[pos + 1] = color.g;
this->buffer_[pos + 2] = color.b;
break;
}
}

View File

@ -48,12 +48,13 @@ class OnlineImage : public PollingComponent,
* @param buffer_size Size of the buffer used to download the image.
*/
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type,
uint32_t buffer_size);
image::Transparency transparency, uint32_t buffer_size);
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
void update() override;
void loop() override;
void map_chroma_key(Color &color);
/** Set the URL to download the image from. */
void set_url(const std::string &url) {

View File

@ -1,6 +1,7 @@
#pragma once
#include "image_decoder.h"
#include "esphome/core/defines.h"
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include <pngle.h>

View File

@ -1,10 +1,12 @@
from typing import Any
import logging
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import sensor
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266, CONF_TRIGGER_ID
from . import const, schema, validate, generate
CODEOWNERS = ["@olegtarasov"]
@ -20,7 +22,21 @@ CONF_CH2_ACTIVE = "ch2_active"
CONF_SUMMER_MODE_ACTIVE = "summer_mode_active"
CONF_DHW_BLOCK = "dhw_block"
CONF_SYNC_MODE = "sync_mode"
CONF_OPENTHERM_VERSION = "opentherm_version"
CONF_OPENTHERM_VERSION = "opentherm_version" # Deprecated, will be removed
CONF_BEFORE_SEND = "before_send"
CONF_BEFORE_PROCESS_RESPONSE = "before_process_response"
# Triggers
BeforeSendTrigger = generate.opentherm_ns.class_(
"BeforeSendTrigger",
automation.Trigger.template(generate.OpenthermData.operator("ref")),
)
BeforeProcessResponseTrigger = generate.opentherm_ns.class_(
"BeforeProcessResponseTrigger",
automation.Trigger.template(generate.OpenthermData.operator("ref")),
)
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.All(
cv.Schema(
@ -36,7 +52,19 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean,
cv.Optional(CONF_DHW_BLOCK, False): cv.boolean,
cv.Optional(CONF_SYNC_MODE, False): cv.boolean,
cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float,
cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float, # Deprecated
cv.Optional(CONF_BEFORE_SEND): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BeforeSendTrigger),
}
),
cv.Optional(CONF_BEFORE_PROCESS_RESPONSE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
BeforeProcessResponseTrigger
),
}
),
}
)
.extend(
@ -44,6 +72,11 @@ CONFIG_SCHEMA = cv.All(
schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor))
)
)
.extend(
validate.create_entities_schema(
schema.SETTINGS, (lambda s: s.validation_schema)
)
)
.extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
)
@ -60,18 +93,33 @@ async def to_code(config: dict[str, Any]) -> None:
out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN])
cg.add(var.set_out_pin(out_pin))
non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN}
non_sensors = {
CONF_ID,
CONF_IN_PIN,
CONF_OUT_PIN,
CONF_BEFORE_SEND,
CONF_BEFORE_PROCESS_RESPONSE,
}
input_sensors = []
settings = []
for key, value in config.items():
if key in non_sensors:
continue
if key in schema.INPUTS:
input_sensor = await cg.get_variable(value)
cg.add(
getattr(var, f"set_{key}_{const.INPUT_SENSOR.lower()}")(input_sensor)
)
cg.add(getattr(var, f"set_{key}_{const.INPUT_SENSOR}")(input_sensor))
input_sensors.append(key)
elif key in schema.SETTINGS:
if value == schema.SETTINGS[key].default_value:
continue
cg.add(getattr(var, f"set_{key}_{const.SETTING}")(value))
settings.append(key)
else:
if key == CONF_OPENTHERM_VERSION:
_LOGGER.warning(
"opentherm_version is deprecated and will be removed in esphome 2025.2.0\n"
"Please change to 'opentherm_version_controller'."
)
cg.add(getattr(var, f"set_{key}")(value))
if len(input_sensors) > 0:
@ -81,3 +129,21 @@ async def to_code(config: dict[str, Any]) -> None:
)
generate.define_readers(const.INPUT_SENSOR, input_sensors)
generate.add_messages(var, input_sensors, schema.INPUTS)
if len(settings) > 0:
generate.define_has_settings(settings, schema.SETTINGS)
generate.define_message_handler(const.SETTING, settings, schema.SETTINGS)
generate.define_setting_readers(const.SETTING, settings)
generate.add_messages(var, settings, schema.SETTINGS)
for conf in config.get(CONF_BEFORE_SEND, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(generate.OpenthermData.operator("ref"), "x")], conf
)
for conf in config.get(CONF_BEFORE_PROCESS_RESPONSE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(generate.OpenthermData.operator("ref"), "x")], conf
)

View File

@ -0,0 +1,25 @@
#pragma once
#include "esphome/core/automation.h"
#include "hub.h"
#include "opentherm.h"
namespace esphome {
namespace opentherm {
class BeforeSendTrigger : public Trigger<OpenthermData &> {
public:
BeforeSendTrigger(OpenthermHub *hub) {
hub->add_on_before_send_callback([this](OpenthermData &x) { this->trigger(x); });
}
};
class BeforeProcessResponseTrigger : public Trigger<OpenthermData &> {
public:
BeforeProcessResponseTrigger(OpenthermHub *hub) {
hub->add_on_before_process_response_callback([this](OpenthermData &x) { this->trigger(x); });
}
};
} // namespace opentherm
} // namespace esphome

View File

@ -9,3 +9,4 @@ SWITCH = "switch"
NUMBER = "number"
OUTPUT = "output"
INPUT_SENSOR = "input_sensor"
SETTING = "setting"

View File

@ -1,13 +1,14 @@
from collections.abc import Awaitable
from typing import Any, Callable
from typing import Any, Callable, Optional
import esphome.codegen as cg
from esphome.const import CONF_ID
from . import const
from .schema import TSchema
from .schema import TSchema, SettingSchema
opentherm_ns = cg.esphome_ns.namespace("opentherm")
OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component)
OpenthermData = opentherm_ns.class_("OpenthermData")
def define_has_component(component_type: str, keys: list[str]) -> None:
@ -21,6 +22,24 @@ def define_has_component(component_type: str, keys: list[str]) -> None:
cg.add_define(f"OPENTHERM_HAS_{component_type.upper()}_{key}")
# We need a separate set of macros for settings because there are different backing field types we need to take
# into account
def define_has_settings(keys: list[str], schemas: dict[str, SettingSchema]) -> None:
cg.add_define(
"OPENTHERM_SETTING_LIST(F, sep)",
cg.RawExpression(
" sep ".join(
map(
lambda key: f"F({schemas[key].backing_type}, {key}_setting, {schemas[key].default_value})",
keys,
)
)
),
)
for key in keys:
cg.add_define(f"OPENTHERM_HAS_SETTING_{key}")
def define_message_handler(
component_type: str, keys: list[str], schemas: dict[str, TSchema]
) -> None:
@ -74,14 +93,28 @@ def define_readers(component_type: str, keys: list[str]) -> None:
)
def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]):
messages: set[tuple[str, bool]] = set()
def define_setting_readers(component_type: str, keys: list[str]) -> None:
for key in keys:
messages.add((schemas[key].message, schemas[key].keep_updated))
for msg, keep_updated in messages:
cg.add_define(
f"OPENTHERM_READ_{key}",
cg.RawExpression(f"this->{key}_{component_type.lower()}"),
)
def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]):
messages: dict[str, tuple[bool, Optional[int]]] = {}
for key in keys:
messages[schemas[key].message] = (
schemas[key].keep_updated,
schemas[key].order if hasattr(schemas[key], "order") else None,
)
for msg, (keep_updated, order) in messages.items():
msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}")
if keep_updated:
cg.add(hub.add_repeating_message(msg_expr))
else:
if order is not None:
cg.add(hub.add_initial_message(msg_expr, order))
else:
cg.add(hub.add_initial_message(msg_expr))

View File

@ -63,7 +63,7 @@ void write_f88(const float value, OpenthermData &data) { data.f88(value); }
OpenthermData OpenthermHub::build_request_(MessageId request_id) const {
OpenthermData data;
data.type = 0;
data.id = 0;
data.id = request_id;
data.valueHB = 0;
data.valueLB = 0;
@ -82,28 +82,13 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const {
// NOLINTEND
data.type = MessageType::READ_DATA;
data.id = MessageId::STATUS;
data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4) |
(summer_mode_is_active << 5) | (dhw_blocked << 6);
return data;
}
// Another special case is OpenTherm version number which is configured at hub level as a constant
if (request_id == MessageId::OT_VERSION_CONTROLLER) {
data.type = MessageType::WRITE_DATA;
data.id = MessageId::OT_VERSION_CONTROLLER;
data.f88(this->opentherm_version_);
return data;
}
// Disable incomplete switch statement warnings, because the cases in each
// switch are generated based on the configured sensors and inputs.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch"
// Next, we start with the write requests from switches and other inputs,
// Next, we start with write requests from switches and other inputs,
// because we would want to write that data if it is available, rather than
// request a read for that type (in the case that both read and write are
// supported).
@ -116,14 +101,23 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const {
OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, )
OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, ,
OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, )
OPENTHERM_SETTING_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_SETTING, ,
OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, )
default:
break;
}
// Finally, handle the simple read requests, which only change with the message id.
switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) }
switch (request_id) {
OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , )
default:
break;
}
switch (request_id) {
OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , )
default:
break;
}
#pragma GCC diagnostic pop
// And if we get here, a message was requested which somehow wasn't handled.
// This shouldn't happen due to the way the defines are configured, so we
@ -163,19 +157,37 @@ void OpenthermHub::setup() {
// communicate at least once every second. Sending the status request is
// good practice anyway.
this->add_repeating_message(MessageId::STATUS);
// Also ensure that we start communication with the STATUS message
this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::STATUS);
if (this->opentherm_version_ > 0.0f) {
this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::OT_VERSION_CONTROLLER);
}
this->current_message_iterator_ = this->initial_messages_.begin();
this->write_initial_messages_(this->messages_);
this->message_iterator_ = this->messages_.begin();
}
void OpenthermHub::on_shutdown() { this->opentherm_->stop(); }
// Disabling clang-tidy for this particular line since it keeps removing the trailing underscore (bug?)
void OpenthermHub::write_initial_messages_(std::vector<MessageId> &target) { // NOLINT
std::vector<std::pair<MessageId, uint8_t>> sorted;
std::copy_if(this->configured_messages_.begin(), this->configured_messages_.end(), std::back_inserter(sorted),
[](const std::pair<MessageId, uint8_t> &pair) { return pair.second < REPEATING_MESSAGE_ORDER; });
std::sort(sorted.begin(), sorted.end(),
[](const std::pair<MessageId, uint8_t> &a, const std::pair<MessageId, uint8_t> &b) {
return a.second < b.second;
});
target.clear();
std::transform(sorted.begin(), sorted.end(), std::back_inserter(target),
[](const std::pair<MessageId, uint8_t> &pair) { return pair.first; });
}
// Disabling clang-tidy for this particular line since it keeps removing the trailing underscore (bug?)
void OpenthermHub::write_repeating_messages_(std::vector<MessageId> &target) { // NOLINT
target.clear();
for (auto const &pair : this->configured_messages_) {
if (pair.second == REPEATING_MESSAGE_ORDER) {
target.push_back(pair.first);
}
}
}
void OpenthermHub::loop() {
if (this->sync_mode_) {
this->sync_loop_();
@ -184,29 +196,18 @@ void OpenthermHub::loop() {
auto cur_time = millis();
auto const cur_mode = this->opentherm_->get_mode();
if (this->handle_error_(cur_mode)) {
return;
}
switch (cur_mode) {
case OperationMode::WRITE:
case OperationMode::READ:
case OperationMode::LISTEN:
if (!this->check_timings_(cur_time)) {
break;
}
this->last_mode_ = cur_mode;
break;
case OperationMode::ERROR_PROTOCOL:
if (this->last_mode_ == OperationMode::WRITE) {
this->handle_protocol_write_error_();
} else if (this->last_mode_ == OperationMode::READ) {
this->handle_protocol_read_error_();
}
this->stop_opentherm_();
break;
case OperationMode::ERROR_TIMEOUT:
this->handle_timeout_error_();
this->stop_opentherm_();
break;
case OperationMode::IDLE:
this->check_timings_(cur_time);
if (this->should_skip_loop_(cur_time)) {
break;
}
@ -219,6 +220,28 @@ void OpenthermHub::loop() {
case OperationMode::RECEIVED:
this->read_response_();
break;
default:
break;
}
this->last_mode_ = cur_mode;
}
bool OpenthermHub::handle_error_(OperationMode mode) {
switch (mode) {
case OperationMode::ERROR_PROTOCOL:
// Protocol error can happen only while reading boiler response.
this->handle_protocol_error_();
return true;
case OperationMode::ERROR_TIMEOUT:
// Timeout error might happen while we wait for device to respond.
this->handle_timeout_error_();
return true;
case OperationMode::ERROR_TIMER:
// Timer error can happen only on ESP32.
this->handle_timer_error_();
return true;
default:
return false;
}
}
@ -237,16 +260,20 @@ void OpenthermHub::sync_loop_() {
}
this->start_conversation_();
// There may be a timer error at this point
if (this->handle_error_(this->opentherm_->get_mode())) {
return;
}
// Spin while message is being sent to device
if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
ESP_LOGE(TAG, "Hub timeout triggered during send");
this->stop_opentherm_();
return;
}
if (this->opentherm_->is_error()) {
this->handle_protocol_write_error_();
this->stop_opentherm_();
// Check for errors and ensure we are in the right state (message sent successfully)
if (this->handle_error_(this->opentherm_->get_mode())) {
return;
} else if (!this->opentherm_->is_sent()) {
ESP_LOGW(TAG, "Unexpected state after sending request: %s",
@ -257,19 +284,20 @@ void OpenthermHub::sync_loop_() {
// Listen for the response
this->opentherm_->listen();
// There may be a timer error at this point
if (this->handle_error_(this->opentherm_->get_mode())) {
return;
}
// Spin while response is being received
if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
ESP_LOGE(TAG, "Hub timeout triggered during receive");
this->stop_opentherm_();
return;
}
if (this->opentherm_->is_timeout()) {
this->handle_timeout_error_();
this->stop_opentherm_();
return;
} else if (this->opentherm_->is_protocol_error()) {
this->handle_protocol_read_error_();
this->stop_opentherm_();
// Check for errors and ensure we are in the right state (message received successfully)
if (this->handle_error_(this->opentherm_->get_mode())) {
return;
} else if (!this->opentherm_->has_message()) {
ESP_LOGW(TAG, "Unexpected state after receiving response: %s",
@ -281,17 +309,13 @@ void OpenthermHub::sync_loop_() {
this->read_response_();
}
bool OpenthermHub::check_timings_(uint32_t cur_time) {
void OpenthermHub::check_timings_(uint32_t cur_time) {
if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) {
ESP_LOGW(TAG,
"%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other "
"components that might slow the loop down.",
(int) (cur_time - this->last_conversation_start_));
this->stop_opentherm_();
return false;
}
return true;
}
bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const {
@ -304,14 +328,17 @@ bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const {
}
void OpenthermHub::start_conversation_() {
if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) {
if (this->message_iterator_ == this->messages_.end()) {
if (this->sending_initial_) {
this->sending_initial_ = false;
this->current_message_iterator_ = this->repeating_messages_.begin();
} else if (this->current_message_iterator_ == this->repeating_messages_.end()) {
this->current_message_iterator_ = this->repeating_messages_.begin();
this->write_repeating_messages_(this->messages_);
}
this->message_iterator_ = this->messages_.begin();
}
auto request = this->build_request_(*this->current_message_iterator_);
auto request = this->build_request_(*this->message_iterator_);
this->before_send_callback_.call(request);
ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id,
this->opentherm_->message_id_to_str((MessageId) request.id));
@ -331,37 +358,48 @@ void OpenthermHub::read_response_() {
this->stop_opentherm_();
this->before_process_response_callback_.call(response);
this->process_response(response);
this->current_message_iterator_++;
this->message_iterator_++;
}
void OpenthermHub::stop_opentherm_() {
this->opentherm_->stop();
this->last_conversation_end_ = millis();
}
void OpenthermHub::handle_protocol_write_error_() {
ESP_LOGW(TAG, "Error while sending request: %s",
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
this->opentherm_->debug_data(this->last_request_);
}
void OpenthermHub::handle_protocol_read_error_() {
void OpenthermHub::handle_protocol_error_() {
OpenThermError error;
this->opentherm_->get_protocol_error(error);
ESP_LOGW(TAG, "Protocol error occured while receiving response: %s",
this->opentherm_->protocol_error_to_to_str(error.error_type));
this->opentherm_->protocol_error_to_str(error.error_type));
this->opentherm_->debug_error(error);
}
void OpenthermHub::handle_timeout_error_() {
ESP_LOGW(TAG, "Receive response timed out at a protocol level");
this->stop_opentherm_();
}
void OpenthermHub::handle_timeout_error_() {
ESP_LOGW(TAG, "Timeout while waiting for response from device");
this->stop_opentherm_();
}
void OpenthermHub::handle_timer_error_() {
this->opentherm_->report_and_reset_timer_error();
this->stop_opentherm_();
// Timer error is critical, there is no point in retrying.
this->mark_failed();
}
void OpenthermHub::dump_config() {
std::vector<MessageId> initial_messages;
std::vector<MessageId> repeating_messages;
this->write_initial_messages_(initial_messages);
this->write_repeating_messages_(repeating_messages);
ESP_LOGCONFIG(TAG, "OpenTherm:");
LOG_PIN(" In: ", this->in_pin_);
LOG_PIN(" Out: ", this->out_pin_);
ESP_LOGCONFIG(TAG, " Sync mode: %d", this->sync_mode_);
ESP_LOGCONFIG(TAG, " Sync mode: %s", YESNO(this->sync_mode_));
ESP_LOGCONFIG(TAG, " Sensors: %s", SHOW(OPENTHERM_SENSOR_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Binary sensors: %s", SHOW(OPENTHERM_BINARY_SENSOR_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Switches: %s", SHOW(OPENTHERM_SWITCH_LIST(ID, )));
@ -369,12 +407,12 @@ void OpenthermHub::dump_config() {
ESP_LOGCONFIG(TAG, " Outputs: %s", SHOW(OPENTHERM_OUTPUT_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Initial requests:");
for (auto type : this->initial_messages_) {
ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str((type)));
for (auto type : initial_messages) {
ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str(type));
}
ESP_LOGCONFIG(TAG, " Repeating requests:");
for (auto type : this->repeating_messages_) {
ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str((type)));
for (auto type : repeating_messages) {
ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str(type));
}
}

View File

@ -38,6 +38,9 @@
namespace esphome {
namespace opentherm {
static const uint8_t REPEATING_MESSAGE_ORDER = 255;
static const uint8_t INITIAL_UNORDERED_MESSAGE_ORDER = 254;
// OpenTherm component for ESPHome
class OpenthermHub : public Component {
protected:
@ -58,15 +61,12 @@ class OpenthermHub : public Component {
OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, )
// The set of initial messages to send on starting communication with the boiler
std::vector<MessageId> initial_messages_;
// and the repeating messages which are sent repeatedly to update various sensors
// and boiler parameters (like the setpoint).
std::vector<MessageId> repeating_messages_;
// Indicates if we are still working on the initial requests or not
OPENTHERM_SETTING_LIST(OPENTHERM_DECLARE_SETTING, )
bool sending_initial_ = true;
// Index for the current request in one of the _requests sets.
std::vector<MessageId>::const_iterator current_message_iterator_;
std::unordered_map<MessageId, uint8_t> configured_messages_;
std::vector<MessageId> messages_;
std::vector<MessageId>::const_iterator message_iterator_;
uint32_t last_conversation_start_ = 0;
uint32_t last_conversation_end_ = 0;
@ -78,20 +78,25 @@ class OpenthermHub : public Component {
// Very likely to happen while using Dallas temperature sensors.
bool sync_mode_ = false;
float opentherm_version_ = 0.0f;
CallbackManager<void(OpenthermData &)> before_send_callback_;
CallbackManager<void(OpenthermData &)> before_process_response_callback_;
// Create OpenTherm messages based on the message id
OpenthermData build_request_(MessageId request_id) const;
void handle_protocol_write_error_();
void handle_protocol_read_error_();
bool handle_error_(OperationMode mode);
void handle_protocol_error_();
void handle_timeout_error_();
void handle_timer_error_();
void stop_opentherm_();
void start_conversation_();
void read_response_();
bool check_timings_(uint32_t cur_time);
void check_timings_(uint32_t cur_time);
bool should_skip_loop_(uint32_t cur_time) const;
void sync_loop_();
void write_initial_messages_(std::vector<MessageId> &target);
void write_repeating_messages_(std::vector<MessageId> &target);
template<typename F> bool spin_wait_(uint32_t timeout, F func) {
auto start_time = millis();
while (func()) {
@ -127,13 +132,18 @@ class OpenthermHub : public Component {
OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, )
OPENTHERM_SETTING_LIST(OPENTHERM_SET_SETTING, )
// Add a request to the vector of initial requests
void add_initial_message(MessageId message_id) { this->initial_messages_.push_back(message_id); }
void add_initial_message(MessageId message_id) {
this->configured_messages_[message_id] = INITIAL_UNORDERED_MESSAGE_ORDER;
}
void add_initial_message(MessageId message_id, uint8_t order) { this->configured_messages_[message_id] = order; }
// Add a request to the set of repeating requests. Note that a large number of repeating
// requests will slow down communication with the boiler. Each request may take up to 1 second,
// so with all sensors enabled, it may take about half a minute before a change in setpoint
// will be processed.
void add_repeating_message(MessageId message_id) { this->repeating_messages_.push_back(message_id); }
void add_repeating_message(MessageId message_id) { this->configured_messages_[message_id] = REPEATING_MESSAGE_ORDER; }
// There are seven status variables, which can either be set as a simple variable,
// or using a switch. ch_enable and dhw_enable default to true, the others to false.
@ -149,7 +159,13 @@ class OpenthermHub : public Component {
void set_summer_mode_active(bool value) { this->summer_mode_active = value; }
void set_dhw_block(bool value) { this->dhw_block = value; }
void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; }
void set_opentherm_version(float value) { this->opentherm_version_ = value; }
void add_on_before_send_callback(std::function<void(OpenthermData &)> &&callback) {
this->before_send_callback_.add(std::move(callback));
}
void add_on_before_process_response_callback(std::function<void(OpenthermData &)> &&callback) {
this->before_process_response_callback_.add(std::move(callback));
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }

View File

@ -52,7 +52,9 @@ bool OpenTherm::initialize() {
OpenTherm::instance = this;
#endif
this->in_pin_->pin_mode(gpio::FLAG_INPUT);
this->in_pin_->setup();
this->out_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->out_pin_->setup();
this->out_pin_->digital_write(true);
#if defined(ESP32) || defined(USE_ESP_IDF)
@ -182,7 +184,7 @@ bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) {
}
arg->capture_ = 1; // reset counter
} else if (arg->capture_ > 0xFF) {
// no change for too long, invalid mancheter encoding
// no change for too long, invalid manchester encoding
arg->mode_ = OperationMode::ERROR_PROTOCOL;
arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG;
arg->stop_timer_();
@ -312,21 +314,31 @@ bool OpenTherm::init_esp32_timer_() {
}
void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) {
esp_err_t result;
// We will report timer errors outside of interrupt handler
this->timer_error_ = ESP_OK;
this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR;
result = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to set alarm value. Error: %s", error);
this->timer_error_ = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value);
if (this->timer_error_ != ESP_OK) {
this->timer_error_type_ = TimerErrorType::SET_ALARM_VALUE_ERROR;
return;
}
this->timer_error_ = timer_start(this->timer_group_, this->timer_idx_);
if (this->timer_error_ != ESP_OK) {
this->timer_error_type_ = TimerErrorType::TIMER_START_ERROR;
}
}
void OpenTherm::report_and_reset_timer_error() {
if (this->timer_error_ == ESP_OK) {
return;
}
result = timer_start(this->timer_group_, this->timer_idx_);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to start the timer. Error: %s", error);
return;
}
ESP_LOGE(TAG, "Error occured while manipulating timer (%s): %s", this->timer_error_to_str(this->timer_error_type_),
esp_err_to_name(this->timer_error_));
this->timer_error_ = ESP_OK;
this->timer_error_type_ = NO_TIMER_ERROR;
}
// 5 kHz timer_
@ -343,21 +355,18 @@ void IRAM_ATTR OpenTherm::start_write_timer_() {
void IRAM_ATTR OpenTherm::stop_timer_() {
InterruptLock const lock;
// We will report timer errors outside of interrupt handler
this->timer_error_ = ESP_OK;
this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR;
esp_err_t result;
result = timer_pause(this->timer_group_, this->timer_idx_);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to pause the timer. Error: %s", error);
this->timer_error_ = timer_pause(this->timer_group_, this->timer_idx_);
if (this->timer_error_ != ESP_OK) {
this->timer_error_type_ = TimerErrorType::TIMER_PAUSE_ERROR;
return;
}
result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to set timer counter to 0 after pausing. Error: %s", error);
return;
this->timer_error_ = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0);
if (this->timer_error_ != ESP_OK) {
this->timer_error_type_ = TimerErrorType::SET_COUNTER_VALUE_ERROR;
}
}
@ -386,6 +395,9 @@ void IRAM_ATTR OpenTherm::stop_timer_() {
timer1_detachInterrupt();
}
// There is nothing to report on ESP8266
void OpenTherm::report_and_reset_timer_error() {}
#endif // END ESP8266
// https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd
@ -412,11 +424,12 @@ const char *OpenTherm::operation_mode_to_str(OperationMode mode) {
TO_STRING_MEMBER(SENT)
TO_STRING_MEMBER(ERROR_PROTOCOL)
TO_STRING_MEMBER(ERROR_TIMEOUT)
TO_STRING_MEMBER(ERROR_TIMER)
default:
return "<INVALID>";
}
}
const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) {
const char *OpenTherm::protocol_error_to_str(ProtocolErrorType error_type) {
switch (error_type) {
TO_STRING_MEMBER(NO_ERROR)
TO_STRING_MEMBER(NO_TRANSITION)
@ -427,6 +440,17 @@ const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) {
return "<INVALID>";
}
}
const char *OpenTherm::timer_error_to_str(TimerErrorType error_type) {
switch (error_type) {
TO_STRING_MEMBER(NO_TIMER_ERROR)
TO_STRING_MEMBER(SET_ALARM_VALUE_ERROR)
TO_STRING_MEMBER(TIMER_START_ERROR)
TO_STRING_MEMBER(TIMER_PAUSE_ERROR)
TO_STRING_MEMBER(SET_COUNTER_VALUE_ERROR)
default:
return "<INVALID>";
}
}
const char *OpenTherm::message_type_to_str(MessageType message_type) {
switch (message_type) {
TO_STRING_MEMBER(READ_DATA)

View File

@ -36,11 +36,12 @@ enum OperationMode {
READ = 2, // reading 32-bit data frame
RECEIVED = 3, // data frame received with valid start and stop bit
WRITE = 4, // writing data with timer_
WRITE = 4, // writing data to output
SENT = 5, // all data written to output
ERROR_PROTOCOL = 8, // manchester protocol data transfer error
ERROR_TIMEOUT = 9 // read timeout
ERROR_PROTOCOL = 8, // protocol error, can happed only during READ
ERROR_TIMEOUT = 9, // timeout while waiting for response from device, only during LISTEN
ERROR_TIMER = 10 // error operating the ESP32 timer
};
enum ProtocolErrorType {
@ -51,6 +52,14 @@ enum ProtocolErrorType {
NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks
};
enum TimerErrorType {
NO_TIMER_ERROR = 0, // No error
SET_ALARM_VALUE_ERROR = 1, // No transition in the middle of the bit
TIMER_START_ERROR = 2, // Stop bit wasn't present when expected
TIMER_PAUSE_ERROR = 3, // Parity check didn't pass
SET_COUNTER_VALUE_ERROR = 4, // No level change for too much timer ticks
};
enum MessageType {
READ_DATA = 0,
READ_ACK = 4,
@ -299,7 +308,9 @@ class OpenTherm {
*
* @return true if last listen() or send() operation ends up with an error.
*/
bool is_error() { return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL; }
bool is_error() {
return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL || mode_ == ERROR_TIMER;
}
/**
* Indicates whether last listen() or send() operation ends up with a *timeout* error
@ -313,14 +324,22 @@ class OpenTherm {
*/
bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; }
/**
* Indicates whether start_esp32_timer_() or stop_timer_() had an error. Only relevant when used on ESP32.
* @return true if there was an error.
*/
bool is_timer_error() { return mode_ == OperationMode::ERROR_TIMER; }
bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; }
OperationMode get_mode() { return mode_; }
void debug_data(OpenthermData &data);
void debug_error(OpenThermError &error) const;
void report_and_reset_timer_error();
const char *protocol_error_to_to_str(ProtocolErrorType error_type);
const char *protocol_error_to_str(ProtocolErrorType error_type);
const char *timer_error_to_str(TimerErrorType error_type);
const char *message_type_to_str(MessageType message_type);
const char *operation_mode_to_str(OperationMode mode);
const char *message_id_to_str(MessageId id);
@ -349,10 +368,12 @@ class OpenTherm {
uint32_t data_;
uint8_t bit_pos_;
int32_t timeout_counter_; // <0 no timeout
int32_t device_timeout_;
#if defined(ESP32) || defined(USE_ESP_IDF)
esp_err_t timer_error_ = ESP_OK;
TimerErrorType timer_error_type_ = TimerErrorType::NO_TIMER_ERROR;
bool init_esp32_timer_();
void start_esp32_timer_(uint64_t alarm_value);
#endif

Some files were not shown because too many files have changed in this diff Show More