1
0
mirror of https://github.com/esphome/esphome.git synced 2025-03-15 15:18:16 +00:00

Merge branch 'dev' into defaults2

This commit is contained in:
tomaszduda23 2025-01-16 22:47:29 +01:00 committed by GitHub
commit 88d4091876
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
118 changed files with 2353 additions and 1058 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.12.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.12.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false

View File

@ -48,7 +48,7 @@ jobs:
- name: Set up Docker Buildx
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

@ -93,7 +93,7 @@ jobs:
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.5.0
uses: actions/upload-artifact@v4.6.0
with:
name: digests-${{ steps.sanitize.outputs.name }}
path: /tmp/digests

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

@ -11,14 +11,6 @@ repos:
args: [--fix]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2
hooks:
- id: black
args:
- --safe
- --quiet
files: ^((esphome|script|tests)/.+)?[^/]+\.py$
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
@ -53,6 +45,6 @@ repos:
hooks:
- id: pylint
name: pylint
entry: script/run-in-env.sh pylint
language: script
entry: python script/run-in-env pylint
language: system
types: [python]

View File

@ -131,6 +131,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
esphome/components/ens160_i2c/* @latonita
esphome/components/ens160_spi/* @latonita
esphome/components/ens210/* @itn3rd77
esphome/components/es7210/* @kahrendt
esphome/components/es8311/* @kahrendt @kroimon
esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @Rapsssito @jesserockz
@ -302,7 +303,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
@ -338,7 +339,6 @@ esphome/components/radon_eye_rd200/* @jeffeb3
esphome/components/rc522/* @glmnet
esphome/components/rc522_i2c/* @glmnet
esphome/components/rc522_spi/* @glmnet
esphome/components/resistance_sampler/* @jesserockz
esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz

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

@ -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,86 +34,19 @@ SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
)
TYPED_FILE_SCHEMA = cv.typed_schema(
CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
{
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(
cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Optional(CONF_LOOP): cv.All(
{
CONF_SOURCE: SOURCE_WEB,
CONF_URL: value,
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
cv.Optional(CONF_END_FRAME): cv.positive_int,
cv.Optional(CONF_REPEAT): cv.positive_int,
}
)
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(
{
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,
cv.Optional(CONF_END_FRAME): cv.positive_int,
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

@ -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,6 +29,19 @@ 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()) {
@ -276,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();

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

@ -0,0 +1,67 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE
CODEOWNERS = ["@kahrendt"]
DEPENDENCIES = ["i2c"]
es7210_ns = cg.esphome_ns.namespace("es7210")
ES7210 = es7210_ns.class_("ES7210", cg.Component, i2c.I2CDevice)
es7210_bits_per_sample = es7210_ns.enum("ES7210BitsPerSample")
ES7210_BITS_PER_SAMPLE_ENUM = {
16: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_16,
24: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_24,
32: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_32,
}
es7210_mic_gain = es7210_ns.enum("ES7210MicGain")
ES7210_MIC_GAIN_ENUM = {
"0DB": es7210_mic_gain.ES7210_MIC_GAIN_0DB,
"3DB": es7210_mic_gain.ES7210_MIC_GAIN_3DB,
"6DB": es7210_mic_gain.ES7210_MIC_GAIN_6DB,
"9DB": es7210_mic_gain.ES7210_MIC_GAIN_9DB,
"12DB": es7210_mic_gain.ES7210_MIC_GAIN_12DB,
"15DB": es7210_mic_gain.ES7210_MIC_GAIN_15DB,
"18DB": es7210_mic_gain.ES7210_MIC_GAIN_18DB,
"21DB": es7210_mic_gain.ES7210_MIC_GAIN_21DB,
"24DB": es7210_mic_gain.ES7210_MIC_GAIN_24DB,
"27DB": es7210_mic_gain.ES7210_MIC_GAIN_27DB,
"30DB": es7210_mic_gain.ES7210_MIC_GAIN_30DB,
"33DB": es7210_mic_gain.ES7210_MIC_GAIN_33DB,
"34.5DB": es7210_mic_gain.ES7210_MIC_GAIN_34_5DB,
"36DB": es7210_mic_gain.ES7210_MIC_GAIN_36DB,
"37.5DB": es7210_mic_gain.ES7210_MIC_GAIN_37_5DB,
}
_validate_bits = cv.float_with_unit("bits", "bit")
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ES7210),
cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All(
_validate_bits, cv.enum(ES7210_BITS_PER_SAMPLE_ENUM)
),
cv.Optional(CONF_MIC_GAIN, default="24DB"): cv.enum(
ES7210_MIC_GAIN_ENUM, upper=True
),
cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x40))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_mic_gain(config[CONF_MIC_GAIN]))
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))

View File

@ -0,0 +1,201 @@
#include "es7210.h"
#include "es7210_const.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace es7210 {
static const char *const TAG = "es7210";
static const size_t MCLK_DIV_FRE = 256;
// Mark the component as failed; use only in setup
#define ES7210_ERROR_FAILED(func) \
if (!(func)) { \
this->mark_failed(); \
return; \
}
// Return false; use outside of setup
#define ES7210_ERROR_CHECK(func) \
if (!(func)) { \
return false; \
}
void ES7210::dump_config() {
ESP_LOGCONFIG(TAG, "ES7210 ADC:");
ESP_LOGCONFIG(TAG, " Bits Per Sample: %" PRIu8, this->bits_per_sample_);
ESP_LOGCONFIG(TAG, " Sample Rate: %" PRIu32, this->sample_rate_);
if (this->is_failed()) {
ESP_LOGCONFIG(TAG, " Failed to initialize!");
return;
}
}
void ES7210::setup() {
ESP_LOGCONFIG(TAG, "Setting up ES7210...");
// Software reset
ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0xff));
ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x32));
ES7210_ERROR_FAILED(this->write_byte(ES7210_CLOCK_OFF_REG01, 0x3f));
// Set initialization time when device powers up
ES7210_ERROR_FAILED(this->write_byte(ES7210_TIME_CONTROL0_REG09, 0x30));
ES7210_ERROR_FAILED(this->write_byte(ES7210_TIME_CONTROL1_REG0A, 0x30));
// Configure HFP for all ADC channels
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC12_HPF2_REG23, 0x2a));
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC12_HPF1_REG22, 0x0a));
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF2_REG20, 0x0a));
ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF1_REG21, 0x2a));
// Secondary I2S mode settings
ES7210_ERROR_FAILED(this->es7210_update_reg_bit_(ES7210_MODE_CONFIG_REG08, 0x01, 0x00));
// Configure analog power
ES7210_ERROR_FAILED(this->write_byte(ES7210_ANALOG_REG40, 0xC3));
// Set mic bias
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC12_BIAS_REG41, 0x70));
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC34_BIAS_REG42, 0x70));
// Configure I2S settings, sample rate, and microphone gains
ES7210_ERROR_FAILED(this->configure_i2s_format_());
ES7210_ERROR_FAILED(this->configure_sample_rate_());
ES7210_ERROR_FAILED(this->configure_mic_gain_());
// Power on mics 1 through 4
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC1_POWER_REG47, 0x08));
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC2_POWER_REG48, 0x08));
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC3_POWER_REG49, 0x08));
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC4_POWER_REG4A, 0x08));
// Power down DLL
ES7210_ERROR_FAILED(this->write_byte(ES7210_POWER_DOWN_REG06, 0x04));
// Power on MIC1-4 bias & ADC1-4 & PGA1-4 Power
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x0F));
ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x0F));
// Enable device
ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x71));
ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x41));
}
bool ES7210::configure_sample_rate_() {
int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE;
int coeff = -1;
for (int i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) {
if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre)
coeff = i;
}
if (coeff >= 0) {
// Set adc_div & doubler & dll
uint8_t regv;
ES7210_ERROR_CHECK(this->read_byte(ES7210_MAINCLK_REG02, &regv));
regv = regv & 0x00;
regv |= ES7210_COEFFICIENTS[coeff].adc_div;
regv |= ES7210_COEFFICIENTS[coeff].doubler << 6;
regv |= ES7210_COEFFICIENTS[coeff].dll << 7;
ES7210_ERROR_CHECK(this->write_byte(ES7210_MAINCLK_REG02, regv));
// Set osr
regv = ES7210_COEFFICIENTS[coeff].osr;
ES7210_ERROR_CHECK(this->write_byte(ES7210_OSR_REG07, regv));
// Set lrck
regv = ES7210_COEFFICIENTS[coeff].lrck_h;
ES7210_ERROR_CHECK(this->write_byte(ES7210_LRCK_DIVH_REG04, regv));
regv = ES7210_COEFFICIENTS[coeff].lrck_l;
ES7210_ERROR_CHECK(this->write_byte(ES7210_LRCK_DIVL_REG05, regv));
} else {
// Invalid sample frequency
ESP_LOGE(TAG, "Invalid sample rate");
return false;
}
return true;
}
bool ES7210::configure_mic_gain_() {
for (int i = 0; i < 4; ++i) {
this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00);
}
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0xff));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0xff));
// Configure mic 1
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x0f, this->mic_gain_));
// Configure mic 2
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, this->mic_gain_));
// Configure mic 3
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, this->mic_gain_));
// Configure mic 4
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, this->mic_gain_));
return true;
}
bool ES7210::configure_i2s_format_() {
// Configure bits per sample
uint8_t reg_val = 0;
switch (this->bits_per_sample_) {
case ES7210_BITS_PER_SAMPLE_16:
reg_val = 0x60;
break;
case ES7210_BITS_PER_SAMPLE_18:
reg_val = 0x40;
break;
case ES7210_BITS_PER_SAMPLE_20:
reg_val = 0x20;
break;
case ES7210_BITS_PER_SAMPLE_24:
reg_val = 0x00;
break;
case ES7210_BITS_PER_SAMPLE_32:
reg_val = 0x80;
break;
default:
return false;
}
ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE1_REG11, reg_val));
if (this->enable_tdm_) {
ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE2_REG12, 0x02));
} else {
// Microphones 1 and 2 output on SDOUT1, microphones 3 and 4 output on SDOUT2
ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE2_REG12, 0x00));
}
return true;
}
bool ES7210::es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data) {
uint8_t regv;
ES7210_ERROR_CHECK(this->read_byte(reg_addr, &regv));
regv = (regv & (~update_bits)) | (update_bits & data);
return this->write_byte(reg_addr, regv);
}
} // namespace es7210
} // namespace esphome

View File

@ -0,0 +1,69 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
namespace esphome {
namespace es7210 {
enum ES7210BitsPerSample : uint8_t {
ES7210_BITS_PER_SAMPLE_16 = 16,
ES7210_BITS_PER_SAMPLE_18 = 18,
ES7210_BITS_PER_SAMPLE_20 = 20,
ES7210_BITS_PER_SAMPLE_24 = 24,
ES7210_BITS_PER_SAMPLE_32 = 32,
};
enum ES7210MicGain : uint8_t {
ES7210_MIC_GAIN_0DB = 0,
ES7210_MIC_GAIN_3DB,
ES7210_MIC_GAIN_6DB,
ES7210_MIC_GAIN_9DB,
ES7210_MIC_GAIN_12DB,
ES7210_MIC_GAIN_15DB,
ES7210_MIC_GAIN_18DB,
ES7210_MIC_GAIN_21DB,
ES7210_MIC_GAIN_24DB,
ES7210_MIC_GAIN_27DB,
ES7210_MIC_GAIN_30DB,
ES7210_MIC_GAIN_33DB,
ES7210_MIC_GAIN_34_5DB,
ES7210_MIC_GAIN_36DB,
ES7210_MIC_GAIN_37_5DB,
};
class ES7210 : public Component, public i2c::I2CDevice {
/* Class for configuring an ES7210 ADC for microphone input.
* Based on code from:
* - https://github.com/espressif/esp-bsp/ (accessed 20241219)
* - https://github.com/espressif/esp-adf/ (accessed 20241219)
*/
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void dump_config() override;
void set_bits_per_sample(ES7210BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
void set_mic_gain(ES7210MicGain mic_gain) { this->mic_gain_ = mic_gain; }
void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; }
protected:
/// @brief Updates an I2C registry address by modifying the current state
/// @param reg_addr I2C register address
/// @param update_bits Mask of allowed bits to be modified
/// @param data Bit values to be written
/// @return True if successful, false otherwise
bool es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data);
bool configure_i2s_format_();
bool configure_mic_gain_();
bool configure_sample_rate_();
bool enable_tdm_{false}; // TDM is unsupported in ESPHome as of version 2024.12
ES7210MicGain mic_gain_;
ES7210BitsPerSample bits_per_sample_;
uint32_t sample_rate_;
};
} // namespace es7210
} // namespace esphome

