1
0
mirror of https://github.com/esphome/esphome.git synced 2025-01-19 04:20:56 +00:00

Merge branch 'dev' into v1530l0x-timing-budget

This commit is contained in:
lastradanet 2025-01-17 22:28:49 -05:00 committed by GitHub
commit 44d68a0527
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
184 changed files with 2869 additions and 1320 deletions

View File

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

View File

@ -48,7 +48,7 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.8.0 uses: docker/setup-buildx-action@v3.8.0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0 uses: docker/setup-qemu-action@v3.3.0
- name: Set TAG - name: Set TAG
run: | run: |

View File

@ -93,7 +93,7 @@ jobs:
uses: docker/setup-buildx-action@v3.8.0 uses: docker/setup-buildx-action@v3.8.0
- name: Set up QEMU - name: Set up QEMU
if: matrix.platform != 'linux/amd64' 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 - name: Log in to docker hub
uses: docker/login-action@v3.3.0 uses: docker/login-action@v3.3.0
@ -141,7 +141,7 @@ jobs:
echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
- name: Upload digests - name: Upload digests
uses: actions/upload-artifact@v4.5.0 uses: actions/upload-artifact@v4.6.0
with: with:
name: digests-${{ steps.sanitize.outputs.name }} name: digests-${{ steps.sanitize.outputs.name }}
path: /tmp/digests path: /tmp/digests

View File

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

View File

@ -11,14 +11,6 @@ repos:
args: [--fix] args: [--fix]
# Run the formatter. # Run the formatter.
- id: ruff-format - 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 - repo: https://github.com/PyCQA/flake8
rev: 6.1.0 rev: 6.1.0
hooks: hooks:
@ -53,6 +45,6 @@ repos:
hooks: hooks:
- id: pylint - id: pylint
name: pylint name: pylint
entry: script/run-in-env.sh pylint entry: python script/run-in-env pylint
language: script language: system
types: [python] types: [python]

View File