View File

@ -0,0 +1,126 @@
#pragma once
#include "es7210.h"
namespace esphome {
namespace es7210 {
// ES7210 register addresses
static const uint8_t ES7210_RESET_REG00 = 0x00; /* Reset control */
static const uint8_t ES7210_CLOCK_OFF_REG01 = 0x01; /* Used to turn off the ADC clock */
static const uint8_t ES7210_MAINCLK_REG02 = 0x02; /* Set ADC clock frequency division */
static const uint8_t ES7210_MASTER_CLK_REG03 = 0x03; /* MCLK source $ SCLK division */
static const uint8_t ES7210_LRCK_DIVH_REG04 = 0x04; /* lrck_divh */
static const uint8_t ES7210_LRCK_DIVL_REG05 = 0x05; /* lrck_divl */
static const uint8_t ES7210_POWER_DOWN_REG06 = 0x06; /* power down */
static const uint8_t ES7210_OSR_REG07 = 0x07;
static const uint8_t ES7210_MODE_CONFIG_REG08 = 0x08; /* Set primary/secondary & channels */
static const uint8_t ES7210_TIME_CONTROL0_REG09 = 0x09; /* Set Chip intial state period*/
static const uint8_t ES7210_TIME_CONTROL1_REG0A = 0x0A; /* Set Power up state period */
static const uint8_t ES7210_SDP_INTERFACE1_REG11 = 0x11; /* Set sample & fmt */
static const uint8_t ES7210_SDP_INTERFACE2_REG12 = 0x12; /* Pins state */
static const uint8_t ES7210_ADC_AUTOMUTE_REG13 = 0x13; /* Set mute */
static const uint8_t ES7210_ADC34_MUTERANGE_REG14 = 0x14; /* Set mute range */
static const uint8_t ES7210_ADC12_MUTERANGE_REG15 = 0x15; /* Set mute range */
static const uint8_t ES7210_ADC34_HPF2_REG20 = 0x20; /* HPF */
static const uint8_t ES7210_ADC34_HPF1_REG21 = 0x21; /* HPF */
static const uint8_t ES7210_ADC12_HPF1_REG22 = 0x22; /* HPF */
static const uint8_t ES7210_ADC12_HPF2_REG23 = 0x23; /* HPF */
static const uint8_t ES7210_ANALOG_REG40 = 0x40; /* ANALOG Power */
static const uint8_t ES7210_MIC12_BIAS_REG41 = 0x41;
static const uint8_t ES7210_MIC34_BIAS_REG42 = 0x42;
static const uint8_t ES7210_MIC1_GAIN_REG43 = 0x43;
static const uint8_t ES7210_MIC2_GAIN_REG44 = 0x44;
static const uint8_t ES7210_MIC3_GAIN_REG45 = 0x45;
static const uint8_t ES7210_MIC4_GAIN_REG46 = 0x46;
static const uint8_t ES7210_MIC1_POWER_REG47 = 0x47;
static const uint8_t ES7210_MIC2_POWER_REG48 = 0x48;
static const uint8_t ES7210_MIC3_POWER_REG49 = 0x49;
static const uint8_t ES7210_MIC4_POWER_REG4A = 0x4A;
static const uint8_t ES7210_MIC12_POWER_REG4B = 0x4B; /* MICBias & ADC & PGA Power */
static const uint8_t ES7210_MIC34_POWER_REG4C = 0x4C;
/*
* Clock coefficient structer
*/
struct ES7210Coefficient {
uint32_t mclk; // mclk frequency
uint32_t lrclk;
uint8_t ss_ds;
uint8_t adc_div;
uint8_t dll; // dll_bypass
uint8_t doubler; // doubler_enable
uint8_t osr; // adc osr
uint8_t mclk_src; // sselect mclk source
uint8_t lrck_h; // High 4 bits of lrck
uint8_t lrck_l; // Low 8 bits of lrck
};
/* Codec hifi mclk clock divider coefficients
* MEMBER REG
* mclk: 0x03
* lrck: standard
* ss_ds: --
* adc_div: 0x02
* dll: 0x06
* doubler: 0x02
* osr: 0x07
* mclk_src: 0x03
* lrckh: 0x04
* lrckl: 0x05
*/
static const ES7210Coefficient ES7210_COEFFICIENTS[] = {
// mclk lrck ss_ds adc_div dll doubler osr mclk_src lrckh lrckl
/* 8k */
{12288000, 8000, 0x00, 0x03, 0x01, 0x00, 0x20, 0x00, 0x06, 0x00},
{16384000, 8000, 0x00, 0x04, 0x01, 0x00, 0x20, 0x00, 0x08, 0x00},
{19200000, 8000, 0x00, 0x1e, 0x00, 0x01, 0x28, 0x00, 0x09, 0x60},
{4096000, 8000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
/* 11.025k */
{11289600, 11025, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
/* 12k */
{12288000, 12000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
{19200000, 12000, 0x00, 0x14, 0x00, 0x01, 0x28, 0x00, 0x06, 0x40},
/* 16k */
{4096000, 16000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
{19200000, 16000, 0x00, 0x0a, 0x00, 0x00, 0x1e, 0x00, 0x04, 0x80},
{16384000, 16000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
{12288000, 16000, 0x00, 0x03, 0x01, 0x01, 0x20, 0x00, 0x03, 0x00},
/* 22.05k */
{11289600, 22050, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
/* 24k */
{12288000, 24000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
{19200000, 24000, 0x00, 0x0a, 0x00, 0x01, 0x28, 0x00, 0x03, 0x20},
/* 32k */
{12288000, 32000, 0x00, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80},
{16384000, 32000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
{19200000, 32000, 0x00, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x02, 0x58},
/* 44.1k */
{11289600, 44100, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
/* 48k */
{12288000, 48000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
{19200000, 48000, 0x00, 0x05, 0x00, 0x01, 0x28, 0x00, 0x01, 0x90},
/* 64k */
{16384000, 64000, 0x01, 0x01, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
{19200000, 64000, 0x00, 0x05, 0x00, 0x01, 0x1e, 0x00, 0x01, 0x2c},
/* 88.2k */
{11289600, 88200, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
/* 96k */
{12288000, 96000, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
{19200000, 96000, 0x01, 0x05, 0x00, 0x01, 0x28, 0x00, 0x00, 0xc8},
};
} // namespace es7210
} // namespace esphome

View File

@ -2,7 +2,7 @@ import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.audio_dac import AudioDac
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_SAMPLE_RATE
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE
CODEOWNERS = ["@kroimon", "@kahrendt"]
DEPENDENCIES = ["i2c"]
@ -10,7 +10,6 @@ DEPENDENCIES = ["i2c"]
es8311_ns = cg.esphome_ns.namespace("es8311")
ES8311 = es8311_ns.class_("ES8311", AudioDac, cg.Component, i2c.I2CDevice)
CONF_MIC_GAIN = "mic_gain"
CONF_USE_MCLK = "use_mclk"
CONF_USE_MICROPHONE = "use_microphone"

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

@ -1,4 +1,5 @@
from dataclasses import dataclass
import logging
from esphome import pins
import esphome.codegen as cg
@ -15,6 +16,9 @@ from esphome.const import (
CONF_RMT_CHANNEL,
CONF_RMT_SYMBOLS,
)
from esphome.core import CORE
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"]
@ -64,13 +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() and CONF_RMT_CHANNEL not in config:
raise cv.Invalid("rmt_channel is a required option.")
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(
{
@ -79,9 +123,9 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
cv.Optional(CONF_RMT_CHANNEL): cv.All(
cv.only_with_arduino, esp32_rmt.validate_rmt_channel(tx=True)
not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True)
),
cv.SplitDefault(
OptionalForIDF5(
CONF_RMT_SYMBOLS,
esp32_idf=64,
esp32_s2_idf=64,
@ -89,7 +133,7 @@ CONFIG_SCHEMA = cv.All(
esp32_c3_idf=48,
esp32_c6_idf=48,
esp32_h2_idf=48,
): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)),
): 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,

View File

@ -8,11 +8,13 @@ namespace event {
static const char *const TAG = "event";
void Event::trigger(const std::string &event_type) {
if (types_.find(event_type) == types_.end()) {
auto found = types_.find(event_type);
if (found == types_.end()) {
ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str());
return;
}
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), event_type.c_str());
last_event_type = &(*found);
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str());
this->event_callback_.call(event_type);
}

View File

@ -23,6 +23,8 @@ namespace event {
class Event : public EntityBase, public EntityBase_DeviceClass {
public:
const std::string *last_event_type;
void trigger(const std::string &event_type);
void set_event_types(const std::set<std::string> &event_types) { this->types_ = event_types; }
std::set<std::string> get_event_types() const { return this->types_; }

View File

@ -12,6 +12,8 @@
#include "esp_crt_bundle.h"
#endif
#include "esp_task_wdt.h"
namespace esphome {
namespace http_request {
@ -117,11 +119,11 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
return nullptr;
}
App.feed_wdt();
container->feed_wdt();
container->content_length = esp_http_client_fetch_headers(client);
App.feed_wdt();
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
App.feed_wdt();
container->feed_wdt();
if (is_success(container->status_code)) {
container->duration_ms = millis() - start;
return container;
@ -151,11 +153,11 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
return nullptr;
}
App.feed_wdt();
container->feed_wdt();
container->content_length = esp_http_client_fetch_headers(client);
App.feed_wdt();
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
App.feed_wdt();
container->feed_wdt();
if (is_success(container->status_code)) {
container->duration_ms = millis() - start;
return container;
@ -185,8 +187,9 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
return 0;
}
App.feed_wdt();
this->feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
this->feed_wdt();
this->bytes_read_ += read_len;
this->duration_ms += (millis() - start);
@ -201,6 +204,13 @@ void HttpContainerIDF::end() {
esp_http_client_cleanup(this->client_);
}
void HttpContainerIDF::feed_wdt() {
// Tests to see if the executing task has a watchdog timer attached
if (esp_task_wdt_status(nullptr) == ESP_OK) {
App.feed_wdt();
}
}
} // namespace http_request
} // namespace esphome

View File

@ -18,6 +18,9 @@ class HttpContainerIDF : public HttpContainer {
int read(uint8_t *buf, size_t max_len) override;
void end() override;
/// @brief Feeds the watchdog timer if the executing task has one attached
void feed_wdt();
protected:
esp_http_client_handle_t client_;
};

View File

@ -9,6 +9,13 @@
namespace esphome {
namespace http_request {
// The update function runs in a task only on ESP32s.
#ifdef USE_ESP32
#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task
#else
#define UPDATE_RETURN return
#endif
static const char *const TAG = "http_request.update";
static const size_t MAX_READ_SIZE = 256;
@ -29,113 +36,131 @@ void HttpRequestUpdate::setup() {
}
void HttpRequestUpdate::update() {
auto container = this->request_parent_->get(this->source_url_);
#ifdef USE_ESP32
xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
#else
this->update_task(this);
#endif
}
void HttpRequestUpdate::update_task(void *params) {
HttpRequestUpdate *this_update = (HttpRequestUpdate *) params;
auto container = this_update->request_parent_->get(this_update->source_url_);
if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
std::string msg = str_sprintf("Failed to fetch manifest from %s", this->source_url_.c_str());
this->status_set_error(msg.c_str());
return;
std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str());
this_update->status_set_error(msg.c_str());
UPDATE_RETURN;
}
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
uint8_t *data = allocator.allocate(container->content_length);
if (data == nullptr) {
std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length);
this->status_set_error(msg.c_str());
this_update->status_set_error(msg.c_str());
container->end();
return;
UPDATE_RETURN;
}
size_t read_index = 0;
while (container->get_bytes_read() < container->content_length) {
int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
App.feed_wdt();
yield();
read_index += read_bytes;
}
std::string response((char *) data, read_index);
allocator.deallocate(data, container->content_length);
bool valid = false;
{ // Ensures the response string falls out of scope and deallocates before the task ends
std::string response((char *) data, read_index);
allocator.deallocate(data, container->content_length);
container->end();
container->end();
container.reset(); // Release ownership of the container's shared_ptr
bool valid = json::parse_json(response, [this](JsonObject root) -> bool {
if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
this->update_info_.title = root["name"].as<std::string>();
this->update_info_.latest_version = root["version"].as<std::string>();
for (auto build : root["builds"].as<JsonArray>()) {
if (!build.containsKey("chipFamily")) {
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
if (build["chipFamily"] == ESPHOME_VARIANT) {
if (!build.containsKey("ota")) {
this_update->update_info_.title = root["name"].as<std::string>();
this_update->update_info_.latest_version = root["version"].as<std::string>();
for (auto build : root["builds"].as<JsonArray>()) {
if (!build.containsKey("chipFamily")) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
auto ota = build["ota"];
if (!ota.containsKey("path") || !ota.containsKey("md5")) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
if (build["chipFamily"] == ESPHOME_VARIANT) {
if (!build.containsKey("ota")) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
auto ota = build["ota"];
if (!ota.containsKey("path") || !ota.containsKey("md5")) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
this_update->update_info_.firmware_url = ota["path"].as<std::string>();
this_update->update_info_.md5 = ota["md5"].as<std::string>();
if (ota.containsKey("summary"))
this_update->update_info_.summary = ota["summary"].as<std::string>();
if (ota.containsKey("release_url"))
this_update->update_info_.release_url = ota["release_url"].as<std::string>();
return true;
}
this->update_info_.firmware_url = ota["path"].as<std::string>();
this->update_info_.md5 = ota["md5"].as<std::string>();
if (ota.containsKey("summary"))
this->update_info_.summary = ota["summary"].as<std::string>();
if (ota.containsKey("release_url"))
this->update_info_.release_url = ota["release_url"].as<std::string>();
return true;
}
}
return false;
});
return false;
});
}
if (!valid) {
std::string msg = str_sprintf("Failed to parse JSON from %s", this->source_url_.c_str());
this->status_set_error(msg.c_str());
return;
std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str());
this_update->status_set_error(msg.c_str());
UPDATE_RETURN;
}
// Merge source_url_ and this->update_info_.firmware_url
if (this->update_info_.firmware_url.find("http") == std::string::npos) {
std::string path = this->update_info_.firmware_url;
// Merge source_url_ and this_update->update_info_.firmware_url
if (this_update->update_info_.firmware_url.find("http") == std::string::npos) {
std::string path = this_update->update_info_.firmware_url;
if (path[0] == '/') {
std::string domain = this->source_url_.substr(0, this->source_url_.find('/', 8));
this->update_info_.firmware_url = domain + path;
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
this_update->update_info_.firmware_url = domain + path;
} else {
std::string domain = this->source_url_.substr(0, this->source_url_.rfind('/') + 1);
this->update_info_.firmware_url = domain + path;
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
this_update->update_info_.firmware_url = domain + path;
}
}
std::string current_version;
{ // Ensures the current version string falls out of scope and deallocates before the task ends
std::string current_version;
#ifdef ESPHOME_PROJECT_VERSION
current_version = ESPHOME_PROJECT_VERSION;
current_version = ESPHOME_PROJECT_VERSION;
#else
current_version = ESPHOME_VERSION;
current_version = ESPHOME_VERSION;
#endif
this->update_info_.current_version = current_version;
if (this->update_info_.latest_version.empty() || this->update_info_.latest_version == update_info_.current_version) {
this->state_ = update::UPDATE_STATE_NO_UPDATE;
} else {
this->state_ = update::UPDATE_STATE_AVAILABLE;
this_update->update_info_.current_version = current_version;
}
this->update_info_.has_progress = false;
this->update_info_.progress = 0.0f;
if (this_update->update_info_.latest_version.empty() ||
this_update->update_info_.latest_version == this_update->update_info_.current_version) {
this_update->state_ = update::UPDATE_STATE_NO_UPDATE;
} else {
this_update->state_ = update::UPDATE_STATE_AVAILABLE;
}
this->status_clear_error();
this->publish_state();
this_update->update_info_.has_progress = false;
this_update->update_info_.progress = 0.0f;
this_update->status_clear_error();
this_update->publish_state();
UPDATE_RETURN;
}
void HttpRequestUpdate::perform(bool force) {

View File

@ -7,6 +7,10 @@
#include "esphome/components/http_request/ota/ota_http_request.h"
#include "esphome/components/update/update_entity.h"
#ifdef USE_ESP32
#include <freertos/FreeRTOS.h>
#endif
namespace esphome {
namespace http_request {
@ -29,6 +33,11 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent {
HttpRequestComponent *request_parent_;
OtaHttpRequestComponent *ota_parent_;
std::string source_url_;
static void update_task(void *params);
#ifdef USE_ESP32
TaskHandle_t update_task_handle_{nullptr};
#endif
};
} // namespace http_request

View File

@ -1,9 +1,12 @@
import logging
from esphome import core, pins
import esphome.codegen as cg
from esphome.components import display, spi
from esphome.components.display import validate_rotation
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_COLOR_ORDER,
CONF_COLOR_PALETTE,
CONF_DC_PIN,
@ -27,17 +30,12 @@ from esphome.const import (
CONF_WIDTH,
)
from esphome.core import CORE, HexInt
from esphome.final_validate import full_config
DEPENDENCIES = ["spi"]
def AUTO_LOAD():
if CORE.is_esp32:
return ["psram"]
return []
CODEOWNERS = ["@nielsnl68", "@clydebarrow"]
LOGGER = logging.getLogger(__name__)
ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx")
ILI9XXXDisplay = ili9xxx_ns.class_(
@ -84,7 +82,7 @@ COLOR_ORDERS = {
"BGR": ColorOrder.COLOR_ORDER_BGR,
}
COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE")
COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE", "8BIT", upper=True)
CONF_LED_PIN = "led_pin"
CONF_COLOR_PALETTE_IMAGES = "color_palette_images"
@ -195,9 +193,27 @@ CONFIG_SCHEMA = cv.All(
_validate,
)
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
"ili9xxx", require_miso=False, require_mosi=True
)
def final_validate(config):
global_config = full_config.get()
# Ideally would calculate buffer size here, but that info is not available on the Python side
needs_buffer = (
CONF_LAMBDA in config or CONF_PAGES in config or config[CONF_AUTO_CLEAR_ENABLED]
)
if (
CORE.is_esp32
and config[CONF_COLOR_PALETTE] == "NONE"
and "psram" not in global_config
and needs_buffer
):
LOGGER.info("Consider enabling PSRAM if available for the display buffer")
return spi.final_validate_device_schema(
"ili9xxx", require_miso=False, require_mosi=True
)
FINAL_VALIDATE_SCHEMA = final_validate
async def to_code(config):
@ -283,6 +299,8 @@ async def to_code(config):
palette = converted.getpalette()
assert len(palette) == 256 * 3
rhs = palette
elif config[CONF_COLOR_PALETTE] == "8BIT":
cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8))
else:
cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_16))

View File

@ -66,12 +66,9 @@ void ILI9XXXDisplay::setup() {
void ILI9XXXDisplay::alloc_buffer_() {
if (this->buffer_color_mode_ == BITS_16) {
this->init_internal_(this->get_buffer_length_() * 2);
if (this->buffer_ != nullptr) {
return;
}
this->buffer_color_mode_ = BITS_8;
} else {
this->init_internal_(this->get_buffer_length_());
}
this->init_internal_(this->get_buffer_length_());
if (this->buffer_ == nullptr) {
this->mark_failed();
}

View File

@ -98,7 +98,8 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
protected:
inline bool check_buffer_() {
if (this->buffer_ == nullptr) {
this->alloc_buffer_();
if (!this->is_failed())
this->alloc_buffer_();
return !this->is_failed();
}
return true;

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,259 @@ _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
self.path = ""
def convert(self, image, path):
"""
Convert the image format
:param image: Input image
:param path: Path to the image file
: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:
"""
def is_alpha_only(image: Image):
"""
Check if an image (assumed to be RGBA) is only alpha
"""
# Any alpha data?
if image.split()[-1].getextrema()[0] == 0xFF:
return False
return all(b.getextrema()[1] == 0 for b in image.split()[:-1])
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, path):
if is_alpha_only(image):
image = image.split()[-1]
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, path):
if is_alpha_only(image):
if self.transparency != CONF_ALPHA_CHANNEL:
_LOGGER.warning(
"Grayscale image %s is alpha only, but transparency is set to %s",
path,
self.transparency,
)
self.transparency = CONF_ALPHA_CHANNEL
image = image.split()[-1]
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, path):
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, path):
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 +291,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 +303,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 +350,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 +398,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(
{
cv.Required(CONF_ID): cv.declare_id(Image_),
cv.Required(CONF_FILE): 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.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
},
validate_cross_dependencies,
)
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): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
cv.Optional(CONF_RESIZE): cv.dimensions,
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),
}
).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 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,)
),
}
)
),
)
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,
)
else:
svg_image = svg2png(file)
return Image.open(io.BytesIO(svg_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),
)
async def to_code(config):
# Local import only to allow "validate_pillow_installed" to run *before* importing it
from PIL import Image
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)
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(io.BytesIO(file_contents))
image = Image.open(path)
width, height = image.size
if resize:
image.thumbnail(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)
width, height = image.size
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)), path).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]]
)
cg.add(var.set_transparency(transparent))
image_type = get_image_type_enum(type)
trans_value = get_transparency_enum(encoder.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
)

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);
}
}
@ -22,10 +22,27 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
case IMAGE_TYPE_GRAYSCALE:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_grayscale_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
const uint32_t pos = (img_x + img_y * this->width_);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
Color color = Color(gray, gray, gray, 0xFF);
switch (this->transparency_) {
case TRANSPARENCY_CHROMA_KEY:
if (gray == 1) {
continue; // skip drawing
}
break;
case TRANSPARENCY_ALPHA_CHANNEL: {
auto on = (float) gray / 255.0f;
auto off = 1.0f - on;
// blend color_on and color_off
color = Color(color_on.r * on + color_off.r * off, color_on.g * on + color_off.g * off,
color_on.b * on + color_off.b * off, 0xFF);
break;
}
default:
break;
}
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
break;
@ -39,20 +56,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 +68,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 +105,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;
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 = 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
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
#endif
break;
}
@ -128,51 +152,81 @@ 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)
color.w = 0;
} else {
color.w = 0xFF;
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;
}
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;
return Color(gray, gray, gray, alpha);
switch (this->transparency_) {
case TRANSPARENCY_CHROMA_KEY:
if (gray == 1)
return Color(0, 0, 0, 0);
return Color(gray, gray, gray, 0xFF);
case TRANSPARENCY_ALPHA_CHANNEL:
return Color(0, 0, 0, gray);
default:
return Color(gray, gray, gray, 0xFF);
}
}
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