@ -131,6 +131,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
esphome/components/ens160_i2c/* @latonita esphome/components/ens160_i2c/* @latonita
esphome/components/ens160_spi/* @latonita esphome/components/ens160_spi/* @latonita
esphome/components/ens210/* @itn3rd77 esphome/components/ens210/* @itn3rd77
esphome/components/es7210/* @kahrendt
esphome/components/es8311/* @kahrendt @kroimon esphome/components/es8311/* @kahrendt @kroimon
esphome/components/esp32/* @esphome/core esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @Rapsssito @jesserockz esphome/components/esp32_ble/* @Rapsssito @jesserockz
@ -302,7 +303,7 @@ esphome/components/noblex/* @AGalfra
esphome/components/npi19/* @bakerkj esphome/components/npi19/* @bakerkj
esphome/components/number/* @esphome/core esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @guillempages esphome/components/online_image/* @clydebarrow @guillempages
esphome/components/opentherm/* @olegtarasov esphome/components/opentherm/* @olegtarasov
esphome/components/ota/* @esphome/core esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core esphome/components/output/* @esphome/core
@ -338,7 +339,6 @@ esphome/components/radon_eye_rd200/* @jeffeb3
esphome/components/rc522/* @glmnet esphome/components/rc522/* @glmnet
esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_i2c/* @glmnet
esphome/components/rc522_spi/* @glmnet esphome/components/rc522_spi/* @glmnet
esphome/components/resistance_sampler/* @jesserockz
esphome/components/restart/* @esphome/core esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @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 [![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/ **Documentation:** https://esphome.io/

View File

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

View File

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

View File

@ -1,28 +1,10 @@
import logging import logging
from esphome import automation, core from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.components.image as espImage 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 import esphome.config_validation as cv
from esphome.const import ( from esphome.const import CONF_ID, CONF_REPEAT
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
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,6 +12,7 @@ AUTO_LOAD = ["image"]
CODEOWNERS = ["@syndlex"] CODEOWNERS = ["@syndlex"]
DEPENDENCIES = ["display"] DEPENDENCIES = ["display"]
MULTI_CONF = True MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
CONF_LOOP = "loop" CONF_LOOP = "loop"
CONF_START_FRAME = "start_frame" CONF_START_FRAME = "start_frame"
@ -51,86 +34,19 @@ SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
) )
TYPED_FILE_SCHEMA = cv.typed_schema( CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
{ {
SOURCE_LOCAL: LOCAL_SCHEMA, cv.Required(CONF_ID): cv.declare_id(Animation_),
SOURCE_WEB: WEB_SCHEMA, cv.Optional(CONF_LOOP): cv.All(
},
key=CONF_SOURCE,
)
def _file_schema(value):
if isinstance(value, str):
return validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(value)
FILE_SCHEMA = cv.Schema(_file_schema)
def validate_file_shorthand(value):
value = cv.string_strict(value)
if value.startswith("http://") or value.startswith("https://"):
return FILE_SCHEMA(
{ {
CONF_SOURCE: SOURCE_WEB, cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
CONF_URL: value, 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( 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): 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( var = cg.new_Pvariable(
config[CONF_ID], config[CONF_ID],
prog_arr, prog_arr,
width, width,
height, height,
frames, frame_count,
espImage.IMAGE_TYPE[config[CONF_TYPE]], image_type,
trans_value,
) )
cg.add(var.set_transparency(transparent))
if loop_config := config.get(CONF_LOOP): if loop_config := config.get(CONF_LOOP):
start = loop_config[CONF_START_FRAME] 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) count = loop_config.get(CONF_REPEAT, -1)
cg.add(var.set_loop(start, end, count)) cg.add(var.set_loop(start, end, count))

View File

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

View File

@ -8,7 +8,8 @@ namespace animation {
class Animation : public image::Image { class Animation : public image::Image {
public: 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; uint32_t get_animation_frame_count() const;
int get_current_frame() const; int get_current_frame() const;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -298,6 +298,12 @@ void DalyBmsComponent::decode_data_(std::vector<uint8_t> data) {
if (this->cell_16_voltage_sensor_) { if (this->cell_16_voltage_sensor_) {
this->cell_16_voltage_sensor_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); this->cell_16_voltage_sensor_->publish_state((float) encode_uint16(it[5], it[6]) / 1000);
} }
if (this->cell_17_voltage_sensor_) {
this->cell_17_voltage_sensor_->publish_state((float) encode_uint16(it[7], it[8]) / 1000);
}
if (this->cell_18_voltage_sensor_) {
this->cell_18_voltage_sensor_->publish_state((float) encode_uint16(it[9], it[10]) / 1000);
}
break; break;
} }
break; break;

View File

@ -54,6 +54,8 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice {
SUB_SENSOR(cell_14_voltage) SUB_SENSOR(cell_14_voltage)
SUB_SENSOR(cell_15_voltage) SUB_SENSOR(cell_15_voltage)
SUB_SENSOR(cell_16_voltage) SUB_SENSOR(cell_16_voltage)
SUB_SENSOR(cell_17_voltage)
SUB_SENSOR(cell_18_voltage)
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR

View File

@ -52,6 +52,8 @@ CONF_CELL_13_VOLTAGE = "cell_13_voltage"
CONF_CELL_14_VOLTAGE = "cell_14_voltage" CONF_CELL_14_VOLTAGE = "cell_14_voltage"
CONF_CELL_15_VOLTAGE = "cell_15_voltage" CONF_CELL_15_VOLTAGE = "cell_15_voltage"
CONF_CELL_16_VOLTAGE = "cell_16_voltage" CONF_CELL_16_VOLTAGE = "cell_16_voltage"
CONF_CELL_17_VOLTAGE = "cell_17_voltage"
CONF_CELL_18_VOLTAGE = "cell_18_voltage"
ICON_CURRENT_DC = "mdi:current-dc" ICON_CURRENT_DC = "mdi:current-dc"
ICON_BATTERY_OUTLINE = "mdi:battery-outline" ICON_BATTERY_OUTLINE = "mdi:battery-outline"
ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up" ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up"
@ -92,6 +94,8 @@ TYPES = [
CONF_CELL_14_VOLTAGE, CONF_CELL_14_VOLTAGE,
CONF_CELL_15_VOLTAGE, CONF_CELL_15_VOLTAGE,
CONF_CELL_16_VOLTAGE, CONF_CELL_16_VOLTAGE,
CONF_CELL_17_VOLTAGE,
CONF_CELL_18_VOLTAGE,
] ]
CELL_VOLTAGE_SCHEMA = sensor.sensor_schema( CELL_VOLTAGE_SCHEMA = sensor.sensor_schema(
@ -212,6 +216,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_CELL_14_VOLTAGE): CELL_VOLTAGE_SCHEMA, cv.Optional(CONF_CELL_14_VOLTAGE): CELL_VOLTAGE_SCHEMA,
cv.Optional(CONF_CELL_15_VOLTAGE): CELL_VOLTAGE_SCHEMA, cv.Optional(CONF_CELL_15_VOLTAGE): CELL_VOLTAGE_SCHEMA,
cv.Optional(CONF_CELL_16_VOLTAGE): CELL_VOLTAGE_SCHEMA, cv.Optional(CONF_CELL_16_VOLTAGE): CELL_VOLTAGE_SCHEMA,
cv.Optional(CONF_CELL_17_VOLTAGE): CELL_VOLTAGE_SCHEMA,
cv.Optional(CONF_CELL_18_VOLTAGE): CELL_VOLTAGE_SCHEMA,
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
) )

View File

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

View File

@ -55,6 +55,20 @@ class DebugComponent : public PollingComponent {
#endif // USE_ESP32 #endif // USE_ESP32
#endif // USE_SENSOR #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 #ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *device_info_{nullptr}; text_sensor::TextSensor *device_info_{nullptr};
text_sensor::TextSensor *reset_reason_{nullptr}; text_sensor::TextSensor *reset_reason_{nullptr};

View File

@ -5,6 +5,7 @@
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
#include <esp_system.h> #include <esp_system.h>
#include <esp_chip_info.h> #include <esp_chip_info.h>
#include <esp_partition.h>
#if defined(USE_ESP32_VARIANT_ESP32) #if defined(USE_ESP32_VARIANT_ESP32)
#include <esp32/rom/rtc.h> #include <esp32/rom/rtc.h>
@ -28,6 +29,19 @@ namespace debug {
static const char *const TAG = "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 DebugComponent::get_reset_reason_() {
std::string reset_reason; std::string reset_reason;
switch (esp_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 += " Cores:" + to_string(info.cores);
device_info += " Revision:" + to_string(info.revision); 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()); ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version());
device_info += "|ESP-IDF: "; device_info += "|ESP-IDF: ";
device_info += esp_get_idf_version(); device_info += esp_get_idf_version();

View File

@ -159,6 +159,15 @@ void DFPlayer::loop() {
} }
break; break;
case 9: // End byte 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) { if (byte != 0xEF) {
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte); ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
this->read_pos_ = 0; this->read_pos_ = 0;
@ -238,13 +247,17 @@ void DFPlayer::loop() {
this->ack_set_is_playing_ = false; this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false; this->ack_reset_is_playing_ = false;
break; break;
case 0x3C:
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
case 0x3D: case 0x3D:
ESP_LOGV(TAG, "Playback finished"); ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false; this->is_playing_ = false;
this->on_finished_playback_callback_.call(); this->on_finished_playback_callback_.call();
break; break;
default: 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->sent_cmd_ = 0;
this->read_pos_ = 0; this->read_pos_ = 0;

View File

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

View File

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

View File

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

View File

@ -266,8 +266,9 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
if (dymax < float(-dxmax) * tan_a) { if (dymax < float(-dxmax) * tan_a) {
upd_dxmax = ceil(float(dymax) / tan_a); upd_dxmax = ceil(float(dymax) / tan_a);
hline_width = -dxmax - upd_dxmax + 1; hline_width = -dxmax - upd_dxmax + 1;
} else } else {
hline_width = 0; hline_width = 0;
}
} }
if (hline_width > 0) if (hline_width > 0)
this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color); this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color);

View File

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

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 import i2c
from esphome.components.audio_dac import AudioDac from esphome.components.audio_dac import AudioDac
import esphome.config_validation as cv 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"] CODEOWNERS = ["@kroimon", "@kahrendt"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@ -10,7 +10,6 @@ DEPENDENCIES = ["i2c"]
es8311_ns = cg.esphome_ns.namespace("es8311") es8311_ns = cg.esphome_ns.namespace("es8311")
ES8311 = es8311_ns.class_("ES8311", AudioDac, cg.Component, i2c.I2CDevice) ES8311 = es8311_ns.class_("ES8311", AudioDac, cg.Component, i2c.I2CDevice)
CONF_MIC_GAIN = "mic_gain"
CONF_USE_MCLK = "use_mclk" CONF_USE_MCLK = "use_mclk"
CONF_USE_MICROPHONE = "use_microphone" 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 #else
uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); } uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
#endif #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 #ifdef USE_ESP_IDF
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,6 +33,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
this->mark_failed(); this->mark_failed();
return; return;
} }
memset(this->buf_, 0, buffer_size);
this->effect_data_ = allocator.allocate(this->num_leds_); this->effect_data_ = allocator.allocate(this->num_leds_);
if (this->effect_data_ == nullptr) { if (this->effect_data_ == nullptr) {

View File

@ -1,4 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
import logging
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
@ -15,6 +16,9 @@ from esphome.const import (
CONF_RMT_CHANNEL, CONF_RMT_CHANNEL,
CONF_RMT_SYMBOLS, CONF_RMT_SYMBOLS,
) )
from esphome.core import CORE
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
@ -64,13 +68,53 @@ CONF_RESET_HIGH = "reset_high"
CONF_RESET_LOW = "reset_low" 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): def final_validation(config):
if not esp32_rmt.use_new_rmt_driver() and CONF_RMT_CHANNEL not in config: if not esp32_rmt.use_new_rmt_driver():
raise cv.Invalid("rmt_channel is a required option.") 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 FINAL_VALIDATE_SCHEMA = final_validation
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
light.ADDRESSABLE_LIGHT_SCHEMA.extend( 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_NUM_LEDS): cv.positive_not_null_int,
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
cv.Optional(CONF_RMT_CHANNEL): cv.All( 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, CONF_RMT_SYMBOLS,
esp32_idf=64, esp32_idf=64,
esp32_s2_idf=64, esp32_s2_idf=64,
@ -89,7 +133,7 @@ CONFIG_SCHEMA = cv.All(
esp32_c3_idf=48, esp32_c3_idf=48,
esp32_c6_idf=48, esp32_c6_idf=48,
esp32_h2_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_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,

View File

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

View File

@ -8,11 +8,13 @@ namespace event {
static const char *const TAG = "event"; static const char *const TAG = "event";
void Event::trigger(const std::string &event_type) { 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()); ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str());
return; 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); this->event_callback_.call(event_type);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,13 @@
namespace esphome { namespace esphome {
namespace http_request { 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 char *const TAG = "http_request.update";
static const size_t MAX_READ_SIZE = 256; static const size_t MAX_READ_SIZE = 256;
@ -29,113 +36,131 @@ void HttpRequestUpdate::setup() {
} }
void HttpRequestUpdate::update() { 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) { 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()); std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str());
this->status_set_error(msg.c_str()); this_update->status_set_error(msg.c_str());
return; UPDATE_RETURN;
} }
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
uint8_t *data = allocator.allocate(container->content_length); uint8_t *data = allocator.allocate(container->content_length);
if (data == nullptr) { if (data == nullptr) {
std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length); 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(); container->end();
return; UPDATE_RETURN;
} }
size_t read_index = 0; size_t read_index = 0;
while (container->get_bytes_read() < container->content_length) { while (container->get_bytes_read() < container->content_length) {
int read_bytes = container->read(data + read_index, MAX_READ_SIZE); int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
App.feed_wdt();
yield(); yield();
read_index += read_bytes; read_index += read_bytes;
} }
std::string response((char *) data, read_index); bool valid = false;
allocator.deallocate(data, container->content_length); { // 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 { valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { 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")) {
ESP_LOGE(TAG, "Manifest does not contain required fields"); ESP_LOGE(TAG, "Manifest does not contain required fields");
return false; return false;
} }
if (build["chipFamily"] == ESPHOME_VARIANT) { this_update->update_info_.title = root["name"].as<std::string>();
if (!build.containsKey("ota")) { 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"); ESP_LOGE(TAG, "Manifest does not contain required fields");
return false; return false;
} }
auto ota = build["ota"]; if (build["chipFamily"] == ESPHOME_VARIANT) {
if (!ota.containsKey("path") || !ota.containsKey("md5")) { if (!build.containsKey("ota")) {
ESP_LOGE(TAG, "Manifest does not contain required fields"); ESP_LOGE(TAG, "Manifest does not contain required fields");
return false; 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) { if (!valid) {
std::string msg = str_sprintf("Failed to parse JSON from %s", this->source_url_.c_str()); std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str());
this->status_set_error(msg.c_str()); this_update->status_set_error(msg.c_str());
return; UPDATE_RETURN;
} }
// Merge source_url_ and this->update_info_.firmware_url // Merge source_url_ and this_update->update_info_.firmware_url
if (this->update_info_.firmware_url.find("http") == std::string::npos) { if (this_update->update_info_.firmware_url.find("http") == std::string::npos) {
std::string path = this->update_info_.firmware_url; std::string path = this_update->update_info_.firmware_url;
if (path[0] == '/') { if (path[0] == '/') {
std::string domain = this->source_url_.substr(0, this->source_url_.find('/', 8)); std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
this->update_info_.firmware_url = domain + path; this_update->update_info_.firmware_url = domain + path;
} else { } else {
std::string domain = this->source_url_.substr(0, this->source_url_.rfind('/') + 1); std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
this->update_info_.firmware_url = domain + path; 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 #ifdef ESPHOME_PROJECT_VERSION
current_version = ESPHOME_PROJECT_VERSION; current_version = ESPHOME_PROJECT_VERSION;
#else #else
current_version = ESPHOME_VERSION; current_version = ESPHOME_VERSION;
#endif #endif
this->update_info_.current_version = current_version; this_update->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_info_.has_progress = false; if (this_update->update_info_.latest_version.empty() ||
this->update_info_.progress = 0.0f; 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_update->update_info_.has_progress = false;
this->publish_state(); this_update->update_info_.progress = 0.0f;
this_update->status_clear_error();
this_update->publish_state();
UPDATE_RETURN;
} }
void HttpRequestUpdate::perform(bool force) { void HttpRequestUpdate::perform(bool force) {

View File

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

View File

@ -25,11 +25,13 @@ void I2SAudioMicrophone::setup() {
} }
} else } else
#endif #endif
if (this->pdm_) { {
if (this->parent_->get_port() != I2S_NUM_0) { if (this->pdm_) {
ESP_LOGE(TAG, "PDM only works on I2S0!"); if (this->parent_->get_port() != I2S_NUM_0) {
this->mark_failed(); ESP_LOGE(TAG, "PDM only works on I2S0!");
return; this->mark_failed();
return;
}
} }
} }
} }

View File

@ -1,9 +1,12 @@
import logging
from esphome import core, pins from esphome import core, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import display, spi from esphome.components import display, spi
from esphome.components.display import validate_rotation from esphome.components.display import validate_rotation
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_COLOR_ORDER, CONF_COLOR_ORDER,
CONF_COLOR_PALETTE, CONF_COLOR_PALETTE,
CONF_DC_PIN, CONF_DC_PIN,
@ -27,17 +30,12 @@ from esphome.const import (
CONF_WIDTH, CONF_WIDTH,
) )
from esphome.core import CORE, HexInt from esphome.core import CORE, HexInt
from esphome.final_validate import full_config
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
def AUTO_LOAD():
if CORE.is_esp32:
return ["psram"]
return []
CODEOWNERS = ["@nielsnl68", "@clydebarrow"] CODEOWNERS = ["@nielsnl68", "@clydebarrow"]
LOGGER = logging.getLogger(__name__)
ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx") ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx")
ILI9XXXDisplay = ili9xxx_ns.class_( ILI9XXXDisplay = ili9xxx_ns.class_(
@ -84,7 +82,7 @@ COLOR_ORDERS = {
"BGR": ColorOrder.COLOR_ORDER_BGR, "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_LED_PIN = "led_pin"
CONF_COLOR_PALETTE_IMAGES = "color_palette_images" CONF_COLOR_PALETTE_IMAGES = "color_palette_images"
@ -195,9 +193,27 @@ CONFIG_SCHEMA = cv.All(
_validate, _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): async def to_code(config):
@ -283,6 +299,8 @@ async def to_code(config):
palette = converted.getpalette() palette = converted.getpalette()
assert len(palette) == 256 * 3 assert len(palette) == 256 * 3
rhs = palette rhs = palette
elif config[CONF_COLOR_PALETTE] == "8BIT":
cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8))
else: else:
cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_16)) cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_16))

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import logging
from pathlib import Path from pathlib import Path
import re import re
import puremagic from PIL import Image, UnidentifiedImageError
from esphome import core, external_files from esphome import core, external_files
import esphome.codegen as cg import esphome.codegen as cg
@ -29,21 +29,259 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "image" DOMAIN = "image"
DEPENDENCIES = ["display"] DEPENDENCIES = ["display"]
MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
image_ns = cg.esphome_ns.namespace("image") image_ns = cg.esphome_ns.namespace("image")
ImageType = image_ns.enum("ImageType") 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 = { IMAGE_TYPE = {
"BINARY": ImageType.IMAGE_TYPE_BINARY, "BINARY": ImageBinary,
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY, "GRAYSCALE": ImageGrayscale,
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, "RGB565": ImageRGB565,
"RGB565": ImageType.IMAGE_TYPE_RGB565, "RGB": ImageRGB,
"RGB24": ImageType.IMAGE_TYPE_RGB24, "TRANSPARENT_BINARY": ReplaceWith(
"RGBA": ImageType.IMAGE_TYPE_RGBA, "'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" CONF_USE_TRANSPARENCY = "use_transparency"
# If the MDI file cannot be downloaded within this time, abort. # If the MDI file cannot be downloaded within this time, abort.
@ -53,17 +291,11 @@ SOURCE_LOCAL = "local"
SOURCE_MDI = "mdi" SOURCE_MDI = "mdi"
SOURCE_WEB = "web" SOURCE_WEB = "web"
Image_ = image_ns.class_("Image") Image_ = image_ns.class_("Image")
def _compute_local_icon_path(value: dict) -> Path: def compute_local_image_path(value) -> Path:
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" url = value[CONF_URL] if isinstance(value, dict) else value
return base_dir / f"{value[CONF_ICON]}.svg"
def compute_local_image_path(value: dict) -> Path:
url = value[CONF_URL]
h = hashlib.new("sha256") h = hashlib.new("sha256")
h.update(url.encode()) h.update(url.encode())
key = h.hexdigest()[:8] key = h.hexdigest()[:8]
@ -71,30 +303,38 @@ def compute_local_image_path(value: dict) -> Path:
return base_dir / key return base_dir / key
def download_mdi(value): def local_path(value):
validate_cairosvg_installed(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" url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
return download_file(url, path)
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return value
def download_image(value): def download_image(value):
url = value[CONF_URL] value = value[CONF_URL] if isinstance(value, dict) else value
path = compute_local_image_path(value) return download_file(value, compute_local_image_path(value))
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return value
def validate_cairosvg_installed(value): def is_svg_file(file):
"""Validate that cairosvg is installed""" if not file:
return False
with open(file, "rb") as f:
return "<svg" in str(f.read(1024))
def validate_cairosvg_installed():
try: try:
import cairosvg import cairosvg
except ImportError as err: except ImportError as err:
@ -110,73 +350,28 @@ def validate_cairosvg_installed(value):
"(pip install -U cairosvg)" "(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): def validate_file_shorthand(value):
value = cv.string_strict(value) value = cv.string_strict(value)
if value.startswith("mdi:"): if value.startswith("mdi:"):
validate_cairosvg_installed(value)
match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value) match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value)
if match is None: if match is None:
raise cv.Invalid("Could not parse mdi icon name.") raise cv.Invalid("Could not parse mdi icon name.")
icon = match.group(1) icon = match.group(1)
return FILE_SCHEMA( return download_mdi(icon)
{
CONF_SOURCE: SOURCE_MDI,
CONF_ICON: icon,
}
)
if value.startswith("http://") or value.startswith("https://"): if value.startswith("http://") or value.startswith("https://"):
return FILE_SCHEMA( return download_image(value)
{
CONF_SOURCE: SOURCE_WEB, value = cv.file_(value)
CONF_URL: value, return local_path(value)
}
)
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_LOCAL,
CONF_PATH: value,
}
)
LOCAL_SCHEMA = cv.Schema( LOCAL_SCHEMA = cv.All(
{ {
cv.Required(CONF_PATH): cv.file_, cv.Required(CONF_PATH): cv.file_,
} },
local_path,
) )
MDI_SCHEMA = cv.All( MDI_SCHEMA = cv.All(
@ -203,205 +398,202 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
) )
def _file_schema(value): def validate_transparency(choices=TRANSPARENCY_TYPES):
if isinstance(value, str): def validate(value):
return validate_file_shorthand(value) if isinstance(value, bool):
return TYPED_FILE_SCHEMA(value) 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( return validate
cv.All(
{
cv.Required(CONF_ID): cv.declare_id(Image_), def validate_settings(value):
cv.Required(CONF_FILE): FILE_SCHEMA, type = value[CONF_TYPE]
cv.Optional(CONF_RESIZE): cv.dimensions, transparency = value[CONF_USE_TRANSPARENCY].lower()
# Not setting default here on purpose; the default depends on the source type allow_config = IMAGE_TYPE[type].allow_config
# (file or mdi), and will be set in the "validate_cross_dependencies" validator. if transparency not in allow_config:
cv.Optional(CONF_TYPE): cv.enum(IMAGE_TYPE, upper=True), raise cv.Invalid(
# Not setting default here on purpose; the default depends on the image type, f"Image format '{type}' cannot have transparency: {transparency}"
# and thus will be set in the "validate_cross_dependencies" validator. )
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, invert_alpha = value.get(CONF_INVERT_ALPHA, False)
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( if (
"NONE", "FLOYDSTEINBERG", upper=True invert_alpha
), and transparency != CONF_ALPHA_CHANNEL
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), and CONF_INVERT_ALPHA not in allow_config
}, ):
validate_cross_dependencies, 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]): # The config schema can be a (possibly empty) single list of images,
# Local imports only to allow "validate_pillow_installed" to run *before* importing it # or a dictionary of image types each with a list of images
# cairosvg is only needed in case of SVG images; adding it CONFIG_SCHEMA = cv.Any(
# to the top would force configurations not using SVG to also have it cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
# installed for no reason. cv.ensure_list(IMAGE_SCHEMA),
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))
async def to_code(config): async def write_image(config, all_frames=False):
# Local import only to allow "validate_pillow_installed" to run *before* importing it path = Path(config[CONF_FILE])
from PIL import Image if not path.is_file():
raise core.EsphomeError(f"Could not load image file {path}")
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)
resize = config.get(CONF_RESIZE) resize = config.get(CONF_RESIZE)
if "svg" in file_type: if is_svg_file(path):
image = load_svg_image(file_contents, resize) # 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: else:
image = Image.open(io.BytesIO(file_contents)) image = Image.open(path)
width, height = image.size
if resize: 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 not resize and (width > 500 or height > 500):
if CONF_RESIZE not in config and (width > 500 or height > 500):
_LOGGER.warning( _LOGGER.warning(
'The image "%s" you requested is very big. Please consider' 'The image "%s" you requested is very big. Please consider'
" using the resize parameter.", " using the resize parameter.",
path, path,
) )
transparent = config[CONF_USE_TRANSPARENCY]
dither = ( dither = (
Image.Dither.NONE Image.Dither.NONE
if config[CONF_DITHER] == "NONE" if config[CONF_DITHER] == "NONE"
else Image.Dither.FLOYDSTEINBERG else Image.Dither.FLOYDSTEINBERG
) )
if config[CONF_TYPE] == "GRAYSCALE": type = config[CONF_TYPE]
image = image.convert("LA", dither=dither) transparency = config[CONF_USE_TRANSPARENCY]
pixels = list(image.getdata()) invert_alpha = config[CONF_INVERT_ALPHA]
data = [0 for _ in range(height * width)] frame_count = 1
pos = 0 if all_frames:
for g, a in pixels: try:
if transparent: frame_count = image.n_frames
if g == 1: except AttributeError:
g = 0 pass
if a < 0x80: if frame_count <= 1:
g = 1 _LOGGER.warning("Image file %s has no animation frames", path)
data[pos] = g total_rows = height * frame_count
pos += 1 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": rhs = [HexInt(x) for x in encoder.data]
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]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
var = cg.new_Pvariable( image_type = get_image_type_enum(type)
config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]] trans_value = get_transparency_enum(encoder.transparency)
)
cg.add(var.set_transparency(transparent)) 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++) { for (int img_y = 0; img_y < height_; img_y++) {
if (this->get_binary_pixel_(img_x, img_y)) { if (this->get_binary_pixel_(img_x, img_y)) {
display->draw_pixel_at(x + img_x, y + img_y, color_on); 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); 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: case IMAGE_TYPE_GRAYSCALE:
for (int img_x = 0; img_x < width_; img_x++) { for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) { for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_grayscale_pixel_(img_x, img_y); const uint32_t pos = (img_x + img_y * this->width_);
if (color.w >= 0x80) { const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
display->draw_pixel_at(x + img_x, y + img_y, color); 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; break;
@ -39,20 +56,10 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
} }
} }
break; break;
case IMAGE_TYPE_RGB24: case IMAGE_TYPE_RGB:
for (int img_x = 0; img_x < width_; img_x++) { for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) { for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgb24_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);
}
}
}
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);
if (color.w >= 0x80) { if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color); 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; 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_) if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return color_off; return color_off;
switch (this->type_) { switch (this->type_) {
case IMAGE_TYPE_BINARY: 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: case IMAGE_TYPE_GRAYSCALE:
return this->get_grayscale_pixel_(x, y); return this->get_grayscale_pixel_(x, y);
case IMAGE_TYPE_RGB565: case IMAGE_TYPE_RGB565:
return this->get_rgb565_pixel_(x, y); return this->get_rgb565_pixel_(x, y);
case IMAGE_TYPE_RGB24: case IMAGE_TYPE_RGB:
return this->get_rgb24_pixel_(x, y); return this->get_rgb_pixel_(x, y);
case IMAGE_TYPE_RGBA:
return this->get_rgba_pixel_(x, y);
default: default:
return color_off; 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; this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT;
break; break;
case IMAGE_TYPE_RGB24: case IMAGE_TYPE_RGB:
this->dsc_.header.cf = LV_IMG_CF_RGB888; #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; break;
case IMAGE_TYPE_RGB565: case IMAGE_TYPE_RGB565:
#if LV_COLOR_DEPTH == 16 #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 #else
this->dsc_.header.cf = LV_IMG_CF_RGB565; this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : 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;
#endif #endif
break; break;
} }
@ -128,51 +152,81 @@ bool Image::get_binary_pixel_(int x, int y) const {
const uint32_t pos = x + y * width_8; const uint32_t pos = x + y * width_8;
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
} }
Color Image::get_rgba_pixel_(int x, int y) const { Color Image::get_rgb_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 4; const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
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 color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), 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)); progmem_read_byte(this->data_start_ + pos + 2), 0xFF);
if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
// (0, 0, 1) has been defined as transparent color for non-alpha images. switch (this->transparency_) {
// putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) case TRANSPARENCY_CHROMA_KEY:
color.w = 0; if (color.g == 1 && color.r == 0 && color.b == 0) {
} else { // (0, 1, 0) has been defined as transparent color for non-alpha images.
color.w = 0xFF; color.w = 0;
}
break;
case TRANSPARENCY_ALPHA_CHANNEL:
color.w = progmem_read_byte(this->data_start_ + (pos + 3));
break;
default:
break;
} }
return color; return color;
} }
Color Image::get_rgb565_pixel_(int x, int y) const { Color Image::get_rgb565_pixel_(int x, int y) const {
const uint8_t *pos = this->data_start_; const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
if (this->transparent_) {
pos += (x + y * this->width_) * 3;
} else {
pos += (x + y * this->width_) * 2;
}
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1)); uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
auto r = (rgb565 & 0xF800) >> 11; auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5; auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F; auto b = rgb565 & 0x001F;
auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF; auto a = 0xFF;
Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a); switch (this->transparency_) {
return color; 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 { Color Image::get_grayscale_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_); const uint32_t pos = (x + y * this->width_);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos); const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; switch (this->transparency_) {
return Color(gray, gray, gray, alpha); 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_width() const { return this->width_; }
int Image::get_height() const { return this->height_; } int Image::get_height() const { return this->height_; }
ImageType Image::get_type() const { return this->type_; } ImageType Image::get_type() const { return this->type_; }
Image::Image(const uint8_t *data_start, int width, int height, ImageType type) 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) {} : 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 image
} // namespace esphome } // namespace esphome

View File

@ -12,51 +12,40 @@ namespace image {
enum ImageType { enum ImageType {
IMAGE_TYPE_BINARY = 0, IMAGE_TYPE_BINARY = 0,
IMAGE_TYPE_GRAYSCALE = 1, IMAGE_TYPE_GRAYSCALE = 1,
IMAGE_TYPE_RGB24 = 2, IMAGE_TYPE_RGB = 2,
IMAGE_TYPE_RGB565 = 3, 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 { class Image : public display::BaseImage {
public: 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; 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_width() const override;
int get_height() const override; int get_height() const override;
const uint8_t *get_data_start() const { return this->data_start_; } const uint8_t *get_data_start() const { return this->data_start_; }
ImageType get_type() const; ImageType get_type() const;
int get_bpp() const { int get_bpp() const { return this->bpp_; }
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;
}
/// Return the stride of the image in bytes, that is, the distance in bytes /// Return the stride of the image in bytes, that is, the distance in bytes
/// between two consecutive rows of pixels. /// 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 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 this->transparency_ != TRANSPARENCY_OPAQUE; }
bool has_transparency() const { return transparent_; }
#ifdef USE_LVGL #ifdef USE_LVGL
lv_img_dsc_t *get_lv_img_dsc(); lv_img_dsc_t *get_lv_img_dsc();
#endif #endif
protected: protected:
bool get_binary_pixel_(int x, int y) const; bool get_binary_pixel_(int x, int y) const;
Color get_rgb24_pixel_(int x, int y) const; Color get_rgb_pixel_(int x, int y) const;
Color get_rgba_pixel_(int x, int y) const;
Color get_rgb565_pixel_(int x, int y) const; Color get_rgb565_pixel_(int x, int y) const;
Color get_grayscale_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_; int height_;
ImageType type_; ImageType type_;
const uint8_t *data_start_; const uint8_t *data_start_;
bool transparent_; Transparency transparency_;
size_t bpp_{};
size_t stride_{};
#ifdef USE_LVGL #ifdef USE_LVGL
lv_img_dsc_t dsc_{}; lv_img_dsc_t dsc_{};
#endif #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(); auto free_heap = ALLOCATOR.get_max_free_block_size();
size_t request_size = std::min(free_heap, (size_t) 512); size_t request_size = std::min(free_heap, (size_t) 512);
while (true) { 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); DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) { if (json_document.capacity() == 0) {
ESP_LOGE(TAG, 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); request_size, free_heap);
return "{}"; return "{}";
} }
@ -29,7 +29,7 @@ std::string build_json(const json_build_t &f) {
f(root); f(root);
if (json_document.overflowed()) { if (json_document.overflowed()) {
if (request_size == free_heap) { 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); free_heap);
return "{}"; return "{}";
} }
@ -37,7 +37,7 @@ std::string build_json(const json_build_t &f) {
continue; continue;
} }
json_document.shrinkToFit(); 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; std::string output;
serializeJson(json_document, output); serializeJson(json_document, output);
return 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()); ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str());
return true; 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); 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); size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob);
if (actual_len != kv.value_len) { if (actual_len != kv.value_len) {

View File

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

View File

@ -197,11 +197,11 @@ def final_validation(configs):
for display_id in config[df.CONF_DISPLAYS]: for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1] path = global_config.get_path_for_id(display_id)[:-1]
display = global_config.get_config_for_path(path) display = global_config.get_config_for_path(path)
if CONF_LAMBDA in display: if CONF_LAMBDA in display or CONF_PAGES in display:
raise cv.Invalid( raise cv.Invalid(
"Using lambda: in display config not compatible with LVGL" "Using lambda: or pages: in display config is not compatible with LVGL"
) )
if display[CONF_AUTO_CLEAR_ENABLED]: if display.get(CONF_AUTO_CLEAR_ENABLED) is True:
raise cv.Invalid( raise cv.Invalid(
"Using auto_clear_enabled: true in display config not compatible with LVGL" "Using auto_clear_enabled: true in display config not compatible with LVGL"
) )

View File

@ -4,24 +4,27 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT
from esphome.cpp_generator import get_variable from esphome.cpp_generator import TemplateArguments, get_variable
from esphome.cpp_types import nullptr from esphome.cpp_types import nullptr
from .defines import ( from .defines import (
CONF_DISP_BG_COLOR, CONF_DISP_BG_COLOR,
CONF_DISP_BG_IMAGE, CONF_DISP_BG_IMAGE,
CONF_DISP_BG_OPA,
CONF_EDITING, CONF_EDITING,
CONF_FREEZE, CONF_FREEZE,
CONF_LVGL_ID, CONF_LVGL_ID,
CONF_SHOW_SNOW, CONF_SHOW_SNOW,
PARTS,
literal, literal,
) )
from .lv_validation import lv_bool, lv_color, lv_image from .lv_validation import lv_bool, lv_color, lv_image, opacity
from .lvcode import ( from .lvcode import (
LVGL_COMP_ARG, LVGL_COMP_ARG,
UPDATE_EVENT, UPDATE_EVENT,
LambdaContext, LambdaContext,
LocalVariable, LocalVariable,
LvglComponent,
ReturnStatement, ReturnStatement,
add_line_marks, add_line_marks,
lv, lv,
@ -31,7 +34,7 @@ from .lvcode import (
lvgl_comp, lvgl_comp,
static_cast, 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 ( from .types import (
LV_STATE, LV_STATE,
LvglAction, LvglAction,
@ -39,6 +42,7 @@ from .types import (
ObjUpdateAction, ObjUpdateAction,
lv_disp_t, lv_disp_t,
lv_group_t, lv_group_t,
lv_obj_base_t,
lv_obj_t, lv_obj_t,
lv_pseudo_button_t, lv_pseudo_button_t,
) )
@ -92,7 +96,11 @@ async def lvgl_is_paused(config, condition_id, template_arg, args):
lvgl = config[CONF_LVGL_ID] lvgl = config[CONF_LVGL_ID]
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
lv_add(ReturnStatement(lvgl_comp.is_paused())) lv_add(ReturnStatement(lvgl_comp.is_paused()))
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) var = cg.new_Pvariable(
condition_id,
TemplateArguments(LvglComponent, *template_arg),
await context.get_lambda(),
)
await cg.register_parented(var, lvgl) await cg.register_parented(var, lvgl)
return var return var
@ -113,19 +121,32 @@ async def lvgl_is_idle(config, condition_id, template_arg, args):
timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32) timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32)
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
lv_add(ReturnStatement(lvgl_comp.is_idle(timeout))) lv_add(ReturnStatement(lvgl_comp.is_idle(timeout)))
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) var = cg.new_Pvariable(
condition_id,
TemplateArguments(LvglComponent, *template_arg),
await context.get_lambda(),
)
await cg.register_parented(var, lvgl) await cg.register_parented(var, lvgl)
return var return var
async def disp_update(disp, config: dict): async def disp_update(disp, config: dict):
if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config: if (
CONF_DISP_BG_COLOR not in config
and CONF_DISP_BG_IMAGE not in config
and CONF_DISP_BG_OPA not in config
):
return return
with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp: with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp:
if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None: if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None:
lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color)) lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
if bg_image := config.get(CONF_DISP_BG_IMAGE): if bg_image := config.get(CONF_DISP_BG_IMAGE):
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) if bg_image == "none":
lv.disp_set_bg_image(disp_temp, static_cast("void *", "nullptr"))
else:
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))
if (bg_opa := config.get(CONF_DISP_BG_OPA)) is not None:
lv.disp_set_bg_opa(disp_temp, await opacity.process(bg_opa))
@automation.register_action( @automation.register_action(
@ -317,3 +338,14 @@ async def widget_focus(config, action_id, template_arg, args):
lv.group_focus_freeze(group, True) lv.group_focus_freeze(group, True)
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
return var 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

@ -215,7 +215,7 @@ LV_LONG_MODES = LvConstant(
) )
STATES = ( STATES = (
"default", # default state not included here
"checked", "checked",
"focused", "focused",
"focus_key", "focus_key",
@ -403,6 +403,7 @@ CONF_COLUMN = "column"
CONF_DIGITS = "digits" CONF_DIGITS = "digits"
CONF_DISP_BG_COLOR = "disp_bg_color" CONF_DISP_BG_COLOR = "disp_bg_color"
CONF_DISP_BG_IMAGE = "disp_bg_image" CONF_DISP_BG_IMAGE = "disp_bg_image"
CONF_DISP_BG_OPA = "disp_bg_opa"
CONF_BODY = "body" CONF_BODY = "body"
CONF_BUTTONS = "buttons" CONF_BUTTONS = "buttons"
CONF_BYTE_ORDER = "byte_order" CONF_BYTE_ORDER = "byte_order"

View File

@ -119,6 +119,7 @@ void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_ev
} }
void LvglComponent::add_page(LvPageType *page) { void LvglComponent::add_page(LvPageType *page) {
this->pages_.push_back(page); this->pages_.push_back(page);
page->set_parent(this);
page->setup(this->pages_.size() - 1); page->setup(this->pages_.size() - 1);
} }
void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) {
@ -143,6 +144,8 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
} while (this->pages_[this->current_page_]->skip); // skip empty pages() } while (this->pages_[this->current_page_]->skip); // skip empty pages()
this->show_page(this->current_page_, anim, time); this->show_page(this->current_page_, anim, time);
} }
size_t LvglComponent::get_current_page() const { return this->current_page_; }
bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; }
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
auto width = lv_area_get_width(area); auto width = lv_area_get_width(area);
auto height = lv_area_get_height(area); auto height = lv_area_get_height(area);
@ -498,9 +501,7 @@ size_t lv_millis(void) { return esphome::millis(); }
void *lv_custom_mem_alloc(size_t size) { void *lv_custom_mem_alloc(size_t size) {
auto *ptr = malloc(size); // NOLINT auto *ptr = malloc(size); // NOLINT
if (ptr == nullptr) { if (ptr == nullptr) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
#endif
} }
return ptr; return ptr;
} }
@ -517,30 +518,22 @@ void *lv_custom_mem_alloc(size_t size) {
ptr = heap_caps_malloc(size, cap_bits); ptr = heap_caps_malloc(size, cap_bits);
} }
if (ptr == nullptr) { if (ptr == nullptr) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
#endif
return nullptr; return nullptr;
} }
#ifdef ESPHOME_LOG_HAS_VERBOSE ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr);
esphome::ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr);
#endif
return ptr; return ptr;
} }
void lv_custom_mem_free(void *ptr) { void lv_custom_mem_free(void *ptr) {
#ifdef ESPHOME_LOG_HAS_VERBOSE ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
esphome::ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
#endif
if (ptr == nullptr) if (ptr == nullptr)
return; return;
heap_caps_free(ptr); heap_caps_free(ptr);
} }
void *lv_custom_mem_realloc(void *ptr, size_t size) { void *lv_custom_mem_realloc(void *ptr, size_t size) {
#ifdef ESPHOME_LOG_HAS_VERBOSE ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
esphome::ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
#endif
return heap_caps_realloc(ptr, size, cap_bits); return heap_caps_realloc(ptr, size, cap_bits);
} }
#endif #endif

View File

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

View File

@ -19,7 +19,7 @@ from esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid from . import defines as df, lv_validation as lvalid
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
from .helpers import add_lv_use, requires_component, validate_printf from .helpers import add_lv_use, requires_component, validate_printf
from .lv_validation import lv_color, lv_font, lv_gradient, lv_image from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity
from .lvcode import LvglComponent, lv_event_t_ptr from .lvcode import LvglComponent, lv_event_t_ptr
from .types import ( from .types import (
LVEncoderListener, LVEncoderListener,
@ -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) 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 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 :param parts: The parts to include in the schema
:return: :return: The schema
""" """
parts = widget_type.parts
return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend( return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend(
STATE_SCHEMA 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 ( return (
part_schema(widget_type) part_schema(parts)
.extend( .extend(
{ {
cv.Required(CONF_ID): cv.ensure_list( cv.Required(CONF_ID): cv.ensure_list(
@ -245,7 +250,12 @@ def create_modify_schema(widget_type):
} }
) )
.extend(FLAG_SCHEMA) .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:
""" """
return ( return (
part_schema(widget_type) part_schema(widget_type.parts)
.extend(FLAG_SCHEMA) .extend(FLAG_SCHEMA)
.extend(LAYOUT_SCHEMA) .extend(LAYOUT_SCHEMA)
.extend(ALIGN_TO_SCHEMA) .extend(ALIGN_TO_SCHEMA)
@ -341,11 +351,13 @@ FLEX_OBJ_SCHEMA = {
cv.Optional(df.CONF_FLEX_GROW): cv.int_, cv.Optional(df.CONF_FLEX_GROW): cv.int_,
} }
DISP_BG_SCHEMA = cv.Schema( DISP_BG_SCHEMA = cv.Schema(
{ {
cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image, cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any(
cv.one_of("none", lower=True), lv_image
),
cv.Optional(df.CONF_DISP_BG_COLOR): lv_color, cv.Optional(df.CONF_DISP_BG_COLOR): lv_color,
cv.Optional(df.CONF_DISP_BG_OPA): opacity,
} }
) )

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_INDEX, CONF_SELECTED_TEXT): lv_int,
cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text, cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of, 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), 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( lv_keyboard_t = LvType(
"LvKeyboardType", "LvKeyboardType",
parents=(KeyProvider, LvCompound), parents=(KeyProvider, LvCompound),
@ -32,6 +37,7 @@ class KeyboardType(WidgetType):
lv_keyboard_t, lv_keyboard_t,
(CONF_MAIN, CONF_ITEMS), (CONF_MAIN, CONF_ITEMS),
KEYBOARD_SCHEMA, KEYBOARD_SCHEMA,
modify_schema=KEYBOARD_MODIFY_SCHEMA,
) )
def get_uses(self): def get_uses(self):
@ -41,7 +47,8 @@ class KeyboardType(WidgetType):
lvgl_components_required.add("KEY_LISTENER") lvgl_components_required.add("KEY_LISTENER")
lvgl_components_required.add(CONF_KEYBOARD) lvgl_components_required.add(CONF_KEYBOARD)
add_lv_use("btnmatrix") 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): if ta := await get_widgets(config, CONF_TEXTAREA):
await w.set_property(CONF_TEXTAREA, ta[0].obj) await w.set_property(CONF_TEXTAREA, ta[0].obj)

View File

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

View File

@ -51,7 +51,7 @@ MSGBOX_SCHEMA = container_schema(
cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA, cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA,
cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA, cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA,
cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_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.Optional(CONF_CLOSE_BUTTON, default=True): lv_bool,
cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), 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 ..defines import CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR
from ..schemas import create_modify_schema from ..types import WidgetType, lv_obj_t
from ..types import ObjUpdateAction, WidgetType, lv_obj_t
class ObjType(WidgetType): class ObjType(WidgetType):
@ -21,10 +17,3 @@ class ObjType(WidgetType):
obj_spec = ObjType() 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

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

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_POSITION, default="top"): DIRECTIONS.one_of,
cv.Optional(CONF_SIZE, default="10%"): size, cv.Optional(CONF_SIZE, default="10%"): size,
} }

View File

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

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]: if config[CONF_USE_ABBREVIATIONS]:
cg.add_define("USE_MQTT_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_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); } 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_; } 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_; } const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) { void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) {
this->publish_nan_as_none_ = 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. * @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 /// Get the topic prefix of this device, using default if necessary
const std::string &get_topic_prefix() const; 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...> { template<typename... Ts> class NextionPublishFloatAction : public Action<Ts...> {
public: public:
explicit NextionPublishFloatAction(NextionComponent *component) : component_(component) {} 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 import automation
from esphome.components import display, uart import esphome.codegen as cg
from esphome.components import esp32 from esphome.components import display, esp32, uart
import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BRIGHTNESS,
CONF_ID, CONF_ID,
CONF_LAMBDA, CONF_LAMBDA,
CONF_BRIGHTNESS,
CONF_TRIGGER_ID,
CONF_ON_TOUCH, CONF_ON_TOUCH,
CONF_TRIGGER_ID,
) )
from esphome.core import CORE from esphome.core import CORE
from . import Nextion, nextion_ns, nextion_ref from . import Nextion, nextion_ns, nextion_ref
from .base_component import ( from .base_component import (
CONF_AUTO_WAKE_ON_TOUCH,
CONF_EXIT_REPARSE_ON_START,
CONF_ON_BUFFER_OVERFLOW, CONF_ON_BUFFER_OVERFLOW,
CONF_ON_PAGE,
CONF_ON_SETUP,
CONF_ON_SLEEP, CONF_ON_SLEEP,
CONF_ON_WAKE, CONF_ON_WAKE,
CONF_ON_SETUP, CONF_SKIP_CONNECTION_HANDSHAKE,
CONF_ON_PAGE, CONF_START_UP_PAGE,
CONF_TFT_URL, CONF_TFT_URL,
CONF_TOUCH_SLEEP_TIMEOUT, CONF_TOUCH_SLEEP_TIMEOUT,
CONF_WAKE_UP_PAGE, 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"] CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"]
@ -32,6 +32,9 @@ CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"]
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"]
NextionSetBrightnessAction = nextion_ns.class_(
"NextionSetBrightnessAction", automation.Action
)
SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template())
SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template())
WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template())
@ -46,7 +49,7 @@ CONFIG_SCHEMA = (
{ {
cv.GenerateID(): cv.declare_id(Nextion), cv.GenerateID(): cv.declare_id(Nextion),
cv.Optional(CONF_TFT_URL): cv.url, 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.Optional(CONF_ON_SETUP): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger), 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): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await uart.register_uart_device(var, config) await uart.register_uart_device(var, config)
if CONF_BRIGHTNESS in config: if CONF_BRIGHTNESS in config:
cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))
if CONF_LAMBDA in config: if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda( lambda_ = await cg.process_lambda(
config[CONF_LAMBDA], [(nextion_ref, "it")], return_type=cg.void 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->sent_setup_commands_ = true;
this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command. 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 // Check if a startup page has been set and send the command
if (this->start_up_page_ != -1) { 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_{}; CallbackManager<void()> buffer_overflow_callback_{};
optional<nextion_writer_t> writer_; optional<nextion_writer_t> writer_;
float brightness_{1.0}; optional<float> brightness_;
std::string device_model_; std::string device_model_;
std::string firmware_version_; std::string firmware_version_;

View File

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

View File

@ -4,14 +4,18 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import ( from esphome.components.image import (
CONF_INVERT_ALPHA,
CONF_USE_TRANSPARENCY, CONF_USE_TRANSPARENCY,
IMAGE_TYPE, IMAGE_SCHEMA,
Image_, Image_,
validate_cross_dependencies, get_image_type_enum,
get_transparency_enum,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUFFER_SIZE, CONF_BUFFER_SIZE,
CONF_DITHER,
CONF_FILE,
CONF_FORMAT, CONF_FORMAT,
CONF_ID, CONF_ID,
CONF_ON_ERROR, CONF_ON_ERROR,
@ -23,7 +27,7 @@ from esphome.const import (
AUTO_LOAD = ["image"] AUTO_LOAD = ["image"]
DEPENDENCIES = ["display", "http_request"] DEPENDENCIES = ["display", "http_request"]
CODEOWNERS = ["@guillempages"] CODEOWNERS = ["@guillempages", "@clydebarrow"]
MULTI_CONF = True MULTI_CONF = True
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" 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") 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_) OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_)
@ -57,48 +82,54 @@ DownloadErrorTrigger = online_image_ns.class_(
"DownloadErrorTrigger", automation.Trigger.template() "DownloadErrorTrigger", automation.Trigger.template()
) )
ONLINE_IMAGE_SCHEMA = cv.Schema(
{ def remove_options(*options):
cv.Required(CONF_ID): cv.declare_id(OnlineImage), return {
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), cv.Optional(option): cv.invalid(
# f"{option} is an invalid option for online_image"
# Common image options )
# for option in 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),
}
),
} }
).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( CONFIG_SCHEMA = cv.Schema(
cv.All( cv.All(
ONLINE_IMAGE_SCHEMA, ONLINE_IMAGE_SCHEMA,
validate_cross_dependencies,
cv.require_framework_version( cv.require_framework_version(
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed # esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
# esp8266_arduino=cv.Version(2, 7, 0), # esp8266_arduino=cv.Version(2, 7, 0),
esp32_arduino=cv.Version(0, 0, 0), esp32_arduino=cv.Version(0, 0, 0),
esp_idf=cv.Version(4, 0, 0), esp_idf=cv.Version(4, 0, 0),
rp2040_arduino=cv.Version(0, 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): async def to_code(config):
format = config[CONF_FORMAT] image_format = IMAGE_FORMATS[config[CONF_FORMAT]]
if format in [FORMAT_PNG]: image_format.actions()
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
cg.add_library("pngle", "1.0.2")
url = config[CONF_URL] url = config[CONF_URL]
width, height = config.get(CONF_RESIZE, (0, 0)) 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( var = cg.new_Pvariable(
config[CONF_ID], config[CONF_ID],
url, url,
width, width,
height, height,
format, image_format.enum,
config[CONF_TYPE], get_image_type_enum(config[CONF_TYPE]),
transparent,
config[CONF_BUFFER_SIZE], config[CONF_BUFFER_SIZE],
) )
await cg.register_component(var, config) await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
cg.add(var.set_transparency(transparent))
if placeholder_id := config.get(CONF_PLACEHOLDER): if placeholder_id := config.get(CONF_PLACEHOLDER):
placeholder = await cg.get_variable(placeholder_id) placeholder = await cg.get_variable(placeholder_id)
cg.add(var.set_placeholder(placeholder)) cg.add(var.set_placeholder(placeholder))

View File

@ -1,5 +1,4 @@
#pragma once #pragma once
#include "esphome/core/defines.h"
#include "esphome/core/color.h" #include "esphome/core/color.h"
namespace esphome { namespace esphome {
@ -23,7 +22,7 @@ class ImageDecoder {
/** /**
* @brief Initialize the decoder. * @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; } 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 * @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. * 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. * @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); 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. * 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. * 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. * 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 y The top-most coordinate of the rectangle.
* @param w The width of the rectangle. * @param w The width of the rectangle.
* @param h The height 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); void draw(int x, int y, int w, int h, const Color &color);
@ -67,7 +66,7 @@ class ImageDecoder {
protected: protected:
OnlineImage *image_; 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. // Will be overwritten anyway once the download size is known.
uint32_t download_size_ = 1; uint32_t download_size_ = 1;
uint32_t decoded_bytes_ = 0; 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, OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
uint32_t download_buffer_size) image::Transparency transparency, uint32_t download_buffer_size)
: Image(nullptr, 0, 0, type), : Image(nullptr, 0, 0, type, transparency),
buffer_(nullptr), buffer_(nullptr),
download_buffer_(download_buffer_size), download_buffer_(download_buffer_size),
format_(format), format_(format),
@ -45,7 +45,7 @@ void OnlineImage::draw(int x, int y, display::Display *display, Color color_on,
void OnlineImage::release() { void OnlineImage::release() {
if (this->buffer_) { 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->allocator_.deallocate(this->buffer_, this->get_buffer_size_());
this->data_start_ = nullptr; this->data_start_ = nullptr;
this->buffer_ = nullptr; this->buffer_ = nullptr;
@ -70,20 +70,19 @@ bool OnlineImage::resize_(int width_in, int height_in) {
if (this->buffer_) { if (this->buffer_) {
return false; return false;
} }
auto new_size = this->get_buffer_size_(width, height); size_t new_size = this->get_buffer_size_(width, height);
ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size); ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size);
delay_microseconds_safe(2000);
this->buffer_ = this->allocator_.allocate(new_size); this->buffer_ = this->allocator_.allocate(new_size);
if (this->buffer_) { if (this->buffer_ == nullptr) {
this->buffer_width_ = width; ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size,
this->buffer_height_ = height; this->allocator_.get_max_free_block_size());
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());
this->end_connection_(); this->end_connection_();
return false; return false;
} }
this->buffer_width_ = width;
this->buffer_height_ = height;
this->width_ = width;
ESP_LOGV(TAG, "New size: (%d, %d)", width, height);
return true; return true;
} }
@ -91,9 +90,8 @@ void OnlineImage::update() {
if (this->decoder_) { if (this->decoder_) {
ESP_LOGW(TAG, "Image already being updated."); ESP_LOGW(TAG, "Image already being updated.");
return; return;
} else {
ESP_LOGI(TAG, "Updating image");
} }
ESP_LOGI(TAG, "Updating image %s", this->url_.c_str());
this->downloader_ = this->parent_->get(this->url_); this->downloader_ = this->parent_->get(this->url_);
@ -142,10 +140,11 @@ void OnlineImage::loop() {
return; return;
} }
if (!this->downloader_ || this->decoder_->is_finished()) { if (!this->downloader_ || this->decoder_->is_finished()) {
ESP_LOGD(TAG, "Image fully downloaded");
this->data_start_ = buffer_; this->data_start_ = buffer_;
this->width_ = buffer_width_; this->width_ = buffer_width_;
this->height_ = buffer_height_; 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->end_connection_();
this->download_finished_callback_.call(); this->download_finished_callback_.call();
return; 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) { void OnlineImage::draw_pixel_(int x, int y, Color color) {
if (!this->buffer_) { if (!this->buffer_) {
ESP_LOGE(TAG, "Buffer not allocated!"); ESP_LOGE(TAG, "Buffer not allocated!");
@ -184,57 +196,53 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
switch (this->type_) { switch (this->type_) {
case ImageType::IMAGE_TYPE_BINARY: { case ImageType::IMAGE_TYPE_BINARY: {
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
const uint32_t pos = x + y * width_8; pos = x + y * width_8;
if ((this->has_transparency() && color.w > 127) || is_color_on(color)) { auto bitno = 0x80 >> (pos % 8u);
this->buffer_[pos / 8u] |= (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 { } else {
this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u)); this->buffer_[pos] &= ~bitno;
} }
break; break;
} }
case ImageType::IMAGE_TYPE_GRAYSCALE: { case ImageType::IMAGE_TYPE_GRAYSCALE: {
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); 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) { if (gray == 1) {
gray = 0; gray = 0;
} }
if (color.w < 0x80) { if (color.w < 0x80) {
gray = 1; gray = 1;
} }
} else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
if (color.w != 0xFF)
gray = color.w;
} }
this->buffer_[pos] = gray; this->buffer_[pos] = gray;
break; break;
} }
case ImageType::IMAGE_TYPE_RGB565: { case ImageType::IMAGE_TYPE_RGB565: {
this->map_chroma_key(color);
uint16_t col565 = display::ColorUtil::color_to_565(color); uint16_t col565 = display::ColorUtil::color_to_565(color);
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF); this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 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; 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 + 0] = color.r;
this->buffer_[pos + 1] = color.g; this->buffer_[pos + 1] = color.g;
this->buffer_[pos + 2] = color.b; this->buffer_[pos + 2] = color.b;
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 3] = color.w;
}
break; break;
} }
} }

View File

@ -48,12 +48,13 @@ class OnlineImage : public PollingComponent,
* @param buffer_size Size of the buffer used to download the image. * @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, 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 draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
void update() override; void update() override;
void loop() override; void loop() override;
void map_chroma_key(Color &color);
/** Set the URL to download the image from. */ /** Set the URL to download the image from. */
void set_url(const std::string &url) { void set_url(const std::string &url) {

View File

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

View File

@ -59,6 +59,24 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
this->text_sensor_row_(stream, obj, area, node, friendly_name); this->text_sensor_row_(stream, obj, area, node, friendly_name);
#endif #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); req->send(stream);
} }
@ -511,6 +529,156 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso
} }
#endif #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 prometheus
} // namespace esphome } // namespace esphome
#endif #endif