@ -17,11 +17,11 @@ std::string build_json(const json_build_t &f) {
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 "{}";
}
@ -29,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 "{}";
}
@ -37,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;

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

@ -15,6 +15,7 @@ from .defines import (
CONF_FREEZE,
CONF_LVGL_ID,
CONF_SHOW_SNOW,
PARTS,
literal,
)
from .lv_validation import lv_bool, lv_color, lv_image, opacity
@ -33,7 +34,7 @@ from .lvcode import (
lvgl_comp,
static_cast,
)
from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA
from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA, base_update_schema
from .types import (
LV_STATE,
LvglAction,
@ -41,6 +42,7 @@ from .types import (
ObjUpdateAction,
lv_disp_t,
lv_group_t,
lv_obj_base_t,
lv_obj_t,
lv_pseudo_button_t,
)
@ -336,3 +338,14 @@ async def widget_focus(config, action_id, template_arg, args):
lv.group_focus_freeze(group, True)
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
return var
@automation.register_action(
"lvgl.widget.update", ObjUpdateAction, base_update_schema(lv_obj_base_t, PARTS)
)
async def obj_update_to_code(config, action_id, template_arg, args):
async def do_update(widget: Widget):
await set_obj_properties(widget, config)
widgets = await get_widgets(config[CONF_ID])
return await action_to_code(widgets, do_update, action_id, template_arg, args)

View File

@ -501,9 +501,7 @@ size_t lv_millis(void) { return esphome::millis(); }
void *lv_custom_mem_alloc(size_t size) {
auto *ptr = malloc(size); // NOLINT
if (ptr == nullptr) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
#endif
ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
}
return ptr;
}
@ -520,30 +518,22 @@ void *lv_custom_mem_alloc(size_t size) {
ptr = heap_caps_malloc(size, cap_bits);
}
if (ptr == nullptr) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
#endif
ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
return nullptr;
}
#ifdef ESPHOME_LOG_HAS_VERBOSE
esphome::ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr);
#endif
ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr);
return ptr;
}
void lv_custom_mem_free(void *ptr) {
#ifdef ESPHOME_LOG_HAS_VERBOSE
esphome::ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
#endif
ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
if (ptr == nullptr)
return;
heap_caps_free(ptr);
}
void *lv_custom_mem_realloc(void *ptr, size_t size) {
#ifdef ESPHOME_LOG_HAS_VERBOSE
esphome::ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
#endif
ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
return heap_caps_realloc(ptr, size, cap_bits);
}
#endif

View File

@ -199,13 +199,12 @@ FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FL
FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of)
def part_schema(widget_type: WidgetType):
def part_schema(parts):
"""
Generate a schema for the various parts (e.g. main:, indicator:) of a widget type
:param widget_type: The type of widget to generate for
:return:
:param parts: The parts to include in the schema
:return: The schema
"""
parts = widget_type.parts
return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend(
STATE_SCHEMA
)
@ -228,9 +227,15 @@ def automation_schema(typ: LvType):
}
def create_modify_schema(widget_type):
def base_update_schema(widget_type, parts):
"""
Create a schema for updating a widgets style properties, states and flags
:param widget_type: The type of the ID
:param parts: The allowable parts to specify
:return:
"""
return (
part_schema(widget_type)
part_schema(parts)
.extend(
{
cv.Required(CONF_ID): cv.ensure_list(
@ -245,7 +250,12 @@ def create_modify_schema(widget_type):
}
)
.extend(FLAG_SCHEMA)
.extend(widget_type.modify_schema)
)
def create_modify_schema(widget_type):
return base_update_schema(widget_type.w_type, widget_type.parts).extend(
widget_type.modify_schema
)
@ -256,7 +266,7 @@ def obj_schema(widget_type: WidgetType):
:return:
"""
return (
part_schema(widget_type)
part_schema(widget_type.parts)
.extend(FLAG_SCHEMA)
.extend(LAYOUT_SCHEMA)
.extend(ALIGN_TO_SCHEMA)
@ -341,7 +351,6 @@ FLEX_OBJ_SCHEMA = {
cv.Optional(df.CONF_FLEX_GROW): cv.int_,
}
DISP_BG_SCHEMA = cv.Schema(
{
cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any(

View File

@ -37,7 +37,7 @@ DROPDOWN_BASE_SCHEMA = cv.Schema(
cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int,
cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of,
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec),
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts),
}
)

View File

@ -16,6 +16,11 @@ KEYBOARD_SCHEMA = {
cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t),
}
KEYBOARD_MODIFY_SCHEMA = {
cv.Optional(CONF_MODE): KEYBOARD_MODES.one_of,
cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t),
}
lv_keyboard_t = LvType(
"LvKeyboardType",
parents=(KeyProvider, LvCompound),
@ -32,6 +37,7 @@ class KeyboardType(WidgetType):
lv_keyboard_t,
(CONF_MAIN, CONF_ITEMS),
KEYBOARD_SCHEMA,
modify_schema=KEYBOARD_MODIFY_SCHEMA,
)
def get_uses(self):
@ -41,7 +47,8 @@ class KeyboardType(WidgetType):
lvgl_components_required.add("KEY_LISTENER")
lvgl_components_required.add(CONF_KEYBOARD)
add_lv_use("btnmatrix")
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(config[CONF_MODE]))
if mode := config.get(CONF_MODE):
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode))
if ta := await get_widgets(config, CONF_TEXTAREA):
await w.set_property(CONF_TEXTAREA, ta[0].obj)

View File

@ -51,7 +51,7 @@ MSGBOX_SCHEMA = container_schema(
cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA,
cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA,
cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA),
cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec),
cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec.parts),
cv.Optional(CONF_CLOSE_BUTTON, default=True): lv_bool,
cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr),
}

View File

@ -1,9 +1,5 @@
from esphome import automation
from ..automation import update_to_code
from ..defines import CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR
from ..schemas import create_modify_schema
from ..types import ObjUpdateAction, WidgetType, lv_obj_t
from ..types import WidgetType, lv_obj_t
class ObjType(WidgetType):
@ -21,10 +17,3 @@ class ObjType(WidgetType):
obj_spec = ObjType()
@automation.register_action(
"lvgl.widget.update", ObjUpdateAction, create_modify_schema(obj_spec)
)
async def obj_update_to_code(config, action_id, template_arg, args):
return await update_to_code(config, action_id, template_arg, args)

View File

@ -38,7 +38,7 @@ TABVIEW_SCHEMA = cv.Schema(
},
)
),
cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec),
cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec.parts),
cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of,
cv.Optional(CONF_SIZE, default="10%"): size,
}

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

View File

@ -273,7 +273,9 @@ 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) {

View File

@ -1339,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

@ -2,7 +2,7 @@ from math import log
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.components import sensor, resistance_sampler
from esphome.components import sensor
from esphome.const import (
CONF_CALIBRATION,
CONF_REFERENCE_RESISTANCE,
@ -15,8 +15,6 @@ from esphome.const import (
UNIT_CELSIUS,
)
AUTO_LOAD = ["resistance_sampler"]
ntc_ns = cg.esphome_ns.namespace("ntc")
NTC = ntc_ns.class_("NTC", cg.Component, sensor.Sensor)
@ -126,7 +124,7 @@ CONFIG_SCHEMA = (
)
.extend(
{
cv.Required(CONF_SENSOR): cv.use_id(resistance_sampler.ResistanceSampler),
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_CALIBRATION): process_calibration,
}
)

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,48 +82,54 @@ DownloadErrorTrigger = online_image_ns.class_(
"DownloadErrorTrigger", automation.Trigger.template()
)
ONLINE_IMAGE_SCHEMA = cv.Schema(
{
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.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.Optional(CONF_ON_ERROR): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger),
}
),
def remove_options(*options):
return {
cv.Optional(option): cv.invalid(
f"{option} is an invalid option for online_image"
)
for option in options
}
).extend(cv.polling_component_schema("never"))
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),
# Online Image specific options
cv.Required(CONF_URL): cv.url,
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.Optional(CONF_ON_ERROR): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger),
}
),
}
)
.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,20 +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 {
ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %zu Bytes", this->allocator_.get_max_free_block_size());
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;
}
@ -91,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_);
@ -142,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;
@ -171,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!");
@ -184,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: {
this->buffer_[pos + 0] = color.r;
this->buffer_[pos + 1] = color.g;
this->buffer_[pos + 2] = color.b;
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;
}
}
break;
}
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;
}
}

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

@ -59,6 +59,24 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
this->text_sensor_row_(stream, obj, area, node, friendly_name);
#endif
#ifdef USE_NUMBER
this->number_type_(stream);
for (auto *obj : App.get_numbers())
this->number_row_(stream, obj, area, node, friendly_name);
#endif
#ifdef USE_SELECT
this->select_type_(stream);
for (auto *obj : App.get_selects())
this->select_row_(stream, obj, area, node, friendly_name);
#endif
#ifdef USE_MEDIA_PLAYER
this->media_player_type_(stream);
for (auto *obj : App.get_media_players())
this->media_player_row_(stream, obj, area, node, friendly_name);
#endif
req->send(stream);
}
@ -511,6 +529,156 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso
}
#endif
// Type-specific implementation
#ifdef USE_NUMBER
void PrometheusHandler::number_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_number_value gauge\n"));
stream->print(F("#TYPE esphome_number_failed gauge\n"));
}
void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number *obj, std::string &area,
std::string &node, std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_)
return;
if (!std::isnan(obj->state)) {
// We have a valid value, output this value
stream->print(F("esphome_number_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n"));
// Data itself
stream->print(F("esphome_number_value{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} "));
stream->print(obj->state);
stream->print(F("\n"));
} else {
// Invalid state
stream->print(F("esphome_number_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 1\n"));
}
}
#endif
#ifdef USE_SELECT
void PrometheusHandler::select_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_select_value gauge\n"));
stream->print(F("#TYPE esphome_select_failed gauge\n"));
}
void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select *obj, std::string &area,
std::string &node, std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_)
return;
if (obj->has_state()) {
// We have a valid value, output this value
stream->print(F("esphome_select_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n"));
// Data itself
stream->print(F("esphome_select_value{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\",value=\""));
stream->print(obj->state.c_str());
stream->print(F("\"} "));
stream->print(F("1.0"));
stream->print(F("\n"));
} else {
// Invalid state
stream->print(F("esphome_select_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 1\n"));
}
}
#endif
#ifdef USE_MEDIA_PLAYER
void PrometheusHandler::media_player_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_media_player_state_value gauge\n"));
stream->print(F("#TYPE esphome_media_player_volume gauge\n"));
stream->print(F("#TYPE esphome_media_player_is_muted gauge\n"));
stream->print(F("#TYPE esphome_media_player_failed gauge\n"));
}
void PrometheusHandler::media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj,
std::string &area, std::string &node, std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_)
return;
stream->print(F("esphome_media_player_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n"));
// Data itself
stream->print(F("esphome_media_player_state_value{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\",value=\""));
stream->print(media_player::media_player_state_to_string(obj->state));
stream->print(F("\"} "));
stream->print(F("1.0"));
stream->print(F("\n"));
stream->print(F("esphome_media_player_volume{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} "));
stream->print(obj->volume);
stream->print(F("\n"));
stream->print(F("esphome_media_player_is_muted{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} "));
if (obj->is_muted()) {
stream->print(F("1.0"));
} else {
stream->print(F("0.0"));
}
stream->print(F("\n"));
}
#endif
} // namespace prometheus
} // namespace esphome
#endif

View File

@ -128,6 +128,30 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
std::string &friendly_name);
#endif
#ifdef USE_NUMBER
/// Return the type for prometheus
void number_type_(AsyncResponseStream *stream);
/// Return the sensor state as prometheus data point
void number_row_(AsyncResponseStream *stream, number::Number *obj, std::string &area, std::string &node,
std::string &friendly_name);
#endif
#ifdef USE_SELECT
/// Return the type for prometheus
void select_type_(AsyncResponseStream *stream);
/// Return the select state as prometheus data point
void select_row_(AsyncResponseStream *stream, select::Select *obj, std::string &area, std::string &node,
std::string &friendly_name);
#endif
#ifdef USE_MEDIA_PLAYER
/// Return the type for prometheus
void media_player_type_(AsyncResponseStream *stream);
/// Return the select state as prometheus data point
void media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj, std::string &area,
std::string &node, std::string &friendly_name);
#endif
web_server_base::WebServerBase *base_;
bool include_internal_{false};
std::map<EntityBase *, std::string> relabel_map_id_;

View File

@ -21,7 +21,14 @@ void PsramComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available));
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0)
if (available) {
ESP_LOGCONFIG(TAG, " Size: %d KB", heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024);
const size_t psram_total_size_bytes = heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
const float psram_total_size_kb = psram_total_size_bytes / 1024.0f;
if (abs(std::round(psram_total_size_kb) - psram_total_size_kb) < 0.05f) {
ESP_LOGCONFIG(TAG, " Size: %.0f KB", psram_total_size_kb);
} else {
ESP_LOGCONFIG(TAG, " Size: %zu bytes", psram_total_size_bytes);
}
}
#endif
}

View File

@ -1,8 +1,7 @@
#pragma once
#include "esphome/components/resistance_sampler/resistance_sampler.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace resistance {
@ -12,7 +11,7 @@ enum ResistanceConfiguration {
DOWNSTREAM,
};
class ResistanceSensor : public Component, public sensor::Sensor, resistance_sampler::ResistanceSampler {
class ResistanceSensor : public Component, public sensor::Sensor {
public:
void set_sensor(Sensor *sensor) { sensor_ = sensor; }
void set_configuration(ResistanceConfiguration configuration) { configuration_ = configuration; }

View File

@ -1,6 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, resistance_sampler
from esphome.components import sensor
from esphome.const import (
CONF_REFERENCE_VOLTAGE,
CONF_SENSOR,
@ -9,15 +9,8 @@ from esphome.const import (
ICON_FLASH,
)
AUTO_LOAD = ["resistance_sampler"]
resistance_ns = cg.esphome_ns.namespace("resistance")
ResistanceSensor = resistance_ns.class_(
"ResistanceSensor",
cg.Component,
sensor.Sensor,
resistance_sampler.ResistanceSampler,
)
ResistanceSensor = resistance_ns.class_("ResistanceSensor", cg.Component, sensor.Sensor)
CONF_CONFIGURATION = "configuration"
CONF_RESISTOR = "resistor"

View File

@ -1,6 +0,0 @@
import esphome.codegen as cg
resistance_sampler_ns = cg.esphome_ns.namespace("resistance_sampler")
ResistanceSampler = resistance_sampler_ns.class_("ResistanceSampler")
CODEOWNERS = ["@jesserockz"]

View File

@ -1,10 +0,0 @@
#pragma once
namespace esphome {
namespace resistance_sampler {
/// Abstract interface to mark components that provide resistance values.
class ResistanceSampler {};
} // namespace resistance_sampler
} // namespace esphome

View File

@ -7,6 +7,10 @@ namespace spi {
const char *const TAG = "spi";
SPIDelegate *const SPIDelegate::NULL_DELEGATE = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
new SPIDelegateDummy();
// https://bugs.llvm.org/show_bug.cgi?id=48040
bool SPIDelegate::is_ready() { return true; }
GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@ -75,6 +79,8 @@ void SPIComponent::dump_config() {
}
}
void SPIDelegateDummy::begin_transaction() { ESP_LOGE(TAG, "SPIDevice not initialised - did you call spi_setup()?"); }
uint8_t SPIDelegateBitBash::transfer(uint8_t data) { return this->transfer_(data, 8); }
void SPIDelegateBitBash::write(uint16_t data, size_t num_bits) { this->transfer_(data, num_bits); }

View File

@ -163,6 +163,8 @@ class Utility {
}
};
class SPIDelegateDummy;
// represents a device attached to an SPI bus, with a defined clock rate, mode and bit order. On Arduino this is
// a thin wrapper over SPIClass.
class SPIDelegate {
@ -248,6 +250,21 @@ class SPIDelegate {
uint32_t data_rate_{1000000};
SPIMode mode_{MODE0};
GPIOPin *cs_pin_{NullPin::NULL_PIN};
static SPIDelegate *const NULL_DELEGATE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
};
/**
* A dummy SPIDelegate that complains if it's used.
*/
class SPIDelegateDummy : public SPIDelegate {
public:
SPIDelegateDummy() = default;
uint8_t transfer(uint8_t data) override { return 0; }
void end_transaction() override{};
void begin_transaction() override;
};
/**
@ -365,7 +382,7 @@ class SPIClient {
virtual void spi_teardown() {
this->parent_->unregister_device(this);
this->delegate_ = nullptr;
this->delegate_ = SPIDelegate::NULL_DELEGATE;
}
bool spi_is_ready() { return this->delegate_->is_ready(); }
@ -376,7 +393,7 @@ class SPIClient {
uint32_t data_rate_{1000000};
SPIComponent *parent_{nullptr};
GPIOPin *cs_{nullptr};
SPIDelegate *delegate_{nullptr};
SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE};
};
/**

View File

@ -1,14 +1,14 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, time
import esphome.config_validation as cv
from esphome.const import (
CONF_TIME_ID,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_TIMESTAMP,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_TIMER,
STATE_CLASS_TOTAL_INCREASING,
UNIT_SECOND,
ICON_TIMER,
DEVICE_CLASS_DURATION,
)
uptime_ns = cg.esphome_ns.namespace("uptime")

View File

@ -0,0 +1,19 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC, ICON_TIMER
uptime_ns = cg.esphome_ns.namespace("uptime")
UptimeTextSensor = uptime_ns.class_(
"UptimeTextSensor", text_sensor.TextSensor, cg.PollingComponent
)
CONFIG_SCHEMA = text_sensor.text_sensor_schema(
UptimeTextSensor,
icon=ICON_TIMER,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("60s"))
async def to_code(config):
var = await text_sensor.new_text_sensor(config)
await cg.register_component(var, config)

View File

@ -0,0 +1,46 @@
#include "uptime_text_sensor.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace uptime {
static const char *const TAG = "uptime.sensor";
void UptimeTextSensor::setup() { this->last_ms_ = millis(); }
void UptimeTextSensor::update() {
const auto now = millis();
// get whole seconds since last update. Note that even if the millis count has overflowed between updates,
// the difference will still be correct due to the way twos-complement arithmetic works.
const uint32_t delta = (now - this->last_ms_) / 1000;
if (delta == 0)
return;
// set last_ms_ to the last second boundary
this->last_ms_ = now - (now % 1000);
this->uptime_ += delta;
auto uptime = this->uptime_;
unsigned days = uptime / (24 * 3600);
unsigned seconds = uptime % (24 * 3600);
unsigned hours = seconds / 3600;
seconds %= 3600;
unsigned minutes = seconds / 60;
seconds %= 60;
if (days != 0) {
this->publish_state(str_sprintf("%dd%dh%dm%ds", days, hours, minutes, seconds));
} else if (hours != 0) {
this->publish_state(str_sprintf("%dh%dm%ds", hours, minutes, seconds));
} else if (minutes != 0) {
this->publish_state(str_sprintf("%dm%ds", minutes, seconds));
} else {
this->publish_state(str_sprintf("%ds", seconds));
}
}
float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
void UptimeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Uptime Text Sensor", this); }
} // namespace uptime
} // namespace esphome

View File

@ -0,0 +1,25 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace uptime {
class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent {
public:
void update() override;
void dump_config() override;
void setup() override;
float get_setup_priority() const override;
protected:
uint64_t uptime_{0};
uint64_t last_ms_{0};
};
} // namespace uptime
} // namespace esphome

View File

@ -2425,28 +2425,21 @@ void WaveshareEPaper7P5InBV3BWR::init_display_() {
this->command(0x01);
// 1-0=11: internal power
this->data(0x07);
this->data(0x17); // VGH&VGL
this->data(0x3F); // VSH
this->data(0x26); // VSL
this->data(0x11); // VSHR
this->data(0x07); // VRS_EN=1, VS_EN=1, VG_EN=1
this->data(0x17); // VGH&VGL ??? VCOM_SLEW=1 but this is fixed, VG_LVL[2:0]=111 => VGH=20V VGL=-20V, it could be 0x07
this->data(0x3F); // VSH=15V?
this->data(0x26); // VSL=-9.4V?
this->data(0x11); // VSHR=5.8V?
// VCOM DC Setting
this->command(0x82);
this->data(0x24); // VCOM
// Booster Setting
this->command(0x06);
this->data(0x27);
this->data(0x27);
this->data(0x2F);
this->data(0x17);
this->data(0x24); // VCOM=-1.9V
// POWER ON
this->command(0x04);
delay(100); // NOLINT
this->wait_until_idle_();
// COMMAND PANEL SETTING
this->command(0x00);
this->data(0x0F); // KW-3f KWR-2F BWROTP 0f BWOTP 1f
@ -2457,16 +2450,16 @@ void WaveshareEPaper7P5InBV3BWR::init_display_() {
this->data(0x20);
this->data(0x01); // gate 480
this->data(0xE0);
// COMMAND ...?
this->command(0x15);
this->data(0x00);
// COMMAND VCOM AND DATA INTERVAL SETTING
this->command(0x50);
this->data(0x20);
this->data(0x00);
// COMMAND TCON SETTING
this->command(0x60);
this->data(0x22);
// Resolution setting
this->command(0x65);
this->data(0x00);

View File

@ -455,8 +455,9 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
} else if (match.method == "toggle") {
this->schedule_([obj]() { obj->toggle().perform(); });
request->send(200);
} else if (match.method == "turn_on") {
auto call = obj->turn_on();
} else if (match.method == "turn_on" || match.method == "turn_off") {
auto call = match.method == "turn_on" ? obj->turn_on() : obj->turn_off();
if (request->hasParam("speed_level")) {
auto speed_level = request->getParam("speed_level")->value();
auto val = parse_number<int>(speed_level.c_str());
@ -486,9 +487,6 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
}
this->schedule_([call]() mutable { call.perform(); });
request->send(200);
} else if (match.method == "turn_off") {
this->schedule_([obj]() { obj->turn_off().perform(); });
request->send(200);
} else {
request->send(404);
}
@ -1415,6 +1413,30 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
request->send(200, "application/json", data.c_str());
return;
}
auto call = obj->make_call();
if (request->hasParam("code")) {
call.set_code(request->getParam("code")->value().c_str());
}
if (match.method == "disarm") {
call.disarm();
} else if (match.method == "arm_away") {
call.arm_away();
} else if (match.method == "arm_home") {
call.arm_home();
} else if (match.method == "arm_night") {
call.arm_night();
} else if (match.method == "arm_vacation") {
call.arm_vacation();
} else {
request->send(404);
return;
}
this->schedule_([call]() mutable { call.perform(); });
request->send(200);
return;
}
request->send(404);
}
@ -1664,7 +1686,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
#endif
#ifdef USE_ALARM_CONTROL_PANEL
if (request->method() == HTTP_GET && match.domain == "alarm_control_panel")
if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain == "alarm_control_panel")
return true;
#endif

View File

@ -11,10 +11,19 @@
#ifdef USE_WIFI_WPA2_EAP
#include <esp_wpa2.h>
#endif
#ifdef USE_WIFI_AP
#include "dhcpserver/dhcpserver.h"
#endif // USE_WIFI_AP
#include "lwip/apps/sntp.h"
#include "lwip/dns.h"
#include "lwip/err.h"
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
#include "lwip/priv/tcpip_priv.h"
#endif
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
@ -286,11 +295,26 @@ bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
}
if (!manual_ip.has_value()) {
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
// https://github.com/esphome/issues/issues/6591
// https://github.com/espressif/arduino-esp32/issues/10526
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
LOCK_TCPIP_CORE();
}
#endif
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
// https://github.com/esphome/issues/issues/2299
sntp_servermode_dhcp(false);
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
UNLOCK_TCPIP_CORE();
}
#endif
// No manual IP is set; use DHCP client
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
err = esp_netif_dhcpc_start(s_sta_netif);
@ -638,7 +662,12 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
}
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
auto status = WiFiClass::status();
#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
const auto status = WiFiClass::status();
#else
const auto status = WiFi.status();
#endif
if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) {
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
}

View File

@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2025.1.0-dev"
__version__ = "2025.2.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
@ -490,6 +490,7 @@ CONF_MEMORY_BLOCKS = "memory_blocks"
CONF_MESSAGE = "message"
CONF_METHANE = "methane"
CONF_METHOD = "method"
CONF_MIC_GAIN = "mic_gain"
CONF_MICROPHONE = "microphone"
CONF_MIN_BRIGHTNESS = "min_brightness"
CONF_MIN_COOLING_OFF_TIME = "min_cooling_off_time"

View File

@ -2,6 +2,7 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/defines.h"
#include "esphome/core/preferences.h"

View File

@ -45,7 +45,9 @@
#endif
#ifdef USE_ESP32
#include "esp32/rom/crc.h"
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 2)
#include "esp_mac.h"
#endif
#include "esp_efuse.h"
#include "esp_efuse_table.h"
#endif
@ -261,7 +263,7 @@ bool random_bytes(uint8_t *data, size_t len) {
bool str_equals_case_insensitive(const std::string &a, const std::string &b) {
return strcasecmp(a.c_str(), b.c_str()) == 0;
}
#if ESP_IDF_VERSION_MAJOR >= 5
#if __cplusplus >= 202002L
bool str_startswith(const std::string &str, const std::string &start) { return str.starts_with(start); }
bool str_endswith(const std::string &str, const std::string &end) { return str.ends_with(end); }
#else

View File

@ -163,7 +163,7 @@ template<typename T, typename U> T remap(U value, U min, U max, T min_out, T max
return (value - min) * (max_out - min_out) / (max - min) + min_out;
}
/// Calculate a CRC-8 checksum of \p data with size \p len.
/// Calculate a CRC-8 checksum of \p data with size \p len using the CRC-8-Dallas/Maxim polynomial.
uint8_t crc8(const uint8_t *data, uint8_t len);
/// Calculate a CRC-16 checksum of \p data with size \p len.

View File

@ -74,7 +74,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define esph_log_vv(tag, format, ...) \
esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
#define ESPHOME_LOG_HAS_VERY_VERBOSE
#else
@ -83,7 +83,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
#define esph_log_v(tag, format, ...) \
esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
#define ESPHOME_LOG_HAS_VERBOSE
#else
@ -92,9 +92,9 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
#define esph_log_d(tag, format, ...) \
esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
#define esph_log_config(tag, format, ...) \
esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
#define ESPHOME_LOG_HAS_DEBUG
#define ESPHOME_LOG_HAS_CONFIG
@ -105,7 +105,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
#define esph_log_i(tag, format, ...) \
esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
#define ESPHOME_LOG_HAS_INFO
#else
@ -114,7 +114,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
#define esph_log_w(tag, format, ...) \
esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
#define ESPHOME_LOG_HAS_WARN
#else
@ -123,7 +123,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
#define esph_log_e(tag, format, ...) \
esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__)
#define ESPHOME_LOG_HAS_ERROR
#else

View File

@ -58,7 +58,19 @@ file_types = (
)
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
py_include = ("*.py",)
ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf", ".pcf")
ignore_types = (
".ico",
".png",
".woff",
".woff2",
"",
".ttf",
".otf",
".pcf",
".apng",
".gif",
".webp",
)
LINT_FILE_CHECKS = []
LINT_CONTENT_CHECKS = []
@ -669,8 +681,7 @@ def main():
)
args = parser.parse_args()
global EXECUTABLE_BIT
EXECUTABLE_BIT = git_ls_files()
EXECUTABLE_BIT.update(git_ls_files())
files = list(EXECUTABLE_BIT.keys())
# Match against re
file_name_re = re.compile("|".join(args.files))

53
script/run-in-env Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3
import os
from pathlib import Path
import subprocess
import sys
def find_and_activate_virtualenv():
try:
# Get the top-level directory of the git repository
my_path = subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], text=True
).strip()
except subprocess.CalledProcessError:
print(
"Error: Not a git repository or unable to determine the top-level directory.",
file=sys.stderr,
)
sys.exit(1)
# Check for virtual environments
for venv in ["venv", ".venv", "."]:
activate_path = (
Path(my_path)
/ venv
/ ("Scripts" if os.name == "nt" else "bin")
/ "activate"
)
if activate_path.exists():
# Activate the virtual environment by updating PATH
env = os.environ.copy()
venv_bin_dir = activate_path.parent
env["PATH"] = f"{venv_bin_dir}{os.pathsep}{env['PATH']}"
env["VIRTUAL_ENV"] = str(venv_bin_dir.parent)
print(f"Activated virtual environment: {venv_bin_dir.parent}")
# Execute the remaining arguments in the new environment
if len(sys.argv) > 1:
subprocess.run(sys.argv[1:], env=env, check=False)
else:
print(
"No command provided to run in the virtual environment.",
file=sys.stderr,
)
return
print("No virtual environment found.", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
find_and_activate_virtualenv()

View File

@ -1,13 +0,0 @@
#!/usr/bin/env sh
set -eu
my_path=$(git rev-parse --show-toplevel)
for venv in venv .venv .; do
if [ -f "${my_path}/${venv}/bin/activate" ]; then
. "${my_path}/${venv}/bin/activate"
break
fi
done
exec "$@"

View File

@ -0,0 +1,4 @@
*.apng -text
*.webp -text
*.gif -text

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -0,0 +1,23 @@
animation:
- id: rgb565_animation
file: $component_dir/anim.gif
type: RGB565
use_transparency: opaque
resize: 50x50
- id: rgb_animation
file: $component_dir/anim.apng
type: RGB
use_transparency: chroma_key
resize: 50x50
- id: grayscale_animation
file: $component_dir/anim.apng
type: grayscale
display:
lambda: |-
id(rgb565_animation).next_frame();
id(rgb_animation1).next_frame();
id(grayscale_animation2).next_frame();
it.image(0, 0, rgb565_animation);
it.image(120, 0, rgb_animation1);
it.image(240, 0, grayscale_animation2);

View File

@ -13,12 +13,6 @@ display:
reset_pin: 21
invert_colors: false
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
packages:
animation: !include common.yaml
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false

View File

@ -13,12 +13,5 @@ display:
reset_pin: 10
invert_colors: false
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false
packages:
animation: !include common.yaml

View File

@ -13,12 +13,5 @@ display:
reset_pin: 10
invert_colors: false
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false
packages:
animation: !include common.yaml

View File

@ -13,12 +13,5 @@ display:
reset_pin: 21
invert_colors: false
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false
packages:
animation: !include common.yaml

View File

@ -13,12 +13,5 @@ display:
reset_pin: 16
invert_colors: false
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false
packages:
animation: !include common.yaml

View File

@ -13,12 +13,5 @@ display:
reset_pin: 22
invert_colors: false
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false
packages:
animation: !include common.yaml

View File

@ -0,0 +1 @@
<<: !include common.yaml

View File

@ -0,0 +1 @@
<<: !include common.yaml

View File

@ -0,0 +1 @@
<<: !include common.yaml

View File

@ -0,0 +1 @@
<<: !include common.yaml

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