View File

@ -128,6 +128,30 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
std::string &friendly_name); std::string &friendly_name);
#endif #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_; web_server_base::WebServerBase *base_;
bool include_internal_{false}; bool include_internal_{false};
std::map<EntityBase *, std::string> relabel_map_id_; 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)); ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available));
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0)
if (available) { 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 #endif
} }

View File

@ -154,16 +154,16 @@ void RemoteReceiverComponent::setup() {
void RemoteReceiverComponent::dump_config() { void RemoteReceiverComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Remote Receiver:"); ESP_LOGCONFIG(TAG, "Remote Receiver:");
LOG_PIN(" Pin: ", this->pin_); LOG_PIN(" Pin: ", this->pin_);
if (this->pin_->digital_read()) {
ESP_LOGW(TAG, "Remote Receiver Signal starts with a HIGH value. Usually this means you have to "
"invert the signal using 'inverted: True' in the pin schema!");
}
#if ESP_IDF_VERSION_MAJOR >= 5 #if ESP_IDF_VERSION_MAJOR >= 5
ESP_LOGCONFIG(TAG, " Clock resolution: %" PRIu32 " hz", this->clock_resolution_); ESP_LOGCONFIG(TAG, " Clock resolution: %" PRIu32 " hz", this->clock_resolution_);
ESP_LOGCONFIG(TAG, " RMT symbols: %" PRIu32, this->rmt_symbols_); ESP_LOGCONFIG(TAG, " RMT symbols: %" PRIu32, this->rmt_symbols_);
ESP_LOGCONFIG(TAG, " Filter symbols: %" PRIu32, this->filter_symbols_); ESP_LOGCONFIG(TAG, " Filter symbols: %" PRIu32, this->filter_symbols_);
ESP_LOGCONFIG(TAG, " Receive symbols: %" PRIu32, this->receive_symbols_); ESP_LOGCONFIG(TAG, " Receive symbols: %" PRIu32, this->receive_symbols_);
#else #else
if (this->pin_->digital_read()) {
ESP_LOGW(TAG, "Remote Receiver Signal starts with a HIGH value. Usually this means you have to "
"invert the signal using 'inverted: True' in the pin schema!");
}
ESP_LOGCONFIG(TAG, " Channel: %d", this->channel_); ESP_LOGCONFIG(TAG, " Channel: %d", this->channel_);
ESP_LOGCONFIG(TAG, " RMT memory blocks: %d", this->mem_block_num_); ESP_LOGCONFIG(TAG, " RMT memory blocks: %d", this->mem_block_num_);
ESP_LOGCONFIG(TAG, " Clock divider: %u", this->clock_divider_); ESP_LOGCONFIG(TAG, " Clock divider: %u", this->clock_divider_);

View File

@ -7,6 +7,7 @@ from esphome.const import (
CONF_CLOCK_DIVIDER, CONF_CLOCK_DIVIDER,
CONF_CLOCK_RESOLUTION, CONF_CLOCK_RESOLUTION,
CONF_ID, CONF_ID,
CONF_INVERTED,
CONF_PIN, CONF_PIN,
CONF_RMT_CHANNEL, CONF_RMT_CHANNEL,
CONF_RMT_SYMBOLS, CONF_RMT_SYMBOLS,
@ -16,6 +17,7 @@ from esphome.core import CORE
AUTO_LOAD = ["remote_base"] AUTO_LOAD = ["remote_base"]
CONF_EOT_LEVEL = "eot_level"
CONF_ON_TRANSMIT = "on_transmit" CONF_ON_TRANSMIT = "on_transmit"
CONF_ON_COMPLETE = "on_complete" CONF_ON_COMPLETE = "on_complete"
CONF_ONE_WIRE = "one_wire" CONF_ONE_WIRE = "one_wire"
@ -41,6 +43,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_CLOCK_DIVIDER): cv.All( cv.Optional(CONF_CLOCK_DIVIDER): cv.All(
cv.only_on_esp32, cv.only_with_arduino, cv.int_range(min=1, max=255) cv.only_on_esp32, cv.only_with_arduino, cv.int_range(min=1, max=255)
), ),
cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_with_esp_idf, cv.boolean),
cv.Optional(CONF_ONE_WIRE): cv.All(cv.only_with_esp_idf, cv.boolean), cv.Optional(CONF_ONE_WIRE): cv.All(cv.only_with_esp_idf, cv.boolean),
cv.Optional(CONF_USE_DMA): cv.All(cv.only_with_esp_idf, cv.boolean), cv.Optional(CONF_USE_DMA): cv.All(cv.only_with_esp_idf, cv.boolean),
cv.SplitDefault( cv.SplitDefault(
@ -73,6 +76,12 @@ async def to_code(config):
cg.add(var.set_with_dma(config[CONF_USE_DMA])) cg.add(var.set_with_dma(config[CONF_USE_DMA]))
if CONF_ONE_WIRE in config: if CONF_ONE_WIRE in config:
cg.add(var.set_one_wire(config[CONF_ONE_WIRE])) cg.add(var.set_one_wire(config[CONF_ONE_WIRE]))
if CONF_EOT_LEVEL in config:
cg.add(var.set_eot_level(config[CONF_EOT_LEVEL]))
elif CONF_ONE_WIRE in config and config[CONF_ONE_WIRE]:
cg.add(var.set_eot_level(True))
elif CONF_INVERTED in config[CONF_PIN] and config[CONF_PIN][CONF_INVERTED]:
cg.add(var.set_eot_level(True))
else: else:
if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None:
var = cg.new_Pvariable(config[CONF_ID], pin, rmt_channel) var = cg.new_Pvariable(config[CONF_ID], pin, rmt_channel)

View File

@ -41,6 +41,8 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 #if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
void set_one_wire(bool one_wire) { this->one_wire_ = one_wire; } void set_one_wire(bool one_wire) { this->one_wire_ = one_wire; }
void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; }
void digital_write(bool value);
#endif #endif
Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; }; Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; };
@ -68,6 +70,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
std::vector<rmt_symbol_word_t> rmt_temp_; std::vector<rmt_symbol_word_t> rmt_temp_;
bool with_dma_{false}; bool with_dma_{false};
bool one_wire_{false}; bool one_wire_{false};
bool eot_level_{false};
rmt_channel_handle_t channel_{NULL}; rmt_channel_handle_t channel_{NULL};
rmt_encoder_handle_t encoder_{NULL}; rmt_encoder_handle_t encoder_{NULL};
#else #else

View File

@ -11,6 +11,7 @@ static const char *const TAG = "remote_transmitter";
void RemoteTransmitterComponent::setup() { void RemoteTransmitterComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up Remote Transmitter..."); ESP_LOGCONFIG(TAG, "Setting up Remote Transmitter...");
this->inverted_ = this->pin_->is_inverted();
this->configure_rmt_(); this->configure_rmt_();
} }
@ -37,6 +38,31 @@ void RemoteTransmitterComponent::dump_config() {
} }
} }
#if ESP_IDF_VERSION_MAJOR >= 5
void RemoteTransmitterComponent::digital_write(bool value) {
rmt_symbol_word_t symbol = {
.duration0 = 1,
.level0 = value,
.duration1 = 0,
.level1 = value,
};
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.loop_count = 0;
config.flags.eot_level = value;
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
this->status_set_warning();
}
error = rmt_tx_wait_all_done(this->channel_, -1);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
this->status_set_warning();
}
}
#endif
void RemoteTransmitterComponent::configure_rmt_() { void RemoteTransmitterComponent::configure_rmt_() {
#if ESP_IDF_VERSION_MAJOR >= 5 #if ESP_IDF_VERSION_MAJOR >= 5
esp_err_t error; esp_err_t error;
@ -83,6 +109,7 @@ void RemoteTransmitterComponent::configure_rmt_() {
this->mark_failed(); this->mark_failed();
return; return;
} }
this->digital_write(this->one_wire_ || this->inverted_);
this->initialized_ = true; this->initialized_ = true;
} }
@ -120,13 +147,12 @@ void RemoteTransmitterComponent::configure_rmt_() {
} }
c.tx_config.idle_output_en = true; c.tx_config.idle_output_en = true;
if (!this->pin_->is_inverted()) { if (!this->inverted_) {
c.tx_config.carrier_level = RMT_CARRIER_LEVEL_HIGH; c.tx_config.carrier_level = RMT_CARRIER_LEVEL_HIGH;
c.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; c.tx_config.idle_level = RMT_IDLE_LEVEL_LOW;
} else { } else {
c.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; c.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW;
c.tx_config.idle_level = RMT_IDLE_LEVEL_HIGH; c.tx_config.idle_level = RMT_IDLE_LEVEL_HIGH;
this->inverted_ = true;
} }
esp_err_t error = rmt_config(&c); esp_err_t error = rmt_config(&c);
@ -210,7 +236,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
rmt_transmit_config_t config; rmt_transmit_config_t config;
memset(&config, 0, sizeof(config)); memset(&config, 0, sizeof(config));
config.loop_count = 0; config.loop_count = 0;
config.flags.eot_level = this->inverted_; config.flags.eot_level = this->eot_level_;
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(),
this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config); this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config);
if (error != ESP_OK) { if (error != ESP_OK) {
@ -223,8 +249,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if (error != ESP_OK) { if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
this->status_set_warning(); this->status_set_warning();
} else {
this->status_clear_warning();
} }
if (i + 1 < send_times) if (i + 1 < send_times)
delayMicroseconds(send_wait); delayMicroseconds(send_wait);

View File

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

View File

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

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