From adfec578cfd301ee0295bd1dec6a649ff98fcb8b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:13:09 +1200 Subject: [PATCH 001/160] Add ``--version`` handler to cli (#7150) --- esphome/__main__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index b13f96daf7..9e7b7fa15b 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -747,7 +747,14 @@ def parse_args(argv): ) parser = argparse.ArgumentParser( - description=f"ESPHome v{const.__version__}", parents=[options_parser] + description=f"ESPHome {const.__version__}", parents=[options_parser] + ) + + parser.add_argument( + "--version", + action="version", + version=f"Version: {const.__version__}", + help="Print the ESPHome version and exit.", ) mqtt_options = argparse.ArgumentParser(add_help=False) From acf690c87d362b37dc3b8910bcc7ffd4882bc2f2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:05:41 +1200 Subject: [PATCH 002/160] [code-quality] Organise ethernet related imports (#7152) --- esphome/components/ethernet/__init__.py | 36 +++++++++---------- .../components/ethernet_info/text_sensor.py | 4 +-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 697436415b..1c6acda724 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -1,6 +1,4 @@ from esphome import pins -import esphome.config_validation as cv -import esphome.final_validate as fv import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( @@ -8,31 +6,33 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.components.network import IPAddress +from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface +import esphome.config_validation as cv from esphome.const import ( - CONF_DOMAIN, - CONF_ID, - CONF_VALUE, - CONF_MANUAL_IP, - CONF_STATIC_IP, - CONF_TYPE, - CONF_USE_ADDRESS, - CONF_GATEWAY, - CONF_SUBNET, + CONF_ADDRESS, + CONF_CLK_PIN, + CONF_CS_PIN, CONF_DNS1, CONF_DNS2, - CONF_CLK_PIN, + CONF_DOMAIN, + CONF_GATEWAY, + CONF_ID, + CONF_INTERRUPT_PIN, + CONF_MANUAL_IP, CONF_MISO_PIN, CONF_MOSI_PIN, - CONF_CS_PIN, - CONF_INTERRUPT_PIN, + CONF_PAGE_ID, CONF_RESET_PIN, CONF_SPI, - CONF_PAGE_ID, - CONF_ADDRESS, + CONF_STATIC_IP, + CONF_SUBNET, + CONF_TYPE, + CONF_USE_ADDRESS, + CONF_VALUE, ) from esphome.core import CORE, coroutine_with_priority -from esphome.components.network import IPAddress -from esphome.components.spi import get_spi_interface, CONF_INTERFACE_INDEX +import esphome.final_validate as fv CONFLICTS_WITH = ["wifi"] DEPENDENCIES = ["esp32"] diff --git a/esphome/components/ethernet_info/text_sensor.py b/esphome/components/ethernet_info/text_sensor.py index a545475870..31da516e44 100644 --- a/esphome/components/ethernet_info/text_sensor.py +++ b/esphome/components/ethernet_info/text_sensor.py @@ -1,9 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import text_sensor +import esphome.config_validation as cv from esphome.const import ( - CONF_IP_ADDRESS, CONF_DNS_ADDRESS, + CONF_IP_ADDRESS, CONF_MAC_ADDRESS, ENTITY_CATEGORY_DIAGNOSTIC, ) From 20c22465335231017b4300827ad3e68ce968bd4f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:06:08 +1200 Subject: [PATCH 003/160] [code-quality] Organise wifi related imports (#7153) --- esphome/components/wifi/__init__.py | 31 +++++++++++---------- esphome/components/wifi/wpa2_eap.py | 13 ++++----- esphome/components/wifi_info/text_sensor.py | 6 ++-- esphome/components/wifi_signal/sensor.py | 2 +- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 624bcdabdc..ea03cc16d1 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,15 +1,19 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -import esphome.final_validate as fv from esphome import automation from esphome.automation import Condition +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.network import IPAddress +import esphome.config_validation as cv from esphome.const import ( CONF_AP, CONF_BSSID, + CONF_CERTIFICATE, + CONF_CERTIFICATE_AUTHORITY, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, + CONF_EAP, CONF_ENABLE_BTM, CONF_ENABLE_ON_BOOT, CONF_ENABLE_RRM, @@ -17,29 +21,26 @@ from esphome.const import ( CONF_GATEWAY, CONF_HIDDEN, CONF_ID, + CONF_IDENTITY, + CONF_KEY, CONF_MANUAL_IP, CONF_NETWORKS, + CONF_ON_CONNECT, + CONF_ON_DISCONNECT, CONF_PASSWORD, CONF_POWER_SAVE_MODE, + CONF_PRIORITY, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, CONF_SUBNET, - CONF_USE_ADDRESS, - CONF_PRIORITY, - CONF_IDENTITY, - CONF_CERTIFICATE_AUTHORITY, - CONF_CERTIFICATE, - CONF_KEY, - CONF_USERNAME, - CONF_EAP, CONF_TTLS_PHASE_2, - CONF_ON_CONNECT, - CONF_ON_DISCONNECT, + CONF_USE_ADDRESS, + CONF_USERNAME, ) from esphome.core import CORE, HexInt, coroutine_with_priority -from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant, const -from esphome.components.network import IPAddress +import esphome.final_validate as fv + from . import wpa2_eap AUTO_LOAD = ["network"] diff --git a/esphome/components/wifi/wpa2_eap.py b/esphome/components/wifi/wpa2_eap.py index 3985dfef18..5d5bd8dca3 100644 --- a/esphome/components/wifi/wpa2_eap.py +++ b/esphome/components/wifi/wpa2_eap.py @@ -7,16 +7,15 @@ so that it doesn't crash if it's not installed. import logging from pathlib import Path -from esphome.core import CORE import esphome.config_validation as cv from esphome.const import ( - CONF_USERNAME, - CONF_IDENTITY, - CONF_PASSWORD, CONF_CERTIFICATE, + CONF_IDENTITY, CONF_KEY, + CONF_PASSWORD, + CONF_USERNAME, ) - +from esphome.core import CORE _LOGGER = logging.getLogger(__name__) @@ -49,8 +48,8 @@ def wrapped_load_pem_x509_certificate(value): def wrapped_load_pem_private_key(value, password): validate_cryptography_installed() - from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.serialization import load_pem_private_key if password: password = password.encode("UTF-8") @@ -91,7 +90,7 @@ def _validate_load_private_key(key, cert_pw): def _check_private_key_cert_match(key, cert): - from cryptography.hazmat.primitives.asymmetric import rsa, ec + from cryptography.hazmat.primitives.asymmetric import ec, rsa def check_match_a(): return key.public_key().public_numbers() == cert.public_key().public_numbers() diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 75513712dd..4ceb73a695 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -1,13 +1,13 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import text_sensor +import esphome.config_validation as cv from esphome.const import ( CONF_BSSID, + CONF_DNS_ADDRESS, CONF_IP_ADDRESS, + CONF_MAC_ADDRESS, CONF_SCAN_RESULTS, CONF_SSID, - CONF_MAC_ADDRESS, - CONF_DNS_ADDRESS, ENTITY_CATEGORY_DIAGNOSTIC, ) diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py index 77fabf272e..99b51adea0 100644 --- a/esphome/components/wifi_signal/sensor.py +++ b/esphome/components/wifi_signal/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor +import esphome.config_validation as cv from esphome.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, ENTITY_CATEGORY_DIAGNOSTIC, From e64709c37e22148572a182461f98430982e3228f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:07:44 +1200 Subject: [PATCH 004/160] [code-quality] Organise core imports (#7149) --- esphome/__main__.py | 12 ++-- esphome/automation.py | 2 +- esphome/codegen.py | 106 ++++++++++++++++---------------- esphome/config.py | 42 ++++++------- esphome/config_validation.py | 42 ++++++------- esphome/core/__init__.py | 19 +++--- esphome/core/config.py | 6 +- esphome/core/entity_helpers.py | 3 +- esphome/coroutine.py | 2 +- esphome/cpp_generator.py | 2 +- esphome/cpp_helpers.py | 8 +-- esphome/dashboard/core.py | 6 +- esphome/dashboard/dashboard.py | 6 +- esphome/dashboard/entries.py | 2 +- esphome/dashboard/util/file.py | 2 +- esphome/dashboard/web_server.py | 6 +- esphome/external_files.py | 10 +-- esphome/final_validate.py | 4 +- esphome/git.py | 10 +-- esphome/helpers.py | 12 ++-- esphome/mqtt.py | 6 +- esphome/pins.py | 12 ++-- esphome/platformio_api.py | 10 +-- esphome/storage_json.py | 3 +- esphome/types.py | 2 +- esphome/util.py | 5 +- esphome/voluptuous_schema.py | 1 + esphome/vscode.py | 9 +-- esphome/wizard.py | 2 +- esphome/writer.py | 16 ++--- esphome/yaml_util.py | 6 +- esphome/zeroconf.py | 2 +- 32 files changed, 190 insertions(+), 186 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 9e7b7fa15b..13f09e15ed 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1,12 +1,12 @@ # PYTHON_ARGCOMPLETE_OK import argparse +from datetime import datetime import functools import logging import os import re import sys import time -from datetime import datetime import argcomplete @@ -39,14 +39,14 @@ from esphome.const import ( ) from esphome.core import CORE, EsphomeError, coroutine from esphome.helpers import indent, is_ip_address +from esphome.log import Fore, color, setup_log from esphome.util import ( + get_serial_ports, + list_yaml_files, run_external_command, run_external_process, safe_print, - list_yaml_files, - get_serial_ports, ) -from esphome.log import color, setup_log, Fore _LOGGER = logging.getLogger(__name__) @@ -116,6 +116,7 @@ def get_port_type(port): def run_miniterm(config, port): import serial + from esphome import platformio_api if CONF_LOGGER not in config: @@ -596,9 +597,10 @@ def command_update_all(args): def command_idedata(args, config): - from esphome import platformio_api import json + from esphome import platformio_api + logging.disable(logging.INFO) logging.disable(logging.WARNING) diff --git a/esphome/automation.py b/esphome/automation.py index b25ffa5abe..0bd6cf0af0 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -7,10 +7,10 @@ from esphome.const import ( CONF_ELSE, CONF_ID, CONF_THEN, + CONF_TIME, CONF_TIMEOUT, CONF_TRIGGER_ID, CONF_TYPE_ID, - CONF_TIME, CONF_UPDATE_INTERVAL, ) from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor diff --git a/esphome/codegen.py b/esphome/codegen.py index 6b000b53a1..bfa1683ce7 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -8,55 +8,78 @@ # want to break suddenly due to a rename (this file will get backports for features). # pylint: disable=unused-import -from esphome.cpp_generator import ( # noqa +from esphome.cpp_generator import ( # noqa: F401 + ArrayInitializer, Expression, + LineComment, + MockObj, + MockObjClass, + Pvariable, RawExpression, RawStatement, - TemplateArguments, - StructInitializer, - ArrayInitializer, - safe_exp, Statement, - LineComment, - progmem_array, - static_const_array, - statement, - variable, - with_local_variable, - new_variable, - Pvariable, - new_Pvariable, + StructInitializer, + TemplateArguments, add, - add_global, - add_library, add_build_flag, add_define, + add_global, + add_library, add_platformio_option, get_variable, get_variable_with_full_id, - process_lambda, is_template, + new_Pvariable, + new_variable, + process_lambda, + progmem_array, + safe_exp, + statement, + static_const_array, templatable, - MockObj, - MockObjClass, + variable, + with_local_variable, ) -from esphome.cpp_helpers import ( # noqa - gpio_pin_expression, - register_component, +from esphome.cpp_helpers import ( # noqa: F401 build_registry_entry, build_registry_list, extract_registry_entry_config, - register_parented, + gpio_pin_expression, past_safe_mode, + register_component, + register_parented, ) -from esphome.cpp_types import ( # noqa - global_ns, - void, - nullptr, - float_, - double, +from esphome.cpp_types import ( # noqa: F401 + NAN, + App, + Application, + Component, + ComponentPtr, + Controller, + EntityBase, + EntityCategory, + ESPTime, + GPIOPin, + InternalGPIOPin, + JsonObject, + JsonObjectConst, + Parented, + PollingComponent, + arduino_json_ns, bool_, + const_char_ptr, + double, + esphome_ns, + float_, + global_ns, + gpio_Flags, + int16, + int32, + int64, int_, + nullptr, + optional, + size_t, std_ns, std_shared_ptr, std_string, @@ -66,28 +89,5 @@ from esphome.cpp_types import ( # noqa uint16, uint32, uint64, - int16, - int32, - int64, - size_t, - const_char_ptr, - NAN, - esphome_ns, - App, - EntityBase, - Component, - ComponentPtr, - PollingComponent, - Application, - optional, - arduino_json_ns, - JsonObject, - JsonObjectConst, - Controller, - GPIOPin, - InternalGPIOPin, - gpio_Flags, - EntityCategory, - Parented, - ESPTime, + void, ) diff --git a/esphome/config.py b/esphome/config.py index 925a31fed0..a2d0d15477 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -1,40 +1,38 @@ from __future__ import annotations + import abc +from contextlib import contextmanager +import contextvars import functools import heapq import logging import re - -from typing import Union, Any - -from contextlib import contextmanager -import contextvars +from typing import Any, Union import voluptuous as vol -from esphome import core, yaml_util, loader, pins -import esphome.core.config as core_config +from esphome import core, loader, pins, yaml_util +from esphome.config_helpers import Extend, Remove +import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, - CONF_ID, - CONF_PLATFORM, - CONF_PACKAGES, - CONF_SUBSTITUTIONS, CONF_EXTERNAL_COMPONENTS, + CONF_ID, + CONF_PACKAGES, + CONF_PLATFORM, + CONF_SUBSTITUTIONS, TARGET_PLATFORMS, ) -from esphome.core import CORE, EsphomeError, DocumentRange -from esphome.helpers import indent -from esphome.util import safe_print, OrderedDict - -from esphome.config_helpers import Extend, Remove -from esphome.loader import get_component, get_platform, ComponentManifest -from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue -from esphome.voluptuous_schema import ExtraKeysInvalid -from esphome.log import color, Fore +from esphome.core import CORE, DocumentRange, EsphomeError +import esphome.core.config as core_config import esphome.final_validate as fv -import esphome.config_validation as cv -from esphome.types import ConfigType, ConfigFragmentType +from esphome.helpers import indent +from esphome.loader import ComponentManifest, get_component, get_platform +from esphome.log import Fore, color +from esphome.types import ConfigFragmentType, ConfigType +from esphome.util import OrderedDict, safe_print +from esphome.voluptuous_schema import ExtraKeysInvalid +from esphome.yaml_util import ESPForceValue, ESPHomeDataBase, is_secret _LOGGER = logging.getLogger(__name__) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 3ef92ad460..1cd1d6aa31 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1,13 +1,13 @@ """Helpers for config validation using voluptuous.""" +from contextlib import contextmanager from dataclasses import dataclass +from datetime import datetime import logging import os import re -from contextlib import contextmanager -import uuid as uuid_ -from datetime import datetime from string import ascii_letters, digits +import uuid as uuid_ import voluptuous as vol @@ -17,37 +17,37 @@ from esphome.config_helpers import Extend, Remove from esphome.const import ( ALLOWED_NAME_CHARS, CONF_AVAILABILITY, - CONF_COMMAND_TOPIC, CONF_COMMAND_RETAIN, + CONF_COMMAND_TOPIC, + CONF_DAY, CONF_DISABLED_BY_DEFAULT, CONF_DISCOVERY, CONF_ENTITY_CATEGORY, + CONF_HOUR, CONF_ICON, CONF_ID, CONF_INTERNAL, + CONF_MINUTE, + CONF_MONTH, CONF_NAME, + CONF_PASSWORD, + CONF_PATH, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, - CONF_RETAIN, CONF_QOS, + CONF_REF, + CONF_RETAIN, + CONF_SECOND, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, - CONF_YEAR, - CONF_MONTH, - CONF_DAY, - CONF_HOUR, - CONF_MINUTE, - CONF_SECOND, - CONF_VALUE, - CONF_UPDATE_INTERVAL, - CONF_TYPE_ID, CONF_TYPE, - CONF_REF, + CONF_TYPE_ID, + CONF_UPDATE_INTERVAL, CONF_URL, - CONF_PATH, CONF_USERNAME, - CONF_PASSWORD, + CONF_VALUE, + CONF_YEAR, ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_CATEGORY_NONE, @@ -71,15 +71,15 @@ from esphome.core import ( TimePeriod, TimePeriodMicroseconds, TimePeriodMilliseconds, + TimePeriodMinutes, TimePeriodNanoseconds, TimePeriodSeconds, - TimePeriodMinutes, ) -from esphome.helpers import list_starts_with, add_class_to_obj +from esphome.helpers import add_class_to_obj, list_starts_with from esphome.schema_extractors import ( SCHEMA_EXTRACT, - schema_extractor_list, schema_extractor, + schema_extractor_list, schema_extractor_registry, schema_extractor_typed, ) @@ -1686,9 +1686,9 @@ class SplitDefault(Optional): if CORE.is_esp32: from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32.const import ( + VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3, - VARIANT_ESP32C3, ) variant = get_esp32_variant() diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index f25891965a..9d3d14492e 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -7,26 +7,29 @@ from typing import TYPE_CHECKING, Optional, Union from esphome.const import ( CONF_COMMENT, CONF_ESPHOME, - CONF_USE_ADDRESS, CONF_ETHERNET, + CONF_PORT, + CONF_USE_ADDRESS, CONF_WEB_SERVER, CONF_WIFI, - CONF_PORT, KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, - PLATFORM_BK72XX, - PLATFORM_RTL87XX, - PLATFORM_RP2040, PLATFORM_HOST, + PLATFORM_RP2040, + PLATFORM_RTL87XX, ) -from esphome.coroutine import FakeAwaitable as _FakeAwaitable -from esphome.coroutine import FakeEventLoop as _FakeEventLoop # pylint: disable=unused-import -from esphome.coroutine import coroutine, coroutine_with_priority # noqa +from esphome.coroutine import ( # noqa: F401 + FakeAwaitable as _FakeAwaitable, + FakeEventLoop as _FakeEventLoop, + coroutine, + coroutine_with_priority, +) from esphome.helpers import ensure_unique_string, get_str_env, is_ha_addon from esphome.util import OrderedDict diff --git a/esphome/core/config.py b/esphome/core/config.py index 80b731b905..739a8a1aea 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -3,9 +3,9 @@ import multiprocessing import os import re +from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation from esphome.const import ( CONF_ARDUINO_VERSION, CONF_AREA, @@ -16,11 +16,11 @@ from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, CONF_FRAMEWORK, + CONF_FRIENDLY_NAME, CONF_INCLUDES, CONF_LIBRARIES, CONF_MIN_VERSION, CONF_NAME, - CONF_FRIENDLY_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, @@ -34,8 +34,8 @@ from esphome.const import ( CONF_TYPE, CONF_VERSION, KEY_CORE, - TARGET_PLATFORMS, PLATFORM_ESP8266, + TARGET_PLATFORMS, __version__ as ESPHOME_VERSION, ) from esphome.core import CORE, coroutine_with_priority diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index f921711ec2..7f6a9b48ab 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -1,6 +1,5 @@ -import esphome.final_validate as fv - from esphome.const import CONF_ID +import esphome.final_validate as fv def inherit_property_from(property_to_inherit, parent_id_property, transform=None): diff --git a/esphome/coroutine.py b/esphome/coroutine.py index 5f391dc7ad..30ebb8147e 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -43,13 +43,13 @@ the last `yield` expression defines what is returned. """ import collections +from collections.abc import Awaitable, Generator, Iterator import functools import heapq import inspect import logging import types from typing import Any, Callable -from collections.abc import Awaitable, Generator, Iterator _LOGGER = logging.getLogger(__name__) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 9a4cb2269a..7a82d5cba1 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -1,8 +1,8 @@ import abc +from collections.abc import Sequence import inspect import math import re -from collections.abc import Sequence from typing import Any, Callable, Optional, Union from esphome.core import ( diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 825224bb9d..9a775bad33 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -12,15 +12,13 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, KEY_PAST_SAFE_MODE, ) - -from esphome.core import coroutine, ID, CORE +from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.types import ConfigType, ConfigFragmentType from esphome.cpp_generator import add, get_variable from esphome.cpp_types import App +from esphome.helpers import sanitize, snake_case +from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry -from esphome.helpers import snake_case, sanitize - _LOGGER = logging.getLogger(__name__) diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py index 875ff6b91f..eec0777da6 100644 --- a/esphome/dashboard/core.py +++ b/esphome/dashboard/core.py @@ -1,13 +1,13 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine import contextlib -import logging -import threading from dataclasses import dataclass from functools import partial +import logging +import threading from typing import TYPE_CHECKING, Any, Callable -from collections.abc import Coroutine from ..zeroconf import DiscoveredImport from .dns import DNSCache diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 2be98ab3e4..9de2d39ce2 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,14 +1,14 @@ from __future__ import annotations import asyncio +from asyncio import events +from concurrent.futures import ThreadPoolExecutor import logging import os import socket import threading -import traceback -from asyncio import events -from concurrent.futures import ThreadPoolExecutor from time import monotonic +import traceback from typing import Any from esphome.storage_json import EsphomeStorageJSON, esphome_storage_path diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index 7a9bff4ec1..cb0d4a3772 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -1,9 +1,9 @@ from __future__ import annotations import asyncio +from collections import defaultdict import logging import os -from collections import defaultdict from typing import TYPE_CHECKING, Any from esphome import const, util diff --git a/esphome/dashboard/util/file.py b/esphome/dashboard/util/file.py index 661d5f34cf..bb263f9ad7 100644 --- a/esphome/dashboard/util/file.py +++ b/esphome/dashboard/util/file.py @@ -1,7 +1,7 @@ import logging import os -import tempfile from pathlib import Path +import tempfile _LOGGER = logging.getLogger(__name__) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 33c83ffb1a..e4b7b8d342 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio import base64 +from collections.abc import Iterable import datetime import functools import gzip @@ -9,13 +10,12 @@ import hashlib import json import logging import os +from pathlib import Path import secrets import shutil import subprocess import threading import time -from collections.abc import Iterable -from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, TypeVar from urllib.parse import urlparse @@ -26,13 +26,13 @@ import tornado.httpserver import tornado.httputil import tornado.ioloop import tornado.iostream +from tornado.log import access_log import tornado.netutil import tornado.process import tornado.queues import tornado.web import tornado.websocket import yaml -from tornado.log import access_log from yaml.nodes import Node from esphome import const, platformio_api, yaml_util diff --git a/esphome/external_files.py b/esphome/external_files.py index f8eb1dcabe..baf62286e4 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -1,13 +1,15 @@ from __future__ import annotations -import logging -from pathlib import Path -import os from datetime import datetime +import logging +import os +from pathlib import Path + import requests + import esphome.config_validation as cv -from esphome.core import CORE, TimePeriodSeconds from esphome.const import __version__ +from esphome.core import CORE, TimePeriodSeconds _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@landonr"] diff --git a/esphome/final_validate.py b/esphome/final_validate.py index 5e9d2207b0..cebd2f1cda 100644 --- a/esphome/final_validate.py +++ b/esphome/final_validate.py @@ -1,9 +1,9 @@ from abc import ABC, abstractmethod -from typing import Any import contextvars +from typing import Any -from esphome.types import ConfigFragmentType, ID, ConfigPathType import esphome.config_validation as cv +from esphome.types import ID, ConfigFragmentType, ConfigPathType class FinalValidateConfig(ABC): diff --git a/esphome/git.py b/esphome/git.py index e41777f425..144c160b20 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -1,12 +1,12 @@ -import hashlib -import logging -import re -import subprocess -import urllib.parse from dataclasses import dataclass from datetime import datetime +import hashlib +import logging from pathlib import Path +import re +import subprocess from typing import Callable, Optional +import urllib.parse import esphome.config_validation as cv from esphome.core import CORE, TimePeriodSeconds diff --git a/esphome/helpers.py b/esphome/helpers.py index 4c8cb4e2cc..2a7e5cd9b6 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,14 +1,13 @@ import codecs from contextlib import suppress - import logging import os -import platform from pathlib import Path -from typing import Union -import tempfile -from urllib.parse import urlparse +import platform import re +import tempfile +from typing import Union +from urllib.parse import urlparse _LOGGER = logging.getLogger(__name__) @@ -129,9 +128,10 @@ def _resolve_with_zeroconf(host): def resolve_ip_address(host): - from esphome.core import EsphomeError import socket + from esphome.core import EsphomeError + errs = [] if host.endswith(".local"): diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 667a20bcf8..d7e14a1d08 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -1,10 +1,10 @@ from datetime import datetime import hashlib +import json import logging import ssl import sys import time -import json import paho.mqtt.client as mqtt @@ -24,9 +24,9 @@ from esphome.const import ( CONF_USERNAME, ) from esphome.core import CORE, EsphomeError -from esphome.log import color, Fore +from esphome.helpers import get_int_env, get_str_env +from esphome.log import Fore, color from esphome.util import safe_print -from esphome.helpers import get_str_env, get_int_env _LOGGER = logging.getLogger(__name__) diff --git a/esphome/pins.py b/esphome/pins.py index 5ccb696738..724cd25d82 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -1,20 +1,20 @@ -import operator from functools import reduce -import esphome.config_validation as cv -from esphome.core import CORE +import operator +import esphome.config_validation as cv from esphome.const import ( + CONF_ALLOW_OTHER_USES, + CONF_IGNORE_STRAPPING_WARNING, CONF_INPUT, + CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_OPEN_DRAIN, CONF_OUTPUT, CONF_PULLDOWN, CONF_PULLUP, - CONF_IGNORE_STRAPPING_WARNING, - CONF_ALLOW_OTHER_USES, - CONF_INVERTED, ) +from esphome.core import CORE class PinRegistry(dict): diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index c46a3fc767..b81ec4ab37 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -1,12 +1,11 @@ from dataclasses import dataclass import json -from typing import Union -from pathlib import Path - import logging import os +from pathlib import Path import re import subprocess +from typing import Union from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE from esphome.core import CORE, EsphomeError @@ -20,9 +19,10 @@ def patch_structhash(): # removed/added. This might have unintended consequences, but this improves compile # times greatly when adding/removing components and a simple clean build solves # all issues - from platformio.run import helpers, cli - from os.path import join, isdir, getmtime from os import makedirs + from os.path import getmtime, isdir, join + + from platformio.run import cli, helpers def patched_clean_build_dir(build_dir, *args): from platformio import fs diff --git a/esphome/storage_json.py b/esphome/storage_json.py index 0a41a4f738..e2e7514904 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -1,10 +1,11 @@ from __future__ import annotations + import binascii import codecs +from datetime import datetime import json import logging import os -from datetime import datetime from esphome import const from esphome.const import CONF_DISABLED, CONF_MDNS diff --git a/esphome/types.py b/esphome/types.py index 27ec61ceff..4e69e3cbd7 100644 --- a/esphome/types.py +++ b/esphome/types.py @@ -2,7 +2,7 @@ from typing import Union -from esphome.core import ID, Lambda, EsphomeCore +from esphome.core import ID, EsphomeCore, Lambda ConfigFragmentType = Union[ str, diff --git a/esphome/util.py b/esphome/util.py index d5a4c60570..32fd90cd25 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -1,13 +1,12 @@ -from typing import Union - import collections import io import logging import os +from pathlib import Path import re import subprocess import sys -from pathlib import Path +from typing import Union from esphome import const diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 9af6cb717c..7f1573b443 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -2,6 +2,7 @@ import difflib import itertools import voluptuous as vol + from esphome.schema_extractors import schema_extractor_extended diff --git a/esphome/vscode.py b/esphome/vscode.py index 8198d2659a..907ed88216 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -1,13 +1,14 @@ from __future__ import annotations + +from io import StringIO import json import os -from io import StringIO from typing import Any -from esphome.yaml_util import parse_yaml -from esphome.config import validate_config, _format_vol_invalid, Config -from esphome.core import CORE, DocumentRange +from esphome.config import Config, _format_vol_invalid, validate_config import esphome.config_validation as cv +from esphome.core import CORE, DocumentRange +from esphome.yaml_util import parse_yaml def _get_invalid_range(res: Config, invalid: cv.Invalid) -> DocumentRange | None: diff --git a/esphome/wizard.py b/esphome/wizard.py index f8911ae844..319fb31938 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -276,8 +276,8 @@ def wizard(path): from esphome.components.bk72xx import boards as bk72xx_boards from esphome.components.esp32 import boards as esp32_boards from esphome.components.esp8266 import boards as esp8266_boards - from esphome.components.rtl87xx import boards as rtl87xx_boards from esphome.components.rp2040 import boards as rp2040_boards + from esphome.components.rtl87xx import boards as rtl87xx_boards if not path.endswith(".yaml") and not path.endswith(".yml"): safe_print( diff --git a/esphome/writer.py b/esphome/writer.py index 3ad0e60d31..c6111cbe3f 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,27 +1,27 @@ import logging import os -import re from pathlib import Path +import re from typing import Union -from esphome.config import iter_components, iter_component_configs +from esphome import loader +from esphome.config import iter_component_configs, iter_components from esphome.const import ( + ENV_NOGITIGNORE, HEADER_FILE_EXTENSIONS, SOURCE_FILE_EXTENSIONS, __version__, - ENV_NOGITIGNORE, ) from esphome.core import CORE, EsphomeError from esphome.helpers import ( - mkdir_p, - read_file, - write_file_if_changed, - walk_files, copy_file_if_changed, get_bool_env, + mkdir_p, + read_file, + walk_files, + write_file_if_changed, ) from esphome.storage_json import StorageJSON, storage_path -from esphome import loader _LOGGER = logging.getLogger(__name__) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 06bfd8b217..d67511dfec 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -3,16 +3,16 @@ from __future__ import annotations import fnmatch import functools import inspect +from io import TextIOWrapper import logging import math import os -import uuid -from io import TextIOWrapper from typing import Any +import uuid import yaml -import yaml.constructor from yaml import SafeLoader as PurePythonLoader +import yaml.constructor try: from yaml import CSafeLoader as FastestAvailableSafeLoader diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index b67ea41323..b3ee64e259 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -1,8 +1,8 @@ from __future__ import annotations import asyncio -import logging from dataclasses import dataclass +import logging from typing import Callable from zeroconf import IPVersion, ServiceInfo, ServiceStateChange, Zeroconf From b3728697cc3997899bd7cb20fdeb9d8f3b1f06ae Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:13:57 +1200 Subject: [PATCH 005/160] Remove deprecated argument parser (#7151) * Remove deprecated argument parser * Add back removed argcomplete line --- esphome/__main__.py | 68 --------------------------------------------- 1 file changed, 68 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 13f09e15ed..7237a04717 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -957,67 +957,6 @@ def parse_args(argv): # a deprecation warning). arguments = argv[1:] - # On Python 3.9+ we can simply set exit_on_error=False in the constructor - def _raise(x): - raise argparse.ArgumentError(None, x) - - # First, try new-style parsing, but don't exit in case of failure - try: - # duplicate parser so that we can use the original one to raise errors later on - current_parser = argparse.ArgumentParser(add_help=False, parents=[parser]) - current_parser.set_defaults(deprecated_argv_suggestion=None) - current_parser.error = _raise - return current_parser.parse_args(arguments) - except argparse.ArgumentError: - pass - - # Second, try compat parsing and rearrange the command-line if it succeeds - # Disable argparse's built-in help option and add it manually to prevent this - # parser from printing the help messagefor the old format when invoked with -h. - compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False) - compat_parser.add_argument("-h", "--help", action="store_true") - compat_parser.add_argument("configuration", nargs="*") - compat_parser.add_argument( - "command", - choices=[ - "config", - "compile", - "upload", - "logs", - "run", - "clean-mqtt", - "wizard", - "mqtt-fingerprint", - "version", - "clean", - "dashboard", - "vscode", - "update-all", - ], - ) - - try: - compat_parser.error = _raise - result, unparsed = compat_parser.parse_known_args(argv[1:]) - last_option = len(arguments) - len(unparsed) - 1 - len(result.configuration) - unparsed = [ - "--device" if arg in ("--upload-port", "--serial-port") else arg - for arg in unparsed - ] - arguments = ( - arguments[0:last_option] - + [result.command] - + result.configuration - + unparsed - ) - deprecated_argv_suggestion = arguments - except argparse.ArgumentError: - # old-style parsing failed, don't suggest any argument - deprecated_argv_suggestion = None - - # Finally, run the new-style parser again with the possibly swapped arguments, - # and let it error out if the command is unparsable. - parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion) argcomplete.autocomplete(parser) return parser.parse_args(arguments) @@ -1032,13 +971,6 @@ def run_esphome(argv): # Show timestamp for dashboard access logs args.command == "dashboard", ) - if args.deprecated_argv_suggestion is not None and args.command != "vscode": - _LOGGER.warning( - "Calling ESPHome with the configuration before the command is deprecated " - "and will be removed in the future. " - ) - _LOGGER.warning("Please instead use:") - _LOGGER.warning(" esphome %s", " ".join(args.deprecated_argv_suggestion)) if sys.version_info < (3, 8, 0): _LOGGER.error( From 24515546fd25a265dbb704890a6d7c9285775b17 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:22:30 +1200 Subject: [PATCH 006/160] Move ``CONF_ON_ERROR`` to const.py (#7156) --- esphome/components/ota/__init__.py | 12 ++++++--- .../components/voice_assistant/__init__.py | 27 +++++++++---------- esphome/const.py | 1 + 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 4e447bfb2d..d9917a2aae 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -1,10 +1,15 @@ +from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation +from esphome.const import ( + CONF_ESPHOME, + CONF_ON_ERROR, + CONF_OTA, + CONF_PLATFORM, + CONF_TRIGGER_ID, +) from esphome.core import CORE, coroutine_with_priority -from esphome.const import CONF_ESPHOME, CONF_OTA, CONF_PLATFORM, CONF_TRIGGER_ID - CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["md5", "safe_mode"] @@ -13,7 +18,6 @@ IS_PLATFORM_COMPONENT = True CONF_ON_ABORT = "on_abort" CONF_ON_BEGIN = "on_begin" CONF_ON_END = "on_end" -CONF_ON_ERROR = "on_error" CONF_ON_PROGRESS = "on_progress" CONF_ON_STATE_CHANGE = "on_state_change" diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index c18f0a6850..031edbf27a 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -1,18 +1,18 @@ -import esphome.config_validation as cv -import esphome.codegen as cg - -from esphome.const import ( - CONF_ID, - CONF_MICROPHONE, - CONF_SPEAKER, - CONF_MEDIA_PLAYER, - CONF_ON_CLIENT_CONNECTED, - CONF_ON_CLIENT_DISCONNECTED, - CONF_ON_IDLE, -) from esphome import automation from esphome.automation import register_action, register_condition -from esphome.components import microphone, speaker, media_player +import esphome.codegen as cg +from esphome.components import media_player, microphone, speaker +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MEDIA_PLAYER, + CONF_MICROPHONE, + CONF_ON_CLIENT_CONNECTED, + CONF_ON_CLIENT_DISCONNECTED, + CONF_ON_ERROR, + CONF_ON_IDLE, + CONF_SPEAKER, +) AUTO_LOAD = ["socket"] DEPENDENCIES = ["api", "microphone"] @@ -20,7 +20,6 @@ DEPENDENCIES = ["api", "microphone"] CODEOWNERS = ["@jesserockz"] CONF_ON_END = "on_end" -CONF_ON_ERROR = "on_error" CONF_ON_INTENT_END = "on_intent_end" CONF_ON_INTENT_START = "on_intent_start" CONF_ON_LISTENING = "on_listening" diff --git a/esphome/const.py b/esphome/const.py index faf6ce19fa..4357963384 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -539,6 +539,7 @@ CONF_ON_DOUBLE_CLICK = "on_double_click" CONF_ON_ENROLLMENT_DONE = "on_enrollment_done" CONF_ON_ENROLLMENT_FAILED = "on_enrollment_failed" CONF_ON_ENROLLMENT_SCAN = "on_enrollment_scan" +CONF_ON_ERROR = "on_error" CONF_ON_EVENT = "on_event" CONF_ON_FINGER_SCAN_INVALID = "on_finger_scan_invalid" CONF_ON_FINGER_SCAN_MATCHED = "on_finger_scan_matched" From 5b6b7c0d15098f7477bae68329fe76a1d8993cf5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:25:53 +1200 Subject: [PATCH 007/160] [code-quality] Organise esp32 imports (#7154) --- esphome/components/esp32/__init__.py | 19 ++++++++--------- esphome/components/esp32/boards.py | 2 +- esphome/components/esp32/gpio.py | 25 +++++++++++------------ esphome/components/esp32/gpio_esp32.py | 3 +-- esphome/components/esp32/gpio_esp32_c2.py | 3 +-- esphome/components/esp32/gpio_esp32_c3.py | 6 +----- esphome/components/esp32/gpio_esp32_c6.py | 3 +-- esphome/components/esp32/gpio_esp32_h2.py | 3 +-- esphome/components/esp32/gpio_esp32_s2.py | 3 +-- esphome/components/esp32/gpio_esp32_s3.py | 7 +------ 10 files changed, 29 insertions(+), 45 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1effea708f..0a5dd46478 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1,11 +1,12 @@ from dataclasses import dataclass -from typing import Union, Optional -from pathlib import Path import logging import os -import esphome.final_validate as fv +from pathlib import Path +from typing import Optional, Union -from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p +from esphome import git +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( CONF_ADVANCED, CONF_BOARD, @@ -15,6 +16,7 @@ from esphome.const import ( CONF_IGNORE_EFUSE_MAC_CRC, CONF_NAME, CONF_PATH, + CONF_PLATFORM_VERSION, CONF_PLATFORMIO_OPTIONS, CONF_REF, CONF_REFRESH, @@ -32,13 +34,12 @@ from esphome.const import ( TYPE_GIT, TYPE_LOCAL, __version__, - CONF_PLATFORM_VERSION, ) from esphome.core import CORE, HexInt, TimePeriod -import esphome.config_validation as cv -import esphome.codegen as cg -from esphome import git +import esphome.final_validate as fv +from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed +from .boards import BOARDS from .const import ( # noqa KEY_BOARD, KEY_COMPONENTS, @@ -54,12 +55,10 @@ from .const import ( # noqa VARIANT_FRIENDLY, VARIANTS, ) -from .boards import BOARDS # force import gpio to register pin schema from .gpio import esp32_pin_to_code # noqa - _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["preferences"] diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index cd85f3da97..60abcd447c 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1,4 +1,4 @@ -from .const import VARIANT_ESP32, VARIANT_ESP32S2, VARIANT_ESP32C3, VARIANT_ESP32S3 +from .const import VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3 ESP32_BASE_PINS = { "TX": 1, diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 0d9cb5daf0..558ff51af8 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -1,22 +1,22 @@ from dataclasses import dataclass -from typing import Any import logging +from typing import Any +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_IGNORE_PIN_VALIDATION_ERROR, + CONF_IGNORE_STRAPPING_WARNING, CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_OPEN_DRAIN, CONF_OUTPUT, - CONF_IGNORE_PIN_VALIDATION_ERROR, - CONF_IGNORE_STRAPPING_WARNING, PLATFORM_ESP32, ) -from esphome import pins from esphome.core import CORE -import esphome.config_validation as cv -import esphome.codegen as cg from . import boards from .const import ( @@ -24,22 +24,21 @@ from .const import ( KEY_ESP32, KEY_VARIANT, VARIANT_ESP32, - VARIANT_ESP32C3, - VARIANT_ESP32S2, - VARIANT_ESP32S3, VARIANT_ESP32C2, + VARIANT_ESP32C3, VARIANT_ESP32C6, VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, esp32_ns, ) - from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports -from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports -from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports -from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_supports +from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports +from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports +from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin) diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index d10b266c7a..e4d3b6aaf3 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -1,5 +1,6 @@ import logging +import esphome.config_validation as cv from esphome.const import ( CONF_INPUT, CONF_MODE, @@ -8,10 +9,8 @@ from esphome.const import ( CONF_PULLDOWN, CONF_PULLUP, ) -import esphome.config_validation as cv from esphome.pins import check_strapping_pin - _ESP_SDIO_PINS = { 6: "Flash Clock", 7: "Flash Data 0", diff --git a/esphome/components/esp32/gpio_esp32_c2.py b/esphome/components/esp32/gpio_esp32_c2.py index 0bee7d82bf..abdcb1b655 100644 --- a/esphome/components/esp32/gpio_esp32_c2.py +++ b/esphome/components/esp32/gpio_esp32_c2.py @@ -1,10 +1,9 @@ import logging +import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin -import esphome.config_validation as cv - _ESP32C2_STRAPPING_PINS = {8, 9} _LOGGER = logging.getLogger(__name__) diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py index 6c70c09f9e..5b9ec0ebd9 100644 --- a/esphome/components/esp32/gpio_esp32_c3.py +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -1,11 +1,7 @@ import logging -from esphome.const import ( - CONF_INPUT, - CONF_MODE, - CONF_NUMBER, -) import esphome.config_validation as cv +from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin _ESP32C3_SPI_PSRAM_PINS = { diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index a1f777c625..bc735f85c4 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -1,8 +1,7 @@ import logging -from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER - import esphome.config_validation as cv +from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin _ESP32C6_SPI_PSRAM_PINS = { diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index d18ee8a2a6..7413bf4db5 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -1,8 +1,7 @@ import logging -from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER - import esphome.config_validation as cv +from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER _ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21} diff --git a/esphome/components/esp32/gpio_esp32_s2.py b/esphome/components/esp32/gpio_esp32_s2.py index 82449532ec..331aeb9d94 100644 --- a/esphome/components/esp32/gpio_esp32_s2.py +++ b/esphome/components/esp32/gpio_esp32_s2.py @@ -1,5 +1,6 @@ import logging +import esphome.config_validation as cv from esphome.const import ( CONF_INPUT, CONF_MODE, @@ -8,8 +9,6 @@ from esphome.const import ( CONF_PULLDOWN, CONF_PULLUP, ) - -import esphome.config_validation as cv from esphome.pins import check_strapping_pin _ESP32S2_SPI_PSRAM_PINS = { diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index 8dcbf8c7bb..7120504693 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -1,12 +1,7 @@ import logging -from esphome.const import ( - CONF_INPUT, - CONF_MODE, - CONF_NUMBER, -) - import esphome.config_validation as cv +from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin _ESP_32S3_SPI_PSRAM_PINS = { From 341fc659589a592fef60321227209f1014ccf34e Mon Sep 17 00:00:00 2001 From: FreeBear-nc <67865163+FreeBear-nc@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:05:25 +0100 Subject: [PATCH 008/160] Add microAmp and milliAmp to defined units (#7157) --- esphome/const.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/const.py b/esphome/const.py index 4357963384..37844e1047 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1033,11 +1033,13 @@ UNIT_KILOWATT_HOURS = "kWh" UNIT_LUX = "lx" UNIT_METER = "m" UNIT_METER_PER_SECOND_SQUARED = "m/s²" +UNIT_MICROAMP = "µA" UNIT_MICROGRAMS_PER_CUBIC_METER = "µg/m³" UNIT_MICROMETER = "µm" UNIT_MICROSIEMENS_PER_CENTIMETER = "µS/cm" UNIT_MICROSILVERTS_PER_HOUR = "µSv/h" UNIT_MICROTESLA = "µT" +UNIT_MILLIAMP = "mA" UNIT_MILLIGRAMS_PER_CUBIC_METER = "mg/m³" UNIT_MILLIMETER = "mm" UNIT_MILLISECOND = "ms" From 25c8676d80cb9a6b75cd6413aa4f9a99f3a662e5 Mon Sep 17 00:00:00 2001 From: RubyBailey <60991881+RubyBailey@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:20:29 -0700 Subject: [PATCH 009/160] Fix for Mitsubishi units that only support cooling (#7143) --- esphome/components/mitsubishi/mitsubishi.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index a02aabf14d..449c8fc712 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -110,7 +110,7 @@ void MitsubishiClimate::transmit_state() { // Byte 15: HVAC specfic, i.e. POWERFUL, SMART SET, PLASMA, always 0x00 // Byte 16: Constant 0x00 // Byte 17: Checksum: SUM[Byte0...Byte16] - uint8_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x08, 0x00, 0x00, + uint8_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; switch (this->mode) { @@ -136,6 +136,12 @@ void MitsubishiClimate::transmit_state() { break; case climate::CLIMATE_MODE_OFF: default: + remote_state[6] = MITSUBISHI_MODE_COOL; + remote_state[8] = MITSUBISHI_MODE_A_COOL; + if (this->supports_heat_) { + remote_state[6] = MITSUBISHI_MODE_HEAT; + remote_state[8] = MITSUBISHI_MODE_A_HEAT; + } remote_state[5] = MITSUBISHI_OFF; break; } From 12e840ee88705b063817e48c7f8a15f817103ad5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 11:36:09 +1200 Subject: [PATCH 010/160] Bump docker/setup-buildx-action from 3.5.0 to 3.6.1 in the docker-actions group (#7159) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-docker.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 2b4539105b..91c02b0a17 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -46,7 +46,7 @@ jobs: with: python-version: "3.9" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.5.0 + uses: docker/setup-buildx-action@v3.6.1 - name: Set up QEMU uses: docker/setup-qemu-action@v3.2.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index efec556059..d454076c84 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,7 +90,7 @@ jobs: python-version: "3.9" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.5.0 + uses: docker/setup-buildx-action@v3.6.1 - name: Set up QEMU if: matrix.platform != 'linux/amd64' uses: docker/setup-qemu-action@v3.2.0 @@ -184,7 +184,7 @@ jobs: merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.5.0 + uses: docker/setup-buildx-action@v3.6.1 - name: Log in to docker hub if: matrix.registry == 'dockerhub' From 7c1aa771aaa9c9ea2f2c42a0b981ef8267a9d664 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:41:34 +1000 Subject: [PATCH 011/160] LVGL stage 2 (#7129) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lvgl/__init__.py | 38 +++++--- esphome/components/lvgl/btn.py | 25 +++++ esphome/components/lvgl/defines.py | 9 +- esphome/components/lvgl/helpers.py | 1 - esphome/components/lvgl/label.py | 21 ++-- esphome/components/lvgl/lv_validation.py | 79 ++++++++++++--- esphome/components/lvgl/lvgl_esphome.cpp | 6 ++ esphome/components/lvgl/lvgl_esphome.h | 59 ++++++++++-- esphome/components/lvgl/obj.py | 11 +-- esphome/components/lvgl/schemas.py | 64 +++++++------ esphome/components/lvgl/touchscreens.py | 46 +++++++++ esphome/components/lvgl/types.py | 112 +++++++++++++++++----- esphome/components/lvgl/widget.py | 73 ++------------ esphome/core/defines.h | 1 + tests/components/lvgl/common.yaml | 10 ++ tests/components/lvgl/logo-text.svg | 25 +++++ tests/components/lvgl/lvgl-package.yaml | 102 +++++++++++++++++++- tests/components/lvgl/test.esp32-idf.yaml | 4 +- 18 files changed, 503 insertions(+), 183 deletions(-) create mode 100644 esphome/components/lvgl/btn.py create mode 100644 esphome/components/lvgl/touchscreens.py create mode 100644 tests/components/lvgl/logo-text.svg diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 2f3bd69546..c454a61957 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -16,13 +16,20 @@ from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid +from .btn import btn_spec from .label import label_spec from .lvcode import ConstantLiteral, LvContext - -# from .menu import menu_spec from .obj import obj_spec -from .schemas import WIDGET_TYPES, any_widget_schema, obj_schema -from .types import FontEngine, LvglComponent, lv_disp_t_ptr, lv_font_t, lvgl_ns +from .schemas import any_widget_schema, obj_schema +from .touchscreens import touchscreen_schema, touchscreens_to_code +from .types import ( + WIDGET_TYPES, + FontEngine, + LvglComponent, + lv_disp_t_ptr, + lv_font_t, + lvgl_ns, +) from .widget import LvScrActType, Widget, add_widgets, set_obj_properties DOMAIN = "lvgl" @@ -31,11 +38,8 @@ AUTO_LOAD = ("key_provider",) CODEOWNERS = ("@clydebarrow",) LOGGER = logging.getLogger(__name__) -for widg in ( - label_spec, - obj_spec, -): - WIDGET_TYPES[widg.name] = widg +for w_type in (label_spec, obj_spec, btn_spec): + WIDGET_TYPES[w_type.name] = w_type lv_scr_act_spec = LvScrActType() lv_scr_act = Widget.create( @@ -93,7 +97,7 @@ def final_validation(config): "Using auto_clear_enabled: true in display config not compatible with LVGL" ) buffer_frac = config[CONF_BUFFER_SIZE] - if not CORE.is_host and buffer_frac > 0.5 and "psram" not in global_config: + if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: LOGGER.warning("buffer_size: may need to be reduced without PSRAM") @@ -132,7 +136,7 @@ async def to_code(config): cg.add_global(lvgl_ns.using) lv_component = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(lv_component, config) - Widget.create(config[CONF_ID], lv_component, WIDGET_TYPES[df.CONF_OBJ], config) + Widget.create(config[CONF_ID], lv_component, obj_spec, config) for display in config[df.CONF_DISPLAYS]: cg.add(lv_component.add_display(await cg.get_variable(display))) @@ -152,7 +156,7 @@ async def to_code(config): await cg.get_variable(font) cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font)) default_font = config[df.CONF_DEFAULT_FONT] - if default_font not in helpers.lv_fonts_used: + if not lvalid.is_lv_font(default_font): add_define( "LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})" ) @@ -161,12 +165,15 @@ async def to_code(config): True, type=lv_font_t.operator("ptr").operator("const"), ) - cg.new_variable(globfont_id, MockObj(default_font)) + cg.new_variable( + globfont_id, MockObj(await lvalid.lv_font.process(default_font)) + ) add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) else: - add_define("LV_FONT_DEFAULT", default_font) + add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) with LvContext(): + await touchscreens_to_code(lv_component, config) await set_obj_properties(lv_scr_act, config) await add_widgets(lv_scr_act, config) Widget.set_completed() @@ -190,7 +197,7 @@ FINAL_VALIDATE_SCHEMA = final_validation CONFIG_SCHEMA = ( cv.polling_component_schema("1s") - .extend(obj_schema("obj")) + .extend(obj_schema(obj_spec)) .extend( { cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent), @@ -207,6 +214,7 @@ CONFIG_SCHEMA = ( ), cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA), cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, + cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema, } ) ).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS)) diff --git a/esphome/components/lvgl/btn.py b/esphome/components/lvgl/btn.py new file mode 100644 index 0000000000..4f5f88d9e6 --- /dev/null +++ b/esphome/components/lvgl/btn.py @@ -0,0 +1,25 @@ +from esphome.const import CONF_BUTTON +from esphome.cpp_generator import MockObjClass + +from .defines import CONF_MAIN +from .types import LvBoolean, WidgetType + + +class BtnType(WidgetType): + def __init__(self): + super().__init__(CONF_BUTTON, LvBoolean("lv_btn_t"), (CONF_MAIN,)) + + async def to_code(self, w, config): + return [] + + def obj_creator(self, parent: MockObjClass, config: dict): + """ + LVGL 8 calls buttons `btn` + """ + return f"lv_btn_create({parent})" + + def get_uses(self): + return ("btn",) + + +btn_spec = BtnType() diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 50bdac3865..a2b4ac13fb 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -446,6 +446,7 @@ CONF_TILE_ID = "tile_id" CONF_TILES = "tiles" CONF_TITLE = "title" CONF_TOP_LAYER = "top_layer" +CONF_TOUCHSCREENS = "touchscreens" CONF_TRANSPARENCY_KEY = "transparency_key" CONF_THEME = "theme" CONF_VISIBLE_ROW_COUNT = "visible_row_count" @@ -474,14 +475,8 @@ LV_KEYS = LvConstant( ) -# list of widgets and the parts allowed -WIDGET_PARTS = { - CONF_LABEL: (CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED), - CONF_OBJ: (CONF_MAIN,), -} - DEFAULT_ESPHOME_FONT = "esphome_lv_default_font" def join_enums(enums, prefix=""): - return "|".join(f"(int){prefix}{e.upper()}" for e in enums) + return ConstantLiteral("|".join(f"(int){prefix}{e.upper()}" for e in enums)) diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index c8d4948fb1..d67739155c 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -22,7 +22,6 @@ def add_lv_use(*names): lv_fonts_used = set() esphome_fonts_used = set() -REQUIRED_COMPONENTS = {} lvgl_components_required = set() diff --git a/esphome/components/lvgl/label.py b/esphome/components/lvgl/label.py index 5c4ae6ab0d..0498f39474 100644 --- a/esphome/components/lvgl/label.py +++ b/esphome/components/lvgl/label.py @@ -1,16 +1,27 @@ import esphome.config_validation as cv -from .defines import CONF_LABEL, CONF_LONG_MODE, CONF_RECOLOR, CONF_TEXT, LV_LONG_MODES +from .defines import ( + CONF_LABEL, + CONF_LONG_MODE, + CONF_MAIN, + CONF_RECOLOR, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_TEXT, + LV_LONG_MODES, +) from .lv_validation import lv_bool, lv_text from .schemas import TEXT_SCHEMA -from .types import lv_label_t -from .widget import Widget, WidgetType +from .types import LvText, WidgetType +from .widget import Widget class LabelType(WidgetType): def __init__(self): super().__init__( CONF_LABEL, + LvText("lv_label_t"), + (CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED), TEXT_SCHEMA.extend( { cv.Optional(CONF_RECOLOR): lv_bool, @@ -19,10 +30,6 @@ class LabelType(WidgetType): ), ) - @property - def w_type(self): - return lv_label_t - async def to_code(self, w: Widget, config): """For a text object, create and set text""" if value := config.get(CONF_TEXT): diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 1de63c30ce..533dc582f0 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -8,6 +8,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT from esphome.core import HexInt from esphome.cpp_generator import MockObj +from esphome.cpp_types import uint32 from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor @@ -23,6 +24,28 @@ from .lvcode import ConstantLiteral, lv_expr from .types import lv_font_t +def literal_mapper(value, args=()): + if isinstance(value, str): + return ConstantLiteral(value) + return value + + +opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") + + +@schema_extractor("one_of") +def opacity_validator(value): + if value == SCHEMA_EXTRACT: + return opacity_consts.choices + value = cv.Any(cv.percentage, opacity_consts.one_of)(value) + if isinstance(value, float): + return int(value * 255) + return value + + +opacity = LValidator(opacity_validator, uint32, retmapper=literal_mapper) + + @schema_extractor("one_of") def color(value): if value == SCHEMA_EXTRACT: @@ -43,16 +66,24 @@ def color_retmapper(value): return lv_expr.color_from(MockObj(value)) -def pixels_or_percent(value): +lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper) + + +def pixels_or_percent_validator(value): """A length in one axis - either a number (pixels) or a percentage""" if value == SCHEMA_EXTRACT: return ["pixels", "..%"] if isinstance(value, int): - return str(cv.int_(value)) + return cv.int_(value) # Will throw an exception if not a percentage. return f"lv_pct({int(cv.percentage(value) * 100)})" +pixels_or_percent = LValidator( + pixels_or_percent_validator, uint32, retmapper=literal_mapper +) + + def zoom(value): value = cv.float_range(0.1, 10.0)(value) return int(value * 256) @@ -68,7 +99,7 @@ def angle(value): @schema_extractor("one_of") -def size(value): +def size_validator(value): """A size in one axis - one of "size_content", a number (pixels) or a percentage""" if value == SCHEMA_EXTRACT: return ["size_content", "pixels", "..%"] @@ -79,28 +110,42 @@ def size(value): return "LV_SIZE_CONTENT" raise cv.Invalid("must be 'size_content', a pixel position or a percentage") if isinstance(value, int): - return str(cv.int_(value)) + return cv.int_(value) # Will throw an exception if not a percentage. return f"lv_pct({int(cv.percentage(value) * 100)})" +size = LValidator(size_validator, uint32, retmapper=literal_mapper) + +radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") + + @schema_extractor("one_of") -def opacity(value): - consts = LvConstant("LV_OPA_", "TRANSP", "COVER") +def radius_validator(value): if value == SCHEMA_EXTRACT: - return consts.choices - value = cv.Any(cv.percentage, consts.one_of)(value) + return radius_consts.choices + value = cv.Any(size, cv.percentage, radius_consts.one_of)(value) if isinstance(value, float): return int(value * 255) return value +def id_name(value): + if value == SCHEMA_EXTRACT: + return "id" + return cv.validate_id_name(value) + + +radius = LValidator(radius_validator, uint32, retmapper=literal_mapper) + + def stop_value(value): return cv.int_range(0, 255)(value) -lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper) -lv_bool = LValidator(cv.boolean, cg.bool_, BinarySensor, "get_state()") +lv_bool = LValidator( + cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal_mapper +) def lvms_validator_(value): @@ -145,26 +190,32 @@ lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()") +def is_lv_font(font): + return isinstance(font, str) and font.lower() in LV_FONTS + + class LvFont(LValidator): def __init__(self): def lv_builtin_font(value): fontval = cv.one_of(*LV_FONTS, lower=True)(value) lv_fonts_used.add(fontval) - return "&lv_font_" + fontval + return fontval def validator(value): if value == SCHEMA_EXTRACT: return LV_FONTS - if isinstance(value, str) and value.lower() in LV_FONTS: + if is_lv_font(value): return lv_builtin_font(value) fontval = cv.use_id(Font)(value) esphome_fonts_used.add(fontval) - return requires_component("font")(f"{fontval}_engine->get_lv_font()") + return requires_component("font")(fontval) super().__init__(validator, lv_font_t) async def process(self, value, args=()): - return ConstantLiteral(value) + if is_lv_font(value): + return ConstantLiteral(f"&lv_font_{value}") + return ConstantLiteral(f"{value}_engine->get_lv_font()") lv_font = LvFont() diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index bdaf8a4f18..74a1b0e7af 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -38,7 +38,9 @@ void LvglComponent::setup() { auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; auto *buf = lv_custom_mem_alloc(buf_bytes); if (buf == nullptr) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR ESP_LOGE(TAG, "Malloc failed to allocate %zu bytes", buf_bytes); +#endif this->mark_failed(); this->status_set_error("Memory allocation failure"); return; @@ -85,7 +87,9 @@ size_t lv_millis(void) { return esphome::millis(); } void *lv_custom_mem_alloc(size_t size) { auto *ptr = malloc(size); // NOLINT if (ptr == nullptr) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); +#endif } return ptr; } @@ -102,7 +106,9 @@ void *lv_custom_mem_alloc(size_t size) { ptr = heap_caps_malloc(size, cap_bits); } if (ptr == nullptr) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); +#endif return nullptr; } #ifdef ESPHOME_LOG_HAS_VERBOSE diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 988c22917b..a884a27042 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -18,23 +18,27 @@ #ifdef USE_LVGL_FONT #include "esphome/components/font/font.h" #endif +#ifdef USE_LVGL_TOUCHSCREEN +#include "esphome/components/touchscreen/touchscreen.h" +#endif // USE_LVGL_TOUCHSCREEN + namespace esphome { namespace lvgl { extern lv_event_code_t lv_custom_event; // NOLINT #ifdef USE_LVGL_COLOR -static lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); } -#endif +inline lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); } +#endif // USE_LVGL_COLOR #if LV_COLOR_DEPTH == 16 static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565; #elif LV_COLOR_DEPTH == 32 static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_888; -#else +#else // LV_COLOR_DEPTH static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332; -#endif +#endif // LV_COLOR_DEPTH // Parent class for things that wrap an LVGL object -class LvCompound { +class LvCompound final { public: virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } lv_obj_t *obj{}; @@ -99,6 +103,14 @@ class LvglComponent : public PollingComponent { void set_full_refresh(bool full_refresh) { this->full_refresh_ = full_refresh; } void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; } lv_disp_t *get_disp() { return this->disp_; } + void set_paused(bool paused, bool show_snow) { + this->paused_ = paused; + if (!paused && lv_scr_act() != nullptr) { + lv_disp_trig_activity(this->disp_); // resets the inactivity time + lv_obj_invalidate(lv_scr_act()); + } + } + bool is_paused() const { return this->paused_; } protected: void draw_buffer_(const lv_area_t *area, const uint8_t *ptr); @@ -107,13 +119,48 @@ class LvglComponent : public PollingComponent { lv_disp_draw_buf_t draw_buf_{}; lv_disp_drv_t disp_drv_{}; lv_disp_t *disp_{}; + bool paused_{}; std::vector> init_lambdas_; size_t buffer_frac_{1}; bool full_refresh_{}; }; +#ifdef USE_LVGL_TOUCHSCREEN +class LVTouchListener : public touchscreen::TouchListener, public Parented { + public: + LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) { + lv_indev_drv_init(&this->drv_); + this->drv_.long_press_repeat_time = long_press_repeat_time; + this->drv_.long_press_time = long_press_time; + this->drv_.type = LV_INDEV_TYPE_POINTER; + this->drv_.user_data = this; + this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { + auto *l = static_cast(d->user_data); + if (l->touch_pressed_) { + data->point.x = l->touch_point_.x; + data->point.y = l->touch_point_.y; + data->state = LV_INDEV_STATE_PRESSED; + } else { + data->state = LV_INDEV_STATE_RELEASED; + } + }; + } + void update(const touchscreen::TouchPoints_t &tpoints) override { + this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty(); + if (this->touch_pressed_) + this->touch_point_ = tpoints[0]; + } + void release() override { touch_pressed_ = false; } + lv_indev_drv_t *get_drv() { return &this->drv_; } + + protected: + lv_indev_drv_t drv_{}; + touchscreen::TouchPoint touch_point_{}; + bool touch_pressed_{}; +}; +#endif // USE_LVGL_TOUCHSCREEN } // namespace lvgl } // namespace esphome -#endif +#endif // USE_LVGL diff --git a/esphome/components/lvgl/obj.py b/esphome/components/lvgl/obj.py index fba20bef36..92c4f63d2d 100644 --- a/esphome/components/lvgl/obj.py +++ b/esphome/components/lvgl/obj.py @@ -1,6 +1,5 @@ -from .defines import CONF_OBJ -from .types import lv_obj_t -from .widget import WidgetType +from .defines import CONF_MAIN, CONF_OBJ +from .types import WidgetType, lv_obj_t class ObjType(WidgetType): @@ -9,11 +8,7 @@ class ObjType(WidgetType): """ def __init__(self): - super().__init__(CONF_OBJ, schema={}, modify_schema={}) - - @property - def w_type(self): - return lv_obj_t + super().__init__(CONF_OBJ, lv_obj_t, (CONF_MAIN,), schema={}, modify_schema={}) async def to_code(self, w, config): return [] diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 4ae5824151..9f6d984545 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -3,15 +3,9 @@ from esphome.const import CONF_ARGS, CONF_FORMAT, CONF_ID, CONF_STATE, CONF_TYPE from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid, types as ty -from .defines import WIDGET_PARTS -from .helpers import ( - REQUIRED_COMPONENTS, - add_lv_use, - requires_component, - validate_printf, -) +from .helpers import add_lv_use, requires_component, validate_printf from .lv_validation import lv_font -from .types import WIDGET_TYPES, get_widget_type +from .types import WIDGET_TYPES, WidgetType # A schema for text properties TEXT_SCHEMA = cv.Schema( @@ -46,9 +40,9 @@ STYLE_PROPS = { "bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of, "bg_grad_dir": df.LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER").one_of, "bg_grad_stop": lvalid.stop_value, - "bg_img_opa": lvalid.opacity, - "bg_img_recolor": lvalid.lv_color, - "bg_img_recolor_opa": lvalid.opacity, + "bg_image_opa": lvalid.opacity, + "bg_image_recolor": lvalid.lv_color, + "bg_image_recolor_opa": lvalid.opacity, "bg_main_stop": lvalid.stop_value, "bg_opa": lvalid.opacity, "border_color": lvalid.lv_color, @@ -60,8 +54,8 @@ STYLE_PROPS = { "border_width": cv.positive_int, "clip_corner": lvalid.lv_bool, "height": lvalid.size, - "img_recolor": lvalid.lv_color, - "img_recolor_opa": lvalid.opacity, + "image_recolor": lvalid.lv_color, + "image_recolor_opa": lvalid.opacity, "line_width": cv.positive_int, "line_dash_width": cv.positive_int, "line_dash_gap": cv.positive_int, @@ -108,12 +102,21 @@ STYLE_PROPS = { "max_width": lvalid.pixels_or_percent, "min_height": lvalid.pixels_or_percent, "min_width": lvalid.pixels_or_percent, - "radius": cv.Any(lvalid.size, df.LvConstant("LV_RADIUS_", "CIRCLE").one_of), + "radius": lvalid.radius, "width": lvalid.size, "x": lvalid.pixels_or_percent, "y": lvalid.pixels_or_percent, } +STYLE_REMAP = { + "bg_image_opa": "bg_img_opa", + "bg_image_recolor": "bg_img_recolor", + "bg_image_recolor_opa": "bg_img_recolor_opa", + "bg_image_src": "bg_img_src", + "image_recolor": "img_recolor", + "image_recolor_opa": "img_recolor_opa", +} + # Complete object style schema STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( { @@ -132,25 +135,23 @@ SET_STATE_SCHEMA = cv.Schema( {cv.Optional(state): lvalid.lv_bool for state in df.STATES} ) # Setting object flags -FLAG_SCHEMA = cv.Schema({cv.Optional(flag): cv.boolean for flag in df.OBJ_FLAGS}) +FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FLAGS}) FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of) -def part_schema(widget_type): +def part_schema(widget_type: WidgetType): """ Generate a schema for the various parts (e.g. main:, indicator:) of a widget type :param widget_type: The type of widget to generate for :return: """ - parts = WIDGET_PARTS.get(widget_type) - if parts is None: - parts = (df.CONF_MAIN,) + parts = widget_type.parts return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend( STATE_SCHEMA ) -def obj_schema(widget_type: str): +def obj_schema(widget_type: WidgetType): """ Create a schema for a widget type itself i.e. no allowance for children :param widget_type: @@ -187,13 +188,12 @@ STYLED_TEXT_SCHEMA = cv.maybe_simple_value( STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT ) - ALL_STYLES = { **STYLE_PROPS, } -def container_validator(schema, widget_type): +def container_validator(schema, widget_type: WidgetType): """ Create a validator for a container given the widget type :param schema: Base schema to extend @@ -203,13 +203,16 @@ def container_validator(schema, widget_type): def validator(value): result = schema - if w_sch := WIDGET_TYPES[widget_type].schema: + if w_sch := widget_type.schema: result = result.extend(w_sch) if value and (layout := value.get(df.CONF_LAYOUT)): if not isinstance(layout, dict): raise cv.Invalid("Layout value must be a dict") ltype = layout.get(CONF_TYPE) add_lv_use(ltype) + result = result.extend( + {cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())} + ) if value == SCHEMA_EXTRACT: return result return result(value) @@ -217,7 +220,7 @@ def container_validator(schema, widget_type): return validator -def container_schema(widget_type, extras=None): +def container_schema(widget_type: WidgetType, extras=None): """ Create a schema for a container widget of a given type. All obj properties are available, plus the extras passed in, plus any defined for the specific widget being specified. @@ -225,15 +228,16 @@ def container_schema(widget_type, extras=None): :param extras: Additional options to be made available, e.g. layout properties for children :return: The schema for this type of widget. """ - lv_type = get_widget_type(widget_type) - schema = obj_schema(widget_type).extend({cv.GenerateID(): cv.declare_id(lv_type)}) + schema = obj_schema(widget_type).extend( + {cv.GenerateID(): cv.declare_id(widget_type.w_type)} + ) if extras: schema = schema.extend(extras) # Delayed evaluation for recursion return container_validator(schema, widget_type) -def widget_schema(widget_type, extras=None): +def widget_schema(widget_type: WidgetType, extras=None): """ Create a schema for a given widget type :param widget_type: The name of the widget @@ -241,9 +245,9 @@ def widget_schema(widget_type, extras=None): :return: """ validator = container_schema(widget_type, extras=extras) - if required := REQUIRED_COMPONENTS.get(widget_type): + if required := widget_type.required_component: validator = cv.All(validator, requires_component(required)) - return cv.Exclusive(widget_type, df.CONF_WIDGETS), validator + return cv.Exclusive(widget_type.name, df.CONF_WIDGETS), validator # All widget schemas must be defined before this is called. @@ -257,4 +261,4 @@ def any_widget_schema(extras=None): :param extras: Additional schema to be applied to each generated one :return: """ - return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_PARTS)) + return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_TYPES.values())) diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py new file mode 100644 index 0000000000..a0d4a3e4ad --- /dev/null +++ b/esphome/components/lvgl/touchscreens.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +from esphome.components.touchscreen import CONF_TOUCHSCREEN_ID, Touchscreen +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.core import CORE, TimePeriod + +from .defines import ( + CONF_LONG_PRESS_REPEAT_TIME, + CONF_LONG_PRESS_TIME, + CONF_TOUCHSCREENS, +) +from .helpers import lvgl_components_required +from .lv_validation import lv_milliseconds +from .lvcode import lv +from .types import LVTouchListener + +PRESS_TIME = cv.All(lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535))) +CONF_TOUCHSCREEN = "touchscreen" +TOUCHSCREENS_CONFIG = cv.maybe_simple_value( + { + cv.Required(CONF_TOUCHSCREEN_ID): cv.use_id(Touchscreen), + cv.Optional(CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, + cv.Optional(CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, + cv.GenerateID(): cv.declare_id(LVTouchListener), + }, + key=CONF_TOUCHSCREEN_ID, +) + + +def touchscreen_schema(config): + value = cv.ensure_list(TOUCHSCREENS_CONFIG)(config) + if value or CONF_TOUCHSCREEN not in CORE.loaded_integrations: + return value + return [TOUCHSCREENS_CONFIG(config)] + + +async def touchscreens_to_code(var, config): + for tconf in config.get(CONF_TOUCHSCREENS) or (): + lvgl_components_required.add(CONF_TOUCHSCREEN) + touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID]) + lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds + lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds + listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt) + await cg.register_parented(listener, var) + lv.indev_drv_register(listener.get_drv()) + cg.add(touchscreen.register_listener(listener)) diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 3c043d266d..60291ea54a 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,7 +1,22 @@ from esphome import codegen as cg from esphome.core import ID +from esphome.cpp_generator import MockObjClass + +from .defines import CONF_TEXT + + +class LvType(cg.MockObjClass): + def __init__(self, *args, **kwargs): + parens = kwargs.pop("parents", ()) + super().__init__(*args, parents=parens + (lv_obj_base_t,)) + self.args = kwargs.pop("largs", [(lv_obj_t_ptr, "obj")]) + self.value = kwargs.pop("lvalue", lambda w: w.obj) + self.has_on_value = kwargs.pop("has_on_value", False) + self.value_property = None + + def get_arg_type(self): + return self.args[0][0] if len(self.args) else None -from .defines import CONF_LABEL, CONF_OBJ, CONF_TEXT uint16_t_ptr = cg.uint16.operator("ptr") lvgl_ns = cg.esphome_ns.namespace("lvgl") @@ -18,25 +33,15 @@ lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) lv_obj_t_ptr = lv_obj_base_t.operator("ptr") lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr") lv_color_t = cg.global_ns.struct("lv_color_t") +LVTouchListener = lvgl_ns.class_("LVTouchListener") +LVEncoderListener = lvgl_ns.class_("LVEncoderListener") +lv_obj_t = LvType("lv_obj_t") # this will be populated later, in __init__.py to avoid circular imports. WIDGET_TYPES: dict = {} -class LvType(cg.MockObjClass): - def __init__(self, *args, **kwargs): - parens = kwargs.pop("parents", ()) - super().__init__(*args, parents=parens + (lv_obj_base_t,)) - self.args = kwargs.pop("largs", [(lv_obj_t_ptr, "obj")]) - self.value = kwargs.pop("lvalue", lambda w: w.obj) - self.has_on_value = kwargs.pop("has_on_value", False) - self.value_property = None - - def get_arg_type(self): - return self.args[0][0] if len(self.args) else None - - class LvText(LvType): def __init__(self, *args, **kwargs): super().__init__( @@ -48,17 +53,74 @@ class LvText(LvType): self.value_property = CONF_TEXT -lv_obj_t = LvType("lv_obj_t") -lv_label_t = LvText("lv_label_t") - -LV_TYPES = { - CONF_LABEL: lv_label_t, - CONF_OBJ: lv_obj_t, -} - - -def get_widget_type(typestr: str) -> LvType: - return LV_TYPES[typestr] +class LvBoolean(LvType): + def __init__(self, *args, **kwargs): + super().__init__( + *args, + largs=[(cg.bool_, "x")], + lvalue=lambda w: w.is_checked(), + has_on_value=True, + **kwargs, + ) CUSTOM_EVENT = ID("lv_custom_event", False, type=lv_event_code_t) + + +class WidgetType: + """ + Describes a type of Widget, e.g. "bar" or "line" + """ + + def __init__(self, name, w_type, parts, schema=None, modify_schema=None): + """ + :param name: The widget name, e.g. "bar" + :param w_type: The C type of the widget + :param parts: What parts this widget supports + :param schema: The config schema for defining a widget + :param modify_schema: A schema to update the widget + """ + self.name = name + self.w_type = w_type + self.parts = parts + self.schema = schema or {} + if modify_schema is None: + self.modify_schema = schema + else: + self.modify_schema = modify_schema + + @property + def animated(self): + return False + + @property + def required_component(self): + return None + + def is_compound(self): + return self.w_type.inherits_from(LvCompound) + + async def to_code(self, w, config: dict): + """ + Generate code for a given widget + :param w: The widget + :param config: Its configuration + :return: Generated code as a list of text lines + """ + raise NotImplementedError(f"No to_code defined for {self.name}") + + def obj_creator(self, parent: MockObjClass, config: dict): + """ + Create an instance of the widget type + :param parent: The parent to which it should be attached + :param config: Its configuration + :return: Generated code as a single text line + """ + return f"lv_{self.name}_create({parent})" + + def get_uses(self): + """ + Get a list of other widgets used by this one + :return: + """ + return () diff --git a/esphome/components/lvgl/widget.py b/esphome/components/lvgl/widget.py index 44f277f1c3..4755d8b21d 100644 --- a/esphome/components/lvgl/widget.py +++ b/esphome/components/lvgl/widget.py @@ -21,78 +21,19 @@ from .defines import ( ) from .helpers import add_lv_use from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj -from .schemas import ALL_STYLES -from .types import WIDGET_TYPES, LvCompound, lv_obj_t +from .schemas import ALL_STYLES, STYLE_REMAP +from .types import WIDGET_TYPES, WidgetType, lv_obj_t EVENT_LAMB = "event_lamb__" -class WidgetType: - """ - Describes a type of Widget, e.g. "bar" or "line" - """ - - def __init__(self, name, schema=None, modify_schema=None): - """ - :param name: The widget name, e.g. "bar" - :param schema: The config schema for defining a widget - :param modify_schema: A schema to update the widget - """ - self.name = name - self.schema = schema or {} - if modify_schema is None: - self.modify_schema = schema - else: - self.modify_schema = modify_schema - - @property - def animated(self): - return False - - @property - def w_type(self): - """ - Get the type associated with this widget - :return: - """ - return lv_obj_t - - def is_compound(self): - return self.w_type.inherits_from(LvCompound) - - async def to_code(self, w, config: dict): - """ - Generate code for a given widget - :param w: The widget - :param config: Its configuration - :return: Generated code as a list of text lines - """ - raise NotImplementedError(f"No to_code defined for {self.name}") - - def obj_creator(self, parent: MockObjClass, config: dict): - """ - Create an instance of the widget type - :param parent: The parent to which it should be attached - :param config: Its configuration - :return: Generated code as a single text line - """ - return f"lv_{self.name}_create({parent})" - - def get_uses(self): - """ - Get a list of other widgets used by this one - :return: - """ - return () - - class LvScrActType(WidgetType): """ A "widget" representing the active screen. """ def __init__(self): - super().__init__("lv_scr_act()") + super().__init__("lv_scr_act()", lv_obj_t, ()) def obj_creator(self, parent: MockObjClass, config: dict): return [] @@ -263,7 +204,9 @@ async def set_obj_properties(w: Widget, config): }.items(): if isinstance(ALL_STYLES[prop], LValidator): value = await ALL_STYLES[prop].process(value) - w.set_style(prop, value, lv_state) + # Remapping for backwards compatibility of style names + prop_r = STYLE_REMAP.get(prop, prop) + w.set_style(prop_r, value, lv_state) flag_clr = set() flag_set = set() props = parts[CONF_MAIN][CONF_DEFAULT] @@ -291,10 +234,10 @@ async def set_obj_properties(w: Widget, config): else: clears.add(key) if adds: - adds = ConstantLiteral(join_enums(adds, "LV_STATE_")) + adds = join_enums(adds, "LV_STATE_") w.add_state(adds) if clears: - clears = ConstantLiteral(join_enums(clears, "LV_STATE_")) + clears = join_enums(clears, "LV_STATE_") w.clear_state(clears) for key, value in lambs.items(): lamb = await cg.process_lambda(value, [], return_type=cg.bool_) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9d453260ab..6ba5b64761 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -41,6 +41,7 @@ #define USE_LVGL #define USE_LVGL_FONT #define USE_LVGL_IMAGE +#define USE_LVGL_TOUCHSCREEN #define USE_MDNS #define USE_MEDIA_PLAYER #define USE_MQTT diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index e69de29bb2..8b92f8caa7 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -0,0 +1,10 @@ +touchscreen: + - platform: ft63x6 + id: tft_touch + display: tft_display + update_interval: 50ms + threshold: 1 + calibration: + x_max: 240 + y_max: 320 + diff --git a/tests/components/lvgl/logo-text.svg b/tests/components/lvgl/logo-text.svg new file mode 100644 index 0000000000..4950806a36 --- /dev/null +++ b/tests/components/lvgl/logo-text.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 856e7c3e9d..696c749876 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -1,9 +1,10 @@ -color: - - id: light_blue - hex: "3340FF" - lvgl: + log_level: TRACE bg_color: light_blue + touchscreens: + - touchscreen_id: tft_touch + long_press_repeat_time: 200ms + long_press_time: 500ms widgets: - label: text: Hello world @@ -17,8 +18,101 @@ lvgl: text_color: 0xFFFFFF align: bottom_mid text_font: space16 + - obj: + align: center + arc_opa: COVER + arc_color: 0xFF0000 + arc_rounded: false + arc_width: 3 + anim_time: 1s + bg_color: light_blue + bg_grad_color: light_blue + bg_dither_mode: ordered + bg_grad_dir: hor + bg_grad_stop: 128 + bg_image_opa: transp + bg_image_recolor: light_blue + bg_image_recolor_opa: 50% + bg_main_stop: 0 + bg_opa: 20% + border_color: 0x00FF00 + border_opa: cover + border_post: true + border_side: [bottom, left] + border_width: 4 + clip_corner: false + height: 50% + image_recolor: light_blue + image_recolor_opa: cover + line_width: 10 + line_dash_width: 10 + line_dash_gap: 10 + line_rounded: false + line_color: light_blue + opa: cover + opa_layered: cover + outline_color: light_blue + outline_opa: cover + outline_pad: 10px + outline_width: 10px + pad_all: 10px + pad_bottom: 10px + pad_column: 10px + pad_left: 10px + pad_right: 10px + pad_row: 10px + pad_top: 10px + shadow_color: light_blue + shadow_ofs_x: 5 + shadow_ofs_y: 5 + shadow_opa: cover + shadow_spread: 5 + shadow_width: 10 + text_align: auto + text_color: light_blue + text_decor: [underline, strikethrough] + text_font: montserrat_18 + text_letter_space: 4 + text_line_space: 4 + text_opa: cover + transform_angle: 180 + transform_height: 100 + transform_pivot_x: 50% + transform_pivot_y: 50% + transform_zoom: 0.5 + translate_x: 10 + translate_y: 10 + max_height: 100 + max_width: 200 + min_height: 20% + min_width: 20% + radius: circle + width: 10px + x: 100 + y: 120 + - button: + width: 20% + height: 10% + pressed: + bg_color: light_blue + widgets: + - label: + text: Button font: - file: "gfonts://Roboto" id: space16 bpp: 4 + +image: + - id: cat_img + resize: 256x48 + file: $component_dir/logo-text.svg + - id: dog_img + file: $component_dir/logo-text.svg + resize: 256x48 + type: TRANSPARENT_BINARY + +color: + - id: light_blue + hex: "3340FF" diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index f159431b99..eab75b05f3 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -19,7 +19,9 @@ display: mirror_y: true data_rate: 80MHz cs_pin: GPIO20 - dc_pin: GPIO15 + dc_pin: + number: GPIO15 + ignore_strapping_warning: true auto_clear_enabled: false invert_colors: false update_interval: never From 6e21d79bde02b90e4273b7a92f2caa8604a3d687 Mon Sep 17 00:00:00 2001 From: FreeBear-nc <67865163+FreeBear-nc@users.noreply.github.com> Date: Tue, 30 Jul 2024 02:15:27 +0100 Subject: [PATCH 012/160] [pid] Add get_min_integral() and get_max_integral() (#7162) --- esphome/components/pid/pid_climate.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 5ae97ee10b..b5275e9775 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -44,6 +44,8 @@ class PIDClimate : public climate::Climate, public Component { float get_kp() { return controller_.kp_; } float get_ki() { return controller_.ki_; } float get_kd() { return controller_.kd_; } + float get_min_integral() { return controller_.min_integral_; } + float get_max_integral() { return controller_.max_integral_; } float get_proportional_term() const { return controller_.proportional_term_; } float get_integral_term() const { return controller_.integral_term_; } float get_derivative_term() const { return controller_.derivative_term_; } From 83bb7d0266459ebae70d663addabc45a1f112092 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:23:30 +1200 Subject: [PATCH 013/160] [code-quality] Organise bluetooth related imports (#7155) --- esphome/components/ble_client/__init__.py | 8 ++++---- esphome/components/ble_client/output/__init__.py | 2 +- esphome/components/ble_client/sensor/__init__.py | 7 ++++--- esphome/components/ble_client/switch/__init__.py | 3 ++- esphome/components/ble_client/text_sensor/__init__.py | 7 ++++--- esphome/components/ble_presence/binary_sensor.py | 6 +++--- esphome/components/ble_rssi/sensor.py | 4 ++-- esphome/components/ble_scanner/text_sensor.py | 2 +- esphome/components/bluetooth_proxy/__init__.py | 6 +++--- esphome/components/esp32_ble/__init__.py | 6 +++--- esphome/components/esp32_ble_beacon/__init__.py | 10 +++++----- esphome/components/esp32_ble_client/__init__.py | 1 - esphome/components/esp32_ble_server/__init__.py | 4 ++-- esphome/components/esp32_ble_tracker/__init__.py | 4 ++-- 14 files changed, 36 insertions(+), 34 deletions(-) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 34b9868edc..6bf4ff739e 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -1,7 +1,8 @@ -import esphome.codegen as cg -import esphome.config_validation as cv +from esphome import automation from esphome.automation import maybe_simple_id -from esphome.components import esp32_ble_tracker, esp32_ble_client +import esphome.codegen as cg +from esphome.components import esp32_ble_client, esp32_ble_tracker +import esphome.config_validation as cv from esphome.const import ( CONF_CHARACTERISTIC_UUID, CONF_ID, @@ -13,7 +14,6 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VALUE, ) -from esphome import automation AUTO_LOAD = ["esp32_ble_client"] CODEOWNERS = ["@buxtronix", "@clydebarrow"] diff --git a/esphome/components/ble_client/output/__init__.py b/esphome/components/ble_client/output/__init__.py index fd847d80b8..729885eb8b 100644 --- a/esphome/components/ble_client/output/__init__.py +++ b/esphome/components/ble_client/output/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import ble_client, esp32_ble_tracker, output +import esphome.config_validation as cv from esphome.const import CONF_CHARACTERISTIC_UUID, CONF_ID, CONF_SERVICE_UUID from .. import ble_client_ns diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py index d0b27c30a9..0c48902a90 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -1,17 +1,18 @@ +from esphome import automation import esphome.codegen as cg +from esphome.components import ble_client, esp32_ble_tracker, sensor import esphome.config_validation as cv -from esphome.components import sensor, ble_client, esp32_ble_tracker from esphome.const import ( CONF_CHARACTERISTIC_UUID, CONF_LAMBDA, + CONF_SERVICE_UUID, CONF_TRIGGER_ID, CONF_TYPE, - CONF_SERVICE_UUID, DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL_MILLIWATT, ) -from esphome import automation + from .. import ble_client_ns DEPENDENCIES = ["ble_client"] diff --git a/esphome/components/ble_client/switch/__init__.py b/esphome/components/ble_client/switch/__init__.py index 2304d65c01..70314e8f30 100644 --- a/esphome/components/ble_client/switch/__init__.py +++ b/esphome/components/ble_client/switch/__init__.py @@ -1,7 +1,8 @@ import esphome.codegen as cg +from esphome.components import ble_client, switch import esphome.config_validation as cv -from esphome.components import switch, ble_client from esphome.const import ICON_BLUETOOTH + from .. import ble_client_ns BLEClientSwitch = ble_client_ns.class_( diff --git a/esphome/components/ble_client/text_sensor/__init__.py b/esphome/components/ble_client/text_sensor/__init__.py index 7a93c4e4ae..479af1a57e 100644 --- a/esphome/components/ble_client/text_sensor/__init__.py +++ b/esphome/components/ble_client/text_sensor/__init__.py @@ -1,13 +1,14 @@ +from esphome import automation import esphome.codegen as cg +from esphome.components import ble_client, esp32_ble_tracker, text_sensor import esphome.config_validation as cv -from esphome.components import text_sensor, ble_client, esp32_ble_tracker from esphome.const import ( CONF_CHARACTERISTIC_UUID, CONF_ID, - CONF_TRIGGER_ID, CONF_SERVICE_UUID, + CONF_TRIGGER_ID, ) -from esphome import automation + from .. import ble_client_ns DEPENDENCIES = ["ble_client"] diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index 9c24a91a05..d1fdc80289 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -1,13 +1,13 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import binary_sensor, esp32_ble_tracker +import esphome.config_validation as cv from esphome.const import ( - CONF_MAC_ADDRESS, - CONF_SERVICE_UUID, CONF_IBEACON_MAJOR, CONF_IBEACON_MINOR, CONF_IBEACON_UUID, + CONF_MAC_ADDRESS, CONF_MIN_RSSI, + CONF_SERVICE_UUID, CONF_TIMEOUT, ) diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py index 0543eb0578..e3ba1abfd7 100644 --- a/esphome/components/ble_rssi/sensor.py +++ b/esphome/components/ble_rssi/sensor.py @@ -1,12 +1,12 @@ import esphome.codegen as cg +from esphome.components import esp32_ble_tracker, sensor import esphome.config_validation as cv -from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_IBEACON_MAJOR, CONF_IBEACON_MINOR, CONF_IBEACON_UUID, - CONF_SERVICE_UUID, CONF_MAC_ADDRESS, + CONF_SERVICE_UUID, DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL_MILLIWATT, diff --git a/esphome/components/ble_scanner/text_sensor.py b/esphome/components/ble_scanner/text_sensor.py index 743403c6a4..96d71a0399 100644 --- a/esphome/components/ble_scanner/text_sensor.py +++ b/esphome/components/ble_scanner/text_sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg +from esphome.components import esp32_ble_tracker, text_sensor import esphome.config_validation as cv -from esphome.components import text_sensor, esp32_ble_tracker DEPENDENCIES = ["esp32_ble_tracker"] diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index bec1579d8e..5a4ba36666 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -1,8 +1,8 @@ -from esphome.components import esp32_ble_tracker, esp32_ble_client -import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ACTIVE, CONF_ID +from esphome.components import esp32_ble_client, esp32_ble_tracker from esphome.components.esp32 import add_idf_sdkconfig_option +import esphome.config_validation as cv +from esphome.const import CONF_ACTIVE, CONF_ID AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"] DEPENDENCIES = ["api", "esp32"] diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 472669a381..75cf9d707d 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -1,9 +1,9 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +import esphome.config_validation as cv from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID from esphome.core import CORE -from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant, const DEPENDENCIES = ["esp32"] CODEOWNERS = ["@jesserockz", "@Rapsssito"] diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index d063209478..f97f289a0a 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -1,10 +1,10 @@ import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components.esp32_ble import CONF_BLE_ID -from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID, CONF_TX_POWER -from esphome.core import CORE, TimePeriod -from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components import esp32_ble +from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32_ble import CONF_BLE_ID +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TX_POWER, CONF_TYPE, CONF_UUID +from esphome.core import CORE, TimePeriod AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] diff --git a/esphome/components/esp32_ble_client/__init__.py b/esphome/components/esp32_ble_client/__init__.py index 94a5576d0b..25957ed0da 100644 --- a/esphome/components/esp32_ble_client/__init__.py +++ b/esphome/components/esp32_ble_client/__init__.py @@ -1,5 +1,4 @@ import esphome.codegen as cg - from esphome.components import esp32_ble_tracker AUTO_LOAD = ["esp32_ble_tracker"] diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index ce9fdc2cf3..9da7d13999 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -1,9 +1,9 @@ import esphome.codegen as cg +from esphome.components import esp32_ble +from esphome.components.esp32 import add_idf_sdkconfig_option import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MODEL -from esphome.components import esp32_ble from esphome.core import CORE -from esphome.components.esp32 import add_idf_sdkconfig_option AUTO_LOAD = ["esp32_ble"] CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 1edeaadbfd..0aa8eadd0a 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,10 +1,10 @@ import re -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg from esphome.components import esp32_ble from esphome.components.esp32 import add_idf_sdkconfig_option +import esphome.config_validation as cv from esphome.const import ( CONF_ACTIVE, CONF_DURATION, From caa2ea64e35ca883105f1fefeb1aeee5dbf28510 Mon Sep 17 00:00:00 2001 From: Olivier ARCHER Date: Tue, 30 Jul 2024 03:45:19 +0200 Subject: [PATCH 014/160] http_request watchdog as a component (#7161) --- CODEOWNERS | 1 + esphome/components/http_request/__init__.py | 2 +- esphome/components/http_request/http_request_arduino.cpp | 4 ++-- esphome/components/http_request/http_request_idf.cpp | 4 ++-- esphome/components/http_request/ota/ota_http_request.cpp | 2 +- esphome/components/watchdog/__init__.py | 1 + esphome/components/{http_request => watchdog}/watchdog.cpp | 2 -- esphome/components/{http_request => watchdog}/watchdog.h | 2 -- 8 files changed, 8 insertions(+), 10 deletions(-) create mode 100644 esphome/components/watchdog/__init__.py rename esphome/components/{http_request => watchdog}/watchdog.cpp (96%) rename esphome/components/{http_request => watchdog}/watchdog.h (88%) diff --git a/CODEOWNERS b/CODEOWNERS index 2fc030453f..d94c34c019 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -427,6 +427,7 @@ esphome/components/veml7700/* @latonita esphome/components/version/* @esphome/core esphome/components/voice_assistant/* @jesserockz esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 +esphome/components/watchdog/* @oarcher esphome/components/waveshare_epaper/* @clydebarrow esphome/components/web_server_base/* @OttoWinter esphome/components/web_server_idf/* @dentra diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 3ca97b9611..0407bbd326 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -14,7 +14,7 @@ from esphome.const import ( from esphome.core import CORE, Lambda DEPENDENCIES = ["network"] -AUTO_LOAD = ["json"] +AUTO_LOAD = ["json", "watchdog"] http_request_ns = cg.esphome_ns.namespace("http_request") HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 95b1cdc38e..2148d92ad2 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -3,12 +3,12 @@ #ifdef USE_ARDUINO #include "esphome/components/network/util.h" +#include "esphome/components/watchdog/watchdog.h" + #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" -#include "watchdog.h" - namespace esphome { namespace http_request { diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 4fe5585723..3819f5544e 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -3,6 +3,8 @@ #ifdef USE_ESP_IDF #include "esphome/components/network/util.h" +#include "esphome/components/watchdog/watchdog.h" + #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -11,8 +13,6 @@ #include "esp_crt_bundle.h" #endif -#include "watchdog.h" - namespace esphome { namespace http_request { diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index dcc783ea47..1553de0bc1 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -1,11 +1,11 @@ #include "ota_http_request.h" -#include "../watchdog.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" #include "esphome/components/md5/md5.h" +#include "esphome/components/watchdog/watchdog.h" #include "esphome/components/ota/ota_backend.h" #include "esphome/components/ota/ota_backend_arduino_esp32.h" #include "esphome/components/ota/ota_backend_arduino_esp8266.h" diff --git a/esphome/components/watchdog/__init__.py b/esphome/components/watchdog/__init__.py new file mode 100644 index 0000000000..6fb87e45a4 --- /dev/null +++ b/esphome/components/watchdog/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@oarcher"] diff --git a/esphome/components/http_request/watchdog.cpp b/esphome/components/watchdog/watchdog.cpp similarity index 96% rename from esphome/components/http_request/watchdog.cpp rename to esphome/components/watchdog/watchdog.cpp index a8519c59ed..3a94a658e8 100644 --- a/esphome/components/http_request/watchdog.cpp +++ b/esphome/components/watchdog/watchdog.cpp @@ -15,7 +15,6 @@ #endif namespace esphome { -namespace http_request { namespace watchdog { static const char *const TAG = "http_request.watchdog"; @@ -72,5 +71,4 @@ uint32_t WatchdogManager::get_timeout_() { } } // namespace watchdog -} // namespace http_request } // namespace esphome diff --git a/esphome/components/http_request/watchdog.h b/esphome/components/watchdog/watchdog.h similarity index 88% rename from esphome/components/http_request/watchdog.h rename to esphome/components/watchdog/watchdog.h index 9b54ae6c82..899ec3fde0 100644 --- a/esphome/components/http_request/watchdog.h +++ b/esphome/components/watchdog/watchdog.h @@ -5,7 +5,6 @@ #include namespace esphome { -namespace http_request { namespace watchdog { class WatchdogManager { @@ -22,5 +21,4 @@ class WatchdogManager { }; } // namespace watchdog -} // namespace http_request } // namespace esphome From d7231fadb1e174fe88f0d293c40265a26cd42ede Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:50:12 +1200 Subject: [PATCH 015/160] [touchscreen] Allow binary sensor to have multiple pages in config (#7112) * [touchscreen] Allow binary sensor to have multiple pages in config * Sort imports --- .../touchscreen/binary_sensor/__init__.py | 34 +++++++++++++------ .../touchscreen_binary_sensor.cpp | 5 +-- .../binary_sensor/touchscreen_binary_sensor.h | 6 ++-- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/esphome/components/touchscreen/binary_sensor/__init__.py b/esphome/components/touchscreen/binary_sensor/__init__.py index 800bc4c2a9..45fefbf814 100644 --- a/esphome/components/touchscreen/binary_sensor/__init__.py +++ b/esphome/components/touchscreen/binary_sensor/__init__.py @@ -1,10 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv - from esphome.components import binary_sensor, display -from esphome.const import CONF_PAGE_ID +import esphome.config_validation as cv +from esphome.const import CONF_PAGE_ID, CONF_PAGES -from .. import touchscreen_ns, CONF_TOUCHSCREEN_ID, Touchscreen, TouchListener +from .. import CONF_TOUCHSCREEN_ID, TouchListener, Touchscreen, touchscreen_ns DEPENDENCIES = ["touchscreen"] @@ -22,7 +21,7 @@ CONF_Y_MIN = "y_min" CONF_Y_MAX = "y_max" -def validate_coords(config): +def _validate_coords(config): if ( config[CONF_X_MAX] < config[CONF_X_MIN] or config[CONF_Y_MAX] < config[CONF_Y_MIN] @@ -33,6 +32,15 @@ def validate_coords(config): return config +def _set_pages(config: dict) -> dict: + if CONF_PAGES in config or CONF_PAGE_ID not in config: + return config + + config = config.copy() + config[CONF_PAGES] = [config.pop(CONF_PAGE_ID)] + return config + + CONFIG_SCHEMA = cv.All( binary_sensor.binary_sensor_schema(TouchscreenBinarySensor) .extend( @@ -42,11 +50,17 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_X_MAX): cv.int_range(min=0, max=2000), cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=2000), cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=2000), - cv.Optional(CONF_PAGE_ID): cv.use_id(display.DisplayPage), + cv.Exclusive(CONF_PAGE_ID, group_of_exclusion=CONF_PAGES): cv.use_id( + display.DisplayPage + ), + cv.Exclusive(CONF_PAGES, group_of_exclusion=CONF_PAGES): cv.ensure_list( + cv.use_id(display.DisplayPage) + ), } ) .extend(cv.COMPONENT_SCHEMA), - validate_coords, + _validate_coords, + _set_pages, ) @@ -64,6 +78,6 @@ async def to_code(config): ) ) - if CONF_PAGE_ID in config: - page = await cg.get_variable(config[CONF_PAGE_ID]) - cg.add(var.set_page(page)) + for page_id in config.get(CONF_PAGES, []): + page = await cg.get_variable(page_id) + cg.add(var.add_page(page)) diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp index 6c26ae3626..6cd12d4d0d 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp @@ -11,8 +11,9 @@ void TouchscreenBinarySensor::setup() { void TouchscreenBinarySensor::touch(TouchPoint tp) { bool touched = (tp.x >= this->x_min_ && tp.x <= this->x_max_ && tp.y >= this->y_min_ && tp.y <= this->y_max_); - if (this->page_ != nullptr) { - touched &= this->page_ == this->parent_->get_display()->get_active_page(); + if (!this->pages_.empty()) { + auto *current_page = this->parent_->get_display()->get_active_page(); + touched &= std::find(this->pages_.begin(), this->pages_.end(), current_page) != this->pages_.end(); } if (touched) { this->publish_state(true); diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h index b56ae562b1..862f41064c 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h @@ -6,6 +6,8 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include + namespace esphome { namespace touchscreen { @@ -30,14 +32,14 @@ class TouchscreenBinarySensor : public binary_sensor::BinarySensor, int16_t get_width() { return this->x_max_ - this->x_min_; } int16_t get_height() { return this->y_max_ - this->y_min_; } - void set_page(display::DisplayPage *page) { this->page_ = page; } + void add_page(display::DisplayPage *page) { this->pages_.push_back(page); } void touch(TouchPoint tp) override; void release() override; protected: int16_t x_min_, x_max_, y_min_, y_max_; - display::DisplayPage *page_{nullptr}; + std::vector pages_{}; }; } // namespace touchscreen From dff6884bedd1585b49d7864e52039f0f14e74fa8 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 30 Jul 2024 16:57:51 -0400 Subject: [PATCH 016/160] [micro_wake_word] Fix VAD detection and modify detection computation (#7164) --- esphome/components/micro_wake_word/streaming_model.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 013fa2ce6e..d0d2e2df05 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -148,7 +148,7 @@ WakeWordModel::WakeWordModel(const uint8_t *model_start, float probability_cutof }; bool WakeWordModel::determine_detected() { - int32_t sum = 0; + uint32_t sum = 0; for (auto &prob : this->recent_streaming_probabilities_) { sum += prob; } @@ -175,12 +175,14 @@ VADModel::VADModel(const uint8_t *model_start, float probability_cutoff, size_t }; bool VADModel::determine_detected() { - uint8_t max = 0; + uint32_t sum = 0; for (auto &prob : this->recent_streaming_probabilities_) { - max = std::max(prob, max); + sum += prob; } - return max > this->probability_cutoff_; + float sliding_window_average = static_cast(sum) / static_cast(255 * this->sliding_window_size_); + + return sliding_window_average > this->probability_cutoff_; } } // namespace micro_wake_word From dd3dd7a136b9f81a5d3ff7c7296edd031af38f4f Mon Sep 17 00:00:00 2001 From: Adam Allport Date: Tue, 30 Jul 2024 22:30:15 +0100 Subject: [PATCH 017/160] fix: Add `pin->setup();` to matrix_keypad.cpp (#7163) --- esphome/components/matrix_keypad/matrix_keypad.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index 902e574846..f62c75c869 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -8,6 +8,7 @@ static const char *const TAG = "matrix_keypad"; void MatrixKeypad::setup() { for (auto *pin : this->rows_) { + pin->setup(); if (!has_diodes_) { pin->pin_mode(gpio::FLAG_INPUT); } else { @@ -15,6 +16,7 @@ void MatrixKeypad::setup() { } } for (auto *pin : this->columns_) { + pin->setup(); if (has_pulldowns_) { pin->pin_mode(gpio::FLAG_INPUT); } else { From 8849443bf6e9f56e5c0f682a25a78a6782007a1e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:08:11 +1200 Subject: [PATCH 018/160] [update] Implement ``update.perform`` action and ``update.is_available`` condition (#7165) * [update] Fix unimplemented yaml action/condition * Add/update tests --------- Co-authored-by: Keith Burzinski --- .../update/http_request_update.cpp | 4 +- .../http_request/update/http_request_update.h | 2 +- esphome/components/update/__init__.py | 48 +++++++++++++------ esphome/components/update/automation.h | 23 +++++++++ esphome/components/update/update_entity.h | 4 +- tests/components/update/common.yaml | 27 +++++++++++ tests/components/update/test.esp32-ard.yaml | 3 ++ tests/components/update/test.esp8266-ard.yaml | 3 ++ tests/components/update/test.rp2040-ard.yaml | 3 ++ 9 files changed, 98 insertions(+), 19 deletions(-) create mode 100644 esphome/components/update/automation.h diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 0a14dfd933..059148e7e5 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -138,8 +138,8 @@ void HttpRequestUpdate::update() { this->publish_state(); } -void HttpRequestUpdate::perform() { - if (this->state_ != update::UPDATE_STATE_AVAILABLE) { +void HttpRequestUpdate::perform(bool force) { + if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) { return; } diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index a6bc97392b..943231a906 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -15,7 +15,7 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { void setup() override; void update() override; - void perform() override; + void perform(bool force) override; void set_source_url(const std::string &source_url) { this->source_url_ = source_url; } diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 45bf082fa4..ba3b2f20df 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -1,10 +1,11 @@ from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server import esphome.config_validation as cv -import esphome.codegen as cg from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, + CONF_FORCE_UPDATE, CONF_ID, CONF_MQTT_ID, CONF_WEB_SERVER_ID, @@ -23,8 +24,12 @@ UpdateEntity = update_ns.class_("UpdateEntity", cg.EntityBase) UpdateInfo = update_ns.struct("UpdateInfo") -PerformAction = update_ns.class_("PerformAction", automation.Action) -IsAvailableCondition = update_ns.class_("IsAvailableCondition", automation.Condition) +PerformAction = update_ns.class_( + "PerformAction", automation.Action, cg.Parented.template(UpdateEntity) +) +IsAvailableCondition = update_ns.class_( + "IsAvailableCondition", automation.Condition, cg.Parented.template(UpdateEntity) +) DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, @@ -92,24 +97,37 @@ async def to_code(config): cg.add_global(update_ns.using) -UPDATE_AUTOMATION_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.use_id(UpdateEntity), - } +@automation.register_action( + "update.perform", + PerformAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(UpdateEntity), + cv.Optional(CONF_FORCE_UPDATE, default=False): cv.templatable(cv.boolean), + } + ), ) - - -@automation.register_action("update.perform", PerformAction, UPDATE_AUTOMATION_SCHEMA) async def update_perform_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, paren, paren) + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + force = await cg.templatable(config[CONF_FORCE_UPDATE], args, cg.bool_) + cg.add(var.set_force(force)) + return var @automation.register_condition( - "update.is_available", IsAvailableCondition, UPDATE_AUTOMATION_SCHEMA + "update.is_available", + IsAvailableCondition, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(UpdateEntity), + } + ), ) async def update_is_available_condition_to_code( config, condition_id, template_arg, args ): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(condition_id, paren, paren) + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/update/automation.h b/esphome/components/update/automation.h new file mode 100644 index 0000000000..df50f86a0c --- /dev/null +++ b/esphome/components/update/automation.h @@ -0,0 +1,23 @@ +#pragma once + +#include "update_entity.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace update { + +template class PerformAction : public Action, public Parented { + TEMPLATABLE_VALUE(bool, force) + + public: + void play(Ts... x) override { this->parent_->perform(this->force_.value(x...)); } +}; + +template class IsAvailableCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } +}; + +} // namespace update +} // namespace esphome diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 5984c8e35b..568fbe3bb0 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -32,7 +32,9 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { void publish_state(); - virtual void perform() = 0; + void perform() { this->perform(false); } + + virtual void perform(bool force) = 0; const UpdateInfo &update_info = update_info_; const UpdateState &state = state_; diff --git a/tests/components/update/common.yaml b/tests/components/update/common.yaml index 91b8669505..dcb4f42527 100644 --- a/tests/components/update/common.yaml +++ b/tests/components/update/common.yaml @@ -1 +1,28 @@ +substitutions: + verify_ssl: "true" + +esphome: + on_boot: + then: + - if: + condition: + update.is_available: + then: + - logger.log: "Update available" + - update.perform: + force_update: true + +wifi: + ssid: MySSID + password: password1 + +http_request: + verify_ssl: ${verify_ssl} + +ota: + - platform: http_request + update: + - platform: http_request + name: Firmware Update + source: http://example.com/manifest.json diff --git a/tests/components/update/test.esp32-ard.yaml b/tests/components/update/test.esp32-ard.yaml index dade44d145..c1937b5a10 100644 --- a/tests/components/update/test.esp32-ard.yaml +++ b/tests/components/update/test.esp32-ard.yaml @@ -1 +1,4 @@ +substitutions: + verify_ssl: "false" + <<: !include common.yaml diff --git a/tests/components/update/test.esp8266-ard.yaml b/tests/components/update/test.esp8266-ard.yaml index dade44d145..c1937b5a10 100644 --- a/tests/components/update/test.esp8266-ard.yaml +++ b/tests/components/update/test.esp8266-ard.yaml @@ -1 +1,4 @@ +substitutions: + verify_ssl: "false" + <<: !include common.yaml diff --git a/tests/components/update/test.rp2040-ard.yaml b/tests/components/update/test.rp2040-ard.yaml index dade44d145..c1937b5a10 100644 --- a/tests/components/update/test.rp2040-ard.yaml +++ b/tests/components/update/test.rp2040-ard.yaml @@ -1 +1,4 @@ +substitutions: + verify_ssl: "false" + <<: !include common.yaml From 3920029aff9dd85fd5f0775945b8426c03da471c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:31:15 +1000 Subject: [PATCH 019/160] [lvgl] PR stage 3 (#7160) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lvgl/__init__.py | 58 +++++-- esphome/components/lvgl/automation.py | 188 +++++++++++++++++++++ esphome/components/lvgl/btn.py | 6 +- esphome/components/lvgl/defines.py | 30 +++- esphome/components/lvgl/lv_validation.py | 46 ++--- esphome/components/lvgl/lvcode.py | 64 ++++--- esphome/components/lvgl/lvgl_esphome.cpp | 75 +++++++- esphome/components/lvgl/lvgl_esphome.h | 165 ++++++++++++++++-- esphome/components/lvgl/obj.py | 13 +- esphome/components/lvgl/rotary_encoders.py | 62 +++++++ esphome/components/lvgl/schemas.py | 80 ++++++++- esphome/components/lvgl/touchscreens.py | 5 +- esphome/components/lvgl/trigger.py | 61 +++++++ esphome/components/lvgl/types.py | 23 ++- esphome/components/lvgl/widget.py | 58 +++++-- esphome/core/defines.h | 3 + tests/components/lvgl/lvgl-package.yaml | 35 ++++ tests/components/lvgl/test.esp32-idf.yaml | 21 +++ 18 files changed, 895 insertions(+), 98 deletions(-) create mode 100644 esphome/components/lvgl/automation.py create mode 100644 esphome/components/lvgl/rotary_encoders.py create mode 100644 esphome/components/lvgl/trigger.py diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index c454a61957..182d04e038 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -1,5 +1,6 @@ import logging +from esphome.automation import build_automation, register_action, validate_automation import esphome.codegen as cg from esphome.components.display import Display import esphome.config_validation as cv @@ -8,7 +9,11 @@ from esphome.const import ( CONF_BUFFER_SIZE, CONF_ID, CONF_LAMBDA, + CONF_ON_IDLE, CONF_PAGES, + CONF_TIMEOUT, + CONF_TRIGGER_ID, + CONF_TYPE, ) from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import MockObj @@ -16,21 +21,26 @@ from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid +from .automation import update_to_code from .btn import btn_spec from .label import label_spec -from .lvcode import ConstantLiteral, LvContext +from .lv_validation import lv_images_used +from .lvcode import LvContext from .obj import obj_spec -from .schemas import any_widget_schema, obj_schema +from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code +from .schemas import any_widget_schema, create_modify_schema, obj_schema from .touchscreens import touchscreen_schema, touchscreens_to_code +from .trigger import generate_triggers from .types import ( WIDGET_TYPES, FontEngine, + IdleTrigger, LvglComponent, - lv_disp_t_ptr, + ObjUpdateAction, lv_font_t, lvgl_ns, ) -from .widget import LvScrActType, Widget, add_widgets, set_obj_properties +from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties DOMAIN = "lvgl" DEPENDENCIES = ("display",) @@ -41,17 +51,21 @@ LOGGER = logging.getLogger(__name__) for w_type in (label_spec, obj_spec, btn_spec): WIDGET_TYPES[w_type.name] = w_type -lv_scr_act_spec = LvScrActType() -lv_scr_act = Widget.create( - None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None -) - WIDGET_SCHEMA = any_widget_schema() +for w_type in WIDGET_TYPES.values(): + register_action( + f"lvgl.{w_type.name}.update", + ObjUpdateAction, + create_modify_schema(w_type), + )(update_to_code) + async def add_init_lambda(lv_component, init): if init: - lamb = await cg.process_lambda(Lambda(init), [(lv_disp_t_ptr, "lv_disp")]) + lamb = await cg.process_lambda( + Lambda(init), [(LvglComponent.operator("ptr"), "lv_component")] + ) cg.add(lv_component.add_init_lambda(lamb)) @@ -99,6 +113,13 @@ def final_validation(config): buffer_frac = config[CONF_BUFFER_SIZE] if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: LOGGER.warning("buffer_size: may need to be reduced without PSRAM") + for image_id in lv_images_used: + path = global_config.get_path_for_id(image_id)[:-1] + image_conf = global_config.get_config_for_path(path) + if image_conf[CONF_TYPE] in ("RGBA", "RGB24"): + raise cv.Invalid( + "Using RGBA or RGB24 in image config not compatible with LVGL", path + ) async def to_code(config): @@ -174,9 +195,15 @@ async def to_code(config): with LvContext(): await touchscreens_to_code(lv_component, config) + await rotary_encoders_to_code(lv_component, config) await set_obj_properties(lv_scr_act, config) await add_widgets(lv_scr_act, config) - Widget.set_completed() + Widget.set_completed() + await generate_triggers(lv_component) + for conf in config.get(CONF_ON_IDLE, ()): + templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) + idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ) + await build_automation(idle_trigger, [], conf) await add_init_lambda(lv_component, LvContext.get_code()) for comp in helpers.lvgl_components_required: CORE.add_define(f"USE_LVGL_{comp.upper()}") @@ -212,9 +239,18 @@ CONFIG_SCHEMA = ( cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( "big_endian", "little_endian" ), + cv.Optional(CONF_ON_IDLE): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), + cv.Required(CONF_TIMEOUT): cv.templatable( + cv.positive_time_period_milliseconds + ), + } + ), cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA), cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema, + cv.GenerateID(df.CONF_ROTARY_ENCODERS): ROTARY_ENCODER_CONFIG, } ) ).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS)) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py new file mode 100644 index 0000000000..4fd0be185e --- /dev/null +++ b/esphome/components/lvgl/automation.py @@ -0,0 +1,188 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TIMEOUT +from esphome.core import Lambda +from esphome.cpp_generator import RawStatement +from esphome.cpp_types import nullptr + +from .defines import CONF_LVGL_ID, CONF_SHOW_SNOW, literal +from .lv_validation import lv_bool +from .lvcode import ( + LambdaContext, + ReturnStatement, + add_line_marks, + lv, + lv_add, + lv_obj, + lvgl_comp, +) +from .schemas import ACTION_SCHEMA, LVGL_SCHEMA +from .types import ( + LvglAction, + LvglComponent, + LvglComponentPtr, + LvglCondition, + ObjUpdateAction, + lv_obj_t, +) +from .widget import Widget, get_widget, lv_scr_act, set_obj_properties + + +async def action_to_code(action: list, action_id, widget: Widget, template_arg, args): + with LambdaContext() as context: + lv.cond_if(widget.obj == nullptr) + lv_add(RawStatement(" return;")) + lv.cond_endif() + code = context.get_code() + code.extend(action) + action = "\n".join(code) + "\n\n" + lamb = await cg.process_lambda(Lambda(action), args) + var = cg.new_Pvariable(action_id, template_arg, lamb) + return var + + +async def update_to_code(config, action_id, template_arg, args): + if config is not None: + widget = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + await set_obj_properties(widget, config) + await widget.type.to_code(widget, config) + if ( + widget.type.w_type.value_property is not None + and widget.type.w_type.value_property in config + ): + lv.event_send(widget.obj, literal("LV_EVENT_VALUE_CHANGED"), nullptr) + return await action_to_code( + context.get_code(), action_id, widget, template_arg, args + ) + + +@automation.register_condition( + "lvgl.is_paused", + LvglCondition, + LVGL_SCHEMA, +) +async def lvgl_is_paused(config, condition_id, template_arg, args): + lvgl = config[CONF_LVGL_ID] + with LambdaContext( + [(LvglComponentPtr, "lvgl_comp")], return_type=cg.bool_ + ) as context: + lv_add(ReturnStatement(lvgl_comp.is_paused())) + var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, lvgl) + return var + + +@automation.register_condition( + "lvgl.is_idle", + LvglCondition, + LVGL_SCHEMA.extend( + { + cv.Required(CONF_TIMEOUT): cv.templatable( + cv.positive_time_period_milliseconds + ) + } + ), +) +async def lvgl_is_idle(config, condition_id, template_arg, args): + lvgl = config[CONF_LVGL_ID] + timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32) + with LambdaContext( + [(LvglComponentPtr, "lvgl_comp")], return_type=cg.bool_ + ) as context: + lv_add(ReturnStatement(lvgl_comp.is_idle(timeout))) + var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, lvgl) + return var + + +@automation.register_action( + "lvgl.widget.redraw", + ObjUpdateAction, + cv.Schema( + { + cv.Optional(CONF_ID): cv.use_id(lv_obj_t), + cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), + } + ), +) +async def obj_invalidate_to_code(config, action_id, template_arg, args): + if CONF_ID in config: + w = await get_widget(config) + else: + w = lv_scr_act + with LambdaContext() as context: + add_line_marks(action_id) + lv_obj.invalidate(w.obj) + return await action_to_code(context.get_code(), action_id, w, template_arg, args) + + +@automation.register_action( + "lvgl.pause", + LvglAction, + { + cv.GenerateID(): cv.use_id(LvglComponent), + cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool, + }, +) +async def pause_action_to_code(config, action_id, template_arg, args): + with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW])) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "lvgl.resume", + LvglAction, + { + cv.GenerateID(): cv.use_id(LvglComponent), + }, +) +async def resume_action_to_code(config, action_id, template_arg, args): + with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.set_paused(False, False)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action("lvgl.widget.disable", ObjUpdateAction, ACTION_SCHEMA) +async def obj_disable_to_code(config, action_id, template_arg, args): + w = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + w.add_state("LV_STATE_DISABLED") + return await action_to_code(context.get_code(), action_id, w, template_arg, args) + + +@automation.register_action("lvgl.widget.enable", ObjUpdateAction, ACTION_SCHEMA) +async def obj_enable_to_code(config, action_id, template_arg, args): + w = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + w.clear_state("LV_STATE_DISABLED") + return await action_to_code(context.get_code(), action_id, w, template_arg, args) + + +@automation.register_action("lvgl.widget.hide", ObjUpdateAction, ACTION_SCHEMA) +async def obj_hide_to_code(config, action_id, template_arg, args): + w = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + w.add_flag("LV_OBJ_FLAG_HIDDEN") + return await action_to_code(context.get_code(), action_id, w, template_arg, args) + + +@automation.register_action("lvgl.widget.show", ObjUpdateAction, ACTION_SCHEMA) +async def obj_show_to_code(config, action_id, template_arg, args): + w = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + w.clear_flag("LV_OBJ_FLAG_HIDDEN") + return await action_to_code(context.get_code(), action_id, w, template_arg, args) diff --git a/esphome/components/lvgl/btn.py b/esphome/components/lvgl/btn.py index 4f5f88d9e6..064d886d47 100644 --- a/esphome/components/lvgl/btn.py +++ b/esphome/components/lvgl/btn.py @@ -9,9 +9,6 @@ class BtnType(WidgetType): def __init__(self): super().__init__(CONF_BUTTON, LvBoolean("lv_btn_t"), (CONF_MAIN,)) - async def to_code(self, w, config): - return [] - def obj_creator(self, parent: MockObjClass, config: dict): """ LVGL 8 calls buttons `btn` @@ -21,5 +18,8 @@ class BtnType(WidgetType): def get_uses(self): return ("btn",) + async def to_code(self, w, config): + return [] + btn_spec = BtnType() diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index a2b4ac13fb..9f349e3943 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -4,12 +4,32 @@ Constants already defined in esphome.const are not duplicated here and must be i """ +from typing import Union + from esphome import codegen as cg, config_validation as cv from esphome.core import ID, Lambda +from esphome.cpp_generator import Literal from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor -from .lvcode import ConstantLiteral +from .helpers import requires_component + + +class ConstantLiteral(Literal): + __slots__ = ("constant",) + + def __init__(self, constant: str): + super().__init__() + self.constant = constant + + def __str__(self): + return self.constant + + +def literal(arg: Union[str, ConstantLiteral]): + if isinstance(arg, str): + return ConstantLiteral(arg) + return arg class LValidator: @@ -18,14 +38,19 @@ class LValidator: has `process()` to convert a value during code generation """ - def __init__(self, validator, rtype, idtype=None, idexpr=None, retmapper=None): + def __init__( + self, validator, rtype, idtype=None, idexpr=None, retmapper=None, requires=None + ): self.validator = validator self.rtype = rtype self.idtype = idtype self.idexpr = idexpr self.retmapper = retmapper + self.requires = requires def __call__(self, value): + if self.requires: + value = requires_component(self.requires)(value) if isinstance(value, cv.Lambda): return cv.returning_lambda(value) if self.idtype is not None and isinstance(value, ID): @@ -422,6 +447,7 @@ CONF_RECOLOR = "recolor" CONF_RIGHT_BUTTON = "right_button" CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" +CONF_ROTARY_ENCODERS = "rotary_encoders" CONF_ROWS = "rows" CONF_SCALES = "scales" CONF_SCALE_LINES = "scale_lines" diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 533dc582f0..818bde6aed 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components.binary_sensor import BinarySensor from esphome.components.color import ColorStruct from esphome.components.font import Font +from esphome.components.image import Image_ from esphome.components.sensor import Sensor from esphome.components.text_sensor import TextSensor import esphome.config_validation as cv @@ -13,22 +14,15 @@ from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from . import types as ty -from .defines import LV_FONTS, LValidator, LvConstant +from .defines import LV_FONTS, ConstantLiteral, LValidator, LvConstant, literal from .helpers import ( esphome_fonts_used, lv_fonts_used, lvgl_components_required, requires_component, ) -from .lvcode import ConstantLiteral, lv_expr -from .types import lv_font_t - - -def literal_mapper(value, args=()): - if isinstance(value, str): - return ConstantLiteral(value) - return value - +from .lvcode import lv_expr +from .types import lv_font_t, lv_img_t opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") @@ -43,7 +37,7 @@ def opacity_validator(value): return value -opacity = LValidator(opacity_validator, uint32, retmapper=literal_mapper) +opacity = LValidator(opacity_validator, uint32, retmapper=literal) @schema_extractor("one_of") @@ -79,9 +73,7 @@ def pixels_or_percent_validator(value): return f"lv_pct({int(cv.percentage(value) * 100)})" -pixels_or_percent = LValidator( - pixels_or_percent_validator, uint32, retmapper=literal_mapper -) +pixels_or_percent = LValidator(pixels_or_percent_validator, uint32, retmapper=literal) def zoom(value): @@ -115,7 +107,7 @@ def size_validator(value): return f"lv_pct({int(cv.percentage(value) * 100)})" -size = LValidator(size_validator, uint32, retmapper=literal_mapper) +size = LValidator(size_validator, uint32, retmapper=literal) radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") @@ -130,21 +122,37 @@ def radius_validator(value): return value +radius = LValidator(radius_validator, uint32, retmapper=literal) + + def id_name(value): if value == SCHEMA_EXTRACT: return "id" return cv.validate_id_name(value) -radius = LValidator(radius_validator, uint32, retmapper=literal_mapper) - - def stop_value(value): return cv.int_range(0, 255)(value) +lv_images_used = set() + + +def image_validator(value): + value = requires_component("image")(value) + value = cv.use_id(Image_)(value) + lv_images_used.add(value) + return value + + +lv_image = LValidator( + image_validator, + lv_img_t, + retmapper=lambda x: lv_expr.img_from(MockObj(x)), + requires="image", +) lv_bool = LValidator( - cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal_mapper + cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal ) diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 13b4862b4d..3a8a958f2e 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -8,8 +8,8 @@ from esphome.cpp_generator import ( AssignmentExpression, CallExpression, Expression, + ExpressionStatement, LambdaExpression, - Literal, MockObj, RawExpression, RawStatement, @@ -19,7 +19,9 @@ from esphome.cpp_generator import ( statement, ) +from .defines import ConstantLiteral from .helpers import get_line_marks +from .types import lv_group_t _LOGGER = logging.getLogger(__name__) @@ -105,29 +107,40 @@ class LambdaContext(CodeContext): def __init__( self, - parameters: list[tuple[SafeExpType, str]], - return_type: SafeExpType = None, + parameters: list[tuple[SafeExpType, str]] = None, + return_type: SafeExpType = cg.void, + capture: str = "", ): super().__init__() self.code_list: list[Statement] = [] self.parameters = parameters self.return_type = return_type + self.capture = capture def add(self, expression: Union[Expression, Statement]): self.code_list.append(expression) return expression - async def code(self) -> LambdaExpression: + async def get_lambda(self) -> LambdaExpression: + code_text = self.get_code() + return await cg.process_lambda( + Lambda("\n".join(code_text) + "\n\n"), + self.parameters, + capture=self.capture, + return_type=self.return_type, + ) + + def get_code(self): code_text = [] for exp in self.code_list: text = str(statement(exp)) text = text.rstrip() code_text.append(text) - return await cg.process_lambda( - Lambda("\n".join(code_text) + "\n\n"), - self.parameters, - return_type=self.return_type, - ) + return code_text + + def __enter__(self): + super().__enter__() + return self class LocalVariable(MockObj): @@ -187,13 +200,18 @@ class MockLv: return result def cond_if(self, expression: Expression): - CodeContext.append(RawExpression(f"if({expression}) {{")) + CodeContext.append(RawStatement(f"if {expression} {{")) def cond_else(self): - CodeContext.append(RawExpression("} else {")) + CodeContext.append(RawStatement("} else {")) def cond_endif(self): - CodeContext.append(RawExpression("}")) + CodeContext.append(RawStatement("}")) + + +class ReturnStatement(ExpressionStatement): + def __str__(self): + return f"return {self.expression};" class LvExpr(MockLv): @@ -210,6 +228,7 @@ lv = MockLv("lv_") lv_expr = LvExpr("lv_") # Mock for lv_obj_ calls lv_obj = MockLv("lv_obj_") +lvgl_comp = MockObj("lvgl_comp", "->") # equivalent to cg.add() for the lvgl init context @@ -226,12 +245,19 @@ def lv_assign(target, expression): lv_add(RawExpression(f"{target} = {expression}")) -class ConstantLiteral(Literal): - __slots__ = ("constant",) +lv_groups = {} # Widget group names - def __init__(self, constant: str): - super().__init__() - self.constant = constant - def __str__(self): - return self.constant +def add_group(name): + if name is None: + return None + fullname = f"lv_esp_group_{name}" + if name not in lv_groups: + gid = ID(fullname, True, type=lv_group_t.operator("ptr")) + lv_add( + AssignmentExpression( + type_=gid.type, modifier="", name=fullname, rhs=lv_expr.group_create() + ) + ) + lv_groups[name] = ConstantLiteral(fullname) + return lv_groups[name] diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 74a1b0e7af..34f8eaf21f 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -19,13 +19,35 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) { } void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { - auto now = millis(); - this->draw_buffer_(area, (const uint8_t *) color_p); - ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), - lv_area_get_height(area), (int) (millis() - now)); + if (!this->paused_) { + auto now = millis(); + this->draw_buffer_(area, (const uint8_t *) color_p); + ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), + lv_area_get_height(area), (int) (millis() - now)); + } lv_disp_flush_ready(disp_drv); } +void LvglComponent::write_random_() { + // length of 2 lines in 32 bit units + // we write 2 lines for the benefit of displays that won't write one line at a time. + size_t line_len = this->disp_drv_.hor_res * LV_COLOR_DEPTH / 8 / 4 * 2; + for (size_t i = 0; i != line_len; i++) { + ((uint32_t *) (this->draw_buf_.buf1))[i] = random_uint32(); + } + lv_area_t area; + area.x1 = 0; + area.x2 = this->disp_drv_.hor_res - 1; + if (this->snow_line_ == this->disp_drv_.ver_res / 2) { + area.y1 = static_cast(random_uint32() % (this->disp_drv_.ver_res / 2) * 2); + } else { + area.y1 = this->snow_line_++ * 2; + } + // write 2 lines + area.y2 = area.y1 + 1; + this->draw_buffer_(&area, (const uint8_t *) this->draw_buf_.buf1); +} + void LvglComponent::setup() { ESP_LOGCONFIG(TAG, "LVGL Setup starts"); #if LV_USE_LOG @@ -74,10 +96,53 @@ void LvglComponent::setup() { ESP_LOGV(TAG, "sw_rotate = %d, rotated=%d", this->disp_drv_.sw_rotate, this->disp_drv_.rotated); this->disp_ = lv_disp_drv_register(&this->disp_drv_); for (const auto &v : this->init_lambdas_) - v(this->disp_); + v(this); lv_disp_trig_activity(this->disp_); ESP_LOGCONFIG(TAG, "LVGL Setup complete"); } + +#ifdef USE_LVGL_IMAGE +lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) { + if (img_dsc == nullptr) + img_dsc = new lv_img_dsc_t(); // NOLINT + img_dsc->header.always_zero = 0; + img_dsc->header.reserved = 0; + img_dsc->header.w = src->get_width(); + img_dsc->header.h = src->get_height(); + img_dsc->data = src->get_data_start(); + img_dsc->data_size = image_type_to_width_stride(img_dsc->header.w * img_dsc->header.h, src->get_type()); + switch (src->get_type()) { + case image::IMAGE_TYPE_BINARY: + img_dsc->header.cf = LV_IMG_CF_ALPHA_1BIT; + break; + + case image::IMAGE_TYPE_GRAYSCALE: + img_dsc->header.cf = LV_IMG_CF_ALPHA_8BIT; + break; + + case image::IMAGE_TYPE_RGB24: + img_dsc->header.cf = LV_IMG_CF_RGB888; + break; + + case image::IMAGE_TYPE_RGB565: +#if LV_COLOR_DEPTH == 16 + img_dsc->header.cf = src->has_transparency() ? LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED : LV_IMG_CF_TRUE_COLOR; +#else + img_dsc->header.cf = LV_IMG_CF_RGB565; +#endif + break; + + case image::IMAGE_TYPE_RGBA: +#if LV_COLOR_DEPTH == 32 + img_dsc->header.cf = LV_IMG_CF_TRUE_COLOR; +#else + img_dsc->header.cf = LV_IMG_CF_RGBA8888; +#endif + break; + } + return img_dsc; +} +#endif } // namespace lvgl } // namespace esphome diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index a884a27042..a0d3d226ce 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -1,23 +1,32 @@ #pragma once #include "esphome/core/defines.h" -#ifdef USE_LVGL + +#ifdef USE_LVGL_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif // USE_LVGL_BINARY_SENSOR +#ifdef USE_LVGL_ROTARY_ENCODER +#include "esphome/components/rotary_encoder/rotary_encoder.h" +#endif // USE_LVGL_ROTARY_ENCODER // required for clang-tidy #ifndef LV_CONF_H #define LV_CONF_SKIP 1 // NOLINT -#endif +#endif // LV_CONF_H #include "esphome/components/display/display.h" #include "esphome/components/display/display_color_utils.h" #include "esphome/core/component.h" -#include "esphome/core/hal.h" #include "esphome/core/log.h" #include +#include #include +#ifdef USE_LVGL_IMAGE +#include "esphome/components/image/image.h" +#endif // USE_LVGL_IMAGE #ifdef USE_LVGL_FONT #include "esphome/components/font/font.h" -#endif +#endif // USE_LVGL_FONT #ifdef USE_LVGL_TOUCHSCREEN #include "esphome/components/touchscreen/touchscreen.h" #endif // USE_LVGL_TOUCHSCREEN @@ -40,7 +49,7 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT // Parent class for things that wrap an LVGL object class LvCompound final { public: - virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } + void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } lv_obj_t *obj{}; }; @@ -49,6 +58,15 @@ using set_value_lambda_t = std::function; using event_callback_t = void(_lv_event_t *); using text_lambda_t = std::function; +template class ObjUpdateAction : public Action { + public: + explicit ObjUpdateAction(std::function &&lamb) : lamb_(std::move(lamb)) {} + + void play(Ts... x) override { this->lamb_(x...); } + + protected: + std::function lamb_; +}; #ifdef USE_LVGL_FONT class FontEngine { public: @@ -67,6 +85,9 @@ class FontEngine { lv_font_t lv_font_{}; }; #endif // USE_LVGL_FONT +#ifdef USE_LVGL_IMAGE +lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc = nullptr); +#endif // USE_LVGL_IMAGE class LvglComponent : public PollingComponent { constexpr static const char *const TAG = "lvgl"; @@ -92,27 +113,54 @@ class LvglComponent : public PollingComponent { area->y2++; } - void loop() override { lv_timer_handler_run_in_period(5); } void setup() override; - void update() override {} + void update() override { + // update indicators + if (this->paused_) { + return; + } + this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); + } + void loop() override { + if (this->paused_) { + if (this->show_snow_) + this->write_random_(); + } + lv_timer_handler_run_in_period(5); + } + + void add_on_idle_callback(std::function &&callback) { + this->idle_callbacks_.add(std::move(callback)); + } void add_display(display::Display *display) { this->displays_.push_back(display); } - void add_init_lambda(const std::function &lamb) { this->init_lambdas_.push_back(lamb); } + void add_init_lambda(const std::function &lamb) { this->init_lambdas_.push_back(lamb); } void dump_config() override; void set_full_refresh(bool full_refresh) { this->full_refresh_ = full_refresh; } + bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; } void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; } lv_disp_t *get_disp() { return this->disp_; } void set_paused(bool paused, bool show_snow) { this->paused_ = paused; + this->show_snow_ = show_snow; + this->snow_line_ = 0; if (!paused && lv_scr_act() != nullptr) { lv_disp_trig_activity(this->disp_); // resets the inactivity time lv_obj_invalidate(lv_scr_act()); } } + + void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { + lv_obj_add_event_cb(obj, callback, event, this); + if (event == LV_EVENT_VALUE_CHANGED) { + lv_obj_add_event_cb(obj, callback, lv_custom_event, this); + } + } bool is_paused() const { return this->paused_; } protected: + void write_random_(); void draw_buffer_(const lv_area_t *area, const uint8_t *ptr); void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); std::vector displays_{}; @@ -120,12 +168,52 @@ class LvglComponent : public PollingComponent { lv_disp_drv_t disp_drv_{}; lv_disp_t *disp_{}; bool paused_{}; + bool show_snow_{}; + lv_coord_t snow_line_{}; - std::vector> init_lambdas_; + std::vector> init_lambdas_; + CallbackManager idle_callbacks_{}; size_t buffer_frac_{1}; bool full_refresh_{}; }; +class IdleTrigger : public Trigger<> { + public: + explicit IdleTrigger(LvglComponent *parent, TemplatableValue timeout) : timeout_(std::move(timeout)) { + parent->add_on_idle_callback([this](uint32_t idle_time) { + if (!this->is_idle_ && idle_time > this->timeout_.value()) { + this->is_idle_ = true; + this->trigger(); + } else if (this->is_idle_ && idle_time < this->timeout_.value()) { + this->is_idle_ = false; + } + }); + } + + protected: + TemplatableValue timeout_; + bool is_idle_{}; +}; + +template class LvglAction : public Action, public Parented { + public: + explicit LvglAction(std::function &&lamb) : action_(std::move(lamb)) {} + void play(Ts... x) override { this->action_(this->parent_); } + + protected: + std::function action_{}; +}; + +template class LvglCondition : public Condition, public Parented { + public: + LvglCondition(std::function &&condition_lambda) + : condition_lambda_(std::move(condition_lambda)) {} + bool check(Ts... x) override { return this->condition_lambda_(this->parent_); } + + protected: + std::function condition_lambda_{}; +}; + #ifdef USE_LVGL_TOUCHSCREEN class LVTouchListener : public touchscreen::TouchListener, public Parented { public: @@ -160,7 +248,62 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented { + public: + LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) { + lv_indev_drv_init(&this->drv_); + this->drv_.type = type; + this->drv_.user_data = this; + this->drv_.long_press_time = lpt; + this->drv_.long_press_repeat_time = lprt; + this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { + auto *l = static_cast(d->user_data); + data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + data->key = l->key_; + data->enc_diff = (int16_t) (l->count_ - l->last_count_); + l->last_count_ = l->count_; + data->continue_reading = false; + }; + } + + void set_left_button(binary_sensor::BinarySensor *left_button) { + left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); }); + } + void set_right_button(binary_sensor::BinarySensor *right_button) { + right_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_RIGHT, state); }); + } + + void set_enter_button(binary_sensor::BinarySensor *enter_button) { + enter_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_ENTER, state); }); + } + + void set_sensor(rotary_encoder::RotaryEncoderSensor *sensor) { + sensor->register_listener([this](int32_t count) { this->set_count(count); }); + } + + void event(int key, bool pressed) { + if (!this->parent_->is_paused()) { + this->pressed_ = pressed; + this->key_ = key; + } + } + + void set_count(int32_t count) { + if (!this->parent_->is_paused()) + this->count_ = count; + } + + lv_indev_drv_t *get_drv() { return &this->drv_; } + + protected: + lv_indev_drv_t drv_{}; + bool pressed_{}; + int32_t count_{}; + int32_t last_count_{}; + int key_{}; +}; +#endif // USE_LVGL_KEY_LISTENER } // namespace lvgl } // namespace esphome - -#endif // USE_LVGL diff --git a/esphome/components/lvgl/obj.py b/esphome/components/lvgl/obj.py index 92c4f63d2d..40d7e55381 100644 --- a/esphome/components/lvgl/obj.py +++ b/esphome/components/lvgl/obj.py @@ -1,5 +1,9 @@ +from esphome import automation + +from .automation import update_to_code from .defines import CONF_MAIN, CONF_OBJ -from .types import WidgetType, lv_obj_t +from .schemas import create_modify_schema +from .types import ObjUpdateAction, WidgetType, lv_obj_t class ObjType(WidgetType): @@ -15,3 +19,10 @@ class ObjType(WidgetType): obj_spec = ObjType() + + +@automation.register_action( + "lvgl.widget.update", ObjUpdateAction, create_modify_schema(obj_spec) +) +async def obj_update_to_code(config, action_id, template_arg, args): + return await update_to_code(config, action_id, template_arg, args) diff --git a/esphome/components/lvgl/rotary_encoders.py b/esphome/components/lvgl/rotary_encoders.py new file mode 100644 index 0000000000..77dc397c3e --- /dev/null +++ b/esphome/components/lvgl/rotary_encoders.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +from esphome.components.binary_sensor import BinarySensor +from esphome.components.rotary_encoder.sensor import RotaryEncoderSensor +import esphome.config_validation as cv +from esphome.const import CONF_GROUP, CONF_ID, CONF_SENSOR + +from .defines import ( + CONF_ENTER_BUTTON, + CONF_LEFT_BUTTON, + CONF_LONG_PRESS_REPEAT_TIME, + CONF_LONG_PRESS_TIME, + CONF_RIGHT_BUTTON, + CONF_ROTARY_ENCODERS, +) +from .helpers import lvgl_components_required +from .lvcode import add_group, lv, lv_add, lv_expr +from .schemas import ENCODER_SCHEMA +from .types import lv_indev_type_t + +ROTARY_ENCODER_CONFIG = cv.ensure_list( + ENCODER_SCHEMA.extend( + { + cv.Required(CONF_ENTER_BUTTON): cv.use_id(BinarySensor), + cv.Required(CONF_SENSOR): cv.Any( + cv.use_id(RotaryEncoderSensor), + cv.Schema( + { + cv.Required(CONF_LEFT_BUTTON): cv.use_id(BinarySensor), + cv.Required(CONF_RIGHT_BUTTON): cv.use_id(BinarySensor), + } + ), + ), + } + ) +) + + +async def rotary_encoders_to_code(var, config): + for enc_conf in config.get(CONF_ROTARY_ENCODERS, ()): + lvgl_components_required.add("KEY_LISTENER") + lvgl_components_required.add("ROTARY_ENCODER") + lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds + lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds + listener = cg.new_Pvariable( + enc_conf[CONF_ID], lv_indev_type_t.LV_INDEV_TYPE_ENCODER, lpt, lprt + ) + await cg.register_parented(listener, var) + if sensor_config := enc_conf.get(CONF_SENSOR): + if isinstance(sensor_config, dict): + b_sensor = await cg.get_variable(sensor_config[CONF_LEFT_BUTTON]) + cg.add(listener.set_left_button(b_sensor)) + b_sensor = await cg.get_variable(sensor_config[CONF_RIGHT_BUTTON]) + cg.add(listener.set_right_button(b_sensor)) + else: + sensor_config = await cg.get_variable(sensor_config) + lv_add(listener.set_sensor(sensor_config)) + b_sensor = await cg.get_variable(enc_conf[CONF_ENTER_BUTTON]) + cg.add(listener.set_enter_button(b_sensor)) + if group := add_group(enc_conf.get(CONF_GROUP)): + lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) + else: + lv.indev_drv_register(listener.get_drv()) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 9f6d984545..ebef56a882 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,10 +1,21 @@ from esphome import config_validation as cv -from esphome.const import CONF_ARGS, CONF_FORMAT, CONF_ID, CONF_STATE, CONF_TYPE +from esphome.automation import Trigger, validate_automation +from esphome.const import ( + CONF_ARGS, + CONF_FORMAT, + CONF_GROUP, + CONF_ID, + CONF_ON_VALUE, + CONF_STATE, + CONF_TRIGGER_ID, + CONF_TYPE, +) +from esphome.core import TimePeriod from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid, types as ty from .helpers import add_lv_use, requires_component, validate_printf -from .lv_validation import lv_font +from .lv_validation import id_name, lv_font from .types import WIDGET_TYPES, WidgetType # A schema for text properties @@ -27,6 +38,28 @@ TEXT_SCHEMA = cv.Schema( } ) +ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t), + }, + key=CONF_ID, +) + +PRESS_TIME = cv.All( + lvalid.lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535)) +) + +ENCODER_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.All( + cv.declare_id(ty.LVEncoderListener), requires_component("binary_sensor") + ), + cv.Optional(CONF_GROUP): lvalid.id_name, + cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, + cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, + } +) + # All LVGL styles and their validators STYLE_PROPS = { "align": df.CHILD_ALIGNMENTS.one_of, @@ -43,6 +76,7 @@ STYLE_PROPS = { "bg_image_opa": lvalid.opacity, "bg_image_recolor": lvalid.lv_color, "bg_image_recolor_opa": lvalid.opacity, + "bg_image_src": lvalid.lv_image, "bg_main_stop": lvalid.stop_value, "bg_opa": lvalid.opacity, "border_color": lvalid.lv_color, @@ -151,6 +185,39 @@ def part_schema(widget_type: WidgetType): ) +def automation_schema(typ: ty.LvType): + if typ.has_on_value: + events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,) + else: + events = df.LV_EVENT_TRIGGERS + if isinstance(typ, ty.LvType): + template = Trigger.template(typ.get_arg_type()) + else: + template = Trigger.template() + return { + cv.Optional(event): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(template), + } + ) + for event in events + } + + +def create_modify_schema(widget_type): + return ( + part_schema(widget_type) + .extend( + { + cv.Required(CONF_ID): cv.use_id(widget_type), + cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + } + ) + .extend(FLAG_SCHEMA) + .extend(widget_type.modify_schema) + ) + + def obj_schema(widget_type: WidgetType): """ Create a schema for a widget type itself i.e. no allowance for children @@ -161,10 +228,12 @@ def obj_schema(widget_type: WidgetType): part_schema(widget_type) .extend(FLAG_SCHEMA) .extend(ALIGN_TO_SCHEMA) + .extend(automation_schema(widget_type.w_type)) .extend( cv.Schema( { cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + cv.Optional(CONF_GROUP): id_name, } ) ) @@ -188,6 +257,13 @@ STYLED_TEXT_SCHEMA = cv.maybe_simple_value( STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT ) +# For use by platform components +LVGL_SCHEMA = cv.Schema( + { + cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(ty.LvglComponent), + } +) + ALL_STYLES = { **STYLE_PROPS, } diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py index a0d4a3e4ad..499b33aa02 100644 --- a/esphome/components/lvgl/touchscreens.py +++ b/esphome/components/lvgl/touchscreens.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components.touchscreen import CONF_TOUCHSCREEN_ID, Touchscreen import esphome.config_validation as cv from esphome.const import CONF_ID -from esphome.core import CORE, TimePeriod +from esphome.core import CORE from .defines import ( CONF_LONG_PRESS_REPEAT_TIME, @@ -10,11 +10,10 @@ from .defines import ( CONF_TOUCHSCREENS, ) from .helpers import lvgl_components_required -from .lv_validation import lv_milliseconds from .lvcode import lv +from .schemas import PRESS_TIME from .types import LVTouchListener -PRESS_TIME = cv.All(lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535))) CONF_TOUCHSCREEN = "touchscreen" TOUCHSCREENS_CONFIG = cv.maybe_simple_value( { diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py new file mode 100644 index 0000000000..bf92bda5b0 --- /dev/null +++ b/esphome/components/lvgl/trigger.py @@ -0,0 +1,61 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_ON_VALUE, CONF_TRIGGER_ID + +from .defines import ( + CONF_ALIGN, + CONF_ALIGN_TO, + CONF_X, + CONF_Y, + LV_EVENT, + LV_EVENT_TRIGGERS, + literal, +) +from .lvcode import LambdaContext, add_line_marks, lv, lv_add +from .widget import widget_map + +lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr") + + +async def generate_triggers(lv_component): + """ + Generate LVGL triggers for all defined widgets + Must be done after all widgets completed + :param lv_component: The parent component + :return: + """ + + for w in widget_map.values(): + if w.config: + for event, conf in { + event: conf + for event, conf in w.config.items() + if event in LV_EVENT_TRIGGERS + }.items(): + conf = conf[0] + w.add_flag("LV_OBJ_FLAG_CLICKABLE") + event = "LV_EVENT_" + LV_EVENT[event[3:].upper()] + await add_trigger(conf, event, lv_component, w) + for conf in w.config.get(CONF_ON_VALUE, ()): + await add_trigger(conf, "LV_EVENT_VALUE_CHANGED", lv_component, w) + + # Generate align to directives while we're here + if align_to := w.config.get(CONF_ALIGN_TO): + target = widget_map[align_to[CONF_ID]].obj + align = align_to[CONF_ALIGN] + x = align_to[CONF_X] + y = align_to[CONF_Y] + lv.obj_align_to(w.obj, target, align, x, y) + + +async def add_trigger(conf, event, lv_component, w): + tid = conf[CONF_TRIGGER_ID] + add_line_marks(tid) + trigger = cg.new_Pvariable(tid) + args = w.get_args() + value = w.get_value() + await automation.build_automation(trigger, args, conf) + with LambdaContext([(lv_event_t_ptr, "event_data")]) as context: + add_line_marks(tid) + lv_add(trigger.trigger(value)) + lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), literal(event))) diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 60291ea54a..6997207dac 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,4 +1,4 @@ -from esphome import codegen as cg +from esphome import automation, codegen as cg from esphome.core import ID from esphome.cpp_generator import MockObjClass @@ -23,8 +23,14 @@ lvgl_ns = cg.esphome_ns.namespace("lvgl") char_ptr = cg.global_ns.namespace("char").operator("ptr") void_ptr = cg.void.operator("ptr") LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) +LvglComponentPtr = LvglComponent.operator("ptr") lv_event_code_t = cg.global_ns.namespace("lv_event_code_t") +lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t") FontEngine = lvgl_ns.class_("FontEngine") +IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) +ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) +LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) +LvglAction = lvgl_ns.class_("LvglAction", automation.Action) LvCompound = lvgl_ns.class_("LvCompound") lv_font_t = cg.global_ns.class_("lv_font_t") lv_style_t = cg.global_ns.struct("lv_style_t") @@ -33,9 +39,11 @@ lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) lv_obj_t_ptr = lv_obj_base_t.operator("ptr") lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr") lv_color_t = cg.global_ns.struct("lv_color_t") +lv_group_t = cg.global_ns.struct("lv_group_t") LVTouchListener = lvgl_ns.class_("LVTouchListener") LVEncoderListener = lvgl_ns.class_("LVEncoderListener") lv_obj_t = LvType("lv_obj_t") +lv_img_t = LvType("lv_img_t") # this will be populated later, in __init__.py to avoid circular imports. @@ -58,7 +66,7 @@ class LvBoolean(LvType): super().__init__( *args, largs=[(cg.bool_, "x")], - lvalue=lambda w: w.is_checked(), + lvalue=lambda w: w.has_state("LV_STATE_CHECKED"), has_on_value=True, **kwargs, ) @@ -83,11 +91,14 @@ class WidgetType: self.name = name self.w_type = w_type self.parts = parts - self.schema = schema or {} - if modify_schema is None: - self.modify_schema = schema + if schema is None: + self.schema = {} else: - self.modify_schema = modify_schema + self.schema = schema + if modify_schema is None: + self.modify_schema = self.schema + else: + self.modify_schema = self.schema @property def animated(self): diff --git a/esphome/components/lvgl/widget.py b/esphome/components/lvgl/widget.py index 4755d8b21d..83aed341e7 100644 --- a/esphome/components/lvgl/widget.py +++ b/esphome/components/lvgl/widget.py @@ -4,9 +4,9 @@ from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.config_validation import Invalid from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE -from esphome.core import ID, TimePeriod +from esphome.core import CORE, TimePeriod from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass, VariableDeclarationExpression from .defines import ( CONF_DEFAULT, @@ -16,13 +16,15 @@ from .defines import ( OBJ_FLAGS, PARTS, STATES, + ConstantLiteral, LValidator, join_enums, + literal, ) from .helpers import add_lv_use -from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj +from .lvcode import add_group, add_line_marks, lv, lv_add, lv_assign, lv_expr, lv_obj from .schemas import ALL_STYLES, STYLE_REMAP -from .types import WIDGET_TYPES, WidgetType, lv_obj_t +from .types import WIDGET_TYPES, LvType, WidgetType, lv_obj_t, lv_obj_t_ptr EVENT_LAMB = "event_lamb__" @@ -76,17 +78,20 @@ class Widget: return f"{self.var}->obj" return self.var - def add_state(self, *args): - return lv_obj.add_state(self.obj, *args) + def add_state(self, state): + return lv_obj.add_state(self.obj, literal(state)) - def clear_state(self, *args): - return lv_obj.clear_state(self.obj, *args) + def clear_state(self, state): + return lv_obj.clear_state(self.obj, literal(state)) - def add_flag(self, *args): - return lv_obj.add_flag(self.obj, *args) + def has_state(self, state): + return lv_expr.obj_get_state(self.obj) & literal(state) != 0 - def clear_flag(self, *args): - return lv_obj.clear_flag(self.obj, *args) + def add_flag(self, flag): + return lv_obj.add_flag(self.obj, literal(flag)) + + def clear_flag(self, flag): + return lv_obj.clear_flag(self.obj, literal(flag)) def set_property(self, prop, value, animated: bool = None, ltype=None): if isinstance(value, dict): @@ -125,6 +130,16 @@ class Widget: def __str__(self): return f"({self.var}, {self.type})" + def get_args(self): + if isinstance(self.type.w_type, LvType): + return self.type.w_type.args + return [(lv_obj_t_ptr, "obj")] + + def get_value(self): + if isinstance(self.type.w_type, LvType): + return self.type.w_type.value(self) + return self.obj + # Map of widgets to their config, used for trigger generation widget_map: dict[Any, Widget] = {} @@ -146,7 +161,8 @@ def get_widget_generator(wid): yield -async def get_widget(wid: ID) -> Widget: +async def get_widget(config: dict, id: str = CONF_ID) -> Widget: + wid = config[id] if obj := widget_map.get(wid): return obj return await FakeAwaitable(get_widget_generator(wid)) @@ -204,9 +220,10 @@ async def set_obj_properties(w: Widget, config): }.items(): if isinstance(ALL_STYLES[prop], LValidator): value = await ALL_STYLES[prop].process(value) - # Remapping for backwards compatibility of style names prop_r = STYLE_REMAP.get(prop, prop) w.set_style(prop_r, value, lv_state) + if group := add_group(config.get(CONF_GROUP)): + lv.group_add_obj(group, w.obj) flag_clr = set() flag_set = set() props = parts[CONF_MAIN][CONF_DEFAULT] @@ -241,7 +258,7 @@ async def set_obj_properties(w: Widget, config): w.clear_state(clears) for key, value in lambs.items(): lamb = await cg.process_lambda(value, [], return_type=cg.bool_) - state = ConstantLiteral(f"LV_STATE_{key.upper}") + state = f"LV_STATE_{key.upper}" lv.cond_if(lamb) w.add_state(state) lv.cond_else() @@ -281,10 +298,19 @@ async def widget_to_code(w_cnfig, w_type, parent): var = cg.new_Pvariable(wid) lv_add(var.set_obj(creator)) else: - var = cg.Pvariable(wid, cg.nullptr, type_=lv_obj_t) + var = MockObj(wid, "->") + decl = VariableDeclarationExpression(lv_obj_t, "*", wid) + CORE.add_global(decl) + CORE.register_variable(wid, var) lv_assign(var, creator) widget = Widget.create(wid, var, spec, w_cnfig, parent) await set_obj_properties(widget, w_cnfig) await add_widgets(widget, w_cnfig) await spec.to_code(widget, w_cnfig) + + +lv_scr_act_spec = LvScrActType() +lv_scr_act = Widget.create( + None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None +) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 6ba5b64761..726db24592 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -39,9 +39,12 @@ #define USE_LOCK #define USE_LOGGER #define USE_LVGL +#define USE_LVGL_BINARY_SENSOR #define USE_LVGL_FONT #define USE_LVGL_IMAGE +#define USE_LVGL_KEY_LISTENER #define USE_LVGL_TOUCHSCREEN +#define USE_LVGL_ROTARY_ENCODER #define USE_MDNS #define USE_MEDIA_PLAYER #define USE_MQTT diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 696c749876..fde700e0bd 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -7,6 +7,7 @@ lvgl: long_press_time: 500ms widgets: - label: + id: hello_label text: Hello world text_color: 0xFF8000 align: center @@ -95,9 +96,43 @@ lvgl: height: 10% pressed: bg_color: light_blue + checkable: true + checked: + bg_color: 0x000000 widgets: - label: text: Button + on_click: + lvgl.label.update: + id: hello_label + bg_color: 0x123456 + text: clicked + on_value: + logger.log: + format: "state now %d" + args: [x] + on_short_click: + lvgl.widget.hide: hello_label + on_long_press: + lvgl.widget.show: hello_label + on_cancel: + lvgl.widget.enable: hello_label + on_ready: + lvgl.widget.disable: hello_label + on_defocus: + lvgl.widget.hide: hello_label + on_focus: + logger.log: Button clicked + on_scroll: + logger.log: Button clicked + on_scroll_end: + logger.log: Button clicked + on_scroll_begin: + logger.log: Button clicked + on_release: + logger.log: Button clicked + on_long_press_repeat: + logger.log: Button clicked font: - file: "gfonts://Roboto" diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index eab75b05f3..0f740db980 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -6,6 +6,23 @@ i2c: sda: GPIO18 scl: GPIO19 +sensor: + - platform: rotary_encoder + name: "Rotary Encoder" + id: encoder + pin_a: 2 + pin_b: 1 + internal: true + +binary_sensor: + - platform: gpio + id: pushbutton + name: Pushbutton + pin: + number: 0 + inverted: true + ignore_strapping_warning: true + display: - platform: ili9xxx model: st7789v @@ -50,5 +67,9 @@ lvgl: displays: - tft_display - second_display + rotary_encoders: + sensor: encoder + enter_button: pushbutton + group: general <<: !include common.yaml From dfacf1bbfe3aef5ca6796f92e8a46cb62212e924 Mon Sep 17 00:00:00 2001 From: thevogoncoder <6619878+thevogoncoder@users.noreply.github.com> Date: Thu, 25 Jul 2024 04:06:23 +0200 Subject: [PATCH 020/160] Add delay after sending REG_READ_START (#7130) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/pmwcs3/pmwcs3.cpp | 61 ++++++++++++++-------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/esphome/components/pmwcs3/pmwcs3.cpp b/esphome/components/pmwcs3/pmwcs3.cpp index 812018b52e..97ce4c9ae0 100644 --- a/esphome/components/pmwcs3/pmwcs3.cpp +++ b/esphome/components/pmwcs3/pmwcs3.cpp @@ -72,43 +72,44 @@ void PMWCS3Component::dump_config() { LOG_SENSOR(" ", "vwc", this->vwc_sensor_); } void PMWCS3Component::read_data_() { - uint8_t data[8]; - float e25, ec, temperature, vwc; - /////// Super important !!!! first activate reading PMWCS3_REG_READ_START (if not, return always the same values) //// - if (!this->write_bytes(PMWCS3_REG_READ_START, nullptr, 0)) { this->status_set_warning(); ESP_LOGVV(TAG, "Failed to write into REG_READ_START register !!!"); return; } - // NOLINT delay(100); - if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) { - ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers"); - this->mark_failed(); - return; - } - if (this->e25_sensor_ != nullptr) { - e25 = ((data[1] << 8) | data[0]) / 100.0; - this->e25_sensor_->publish_state(e25); - ESP_LOGVV(TAG, "e25: data[0]=%d, data[1]=%d, result=%f", data[0], data[1], e25); - } - if (this->ec_sensor_ != nullptr) { - ec = ((data[3] << 8) | data[2]) / 10.0; - this->ec_sensor_->publish_state(ec); - ESP_LOGVV(TAG, "ec: data[2]=%d, data[3]=%d, result=%f", data[2], data[3], ec); - } - if (this->temperature_sensor_ != nullptr) { - temperature = ((data[5] << 8) | data[4]) / 100.0; - this->temperature_sensor_->publish_state(temperature); - ESP_LOGVV(TAG, "temp: data[4]=%d, data[5]=%d, result=%f", data[4], data[5], temperature); - } - if (this->vwc_sensor_ != nullptr) { - vwc = ((data[7] << 8) | data[6]) / 10.0; - this->vwc_sensor_->publish_state(vwc); - ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc); - } + // Wait for the sensor to be ready. + // 80ms empirically determined (conservative). + this->set_timeout(80, [this] { + uint8_t data[8]; + float e25, ec, temperature, vwc; + if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) { + ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers"); + this->mark_failed(); + return; + } + if (this->e25_sensor_ != nullptr) { + e25 = ((data[1] << 8) | data[0]) / 100.0; + this->e25_sensor_->publish_state(e25); + ESP_LOGVV(TAG, "e25: data[0]=%d, data[1]=%d, result=%f", data[0], data[1], e25); + } + if (this->ec_sensor_ != nullptr) { + ec = ((data[3] << 8) | data[2]) / 10.0; + this->ec_sensor_->publish_state(ec); + ESP_LOGVV(TAG, "ec: data[2]=%d, data[3]=%d, result=%f", data[2], data[3], ec); + } + if (this->temperature_sensor_ != nullptr) { + temperature = ((data[5] << 8) | data[4]) / 100.0; + this->temperature_sensor_->publish_state(temperature); + ESP_LOGVV(TAG, "temp: data[4]=%d, data[5]=%d, result=%f", data[4], data[5], temperature); + } + if (this->vwc_sensor_ != nullptr) { + vwc = ((data[7] << 8) | data[6]) / 10.0; + this->vwc_sensor_->publish_state(vwc); + ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc); + } + }); } } // namespace pmwcs3 From a70f926971264501287ac93b8e39d533461990dc Mon Sep 17 00:00:00 2001 From: RubyBailey <60991881+RubyBailey@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:20:29 -0700 Subject: [PATCH 021/160] Fix for Mitsubishi units that only support cooling (#7143) --- esphome/components/mitsubishi/mitsubishi.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index a02aabf14d..449c8fc712 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -110,7 +110,7 @@ void MitsubishiClimate::transmit_state() { // Byte 15: HVAC specfic, i.e. POWERFUL, SMART SET, PLASMA, always 0x00 // Byte 16: Constant 0x00 // Byte 17: Checksum: SUM[Byte0...Byte16] - uint8_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x08, 0x00, 0x00, + uint8_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; switch (this->mode) { @@ -136,6 +136,12 @@ void MitsubishiClimate::transmit_state() { break; case climate::CLIMATE_MODE_OFF: default: + remote_state[6] = MITSUBISHI_MODE_COOL; + remote_state[8] = MITSUBISHI_MODE_A_COOL; + if (this->supports_heat_) { + remote_state[6] = MITSUBISHI_MODE_HEAT; + remote_state[8] = MITSUBISHI_MODE_A_HEAT; + } remote_state[5] = MITSUBISHI_OFF; break; } From 5ac9d301eaca1d46cc0db942e828a7995289640e Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 30 Jul 2024 16:57:51 -0400 Subject: [PATCH 022/160] [micro_wake_word] Fix VAD detection and modify detection computation (#7164) --- esphome/components/micro_wake_word/streaming_model.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 013fa2ce6e..d0d2e2df05 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -148,7 +148,7 @@ WakeWordModel::WakeWordModel(const uint8_t *model_start, float probability_cutof }; bool WakeWordModel::determine_detected() { - int32_t sum = 0; + uint32_t sum = 0; for (auto &prob : this->recent_streaming_probabilities_) { sum += prob; } @@ -175,12 +175,14 @@ VADModel::VADModel(const uint8_t *model_start, float probability_cutoff, size_t }; bool VADModel::determine_detected() { - uint8_t max = 0; + uint32_t sum = 0; for (auto &prob : this->recent_streaming_probabilities_) { - max = std::max(prob, max); + sum += prob; } - return max > this->probability_cutoff_; + float sliding_window_average = static_cast(sum) / static_cast(255 * this->sliding_window_size_); + + return sliding_window_average > this->probability_cutoff_; } } // namespace micro_wake_word From 0af10c58f53466da19d9e065f764124e3bd31810 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 1 Aug 2024 07:51:23 +1200 Subject: [PATCH 023/160] Bump version to 2024.7.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 9abfafc4a4..1f63497982 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.7.2" +__version__ = "2024.7.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From cb9906b9215f17903ac004af160cc1537998b5a6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:38:36 +1200 Subject: [PATCH 024/160] [api] ``homeassistant.action`` replaces ``homeassistant.service`` (#7171) --- esphome/components/api/__init__.py | 130 +++++++++++++-------- esphome/config_validation.py | 10 ++ esphome/const.py | 2 + tests/components/api/common.yaml | 16 +-- tests/components/homeassistant/common.yaml | 8 +- 5 files changed, 103 insertions(+), 63 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index d6b4416af8..38b50d4b9d 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -1,25 +1,27 @@ import base64 -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( + CONF_ACTION, + CONF_ACTIONS, CONF_DATA, CONF_DATA_TEMPLATE, + CONF_EVENT, CONF_ID, CONF_KEY, + CONF_ON_CLIENT_CONNECTED, + CONF_ON_CLIENT_DISCONNECTED, CONF_PASSWORD, CONF_PORT, CONF_REBOOT_TIMEOUT, CONF_SERVICE, - CONF_VARIABLES, CONF_SERVICES, - CONF_TRIGGER_ID, - CONF_EVENT, CONF_TAG, - CONF_ON_CLIENT_CONNECTED, - CONF_ON_CLIENT_DISCONNECTED, + CONF_TRIGGER_ID, + CONF_VARIABLES, ) from esphome.core import coroutine_with_priority @@ -63,40 +65,51 @@ def validate_encryption_key(value): return value -CONFIG_SCHEMA = cv.Schema( +ACTIONS_SCHEMA = automation.validate_automation( { - cv.GenerateID(): cv.declare_id(APIServer), - cv.Optional(CONF_PORT, default=6053): cv.port, - cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, - cv.Optional( - CONF_REBOOT_TIMEOUT, default="15min" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_SERVICES): automation.validate_automation( + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), + cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name, + cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name, + cv.Optional(CONF_VARIABLES, default={}): cv.Schema( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), - cv.Required(CONF_SERVICE): cv.valid_name, - cv.Optional(CONF_VARIABLES, default={}): cv.Schema( - { - cv.validate_id_name: cv.one_of( - *SERVICE_ARG_NATIVE_TYPES, lower=True - ), - } - ), + cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True), } ), - cv.Optional(CONF_ENCRYPTION): cv.Schema( - { - cv.Required(CONF_KEY): validate_encryption_key, - } - ), - cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( - single=True - ), - cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( - single=True - ), - } -).extend(cv.COMPONENT_SCHEMA) + }, + cv.All( + cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), + cv.rename_key(CONF_SERVICE, CONF_ACTION), + ), +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(APIServer), + cv.Optional(CONF_PORT, default=6053): cv.port, + cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, + cv.Optional( + CONF_REBOOT_TIMEOUT, default="15min" + ): cv.positive_time_period_milliseconds, + cv.Exclusive( + CONF_SERVICES, group_of_exclusion=CONF_ACTIONS + ): ACTIONS_SCHEMA, + cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, + cv.Optional(CONF_ENCRYPTION): cv.Schema( + { + cv.Required(CONF_KEY): validate_encryption_key, + } + ), + cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( + single=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.rename_key(CONF_SERVICES, CONF_ACTIONS), +) @coroutine_with_priority(40.0) @@ -108,7 +121,7 @@ async def to_code(config): cg.add(var.set_password(config[CONF_PASSWORD])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) - for conf in config.get(CONF_SERVICES, []): + for conf in config.get(CONF_ACTIONS, []): template_args = [] func_args = [] service_arg_names = [] @@ -119,7 +132,7 @@ async def to_code(config): service_arg_names.append(name) templ = cg.TemplateArguments(*template_args) trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], templ, conf[CONF_SERVICE], service_arg_names + conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names ) cg.add(var.register_user_service(trigger)) await automation.build_automation(trigger, func_args, conf) @@ -152,28 +165,43 @@ async def to_code(config): KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) -HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.use_id(APIServer), - cv.Required(CONF_SERVICE): cv.templatable(cv.string), - cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_VARIABLES, default={}): cv.Schema( - {cv.string: cv.returning_lambda} - ), - } + +HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.use_id(APIServer), + cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.templatable( + cv.string + ), + cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.templatable( + cv.string + ), + cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_VARIABLES, default={}): cv.Schema( + {cv.string: cv.returning_lambda} + ), + } + ), + cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), + cv.rename_key(CONF_SERVICE, CONF_ACTION), ) +@automation.register_action( + "homeassistant.action", + HomeAssistantServiceCallAction, + HOMEASSISTANT_ACTION_ACTION_SCHEMA, +) @automation.register_action( "homeassistant.service", HomeAssistantServiceCallAction, - HOMEASSISTANT_SERVICE_ACTION_SCHEMA, + HOMEASSISTANT_ACTION_ACTION_SCHEMA, ) async def homeassistant_service_to_code(config, action_id, template_arg, args): serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) - templ = await cg.templatable(config[CONF_SERVICE], args, None) + templ = await cg.templatable(config[CONF_ACTION], args, None) cg.add(var.set_service(templ)) for key, value in config[CONF_DATA].items(): templ = await cg.templatable(value, args, None) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1cd1d6aa31..d93f8aed9a 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2181,3 +2181,13 @@ SOURCE_SCHEMA = Any( } ), ) + + +def rename_key(old_key, new_key): + def validator(config: dict) -> dict: + config = config.copy() + if old_key in config: + config[new_key] = config.pop(old_key) + return config + + return validator diff --git a/esphome/const.py b/esphome/const.py index 37844e1047..39dd48d3f8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -37,8 +37,10 @@ CONF_ACCELERATION_Y = "acceleration_y" CONF_ACCELERATION_Z = "acceleration_z" CONF_ACCURACY = "accuracy" CONF_ACCURACY_DECIMALS = "accuracy_decimals" +CONF_ACTION = "action" CONF_ACTION_ID = "action_id" CONF_ACTION_STATE_TOPIC = "action_state_topic" +CONF_ACTIONS = "actions" CONF_ACTIVE = "active" CONF_ACTIVE_POWER = "active_power" CONF_ACTUAL_GAIN = "actual_gain" diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index e0b900f92d..6c2a333598 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -5,8 +5,8 @@ esphome: event: esphome.button_pressed data: message: Button was pressed - - homeassistant.service: - service: notify.html5 + - homeassistant.action: + action: notify.html5 data: message: Button was pressed - homeassistant.tag_scanned: pulse @@ -21,8 +21,8 @@ api: reboot_timeout: 0min encryption: key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= - services: - - service: hello_world + actions: + - action: hello_world variables: name: string then: @@ -30,10 +30,10 @@ api: format: Hello World %s! args: - name.c_str() - - service: empty_service + - action: empty_action then: - - logger.log: Service Called - - service: all_types + - logger.log: Action Called + - action: all_types variables: bool_: bool int_: int @@ -41,7 +41,7 @@ api: string_: string then: - logger.log: Something happened - - service: array_types + - action: array_types variables: bool_arr: bool[] int_arr: int[] diff --git a/tests/components/homeassistant/common.yaml b/tests/components/homeassistant/common.yaml index ae016a3bea..07a6e8090c 100644 --- a/tests/components/homeassistant/common.yaml +++ b/tests/components/homeassistant/common.yaml @@ -13,12 +13,12 @@ esphome: message: The humidity is {{ my_variable }}%. variables: my_variable: "return id(ha_hello_world_temperature).state;" - - homeassistant.service: - service: notify.html5 + - homeassistant.action: + action: notify.html5 data: message: Button was pressed - - homeassistant.service: - service: notify.html5 + - homeassistant.action: + action: notify.html5 data: title: New Humidity data_template: From a5f18dfe7fda26d0311cfeb2fde42d0bf528f576 Mon Sep 17 00:00:00 2001 From: SimoPk Date: Thu, 1 Aug 2024 12:39:54 +0200 Subject: [PATCH 025/160] ade7953_spi wrong size specified in read_array call (#7172) --- esphome/components/ade7953_spi/ade7953_spi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ade7953_spi/ade7953_spi.cpp b/esphome/components/ade7953_spi/ade7953_spi.cpp index cfd5d71d0a..77a2a8adc7 100644 --- a/esphome/components/ade7953_spi/ade7953_spi.cpp +++ b/esphome/components/ade7953_spi/ade7953_spi.cpp @@ -60,7 +60,7 @@ bool AdE7953Spi::ade_read_16(uint16_t reg, uint16_t *value) { this->write_byte16(reg); this->transfer_byte(0x80); uint8_t recv[2]; - this->read_array(recv, 4); + this->read_array(recv, 2); *value = encode_uint16(recv[0], recv[1]); this->disable(); return false; From aedfb32482cf11feaa6373b73cc8ce8a2da50658 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 2 Aug 2024 10:01:21 +1200 Subject: [PATCH 026/160] Bump improv library to 1.2.4 (#7174) --- esphome/components/improv_base/__init__.py | 5 ++--- platformio.ini | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/improv_base/__init__.py b/esphome/components/improv_base/__init__.py index 5c2853a5c6..aa75f4d89c 100644 --- a/esphome/components/improv_base/__init__.py +++ b/esphome/components/improv_base/__init__.py @@ -1,8 +1,7 @@ import re -import esphome.config_validation as cv import esphome.codegen as cg - +import esphome.config_validation as cv from esphome.const import __version__ CODEOWNERS = ["@esphome/core"] @@ -39,4 +38,4 @@ def _process_next_url(url: str): async def setup_improv_core(var, config): if CONF_NEXT_URL in config: cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL]))) - cg.add_library("esphome/Improv", "1.2.3") + cg.add_library("improv/Improv", "1.2.4") diff --git a/platformio.ini b/platformio.ini index baf0a85d73..e4f363d650 100644 --- a/platformio.ini +++ b/platformio.ini @@ -35,7 +35,7 @@ build_flags = lib_deps = esphome/noise-c@0.1.4 ; api makuna/NeoPixelBus@2.7.3 ; neopixelbus - esphome/Improv@1.2.3 ; improv_serial / esp32_improv + improv/Improv@1.2.4 ; improv_serial / esp32_improv bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 From 4a7570770b0b36cfc2495ad3795d4ae3c7df66bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ebbinghaus?= Date: Fri, 2 Aug 2024 01:58:59 +0200 Subject: [PATCH 027/160] Implement 'round to nearest multiple' filter (#7142) --- esphome/components/sensor/__init__.py | 19 +++++++++++++++++++ esphome/components/sensor/filter.cpp | 8 ++++++++ esphome/components/sensor/filter.h | 9 +++++++++ esphome/config_validation.py | 1 + esphome/const.py | 1 + 5 files changed, 38 insertions(+) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 262e69d75b..3b76466dec 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_IGNORE_OUT_OF_RANGE, + CONF_MULTIPLE, CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, @@ -249,6 +250,7 @@ CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter) ClampFilter = sensor_ns.class_("ClampFilter", Filter) RoundFilter = sensor_ns.class_("RoundFilter", Filter) +RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter) validate_unit_of_measurement = cv.string_strict validate_accuracy_decimals = cv.int_ @@ -734,6 +736,23 @@ async def round_filter_to_code(config, filter_id): ) +@FILTER_REGISTRY.register( + "round_to_multiple_of", + RoundMultipleFilter, + cv.maybe_simple_value( + { + cv.Required(CONF_MULTIPLE): cv.positive_not_null_float, + }, + key=CONF_MULTIPLE, + ), +) +async def round_multiple_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + config[CONF_MULTIPLE], + ) + + async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index eaa909429b..bcf1fc8269 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -472,5 +472,13 @@ optional RoundFilter::new_value(float value) { return value; } +RoundMultipleFilter::RoundMultipleFilter(float multiple) : multiple_(multiple) {} +optional RoundMultipleFilter::new_value(float value) { + if (std::isfinite(value)) { + return value - remainderf(value, this->multiple_); + } + return value; +} + } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index c13cb3420a..92b1d8d240 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -431,5 +431,14 @@ class RoundFilter : public Filter { uint8_t precision_; }; +class RoundMultipleFilter : public Filter { + public: + explicit RoundMultipleFilter(float multiple); + optional new_value(float value) override; + + protected: + float multiple_; +}; + } // namespace sensor } // namespace esphome diff --git a/esphome/config_validation.py b/esphome/config_validation.py index d93f8aed9a..6e1d3ba2f9 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -464,6 +464,7 @@ zero_to_one_float = float_range(min=0, max=1) negative_one_to_one_float = float_range(min=-1, max=1) positive_int = int_range(min=0) positive_not_null_int = int_range(min=0, min_included=False) +positive_not_null_float = float_range(min=0, min_included=False) def validate_id_name(value): diff --git a/esphome/const.py b/esphome/const.py index 39dd48d3f8..fcb630badd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -503,6 +503,7 @@ CONF_MOTION = "motion" CONF_MOVEMENT_COUNTER = "movement_counter" CONF_MQTT = "mqtt" CONF_MQTT_ID = "mqtt_id" +CONF_MULTIPLE = "multiple" CONF_MULTIPLEXER = "multiplexer" CONF_MULTIPLY = "multiply" CONF_NAME = "name" From 61c65811233eb1b678fcfa077d6370e7f07ba091 Mon Sep 17 00:00:00 2001 From: Olivier ARCHER Date: Sat, 3 Aug 2024 01:00:18 +0200 Subject: [PATCH 028/160] git ignore managed_components (#7180) --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 0c9a878400..79820249ac 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,5 @@ sdkconfig.* .tests/ /components +/managed_components + From 81ac9391d1af48bc02b4df9886954474c1fd7d01 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:04:06 +1000 Subject: [PATCH 029/160] [core] Eliminate nuisance messages from `build_codeowners` (#7185) --- esphome/loader.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/loader.py b/esphome/loader.py index 9399c4cb31..d808805119 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -1,3 +1,4 @@ +from contextlib import AbstractContextManager from dataclasses import dataclass import importlib import importlib.abc @@ -7,7 +8,7 @@ import logging from pathlib import Path import sys from types import ModuleType -from typing import Any, Callable, ContextManager, Optional +from typing import Any, Callable, Optional from esphome.const import SOURCE_FILE_EXTENSIONS from esphome.core import CORE @@ -22,7 +23,7 @@ class FileResource: package: str resource: str - def path(self) -> ContextManager[Path]: + def path(self) -> AbstractContextManager[Path]: return importlib.resources.as_file( importlib.resources.files(self.package) / self.resource ) @@ -176,7 +177,7 @@ def _lookup_module(domain): module = importlib.import_module(f"esphome.components.{domain}") except ImportError as e: if "No module named" in str(e): - _LOGGER.error( + _LOGGER.info( "Unable to import component %s: %s", domain, str(e), exc_info=False ) else: From 38c25dec93b17ede8999f0cc6f295ba4a730ab6d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:15:19 +1200 Subject: [PATCH 030/160] [code-quality] More portable shebangs (#7189) Co-authored-by: Keith Burzinski --- docker/docker_entrypoint.sh | 2 +- script/devcontainer-post-create | 2 +- script/quicklint | 2 +- script/setup | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/docker_entrypoint.sh b/docker/docker_entrypoint.sh index 397b1528c5..1b9224244c 100755 --- a/docker/docker_entrypoint.sh +++ b/docker/docker_entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # If /cache is mounted, use that as PIO's coredir # otherwise use path in /config (so that PIO packages aren't downloaded on each compile) diff --git a/script/devcontainer-post-create b/script/devcontainer-post-create index 272d350519..2d376786ac 100755 --- a/script/devcontainer-post-create +++ b/script/devcontainer-post-create @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e # set -x diff --git a/script/quicklint b/script/quicklint index a4fae98195..84e4c97667 100755 --- a/script/quicklint +++ b/script/quicklint @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e diff --git a/script/setup b/script/setup index aeb1b39bc1..824840c392 100755 --- a/script/setup +++ b/script/setup @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Set up ESPHome dev environment set -e From 87944f0c1b5df3019407bc20f0f0c29f8cf13033 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:58:20 +1200 Subject: [PATCH 031/160] Add support for doing update entity refresh/check via API. (#7190) --- esphome/components/api/api.proto | 7 +++++- esphome/components/api/api_connection.cpp | 12 +++++++++- esphome/components/api/api_pb2.cpp | 22 +++++++++++++++---- esphome/components/api/api_pb2.h | 7 +++++- .../http_request/update/http_request_update.h | 1 + esphome/components/update/update_entity.h | 2 +- 6 files changed, 43 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 812a1d74ae..b62fddf815 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1872,6 +1872,11 @@ message UpdateStateResponse { string release_summary = 9; string release_url = 10; } +enum UpdateCommand { + UPDATE_COMMAND_NONE = 0; + UPDATE_COMMAND_UPDATE = 1; + UPDATE_COMMAND_CHECK = 2; +} message UpdateCommandRequest { option (id) = 118; option (source) = SOURCE_CLIENT; @@ -1879,5 +1884,5 @@ message UpdateCommandRequest { option (no_delay) = true; fixed32 key = 1; - bool install = 2; + UpdateCommand command = 2; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2e73a8336e..81fa4cb339 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1328,7 +1328,17 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { if (update == nullptr) return; - update->perform(); + switch (msg.command) { + case enums::UPDATE_COMMAND_UPDATE: + update->perform(); + break; + case enums::UPDATE_COMMAND_CHECK: + update->check(); + break; + default: + ESP_LOGW(TAG, "Unknown update command: %d", msg.command); + break; + } } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index e6e905c6d1..a57627a66c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -567,6 +567,20 @@ template<> const char *proto_enum_to_string(enums::ValveO } } #endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> const char *proto_enum_to_string(enums::UpdateCommand value) { + switch (value) { + case enums::UPDATE_COMMAND_NONE: + return "UPDATE_COMMAND_NONE"; + case enums::UPDATE_COMMAND_UPDATE: + return "UPDATE_COMMAND_UPDATE"; + case enums::UPDATE_COMMAND_CHECK: + return "UPDATE_COMMAND_CHECK"; + default: + return "UNKNOWN"; + } +} +#endif bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -8596,7 +8610,7 @@ void UpdateStateResponse::dump_to(std::string &out) const { bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { - this->install = value.as_bool(); + this->command = value.as_enum(); return true; } default: @@ -8615,7 +8629,7 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_bool(2, this->install); + buffer.encode_enum(2, this->command); } #ifdef HAS_PROTO_MESSAGE_DUMP void UpdateCommandRequest::dump_to(std::string &out) const { @@ -8626,8 +8640,8 @@ void UpdateCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" install: "); - out.append(YESNO(this->install)); + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); out.append("\n"); out.append("}"); } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index ef051eecf1..bb5263cffa 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -227,6 +227,11 @@ enum ValveOperation : uint32_t { VALVE_OPERATION_IS_OPENING = 1, VALVE_OPERATION_IS_CLOSING = 2, }; +enum UpdateCommand : uint32_t { + UPDATE_COMMAND_NONE = 0, + UPDATE_COMMAND_UPDATE = 1, + UPDATE_COMMAND_CHECK = 2, +}; } // namespace enums @@ -2175,7 +2180,7 @@ class UpdateStateResponse : public ProtoMessage { class UpdateCommandRequest : public ProtoMessage { public: uint32_t key{0}; - bool install{false}; + enums::UpdateCommand command{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index 943231a906..45c7e6a447 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -16,6 +16,7 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { void update() override; void perform(bool force) override; + void check() override { this->update(); } void set_source_url(const std::string &source_url) { this->source_url_ = source_url; } diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 568fbe3bb0..cc269e288f 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -33,8 +33,8 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { void publish_state(); void perform() { this->perform(false); } - virtual void perform(bool force) = 0; + virtual void check() = 0; const UpdateInfo &update_info = update_info_; const UpdateState &state = state_; From d18bb34f87758cff3d1a6d881b1b167d1d6f79a1 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:07:05 +1000 Subject: [PATCH 032/160] [lvgl] Stage 4 (#7166) --- esphome/components/lvgl/__init__.py | 110 +++++-- esphome/components/lvgl/animimg.py | 117 +++++++ esphome/components/lvgl/arc.py | 78 +++++ esphome/components/lvgl/automation.py | 186 ++++++----- esphome/components/lvgl/btn.py | 11 +- esphome/components/lvgl/checkbox.py | 25 ++ esphome/components/lvgl/defines.py | 64 ++-- esphome/components/lvgl/helpers.py | 20 -- esphome/components/lvgl/img.py | 85 +++++ esphome/components/lvgl/label.py | 9 +- esphome/components/lvgl/led.py | 29 ++ esphome/components/lvgl/line.py | 51 +++ esphome/components/lvgl/lv_bar.py | 53 ++++ esphome/components/lvgl/lv_switch.py | 20 ++ esphome/components/lvgl/lv_validation.py | 60 +++- esphome/components/lvgl/lvcode.py | 236 +++++++++----- esphome/components/lvgl/lvgl_esphome.cpp | 202 ++++++++++++ esphome/components/lvgl/lvgl_esphome.h | 164 ++++------ esphome/components/lvgl/page.py | 113 +++++++ esphome/components/lvgl/rotary_encoders.py | 3 +- esphome/components/lvgl/schemas.py | 105 ++++++- esphome/components/lvgl/slider.py | 63 ++++ esphome/components/lvgl/spinner.py | 43 +++ esphome/components/lvgl/styles.py | 58 ++++ esphome/components/lvgl/trigger.py | 22 +- esphome/components/lvgl/types.py | 92 ++++-- esphome/components/lvgl/widget.py | 219 +++++++++---- tests/components/lvgl/lvgl-package.yaml | 343 +++++++++++++-------- 28 files changed, 2002 insertions(+), 579 deletions(-) create mode 100644 esphome/components/lvgl/animimg.py create mode 100644 esphome/components/lvgl/arc.py create mode 100644 esphome/components/lvgl/checkbox.py create mode 100644 esphome/components/lvgl/img.py create mode 100644 esphome/components/lvgl/led.py create mode 100644 esphome/components/lvgl/line.py create mode 100644 esphome/components/lvgl/lv_bar.py create mode 100644 esphome/components/lvgl/lv_switch.py create mode 100644 esphome/components/lvgl/page.py create mode 100644 esphome/components/lvgl/slider.py create mode 100644 esphome/components/lvgl/spinner.py create mode 100644 esphome/components/lvgl/styles.py diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 182d04e038..c154689199 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -15,44 +15,91 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_TYPE, ) -from esphome.core import CORE, ID, Lambda +from esphome.core import CORE, ID from esphome.cpp_generator import MockObj from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid -from .automation import update_to_code +from .animimg import animimg_spec +from .arc import arc_spec +from .automation import disp_update, update_to_code from .btn import btn_spec +from .checkbox import checkbox_spec +from .defines import CONF_SKIP +from .img import img_spec from .label import label_spec -from .lv_validation import lv_images_used -from .lvcode import LvContext +from .led import led_spec +from .line import line_spec +from .lv_bar import bar_spec +from .lv_switch import switch_spec +from .lv_validation import lv_bool, lv_images_used +from .lvcode import LvContext, LvglComponent from .obj import obj_spec +from .page import add_pages, page_spec from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code -from .schemas import any_widget_schema, create_modify_schema, obj_schema +from .schemas import ( + DISP_BG_SCHEMA, + FLEX_OBJ_SCHEMA, + GRID_CELL_SCHEMA, + LAYOUT_SCHEMAS, + STYLE_SCHEMA, + WIDGET_TYPES, + any_widget_schema, + container_schema, + create_modify_schema, + grid_alignments, + obj_schema, +) +from .slider import slider_spec +from .spinner import spinner_spec +from .styles import add_top_layer, styles_to_code, theme_to_code from .touchscreens import touchscreen_schema, touchscreens_to_code from .trigger import generate_triggers from .types import ( - WIDGET_TYPES, FontEngine, IdleTrigger, - LvglComponent, ObjUpdateAction, lv_font_t, + lv_style_t, lvgl_ns, ) from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties DOMAIN = "lvgl" -DEPENDENCIES = ("display",) -AUTO_LOAD = ("key_provider",) -CODEOWNERS = ("@clydebarrow",) +DEPENDENCIES = ["display"] +AUTO_LOAD = ["key_provider"] +CODEOWNERS = ["@clydebarrow"] LOGGER = logging.getLogger(__name__) -for w_type in (label_spec, obj_spec, btn_spec): +for w_type in ( + label_spec, + obj_spec, + btn_spec, + bar_spec, + slider_spec, + arc_spec, + line_spec, + spinner_spec, + led_spec, + animimg_spec, + checkbox_spec, + img_spec, + switch_spec, +): WIDGET_TYPES[w_type.name] = w_type WIDGET_SCHEMA = any_widget_schema() +LAYOUT_SCHEMAS[df.TYPE_GRID] = { + cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(GRID_CELL_SCHEMA)) +} +LAYOUT_SCHEMAS[df.TYPE_FLEX] = { + cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(FLEX_OBJ_SCHEMA)) +} +LAYOUT_SCHEMAS[df.TYPE_NONE] = { + cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema()) +} for w_type in WIDGET_TYPES.values(): register_action( f"lvgl.{w_type.name}.update", @@ -61,14 +108,6 @@ for w_type in WIDGET_TYPES.values(): )(update_to_code) -async def add_init_lambda(lv_component, init): - if init: - lamb = await cg.process_lambda( - Lambda(init), [(LvglComponent.operator("ptr"), "lv_component")] - ) - cg.add(lv_component.add_init_lambda(lamb)) - - lv_defines = {} # Dict of #defines to provide as build flags @@ -100,6 +139,9 @@ def generate_lv_conf_h(): def final_validation(config): + if pages := config.get(CONF_PAGES): + if all(p[CONF_SKIP] for p in pages): + raise cv.Invalid("At least one page must not be skipped") global_config = full_config.get() for display_id in config[df.CONF_DISPLAYS]: path = global_config.get_path_for_id(display_id)[:-1] @@ -193,18 +235,23 @@ async def to_code(config): else: add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) - with LvContext(): + async with LvContext(lv_component): await touchscreens_to_code(lv_component, config) await rotary_encoders_to_code(lv_component, config) + await theme_to_code(config) + await styles_to_code(config) await set_obj_properties(lv_scr_act, config) await add_widgets(lv_scr_act, config) + await add_pages(lv_component, config) + await add_top_layer(config) + await disp_update(f"{lv_component}->get_disp()", config) Widget.set_completed() await generate_triggers(lv_component) for conf in config.get(CONF_ON_IDLE, ()): templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ) await build_automation(idle_trigger, [], conf) - await add_init_lambda(lv_component, LvContext.get_code()) + for comp in helpers.lvgl_components_required: CORE.add_define(f"USE_LVGL_{comp.upper()}") for use in helpers.lv_uses: @@ -239,6 +286,16 @@ CONFIG_SCHEMA = ( cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( "big_endian", "little_endian" ), + cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list( + cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}) + .extend(STYLE_SCHEMA) + .extend( + { + cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments, + } + ) + ), cv.Optional(CONF_ON_IDLE): validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), @@ -247,10 +304,19 @@ CONFIG_SCHEMA = ( ), } ), - cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA), + cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(WIDGET_SCHEMA), + cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list( + container_schema(page_spec) + ), + cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, + cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, + cv.Optional(df.CONF_THEME): cv.Schema( + {cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()} + ), cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema, cv.GenerateID(df.CONF_ROTARY_ENCODERS): ROTARY_ENCODER_CONFIG, } ) + .extend(DISP_BG_SCHEMA) ).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS)) diff --git a/esphome/components/lvgl/animimg.py b/esphome/components/lvgl/animimg.py new file mode 100644 index 0000000000..20b85b019c --- /dev/null +++ b/esphome/components/lvgl/animimg.py @@ -0,0 +1,117 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_DURATION, CONF_ID + +from ...cpp_generator import MockObj +from .automation import action_to_code +from .defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC +from .helpers import lvgl_components_required +from .img import CONF_IMAGE +from .label import CONF_LABEL +from .lv_validation import lv_image, lv_milliseconds +from .lvcode import lv, lv_expr +from .types import LvType, ObjUpdateAction, void_ptr +from .widget import Widget, WidgetType, get_widgets + +CONF_ANIMIMG = "animimg" +CONF_SRC_LIST_ID = "src_list_id" + + +def lv_repeat_count(value): + if isinstance(value, str) and value.lower() in ("forever", "infinite"): + value = 0xFFFF + return cv.int_range(min=0, max=0xFFFF)(value) + + +ANIMIMG_BASE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_REPEAT_COUNT, default="forever"): lv_repeat_count, + cv.Optional(CONF_AUTO_START, default=True): cv.boolean, + } +) +ANIMIMG_SCHEMA = ANIMIMG_BASE_SCHEMA.extend( + { + cv.Required(CONF_DURATION): lv_milliseconds, + cv.Required(CONF_SRC): cv.ensure_list(lv_image), + cv.GenerateID(CONF_SRC_LIST_ID): cv.declare_id(void_ptr), + } +) + +ANIMIMG_MODIFY_SCHEMA = ANIMIMG_BASE_SCHEMA.extend( + { + cv.Optional(CONF_DURATION): lv_milliseconds, + } +) + +lv_animimg_t = LvType("lv_animimg_t") + + +class AnimimgType(WidgetType): + def __init__(self): + super().__init__( + CONF_ANIMIMG, + lv_animimg_t, + (CONF_MAIN,), + ANIMIMG_SCHEMA, + ANIMIMG_MODIFY_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + lvgl_components_required.add(CONF_IMAGE) + lvgl_components_required.add(CONF_ANIMIMG) + if CONF_SRC in config: + for x in config[CONF_SRC]: + await cg.get_variable(x) + srcs = [lv_expr.img_from(MockObj(x)) for x in config[CONF_SRC]] + src_id = cg.static_const_array(config[CONF_SRC_LIST_ID], srcs) + count = len(config[CONF_SRC]) + lv.animimg_set_src(w.obj, src_id, count) + lv.animimg_set_repeat_count(w.obj, config[CONF_REPEAT_COUNT]) + lv.animimg_set_duration(w.obj, config[CONF_DURATION]) + if config.get(CONF_AUTO_START): + lv.animimg_start(w.obj) + + def get_uses(self): + return CONF_IMAGE, CONF_LABEL + + +animimg_spec = AnimimgType() + + +@automation.register_action( + "lvgl.animimg.start", + ObjUpdateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_animimg_t), + }, + key=CONF_ID, + ), +) +async def animimg_start(config, action_id, template_arg, args): + widget = await get_widgets(config) + + async def do_start(w: Widget): + lv.animimg_start(w.obj) + + return await action_to_code(widget, do_start, action_id, template_arg, args) + + +@automation.register_action( + "lvgl.animimg.stop", + ObjUpdateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_animimg_t), + }, + key=CONF_ID, + ), +) +async def animimg_stop(config, action_id, template_arg, args): + widget = await get_widgets(config) + + async def do_stop(w: Widget): + lv.animimg_stop(w.obj) + + return await action_to_code(widget, do_stop, action_id, template_arg, args) diff --git a/esphome/components/lvgl/arc.py b/esphome/components/lvgl/arc.py new file mode 100644 index 0000000000..d036464c7a --- /dev/null +++ b/esphome/components/lvgl/arc.py @@ -0,0 +1,78 @@ +import esphome.config_validation as cv +from esphome.const import ( + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_MODE, + CONF_ROTATION, + CONF_VALUE, +) +from esphome.cpp_types import nullptr + +from .defines import ( + ARC_MODES, + CONF_ADJUSTABLE, + CONF_CHANGE_RATE, + CONF_END_ANGLE, + CONF_INDICATOR, + CONF_KNOB, + CONF_MAIN, + CONF_START_ANGLE, + literal, +) +from .lv_validation import angle, get_start_value, lv_float +from .lvcode import lv, lv_obj +from .types import LvNumber, NumberType +from .widget import Widget + +CONF_ARC = "arc" +ARC_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, + cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, + cv.Optional(CONF_START_ANGLE, default=135): angle, + cv.Optional(CONF_END_ANGLE, default=45): angle, + cv.Optional(CONF_ROTATION, default=0.0): angle, + cv.Optional(CONF_ADJUSTABLE, default=False): bool, + cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of, + cv.Optional(CONF_CHANGE_RATE, default=720): cv.uint16_t, + } +) + +ARC_MODIFY_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + } +) + + +class ArcType(NumberType): + def __init__(self): + super().__init__( + CONF_ARC, + LvNumber("lv_arc_t"), + parts=(CONF_MAIN, CONF_INDICATOR, CONF_KNOB), + schema=ARC_SCHEMA, + modify_schema=ARC_MODIFY_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + if CONF_MIN_VALUE in config: + lv.arc_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) + lv.arc_set_bg_angles( + w.obj, config[CONF_START_ANGLE] // 10, config[CONF_END_ANGLE] // 10 + ) + lv.arc_set_rotation(w.obj, config[CONF_ROTATION] // 10) + lv.arc_set_mode(w.obj, literal(config[CONF_MODE])) + lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE]) + + if config.get(CONF_ADJUSTABLE) is False: + lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB")) + w.clear_flag("LV_OBJ_FLAG_CLICKABLE") + + value = await get_start_value(config) + if value is not None: + lv.arc_set_value(w.obj, value) + + +arc_spec = ArcType() diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 4fd0be185e..ffa25783ad 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -1,15 +1,26 @@ +from collections.abc import Awaitable +from typing import Callable + from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TIMEOUT -from esphome.core import Lambda -from esphome.cpp_generator import RawStatement from esphome.cpp_types import nullptr -from .defines import CONF_LVGL_ID, CONF_SHOW_SNOW, literal -from .lv_validation import lv_bool +from .defines import ( + CONF_DISP_BG_COLOR, + CONF_DISP_BG_IMAGE, + CONF_LVGL_ID, + CONF_SHOW_SNOW, + literal, +) +from .lv_validation import lv_bool, lv_color, lv_image from .lvcode import ( + LVGL_COMP_ARG, LambdaContext, + LocalVariable, + LvConditional, + LvglComponent, ReturnStatement, add_line_marks, lv, @@ -17,46 +28,46 @@ from .lvcode import ( lv_obj, lvgl_comp, ) -from .schemas import ACTION_SCHEMA, LVGL_SCHEMA +from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA from .types import ( + LV_EVENT, + LV_STATE, LvglAction, - LvglComponent, - LvglComponentPtr, LvglCondition, ObjUpdateAction, + lv_disp_t, lv_obj_t, ) -from .widget import Widget, get_widget, lv_scr_act, set_obj_properties +from .widget import Widget, get_widgets, lv_scr_act, set_obj_properties -async def action_to_code(action: list, action_id, widget: Widget, template_arg, args): - with LambdaContext() as context: - lv.cond_if(widget.obj == nullptr) - lv_add(RawStatement(" return;")) - lv.cond_endif() - code = context.get_code() - code.extend(action) - action = "\n".join(code) + "\n\n" - lamb = await cg.process_lambda(Lambda(action), args) - var = cg.new_Pvariable(action_id, template_arg, lamb) +async def action_to_code( + widgets: list[Widget], + action: Callable[[Widget], Awaitable[None]], + action_id, + template_arg, + args, +): + async with LambdaContext(parameters=args, where=action_id) as context: + for widget in widgets: + with LvConditional(widget.obj != nullptr): + await action(widget) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) return var async def update_to_code(config, action_id, template_arg, args): - if config is not None: - widget = await get_widget(config) - with LambdaContext() as context: - add_line_marks(action_id) - await set_obj_properties(widget, config) - await widget.type.to_code(widget, config) - if ( - widget.type.w_type.value_property is not None - and widget.type.w_type.value_property in config - ): - lv.event_send(widget.obj, literal("LV_EVENT_VALUE_CHANGED"), nullptr) - return await action_to_code( - context.get_code(), action_id, widget, template_arg, args - ) + async def do_update(widget: Widget): + await set_obj_properties(widget, config) + await widget.type.to_code(widget, config) + if ( + widget.type.w_type.value_property is not None + and widget.type.w_type.value_property in config + ): + lv.event_send(widget.obj, LV_EVENT.VALUE_CHANGED, nullptr) + + widgets = await get_widgets(config[CONF_ID]) + return await action_to_code(widgets, do_update, action_id, template_arg, args) @automation.register_condition( @@ -66,9 +77,7 @@ async def update_to_code(config, action_id, template_arg, args): ) async def lvgl_is_paused(config, condition_id, template_arg, args): lvgl = config[CONF_LVGL_ID] - with LambdaContext( - [(LvglComponentPtr, "lvgl_comp")], 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())) var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) await cg.register_parented(var, lvgl) @@ -89,15 +98,23 @@ async def lvgl_is_paused(config, condition_id, template_arg, args): async def lvgl_is_idle(config, condition_id, template_arg, args): lvgl = config[CONF_LVGL_ID] timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32) - with LambdaContext( - [(LvglComponentPtr, "lvgl_comp")], 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))) var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) await cg.register_parented(var, lvgl) return var +async def disp_update(disp, config: dict): + if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config: + return + with LocalVariable("lv_disp_tmp", lv_disp_t, literal(disp)) as disp_temp: + if bg_color := config.get(CONF_DISP_BG_COLOR): + lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color)) + if bg_image := config.get(CONF_DISP_BG_IMAGE): + lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) + + @automation.register_action( "lvgl.widget.redraw", ObjUpdateAction, @@ -109,14 +126,32 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): ), ) async def obj_invalidate_to_code(config, action_id, template_arg, args): - if CONF_ID in config: - w = await get_widget(config) - else: - w = lv_scr_act - with LambdaContext() as context: - add_line_marks(action_id) - lv_obj.invalidate(w.obj) - return await action_to_code(context.get_code(), action_id, w, template_arg, args) + widgets = await get_widgets(config) or [lv_scr_act] + + async def do_invalidate(widget: Widget): + lv_obj.invalidate(widget.obj) + + return await action_to_code(widgets, do_invalidate, action_id, template_arg, args) + + +@automation.register_action( + "lvgl.update", + LvglAction, + DISP_BG_SCHEMA.extend( + { + cv.GenerateID(): cv.use_id(LvglComponent), + } + ).add_extra(cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE)), +) +async def lvgl_update_to_code(config, action_id, template_arg, args): + widgets = await get_widgets(config) + w = widgets[0] + disp = f"{w.obj}->get_disp()" + async with LambdaContext(parameters=args, where=action_id) as context: + await disp_update(disp, config) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, w.var) + return var @automation.register_action( @@ -128,8 +163,8 @@ async def obj_invalidate_to_code(config, action_id, template_arg, args): }, ) async def pause_action_to_code(config, action_id, template_arg, args): - with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context: - add_line_marks(action_id) + async with LambdaContext(LVGL_COMP_ARG) as context: + add_line_marks(where=action_id) lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW])) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) await cg.register_parented(var, config[CONF_ID]) @@ -144,45 +179,48 @@ async def pause_action_to_code(config, action_id, template_arg, args): }, ) async def resume_action_to_code(config, action_id, template_arg, args): - with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context: - add_line_marks(action_id) + async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: lv_add(lvgl_comp.set_paused(False, False)) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) await cg.register_parented(var, config[CONF_ID]) return var -@automation.register_action("lvgl.widget.disable", ObjUpdateAction, ACTION_SCHEMA) +@automation.register_action("lvgl.widget.disable", ObjUpdateAction, LIST_ACTION_SCHEMA) async def obj_disable_to_code(config, action_id, template_arg, args): - w = await get_widget(config) - with LambdaContext() as context: - add_line_marks(action_id) - w.add_state("LV_STATE_DISABLED") - return await action_to_code(context.get_code(), action_id, w, template_arg, args) + async def do_disable(widget: Widget): + widget.add_state(LV_STATE.DISABLED) + + return await action_to_code( + await get_widgets(config), do_disable, action_id, template_arg, args + ) -@automation.register_action("lvgl.widget.enable", ObjUpdateAction, ACTION_SCHEMA) +@automation.register_action("lvgl.widget.enable", ObjUpdateAction, LIST_ACTION_SCHEMA) async def obj_enable_to_code(config, action_id, template_arg, args): - w = await get_widget(config) - with LambdaContext() as context: - add_line_marks(action_id) - w.clear_state("LV_STATE_DISABLED") - return await action_to_code(context.get_code(), action_id, w, template_arg, args) + async def do_enable(widget: Widget): + widget.clear_state(LV_STATE.DISABLED) + + return await action_to_code( + await get_widgets(config), do_enable, action_id, template_arg, args + ) -@automation.register_action("lvgl.widget.hide", ObjUpdateAction, ACTION_SCHEMA) +@automation.register_action("lvgl.widget.hide", ObjUpdateAction, LIST_ACTION_SCHEMA) async def obj_hide_to_code(config, action_id, template_arg, args): - w = await get_widget(config) - with LambdaContext() as context: - add_line_marks(action_id) - w.add_flag("LV_OBJ_FLAG_HIDDEN") - return await action_to_code(context.get_code(), action_id, w, template_arg, args) + async def do_hide(widget: Widget): + widget.add_flag("LV_OBJ_FLAG_HIDDEN") + + return await action_to_code( + await get_widgets(config), do_hide, action_id, template_arg, args + ) -@automation.register_action("lvgl.widget.show", ObjUpdateAction, ACTION_SCHEMA) +@automation.register_action("lvgl.widget.show", ObjUpdateAction, LIST_ACTION_SCHEMA) async def obj_show_to_code(config, action_id, template_arg, args): - w = await get_widget(config) - with LambdaContext() as context: - add_line_marks(action_id) - w.clear_flag("LV_OBJ_FLAG_HIDDEN") - return await action_to_code(context.get_code(), action_id, w, template_arg, args) + async def do_show(widget: Widget): + widget.clear_flag("LV_OBJ_FLAG_HIDDEN") + + return await action_to_code( + await get_widgets(config), do_show, action_id, template_arg, args + ) diff --git a/esphome/components/lvgl/btn.py b/esphome/components/lvgl/btn.py index 064d886d47..2a2a53e1e2 100644 --- a/esphome/components/lvgl/btn.py +++ b/esphome/components/lvgl/btn.py @@ -1,19 +1,14 @@ from esphome.const import CONF_BUTTON -from esphome.cpp_generator import MockObjClass from .defines import CONF_MAIN from .types import LvBoolean, WidgetType +lv_btn_t = LvBoolean("lv_btn_t") + class BtnType(WidgetType): def __init__(self): - super().__init__(CONF_BUTTON, LvBoolean("lv_btn_t"), (CONF_MAIN,)) - - def obj_creator(self, parent: MockObjClass, config: dict): - """ - LVGL 8 calls buttons `btn` - """ - return f"lv_btn_create({parent})" + super().__init__(CONF_BUTTON, lv_btn_t, (CONF_MAIN,), lv_name="btn") def get_uses(self): return ("btn",) diff --git a/esphome/components/lvgl/checkbox.py b/esphome/components/lvgl/checkbox.py new file mode 100644 index 0000000000..7418d633cf --- /dev/null +++ b/esphome/components/lvgl/checkbox.py @@ -0,0 +1,25 @@ +from .defines import CONF_INDICATOR, CONF_MAIN, CONF_TEXT +from .lv_validation import lv_text +from .lvcode import lv +from .schemas import TEXT_SCHEMA +from .types import LvBoolean +from .widget import Widget, WidgetType + +CONF_CHECKBOX = "checkbox" + + +class CheckboxType(WidgetType): + def __init__(self): + super().__init__( + CONF_CHECKBOX, + LvBoolean("lv_checkbox_t"), + (CONF_MAIN, CONF_INDICATOR), + TEXT_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + if value := config.get(CONF_TEXT): + lv.checkbox_set_text(w.obj, await lv_text.process(value)) + + +checkbox_spec = CheckboxType() diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 9f349e3943..16ec45ae8a 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -4,31 +4,20 @@ Constants already defined in esphome.const are not duplicated here and must be i """ -from typing import Union - from esphome import codegen as cg, config_validation as cv from esphome.core import ID, Lambda -from esphome.cpp_generator import Literal +from esphome.cpp_generator import MockObj from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from .helpers import requires_component - -class ConstantLiteral(Literal): - __slots__ = ("constant",) - - def __init__(self, constant: str): - super().__init__() - self.constant = constant - - def __str__(self): - return self.constant +lvgl_ns = cg.esphome_ns.namespace("lvgl") -def literal(arg: Union[str, ConstantLiteral]): +def literal(arg): if isinstance(arg, str): - return ConstantLiteral(arg) + return MockObj(arg) return arg @@ -93,15 +82,23 @@ class LvConstant(LValidator): return self.prefix + cv.one_of(*choices, upper=True)(value) super().__init__(validator, rtype=uint32) + self.retmapper = self.mapper self.one_of = LValidator(validator, uint32, retmapper=self.mapper) self.several_of = LValidator( cv.ensure_list(self.one_of), uint32, retmapper=self.mapper ) def mapper(self, value, args=()): - if isinstance(value, list): - value = "|".join(value) - return ConstantLiteral(value) + if not isinstance(value, list): + value = [value] + return literal( + "|".join( + [ + str(v) if str(v).startswith(self.prefix) else self.prefix + str(v) + for v in value + ] + ).upper() + ) def extend(self, *choices): """ @@ -112,9 +109,6 @@ class LvConstant(LValidator): return LvConstant(self.prefix, *(self.choices + choices)) -# Widgets -CONF_LABEL = "label" - # Parts CONF_MAIN = "main" CONF_SCROLLBAR = "scrollbar" @@ -123,10 +117,15 @@ CONF_KNOB = "knob" CONF_SELECTED = "selected" CONF_ITEMS = "items" CONF_TICKS = "ticks" -CONF_TICK_STYLE = "tick_style" CONF_CURSOR = "cursor" CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder" +# Layout types + +TYPE_FLEX = "flex" +TYPE_GRID = "grid" +TYPE_NONE = "none" + LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [ "dejavu_16_persian_hebrew", "simsun_16_cjk", @@ -134,7 +133,7 @@ LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [ "unscii_16", ] -LV_EVENT = { +LV_EVENT_MAP = { "PRESS": "PRESSED", "SHORT_CLICK": "SHORT_CLICKED", "LONG_PRESS": "LONG_PRESSED", @@ -150,7 +149,7 @@ LV_EVENT = { "CANCEL": "CANCEL", } -LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT) +LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP) LV_ANIM = LvConstant( @@ -305,7 +304,8 @@ OBJ_FLAGS = ( ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") -BTNMATRIX_CTRLS = ( +BTNMATRIX_CTRLS = LvConstant( + "LV_BTNMATRIX_CTRL_", "HIDDEN", "NO_REPEAT", "DISABLED", @@ -366,7 +366,6 @@ CONF_ACCEPTED_CHARS = "accepted_chars" CONF_ADJUSTABLE = "adjustable" CONF_ALIGN = "align" CONF_ALIGN_TO = "align_to" -CONF_ANGLE_RANGE = "angle_range" CONF_ANIMATED = "animated" CONF_ANIMATION = "animation" CONF_ANTIALIAS = "antialias" @@ -384,8 +383,6 @@ CONF_BYTE_ORDER = "byte_order" CONF_CHANGE_RATE = "change_rate" CONF_CLOSE_BUTTON = "close_button" CONF_COLOR_DEPTH = "color_depth" -CONF_COLOR_END = "color_end" -CONF_COLOR_START = "color_start" CONF_CONTROL = "control" CONF_DEFAULT = "default" CONF_DEFAULT_FONT = "default_font" @@ -414,9 +411,7 @@ CONF_GRID_ROW_ALIGN = "grid_row_align" CONF_GRID_ROWS = "grid_rows" CONF_HEADER_MODE = "header_mode" CONF_HOME = "home" -CONF_INDICATORS = "indicators" CONF_KEY_CODE = "key_code" -CONF_LABEL_GAP = "label_gap" CONF_LAYOUT = "layout" CONF_LEFT_BUTTON = "left_button" CONF_LINE_WIDTH = "line_width" @@ -425,7 +420,6 @@ CONF_LONG_PRESS_TIME = "long_press_time" CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" CONF_LVGL_ID = "lvgl_id" CONF_LONG_MODE = "long_mode" -CONF_MAJOR = "major" CONF_MSGBOXES = "msgboxes" CONF_OBJ = "obj" CONF_OFFSET_X = "offset_x" @@ -434,6 +428,7 @@ CONF_ONE_LINE = "one_line" CONF_ON_SELECT = "on_select" CONF_ONE_CHECKED = "one_checked" CONF_NEXT = "next" +CONF_PAGE = "page" CONF_PAGE_WRAP = "page_wrap" CONF_PASSWORD_MODE = "password_mode" CONF_PIVOT_X = "pivot_x" @@ -442,14 +437,12 @@ CONF_PLACEHOLDER_TEXT = "placeholder_text" CONF_POINTS = "points" CONF_PREVIOUS = "previous" CONF_REPEAT_COUNT = "repeat_count" -CONF_R_MOD = "r_mod" CONF_RECOLOR = "recolor" CONF_RIGHT_BUTTON = "right_button" CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" CONF_ROTARY_ENCODERS = "rotary_encoders" CONF_ROWS = "rows" -CONF_SCALES = "scales" CONF_SCALE_LINES = "scale_lines" CONF_SCROLLBAR_MODE = "scrollbar_mode" CONF_SELECTED_INDEX = "selected_index" @@ -459,8 +452,9 @@ CONF_SRC = "src" CONF_START_ANGLE = "start_angle" CONF_START_VALUE = "start_value" CONF_STATES = "states" -CONF_STRIDE = "stride" CONF_STYLE = "style" +CONF_STYLES = "styles" +CONF_STYLE_DEFINITIONS = "style_definitions" CONF_STYLE_ID = "style_id" CONF_SKIP = "skip" CONF_SYMBOL = "symbol" @@ -505,4 +499,4 @@ DEFAULT_ESPHOME_FONT = "esphome_lv_default_font" def join_enums(enums, prefix=""): - return ConstantLiteral("|".join(f"(int){prefix}{e.upper()}" for e in enums)) + return literal("|".join(f"(int){prefix}{e.upper()}" for e in enums)) diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index d67739155c..e04a0105d5 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -1,10 +1,7 @@ import re from esphome import config_validation as cv -from esphome.config import Config from esphome.const import CONF_ARGS, CONF_FORMAT -from esphome.core import CORE, ID -from esphome.yaml_util import ESPHomeDataBase lv_uses = { "USER_DATA", @@ -44,23 +41,6 @@ def validate_printf(value): return value -def get_line_marks(value) -> list: - """ - If possible, return a preprocessor directive to identify the line number where the given id was defined. - :param id: The id in question - :return: A list containing zero or more line directives - """ - path = None - if isinstance(value, ESPHomeDataBase): - path = value.esp_range - elif isinstance(value, ID) and isinstance(CORE.config, Config): - path = CORE.config.get_path_for_id(value)[:-1] - path = CORE.config.get_deepest_document_range_for_path(path) - if path is None: - return [] - return [path.start_mark.as_line_directive] - - def requires_component(comp): def validator(value): lvgl_components_required.add(comp) diff --git a/esphome/components/lvgl/img.py b/esphome/components/lvgl/img.py new file mode 100644 index 0000000000..e9682def8c --- /dev/null +++ b/esphome/components/lvgl/img.py @@ -0,0 +1,85 @@ +import esphome.config_validation as cv +from esphome.const import CONF_ANGLE, CONF_MODE + +from .defines import ( + CONF_ANTIALIAS, + CONF_MAIN, + CONF_OFFSET_X, + CONF_OFFSET_Y, + CONF_PIVOT_X, + CONF_PIVOT_Y, + CONF_SRC, + CONF_ZOOM, + LvConstant, +) +from .label import CONF_LABEL +from .lv_validation import angle, lv_bool, lv_image, size, zoom +from .lvcode import lv +from .types import lv_img_t +from .widget import Widget, WidgetType + +CONF_IMAGE = "image" + +BASE_IMG_SCHEMA = cv.Schema( + { + cv.Optional(CONF_PIVOT_X, default="50%"): size, + cv.Optional(CONF_PIVOT_Y, default="50%"): size, + cv.Optional(CONF_ANGLE): angle, + cv.Optional(CONF_ZOOM): zoom, + cv.Optional(CONF_OFFSET_X): size, + cv.Optional(CONF_OFFSET_Y): size, + cv.Optional(CONF_ANTIALIAS): lv_bool, + cv.Optional(CONF_MODE): LvConstant( + "LV_IMG_SIZE_MODE_", "VIRTUAL", "REAL" + ).one_of, + } +) + +IMG_SCHEMA = BASE_IMG_SCHEMA.extend( + { + cv.Required(CONF_SRC): lv_image, + } +) + +IMG_MODIFY_SCHEMA = BASE_IMG_SCHEMA.extend( + { + cv.Optional(CONF_SRC): lv_image, + } +) + + +class ImgType(WidgetType): + def __init__(self): + super().__init__( + CONF_IMAGE, + lv_img_t, + (CONF_MAIN,), + IMG_SCHEMA, + IMG_MODIFY_SCHEMA, + lv_name="img", + ) + + def get_uses(self): + return "img", CONF_LABEL + + async def to_code(self, w: Widget, config): + if src := config.get(CONF_SRC): + lv.img_set_src(w.obj, await lv_image.process(src)) + if cf_angle := config.get(CONF_ANGLE): + pivot_x = config[CONF_PIVOT_X] + pivot_y = config[CONF_PIVOT_Y] + lv.img_set_pivot(w.obj, pivot_x, pivot_y) + lv.img_set_angle(w.obj, cf_angle) + if img_zoom := config.get(CONF_ZOOM): + lv.img_set_zoom(w.obj, img_zoom) + if offset := config.get(CONF_OFFSET_X): + lv.img_set_offset_x(w.obj, offset) + if offset := config.get(CONF_OFFSET_Y): + lv.img_set_offset_y(w.obj, offset) + if CONF_ANTIALIAS in config: + lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS]) + if mode := config.get(CONF_MODE): + lv.img_set_mode(w.obj, mode) + + +img_spec = ImgType() diff --git a/esphome/components/lvgl/label.py b/esphome/components/lvgl/label.py index 0498f39474..6c3e1f4a00 100644 --- a/esphome/components/lvgl/label.py +++ b/esphome/components/lvgl/label.py @@ -1,7 +1,6 @@ import esphome.config_validation as cv from .defines import ( - CONF_LABEL, CONF_LONG_MODE, CONF_MAIN, CONF_RECOLOR, @@ -15,6 +14,8 @@ from .schemas import TEXT_SCHEMA from .types import LvText, WidgetType from .widget import Widget +CONF_LABEL = "label" + class LabelType(WidgetType): def __init__(self): @@ -33,9 +34,9 @@ class LabelType(WidgetType): async def to_code(self, w: Widget, config): """For a text object, create and set text""" if value := config.get(CONF_TEXT): - w.set_property(CONF_TEXT, await lv_text.process(value)) - w.set_property(CONF_LONG_MODE, config) - w.set_property(CONF_RECOLOR, config) + await w.set_property(CONF_TEXT, await lv_text.process(value)) + await w.set_property(CONF_LONG_MODE, config) + await w.set_property(CONF_RECOLOR, config) label_spec = LabelType() diff --git a/esphome/components/lvgl/led.py b/esphome/components/lvgl/led.py new file mode 100644 index 0000000000..f920758efb --- /dev/null +++ b/esphome/components/lvgl/led.py @@ -0,0 +1,29 @@ +import esphome.config_validation as cv +from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED + +from .defines import CONF_MAIN +from .lv_validation import lv_brightness, lv_color +from .lvcode import lv +from .types import LvType +from .widget import Widget, WidgetType + +LED_SCHEMA = cv.Schema( + { + cv.Optional(CONF_COLOR): lv_color, + cv.Optional(CONF_BRIGHTNESS): lv_brightness, + } +) + + +class LedType(WidgetType): + def __init__(self): + super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA) + + async def to_code(self, w: Widget, config): + if color := config.get(CONF_COLOR): + lv.led_set_color(w.obj, await lv_color.process(color)) + if brightness := config.get(CONF_BRIGHTNESS): + lv.led_set_brightness(w.obj, await lv_brightness.process(brightness)) + + +led_spec = LedType() diff --git a/esphome/components/lvgl/line.py b/esphome/components/lvgl/line.py new file mode 100644 index 0000000000..ab50832bbf --- /dev/null +++ b/esphome/components/lvgl/line.py @@ -0,0 +1,51 @@ +import functools + +import esphome.codegen as cg +import esphome.config_validation as cv + +from . import defines as df +from .defines import CONF_MAIN, literal +from .lvcode import lv +from .types import LvType +from .widget import Widget, WidgetType + +CONF_LINE = "line" +CONF_POINTS = "points" +CONF_POINT_LIST_ID = "point_list_id" + +lv_point_t = cg.global_ns.struct("lv_point_t") + + +def point_list(il): + il = cv.string(il) + nl = il.replace(" ", "").split(",") + return [int(n) for n in nl] + + +def cv_point_list(value): + if not isinstance(value, list): + raise cv.Invalid("List of points required") + values = [point_list(v) for v in value] + if not functools.reduce(lambda f, v: f and len(v) == 2, values, True): + raise cv.Invalid("Points must be a list of x,y integer pairs") + return values + + +LINE_SCHEMA = { + cv.Required(df.CONF_POINTS): cv_point_list, + cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t), +} + + +class LineType(WidgetType): + def __init__(self): + super().__init__(CONF_LINE, LvType("lv_line_t"), (CONF_MAIN,), LINE_SCHEMA) + + async def to_code(self, w: Widget, config): + """For a line object, create and add the points""" + data = literal(config[CONF_POINTS]) + points = cg.static_const_array(config[CONF_POINT_LIST_ID], data) + lv.line_set_points(w.obj, points, len(data)) + + +line_spec = LineType() diff --git a/esphome/components/lvgl/lv_bar.py b/esphome/components/lvgl/lv_bar.py new file mode 100644 index 0000000000..d5dcff0bf0 --- /dev/null +++ b/esphome/components/lvgl/lv_bar.py @@ -0,0 +1,53 @@ +import esphome.config_validation as cv +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE + +from .defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal +from .lv_validation import animated, get_start_value, lv_float +from .lvcode import lv +from .types import LvNumber, NumberType +from .widget import Widget + +CONF_BAR = "bar" +BAR_MODIFY_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + +BAR_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, + cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, + cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + + +class BarType(NumberType): + def __init__(self): + super().__init__( + CONF_BAR, + LvNumber("lv_bar_t"), + parts=(CONF_MAIN, CONF_INDICATOR), + schema=BAR_SCHEMA, + modify_schema=BAR_MODIFY_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + var = w.obj + if CONF_MIN_VALUE in config: + lv.bar_set_range(var, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) + lv.bar_set_mode(var, literal(config[CONF_MODE])) + value = await get_start_value(config) + if value is not None: + lv.bar_set_value(var, value, literal(config[CONF_ANIMATED])) + + @property + def animated(self): + return True + + +bar_spec = BarType() diff --git a/esphome/components/lvgl/lv_switch.py b/esphome/components/lvgl/lv_switch.py new file mode 100644 index 0000000000..5db2c2ce38 --- /dev/null +++ b/esphome/components/lvgl/lv_switch.py @@ -0,0 +1,20 @@ +from .defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN +from .types import LvBoolean +from .widget import WidgetType + +CONF_SWITCH = "switch" + + +class SwitchType(WidgetType): + def __init__(self): + super().__init__( + CONF_SWITCH, + LvBoolean("lv_switch_t"), + (CONF_MAIN, CONF_INDICATOR, CONF_KNOB), + ) + + async def to_code(self, w, config): + return [] + + +switch_spec = SwitchType() diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 818bde6aed..b351b84af6 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,3 +1,5 @@ +from typing import Union + import esphome.codegen as cg from esphome.components.binary_sensor import BinarySensor from esphome.components.color import ColorStruct @@ -6,7 +8,7 @@ from esphome.components.image import Image_ from esphome.components.sensor import Sensor from esphome.components.text_sensor import TextSensor import esphome.config_validation as cv -from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT +from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_VALUE from esphome.core import HexInt from esphome.cpp_generator import MockObj from esphome.cpp_types import uint32 @@ -14,7 +16,14 @@ from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from . import types as ty -from .defines import LV_FONTS, ConstantLiteral, LValidator, LvConstant, literal +from .defines import ( + CONF_END_VALUE, + CONF_START_VALUE, + LV_FONTS, + LValidator, + LvConstant, + literal, +) from .helpers import ( esphome_fonts_used, lv_fonts_used, @@ -60,6 +69,13 @@ def color_retmapper(value): return lv_expr.color_from(MockObj(value)) +def option_string(value): + value = cv.string(value).strip() + if value.find("\n") != -1: + raise cv.Invalid("Options strings must not contain newlines") + return value + + lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper) @@ -156,6 +172,12 @@ lv_bool = LValidator( ) +def lv_pct(value: Union[int, float]): + if isinstance(value, float): + value = int(value * 100) + return literal(f"lv_pct({value})") + + def lvms_validator_(value): if value == "never": value = "2147483647ms" @@ -189,13 +211,16 @@ class TextValidator(LValidator): args = [str(x) for x in value[CONF_ARGS]] arg_expr = cg.RawExpression(",".join(args)) format_str = cpp_string_escape(value[CONF_FORMAT]) - return f"str_sprintf({format_str}, {arg_expr}).c_str()" + return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") return await super().process(value, args) lv_text = TextValidator() lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()") +lv_brightness = LValidator( + cv.percentage, cg.float_, Sensor, "get_state()", retmapper=lambda x: int(x * 255) +) def is_lv_font(font): @@ -222,8 +247,33 @@ class LvFont(LValidator): async def process(self, value, args=()): if is_lv_font(value): - return ConstantLiteral(f"&lv_font_{value}") - return ConstantLiteral(f"{value}_engine->get_lv_font()") + return literal(f"&lv_font_{value}") + return literal(f"{value}_engine->get_lv_font()") lv_font = LvFont() + + +def animated(value): + if isinstance(value, bool): + value = "ON" if value else "OFF" + return LvConstant("LV_ANIM_", "OFF", "ON").one_of(value) + + +def key_code(value): + value = cv.Any(cv.All(cv.string_strict, cv.Length(min=1, max=1)), cv.uint8_t)(value) + if isinstance(value, str): + return ord(value[0]) + return value + + +async def get_end_value(config): + return await lv_int.process(config.get(CONF_END_VALUE)) + + +async def get_start_value(config): + if CONF_START_VALUE in config: + value = config[CONF_START_VALUE] + else: + value = config.get(CONF_VALUE) + return await lv_int.process(value) diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 3a8a958f2e..f54a032de2 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -1,9 +1,9 @@ import abc -import logging from typing import Union from esphome import codegen as cg -from esphome.core import ID, Lambda +from esphome.config import Config +from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import ( AssignmentExpression, CallExpression, @@ -18,12 +18,47 @@ from esphome.cpp_generator import ( VariableDeclarationExpression, statement, ) +from esphome.yaml_util import ESPHomeDataBase -from .defines import ConstantLiteral -from .helpers import get_line_marks -from .types import lv_group_t +from .defines import literal, lvgl_ns -_LOGGER = logging.getLogger(__name__) +LVGL_COMP = "lv_component" # used as a lambda argument in lvgl_comp() + +# Argument tuple for use in lambdas +LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) +LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)] +lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr") +EVENT_ARG = [(lv_event_t_ptr, "ev")] +CUSTOM_EVENT = literal("lvgl::lv_custom_event") + + +def get_line_marks(value) -> list: + """ + If possible, return a preprocessor directive to identify the line number where the given id was defined. + :param value: The id or other token to get the line number for + :return: A list containing zero or more line directives + """ + path = None + if isinstance(value, ESPHomeDataBase): + path = value.esp_range + elif isinstance(value, ID) and isinstance(CORE.config, Config): + path = CORE.config.get_path_for_id(value)[:-1] + path = CORE.config.get_deepest_document_range_for_path(path) + if path is None: + return [] + return [path.start_mark.as_line_directive] + + +class IndentedStatement(Statement): + def __init__(self, stmt: Statement, indent: int): + self.statement = stmt + self.indent = indent + + def __str__(self): + result = " " * self.indent * 4 + str(self.statement).strip() + if not isinstance(self.statement, RawStatement): + result += ";" + return result class CodeContext(abc.ABC): @@ -39,6 +74,16 @@ class CodeContext(abc.ABC): def add(self, expression: Union[Expression, Statement]): pass + @staticmethod + def start_block(): + CodeContext.append(RawStatement("{")) + CodeContext.code_context.indent() + + @staticmethod + def end_block(): + CodeContext.code_context.detent() + CodeContext.append(RawStatement("}")) + @staticmethod def append(expression: Union[Expression, Statement]): if CodeContext.code_context is not None: @@ -47,14 +92,25 @@ class CodeContext(abc.ABC): def __init__(self): self.previous: Union[CodeContext | None] = None + self.indent_level = 0 - def __enter__(self): + async def __aenter__(self): self.previous = CodeContext.code_context CodeContext.code_context = self + return self - def __exit__(self, *args): + async def __aexit__(self, *args): CodeContext.code_context = self.previous + def indent(self): + self.indent_level += 1 + + def detent(self): + self.indent_level -= 1 + + def indented_statement(self, stmt): + return IndentedStatement(stmt, self.indent_level) + class MainContext(CodeContext): """ @@ -62,42 +118,7 @@ class MainContext(CodeContext): """ def add(self, expression: Union[Expression, Statement]): - return cg.add(expression) - - -class LvContext(CodeContext): - """ - Code generation into the LVGL initialisation code (called in `setup()`) - """ - - lv_init_code: list["Statement"] = [] - - @staticmethod - def lv_add(expression: Union[Expression, Statement]): - if isinstance(expression, Expression): - expression = statement(expression) - if not isinstance(expression, Statement): - raise ValueError( - f"Add '{expression}' must be expression or statement, not {type(expression)}" - ) - LvContext.lv_init_code.append(expression) - _LOGGER.debug("LV Adding: %s", expression) - return expression - - @staticmethod - def get_code(): - code = [] - for exp in LvContext.lv_init_code: - text = str(statement(exp)) - text = text.rstrip() - code.append(text) - return "\n".join(code) + "\n\n" - - def add(self, expression: Union[Expression, Statement]): - return LvContext.lv_add(expression) - - def set_style(self, prop): - return MockObj("lv_set_style_{prop}", "") + return cg.add(self.indented_statement(expression)) class LambdaContext(CodeContext): @@ -110,21 +131,23 @@ class LambdaContext(CodeContext): parameters: list[tuple[SafeExpType, str]] = None, return_type: SafeExpType = cg.void, capture: str = "", + where=None, ): super().__init__() self.code_list: list[Statement] = [] - self.parameters = parameters + self.parameters = parameters or [] self.return_type = return_type self.capture = capture + self.where = where def add(self, expression: Union[Expression, Statement]): - self.code_list.append(expression) + self.code_list.append(self.indented_statement(expression)) return expression async def get_lambda(self) -> LambdaExpression: code_text = self.get_code() return await cg.process_lambda( - Lambda("\n".join(code_text) + "\n\n"), + Lambda("\n".join(code_text) + "\n"), self.parameters, capture=self.capture, return_type=self.return_type, @@ -138,33 +161,59 @@ class LambdaContext(CodeContext): code_text.append(text) return code_text - def __enter__(self): - super().__enter__() + async def __aenter__(self): + await super().__aenter__() + add_line_marks(self.where) return self +class LvContext(LambdaContext): + """ + Code generation into the LVGL initialisation code (called in `setup()`) + """ + + def __init__(self, lv_component, args=None): + self.args = args or LVGL_COMP_ARG + super().__init__(parameters=self.args) + self.lv_component = lv_component + + async def add_init_lambda(self): + cg.add(self.lv_component.add_init_lambda(await self.get_lambda())) + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await super().__aexit__(exc_type, exc_val, exc_tb) + await self.add_init_lambda() + + def add(self, expression: Union[Expression, Statement]): + self.code_list.append(self.indented_statement(expression)) + return expression + + def __call__(self, *args): + return self.add(*args) + + class LocalVariable(MockObj): """ Create a local variable and enclose the code using it within a block. """ - def __init__(self, name, type, modifier=None, rhs=None): - base = ID(name, True, type) + def __init__(self, name, type, rhs=None, modifier="*"): + base = ID(name + "_VAR_", True, type) super().__init__(base, "") self.modifier = modifier self.rhs = rhs def __enter__(self): - CodeContext.append(RawStatement("{")) + CodeContext.start_block() CodeContext.append( VariableDeclarationExpression(self.base.type, self.modifier, self.base.id) ) if self.rhs is not None: CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs)) - return self.base + return MockObj(self.base) def __exit__(self, *args): - CodeContext.append(RawStatement("}")) + CodeContext.end_block() class MockLv: @@ -199,14 +248,27 @@ class MockLv: self.append(result) return result - def cond_if(self, expression: Expression): - CodeContext.append(RawStatement(f"if {expression} {{")) - def cond_else(self): +class LvConditional: + def __init__(self, condition): + self.condition = condition + + def __enter__(self): + if self.condition is not None: + CodeContext.append(RawStatement(f"if ({self.condition}) {{")) + CodeContext.code_context.indent() + return self + + def __exit__(self, *args): + if self.condition is not None: + CodeContext.code_context.detent() + CodeContext.append(RawStatement("}")) + + def else_(self): + assert self.condition is not None + CodeContext.code_context.detent() CodeContext.append(RawStatement("} else {")) - - def cond_endif(self): - CodeContext.append(RawStatement("}")) + CodeContext.code_context.indent() class ReturnStatement(ExpressionStatement): @@ -228,36 +290,56 @@ lv = MockLv("lv_") lv_expr = LvExpr("lv_") # Mock for lv_obj_ calls lv_obj = MockLv("lv_obj_") -lvgl_comp = MockObj("lvgl_comp", "->") +# Operations on the LVGL component +lvgl_comp = MockObj(LVGL_COMP, "->") -# equivalent to cg.add() for the lvgl init context +# equivalent to cg.add() for the current code context def lv_add(expression: Union[Expression, Statement]): return CodeContext.append(expression) def add_line_marks(where): + """ + Add line marks for the current code context + :param where: An object to identify the source of the line marks + :return: + """ for mark in get_line_marks(where): lv_add(cg.RawStatement(mark)) def lv_assign(target, expression): - lv_add(RawExpression(f"{target} = {expression}")) + lv_add(AssignmentExpression("", "", target, expression)) -lv_groups = {} # Widget group names +def lv_Pvariable(type, name): + """ + Create but do not initialise a pointer variable + :param type: Type of the variable target + :param name: name of the variable, or an ID + :return: A MockObj of the variable + """ + if isinstance(name, str): + name = ID(name, True, type) + decl = VariableDeclarationExpression(type, "*", name) + CORE.add_global(decl) + var = MockObj(name, "->") + CORE.register_variable(name, var) + return var -def add_group(name): - if name is None: - return None - fullname = f"lv_esp_group_{name}" - if name not in lv_groups: - gid = ID(fullname, True, type=lv_group_t.operator("ptr")) - lv_add( - AssignmentExpression( - type_=gid.type, modifier="", name=fullname, rhs=lv_expr.group_create() - ) - ) - lv_groups[name] = ConstantLiteral(fullname) - return lv_groups[name] +def lv_variable(type, name): + """ + Create but do not initialise a variable + :param type: Type of the variable target + :param name: name of the variable, or an ID + :return: A MockObj of the variable + """ + if isinstance(name, str): + name = ID(name, True, type) + decl = VariableDeclarationExpression(type, "", name) + CORE.add_global(decl) + var = MockObj(name, ".") + CORE.register_variable(name, var) + return var diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 34f8eaf21f..1221682d28 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -9,8 +9,72 @@ namespace esphome { namespace lvgl { static const char *const TAG = "lvgl"; +#if LV_USE_LOG +static void log_cb(const char *buf) { + esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); +} +#endif // LV_USE_LOG + +static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { + // make sure all coordinates are even + if (area->x1 & 1) + area->x1--; + if (!(area->x2 & 1)) + area->x2++; + if (area->y1 & 1) + area->y1--; + if (!(area->y2 & 1)) + area->y2++; +} + lv_event_code_t lv_custom_event; // NOLINT void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); } +void LvglComponent::set_paused(bool paused, bool show_snow) { + this->paused_ = paused; + this->show_snow_ = show_snow; + this->snow_line_ = 0; + if (!paused && lv_scr_act() != nullptr) { + lv_disp_trig_activity(this->disp_); // resets the inactivity time + lv_obj_invalidate(lv_scr_act()); + } +} +void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { + lv_obj_add_event_cb(obj, callback, event, this); + if (event == LV_EVENT_VALUE_CHANGED) { + lv_obj_add_event_cb(obj, callback, lv_custom_event, this); + } +} +void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, + lv_event_code_t event2) { + this->add_event_cb(obj, callback, event1); + this->add_event_cb(obj, callback, event2); +} +void LvglComponent::add_page(LvPageType *page) { + this->pages_.push_back(page); + page->setup(this->pages_.size() - 1); +} +void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { + if (index >= this->pages_.size()) + return; + this->current_page_ = index; + lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); +} +void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { + if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_)) + return; + do { + this->current_page_ = (this->current_page_ + 1) % this->pages_.size(); + } while (this->pages_[this->current_page_]->skip); // skip empty pages() + this->show_page(this->current_page_, anim, time); +} +void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { + if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_)) + return; + do { + this->current_page_ = (this->current_page_ + this->pages_.size() - 1) % this->pages_.size(); + } while (this->pages_[this->current_page_]->skip); // skip empty pages() + this->show_page(this->current_page_, anim, time); +} void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) { for (auto *display : this->displays_) { display->draw_pixels_at(area->x1, area->y1, lv_area_get_width(area), lv_area_get_height(area), ptr, @@ -27,6 +91,116 @@ void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv } lv_disp_flush_ready(disp_drv); } +IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue timeout) : timeout_(std::move(timeout)) { + parent->add_on_idle_callback([this](uint32_t idle_time) { + if (!this->is_idle_ && idle_time > this->timeout_.value()) { + this->is_idle_ = true; + this->trigger(); + } else if (this->is_idle_ && idle_time < this->timeout_.value()) { + this->is_idle_ = false; + } + }); +} + +#ifdef USE_LVGL_TOUCHSCREEN +LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) { + lv_indev_drv_init(&this->drv_); + this->drv_.long_press_repeat_time = long_press_repeat_time; + this->drv_.long_press_time = long_press_time; + this->drv_.type = LV_INDEV_TYPE_POINTER; + this->drv_.user_data = this; + this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { + auto *l = static_cast(d->user_data); + if (l->touch_pressed_) { + data->point.x = l->touch_point_.x; + data->point.y = l->touch_point_.y; + data->state = LV_INDEV_STATE_PRESSED; + } else { + data->state = LV_INDEV_STATE_RELEASED; + } + }; +} +void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { + this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty(); + if (this->touch_pressed_) + this->touch_point_ = tpoints[0]; +} +#endif // USE_LVGL_TOUCHSCREEN + +#ifdef USE_LVGL_ROTARY_ENCODER +LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) { + lv_indev_drv_init(&this->drv_); + this->drv_.type = type; + this->drv_.user_data = this; + this->drv_.long_press_time = lpt; + this->drv_.long_press_repeat_time = lprt; + this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { + auto *l = static_cast(d->user_data); + data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + data->key = l->key_; + data->enc_diff = (int16_t) (l->count_ - l->last_count_); + l->last_count_ = l->count_; + data->continue_reading = false; + }; +} +#endif // USE_LVGL_ROTARY_ENCODER + +#ifdef USE_LVGL_BUTTONMATRIX +void LvBtnmatrixType::set_obj(lv_obj_t *lv_obj) { + LvCompound::set_obj(lv_obj); + lv_obj_add_event_cb( + lv_obj, + [](lv_event_t *event) { + auto *self = static_cast(event->user_data); + if (self->key_callback_.size() == 0) + return; + auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); + if (key_idx == LV_BTNMATRIX_BTN_NONE) + return; + if (self->key_map_.count(key_idx) != 0) { + self->send_key_(self->key_map_[key_idx]); + return; + } + const auto *str = lv_btnmatrix_get_btn_text(self->obj, key_idx); + auto len = strlen(str); + while (len--) + self->send_key_(*str++); + }, + LV_EVENT_PRESSED, this); +} +#endif // USE_LVGL_BUTTONMATRIX + +#ifdef USE_LVGL_KEYBOARD +static const char *const KB_SPECIAL_KEYS[] = { + "abc", "ABC", "1#", + // maybe add other special keys here +}; + +void LvKeyboardType::set_obj(lv_obj_t *lv_obj) { + LvCompound::set_obj(lv_obj); + lv_obj_add_event_cb( + lv_obj, + [](lv_event_t *event) { + auto *self = static_cast(event->user_data); + if (self->key_callback_.size() == 0) + return; + + auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); + if (key_idx == LV_BTNMATRIX_BTN_NONE) + return; + const char *txt = lv_btnmatrix_get_btn_text(self->obj, key_idx); + if (txt == nullptr) + return; + for (const auto *kb_special_key : KB_SPECIAL_KEYS) { + if (strcmp(txt, kb_special_key) == 0) + return; + } + while (*txt != 0) + self->send_key_(*txt++); + }, + LV_EVENT_PRESSED, this); +} +#endif // USE_LVGL_KEYBOARD void LvglComponent::write_random_() { // length of 2 lines in 32 bit units @@ -97,9 +271,24 @@ void LvglComponent::setup() { this->disp_ = lv_disp_drv_register(&this->disp_drv_); for (const auto &v : this->init_lambdas_) v(this); + this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0); lv_disp_trig_activity(this->disp_); ESP_LOGCONFIG(TAG, "LVGL Setup complete"); } +void LvglComponent::update() { + // update indicators + if (this->paused_) { + return; + } + this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); +} +void LvglComponent::loop() { + if (this->paused_) { + if (this->show_snow_) + this->write_random_(); + } + lv_timer_handler_run_in_period(5); +} #ifdef USE_LVGL_IMAGE lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) { @@ -142,7 +331,20 @@ lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) { } return img_dsc; } +#endif // USE_LVGL_IMAGE + +#ifdef USE_LVGL_ANIMIMG +void lv_animimg_stop(lv_obj_t *obj) { + auto *animg = (lv_animimg_t *) obj; + int32_t duration = animg->anim.time; + lv_animimg_set_duration(obj, 0); + lv_animimg_start(obj); + lv_animimg_set_duration(obj, duration); +} #endif +void LvglComponent::static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { + reinterpret_cast(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p); +} } // namespace lvgl } // namespace esphome diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index a0d3d226ce..b92799addd 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -18,7 +18,6 @@ #include "esphome/core/component.h" #include "esphome/core/log.h" #include -#include #include #ifdef USE_LVGL_IMAGE #include "esphome/components/image/image.h" @@ -31,6 +30,10 @@ #include "esphome/components/touchscreen/touchscreen.h" #endif // USE_LVGL_TOUCHSCREEN +#if defined(USE_LVGL_BUTTONMATRIX) || defined(USE_LVGL_KEYBOARD) +#include "esphome/components/key_provider/key_provider.h" +#endif // USE_LVGL_BUTTONMATRIX + namespace esphome { namespace lvgl { @@ -47,12 +50,25 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT #endif // LV_COLOR_DEPTH // Parent class for things that wrap an LVGL object -class LvCompound final { +class LvCompound { public: - void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } + virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } lv_obj_t *obj{}; }; +class LvPageType { + public: + LvPageType(bool skip) : skip(skip) {} + + void setup(size_t index) { + this->index = index; + this->obj = lv_obj_create(nullptr); + } + lv_obj_t *obj{}; + size_t index{}; + bool skip; +}; + using LvLambdaType = std::function; using set_value_lambda_t = std::function; using event_callback_t = void(_lv_event_t *); @@ -89,48 +105,20 @@ class FontEngine { lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc = nullptr); #endif // USE_LVGL_IMAGE +#ifdef USE_LVGL_ANIMIMG +void lv_animimg_stop(lv_obj_t *obj); +#endif // USE_LVGL_ANIMIMG + class LvglComponent : public PollingComponent { constexpr static const char *const TAG = "lvgl"; public: - static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { - reinterpret_cast(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p); - } + static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); float get_setup_priority() const override { return setup_priority::PROCESSOR; } - static void log_cb(const char *buf) { - esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); - } - static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { - // make sure all coordinates are even - if (area->x1 & 1) - area->x1--; - if (!(area->x2 & 1)) - area->x2++; - if (area->y1 & 1) - area->y1--; - if (!(area->y2 & 1)) - area->y2++; - } - void setup() override; - - void update() override { - // update indicators - if (this->paused_) { - return; - } - this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); - } - - void loop() override { - if (this->paused_) { - if (this->show_snow_) - this->write_random_(); - } - lv_timer_handler_run_in_period(5); - } - + void update() override; + void loop() override; void add_on_idle_callback(std::function &&callback) { this->idle_callbacks_.add(std::move(callback)); } @@ -141,23 +129,15 @@ class LvglComponent : public PollingComponent { bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; } void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; } lv_disp_t *get_disp() { return this->disp_; } - void set_paused(bool paused, bool show_snow) { - this->paused_ = paused; - this->show_snow_ = show_snow; - this->snow_line_ = 0; - if (!paused && lv_scr_act() != nullptr) { - lv_disp_trig_activity(this->disp_); // resets the inactivity time - lv_obj_invalidate(lv_scr_act()); - } - } - - void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { - lv_obj_add_event_cb(obj, callback, event, this); - if (event == LV_EVENT_VALUE_CHANGED) { - lv_obj_add_event_cb(obj, callback, lv_custom_event, this); - } - } + void set_paused(bool paused, bool show_snow); + void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event); + void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); bool is_paused() const { return this->paused_; } + void add_page(LvPageType *page); + void show_page(size_t index, 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 set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } protected: void write_random_(); @@ -168,8 +148,11 @@ class LvglComponent : public PollingComponent { lv_disp_drv_t disp_drv_{}; lv_disp_t *disp_{}; bool paused_{}; + std::vector pages_{}; + size_t current_page_{0}; bool show_snow_{}; lv_coord_t snow_line_{}; + bool page_wrap_{true}; std::vector> init_lambdas_; CallbackManager idle_callbacks_{}; @@ -179,16 +162,7 @@ class LvglComponent : public PollingComponent { class IdleTrigger : public Trigger<> { public: - explicit IdleTrigger(LvglComponent *parent, TemplatableValue timeout) : timeout_(std::move(timeout)) { - parent->add_on_idle_callback([this](uint32_t idle_time) { - if (!this->is_idle_ && idle_time > this->timeout_.value()) { - this->is_idle_ = true; - this->trigger(); - } else if (this->is_idle_ && idle_time < this->timeout_.value()) { - this->is_idle_ = false; - } - }); - } + explicit IdleTrigger(LvglComponent *parent, TemplatableValue timeout); protected: TemplatableValue timeout_; @@ -217,28 +191,8 @@ template class LvglCondition : public Condition, public P #ifdef USE_LVGL_TOUCHSCREEN class LVTouchListener : public touchscreen::TouchListener, public Parented { public: - LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) { - lv_indev_drv_init(&this->drv_); - this->drv_.long_press_repeat_time = long_press_repeat_time; - this->drv_.long_press_time = long_press_time; - this->drv_.type = LV_INDEV_TYPE_POINTER; - this->drv_.user_data = this; - this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { - auto *l = static_cast(d->user_data); - if (l->touch_pressed_) { - data->point.x = l->touch_point_.x; - data->point.y = l->touch_point_.y; - data->state = LV_INDEV_STATE_PRESSED; - } else { - data->state = LV_INDEV_STATE_RELEASED; - } - }; - } - void update(const touchscreen::TouchPoints_t &tpoints) override { - this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty(); - if (this->touch_pressed_) - this->touch_point_ = tpoints[0]; - } + LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time); + void update(const touchscreen::TouchPoints_t &tpoints) override; void release() override { touch_pressed_ = false; } lv_indev_drv_t *get_drv() { return &this->drv_; } @@ -249,24 +203,10 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented { public: - LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) { - lv_indev_drv_init(&this->drv_); - this->drv_.type = type; - this->drv_.user_data = this; - this->drv_.long_press_time = lpt; - this->drv_.long_press_repeat_time = lprt; - this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { - auto *l = static_cast(d->user_data); - data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; - data->key = l->key_; - data->enc_diff = (int16_t) (l->count_ - l->last_count_); - l->last_count_ = l->count_; - data->continue_reading = false; - }; - } + LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt); void set_left_button(binary_sensor::BinarySensor *left_button) { left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); }); @@ -304,6 +244,24 @@ class LVEncoderListener : public Parented { int32_t last_count_{}; int key_{}; }; -#endif // USE_LVGL_KEY_LISTENER +#endif // USE_LVGL_ROTARY_ENCODER +#ifdef USE_LVGL_BUTTONMATRIX +class LvBtnmatrixType : public key_provider::KeyProvider, public LvCompound { + public: + void set_obj(lv_obj_t *lv_obj) override; + uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); } + void set_key(size_t idx, uint8_t key) { this->key_map_[idx] = key; } + + protected: + std::map key_map_{}; +}; +#endif // USE_LVGL_BUTTONMATRIX + +#ifdef USE_LVGL_KEYBOARD +class LvKeyboardType : public key_provider::KeyProvider, public LvCompound { + public: + void set_obj(lv_obj_t *lv_obj) override; +}; +#endif // USE_LVGL_KEYBOARD } // namespace lvgl } // namespace esphome diff --git a/esphome/components/lvgl/page.py b/esphome/components/lvgl/page.py new file mode 100644 index 0000000000..4566b7eea4 --- /dev/null +++ b/esphome/components/lvgl/page.py @@ -0,0 +1,113 @@ +from esphome import automation, codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME + +from .defines import ( + CONF_ANIMATION, + CONF_LVGL_ID, + CONF_PAGE, + CONF_PAGE_WRAP, + CONF_SKIP, + LV_ANIM, +) +from .lv_validation import lv_bool, lv_milliseconds +from .lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp +from .schemas import LVGL_SCHEMA +from .types import LvglAction, lv_page_t +from .widget import Widget, WidgetType, add_widgets, set_obj_properties + + +class PageType(WidgetType): + def __init__(self): + super().__init__( + CONF_PAGE, + lv_page_t, + (), + { + cv.Optional(CONF_SKIP, default=False): lv_bool, + }, + ) + + async def to_code(self, w: Widget, config: dict): + return [] + + +SHOW_SCHEMA = LVGL_SCHEMA.extend( + { + cv.Optional(CONF_ANIMATION, default="NONE"): LV_ANIM.one_of, + cv.Optional(CONF_TIME, default="50ms"): lv_milliseconds, + } +) + + +page_spec = PageType() + + +@automation.register_action( + "lvgl.page.next", + LvglAction, + SHOW_SCHEMA, +) +async def page_next_to_code(config, action_id, template_arg, args): + animation = await LV_ANIM.process(config[CONF_ANIMATION]) + time = await lv_milliseconds.process(config[CONF_TIME]) + async with LambdaContext(LVGL_COMP_ARG) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.show_next_page(animation, time)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_LVGL_ID]) + return var + + +@automation.register_action( + "lvgl.page.previous", + LvglAction, + SHOW_SCHEMA, +) +async def page_previous_to_code(config, action_id, template_arg, args): + animation = await LV_ANIM.process(config[CONF_ANIMATION]) + time = await lv_milliseconds.process(config[CONF_TIME]) + async with LambdaContext(LVGL_COMP_ARG) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.show_prev_page(animation, time)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_LVGL_ID]) + return var + + +@automation.register_action( + "lvgl.page.show", + LvglAction, + cv.maybe_simple_value( + SHOW_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.use_id(lv_page_t), + } + ), + key=CONF_ID, + ), +) +async def page_show_to_code(config, action_id, template_arg, args): + widget = await cg.get_variable(config[CONF_ID]) + animation = await LV_ANIM.process(config[CONF_ANIMATION]) + time = await lv_milliseconds.process(config[CONF_TIME]) + async with LambdaContext(LVGL_COMP_ARG) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.show_page(widget.index, animation, time)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_LVGL_ID]) + return var + + +async def add_pages(lv_component, config): + lv_add(lv_component.set_page_wrap(config[CONF_PAGE_WRAP])) + for pconf in config.get(CONF_PAGES, ()): + id = pconf[CONF_ID] + skip = pconf[CONF_SKIP] + var = cg.new_Pvariable(id, skip) + page = Widget.create(id, var, page_spec, pconf) + lv_add(lv_component.add_page(var)) + # Set outer config first + await set_obj_properties(page, config) + await set_obj_properties(page, pconf) + await add_widgets(page, pconf) diff --git a/esphome/components/lvgl/rotary_encoders.py b/esphome/components/lvgl/rotary_encoders.py index 77dc397c3e..ede6905a67 100644 --- a/esphome/components/lvgl/rotary_encoders.py +++ b/esphome/components/lvgl/rotary_encoders.py @@ -13,9 +13,10 @@ from .defines import ( CONF_ROTARY_ENCODERS, ) from .helpers import lvgl_components_required -from .lvcode import add_group, lv, lv_add, lv_expr +from .lvcode import lv, lv_add, lv_expr from .schemas import ENCODER_SCHEMA from .types import lv_indev_type_t +from .widget import add_group ROTARY_ENCODER_CONFIG = cv.ensure_list( ENCODER_SCHEMA.extend( diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index ebef56a882..796783890d 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -15,8 +15,12 @@ from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid, types as ty from .helpers import add_lv_use, requires_component, validate_printf -from .lv_validation import id_name, lv_font -from .types import WIDGET_TYPES, WidgetType +from .lv_validation import id_name, lv_color, lv_font, lv_image +from .lvcode import LvglComponent +from .types import WidgetType + +# this will be populated later, in __init__.py to avoid circular imports. +WIDGET_TYPES: dict = {} # A schema for text properties TEXT_SCHEMA = cv.Schema( @@ -38,11 +42,13 @@ TEXT_SCHEMA = cv.Schema( } ) -ACTION_SCHEMA = cv.maybe_simple_value( - { - cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t), - }, - key=CONF_ID, +LIST_ACTION_SCHEMA = cv.ensure_list( + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t), + }, + key=CONF_ID, + ) ) PRESS_TIME = cv.All( @@ -154,6 +160,7 @@ STYLE_REMAP = { # Complete object style schema STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( { + cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(ty.lv_style_t)), cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" ).one_of, @@ -209,7 +216,14 @@ def create_modify_schema(widget_type): part_schema(widget_type) .extend( { - cv.Required(CONF_ID): cv.use_id(widget_type), + cv.Required(CONF_ID): cv.ensure_list( + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(widget_type), + }, + key=CONF_ID, + ) + ), cv.Optional(CONF_STATE): SET_STATE_SCHEMA, } ) @@ -227,6 +241,7 @@ def obj_schema(widget_type: WidgetType): return ( part_schema(widget_type) .extend(FLAG_SCHEMA) + .extend(LAYOUT_SCHEMA) .extend(ALIGN_TO_SCHEMA) .extend(automation_schema(widget_type.w_type)) .extend( @@ -240,6 +255,8 @@ def obj_schema(widget_type: WidgetType): ) +LAYOUT_SCHEMAS = {} + ALIGN_TO_SCHEMA = { cv.Optional(df.CONF_ALIGN_TO): cv.Schema( { @@ -252,6 +269,65 @@ ALIGN_TO_SCHEMA = { } +def grid_free_space(value): + value = cv.Upper(value) + if value.startswith("FR(") and value.endswith(")"): + value = value.removesuffix(")").removeprefix("FR(") + return f"LV_GRID_FR({cv.positive_int(value)})" + raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)") + + +grid_spec = cv.Any( + lvalid.size, df.LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space +) + +cell_alignments = df.LV_CELL_ALIGNMENTS.one_of +grid_alignments = df.LV_GRID_ALIGNMENTS.one_of +flex_alignments = df.LV_FLEX_ALIGNMENTS.one_of + +LAYOUT_SCHEMA = { + cv.Optional(df.CONF_LAYOUT): cv.typed_schema( + { + df.TYPE_GRID: { + cv.Required(df.CONF_GRID_ROWS): [grid_spec], + cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], + cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, + cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, + }, + df.TYPE_FLEX: { + cv.Optional( + df.CONF_FLEX_FLOW, default="row_wrap" + ): df.FLEX_FLOWS.one_of, + cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, + cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, + cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, + }, + }, + lower=True, + ) +} + +GRID_CELL_SCHEMA = { + cv.Required(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Required(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments, +} + +FLEX_OBJ_SCHEMA = { + cv.Optional(df.CONF_FLEX_GROW): cv.int_, +} + +DISP_BG_SCHEMA = cv.Schema( + { + cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image, + cv.Optional(df.CONF_DISP_BG_COLOR): lv_color, + } +) + + # A style schema that can include text STYLED_TEXT_SCHEMA = cv.maybe_simple_value( STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT @@ -260,13 +336,11 @@ STYLED_TEXT_SCHEMA = cv.maybe_simple_value( # For use by platform components LVGL_SCHEMA = cv.Schema( { - cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(ty.LvglComponent), + cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(LvglComponent), } ) -ALL_STYLES = { - **STYLE_PROPS, -} +ALL_STYLES = {**STYLE_PROPS, **GRID_CELL_SCHEMA, **FLEX_OBJ_SCHEMA} def container_validator(schema, widget_type: WidgetType): @@ -281,16 +355,17 @@ def container_validator(schema, widget_type: WidgetType): result = schema if w_sch := widget_type.schema: result = result.extend(w_sch) + ltype = df.TYPE_NONE if value and (layout := value.get(df.CONF_LAYOUT)): if not isinstance(layout, dict): raise cv.Invalid("Layout value must be a dict") ltype = layout.get(CONF_TYPE) + if not ltype: + raise (cv.Invalid("Layout schema requires type:")) add_lv_use(ltype) - result = result.extend( - {cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())} - ) if value == SCHEMA_EXTRACT: return result + result = result.extend(LAYOUT_SCHEMAS[ltype.lower()]) return result(value) return validator diff --git a/esphome/components/lvgl/slider.py b/esphome/components/lvgl/slider.py new file mode 100644 index 0000000000..1886f79b44 --- /dev/null +++ b/esphome/components/lvgl/slider.py @@ -0,0 +1,63 @@ +import esphome.config_validation as cv +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE + +from .defines import ( + BAR_MODES, + CONF_ANIMATED, + CONF_INDICATOR, + CONF_KNOB, + CONF_MAIN, + literal, +) +from .helpers import add_lv_use +from .lv_bar import CONF_BAR +from .lv_validation import animated, get_start_value, lv_float +from .lvcode import lv +from .types import LvNumber, NumberType +from .widget import Widget + +CONF_SLIDER = "slider" +SLIDER_MODIFY_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + +SLIDER_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, + cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, + cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + + +class SliderType(NumberType): + def __init__(self): + super().__init__( + CONF_SLIDER, + LvNumber("lv_slider_t"), + parts=(CONF_MAIN, CONF_INDICATOR, CONF_KNOB), + schema=SLIDER_SCHEMA, + modify_schema=SLIDER_MODIFY_SCHEMA, + ) + + @property + def animated(self): + return True + + async def to_code(self, w: Widget, config): + add_lv_use(CONF_BAR) + if CONF_MIN_VALUE in config: + # not modify case + lv.slider_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) + lv.slider_set_mode(w.obj, literal(config[CONF_MODE])) + value = await get_start_value(config) + if value is not None: + lv.slider_set_value(w.obj, value, literal(config[CONF_ANIMATED])) + + +slider_spec = SliderType() diff --git a/esphome/components/lvgl/spinner.py b/esphome/components/lvgl/spinner.py new file mode 100644 index 0000000000..2f798d0fbf --- /dev/null +++ b/esphome/components/lvgl/spinner.py @@ -0,0 +1,43 @@ +import esphome.config_validation as cv +from esphome.cpp_generator import MockObjClass + +from .arc import CONF_ARC +from .defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME +from .lv_validation import angle +from .lvcode import lv_expr +from .types import LvType +from .widget import Widget, WidgetType + +CONF_SPINNER = "spinner" + +SPINNER_SCHEMA = cv.Schema( + { + cv.Required(CONF_ARC_LENGTH): angle, + cv.Required(CONF_SPIN_TIME): cv.positive_time_period_milliseconds, + } +) + + +class SpinnerType(WidgetType): + def __init__(self): + super().__init__( + CONF_SPINNER, + LvType("lv_spinner_t"), + (CONF_MAIN, CONF_INDICATOR), + SPINNER_SCHEMA, + {}, + ) + + async def to_code(self, w: Widget, config): + return [] + + def get_uses(self): + return (CONF_ARC,) + + def obj_creator(self, parent: MockObjClass, config: dict): + spin_time = config[CONF_SPIN_TIME].total_milliseconds + arc_length = config[CONF_ARC_LENGTH] // 10 + return lv_expr.call("spinner_create", parent, spin_time, arc_length) + + +spinner_spec = SpinnerType() diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py new file mode 100644 index 0000000000..7a795bc99d --- /dev/null +++ b/esphome/components/lvgl/styles.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +from esphome.const import CONF_ID +from esphome.core import ID +from esphome.cpp_generator import MockObj + +from .defines import ( + CONF_STYLE_DEFINITIONS, + CONF_THEME, + CONF_TOP_LAYER, + LValidator, + literal, +) +from .helpers import add_lv_use +from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable +from .obj import obj_spec +from .schemas import ALL_STYLES +from .types import lv_lambda_t, lv_obj_t, lv_obj_t_ptr +from .widget import Widget, add_widgets, set_obj_properties, theme_widget_map + +TOP_LAYER = literal("lv_disp_get_layer_top(lv_component->get_disp())") + + +async def styles_to_code(config): + """Convert styles to C__ code.""" + for style in config.get(CONF_STYLE_DEFINITIONS, ()): + svar = cg.new_Pvariable(style[CONF_ID]) + lv.style_init(svar) + for prop, validator in ALL_STYLES.items(): + if value := style.get(prop): + if isinstance(validator, LValidator): + value = await validator.process(value) + if isinstance(value, list): + value = "|".join(value) + lv.call(f"style_set_{prop}", svar, literal(value)) + + +async def theme_to_code(config): + if theme := config.get(CONF_THEME): + add_lv_use(CONF_THEME) + for w_name, style in theme.items(): + if not isinstance(style, dict): + continue + + lname = "lv_theme_apply_" + w_name + apply = lv_variable(lv_lambda_t, lname) + theme_widget_map[w_name] = apply + ow = Widget.create("obj", MockObj(ID("obj")), obj_spec) + async with LambdaContext([(lv_obj_t_ptr, "obj")], where=w_name) as context: + await set_obj_properties(ow, style) + lv_assign(apply, await context.get_lambda()) + + +async def add_top_layer(config): + if top_conf := config.get(CONF_TOP_LAYER): + with LocalVariable("top_layer", lv_obj_t, TOP_LAYER) as top_layer_obj: + top_w = Widget(top_layer_obj, obj_spec, top_conf) + await set_obj_properties(top_w, top_conf) + await add_widgets(top_w, top_conf) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index bf92bda5b0..c640c8abd9 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -7,15 +7,14 @@ from .defines import ( CONF_ALIGN_TO, CONF_X, CONF_Y, - LV_EVENT, + LV_EVENT_MAP, LV_EVENT_TRIGGERS, literal, ) -from .lvcode import LambdaContext, add_line_marks, lv, lv_add +from .lvcode import EVENT_ARG, LambdaContext, LvConditional, lv, lv_add +from .types import LV_EVENT from .widget import widget_map -lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr") - async def generate_triggers(lv_component): """ @@ -34,15 +33,15 @@ async def generate_triggers(lv_component): }.items(): conf = conf[0] w.add_flag("LV_OBJ_FLAG_CLICKABLE") - event = "LV_EVENT_" + LV_EVENT[event[3:].upper()] + event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()]) await add_trigger(conf, event, lv_component, w) for conf in w.config.get(CONF_ON_VALUE, ()): - await add_trigger(conf, "LV_EVENT_VALUE_CHANGED", lv_component, w) + await add_trigger(conf, LV_EVENT.VALUE_CHANGED, lv_component, w) # Generate align to directives while we're here if align_to := w.config.get(CONF_ALIGN_TO): target = widget_map[align_to[CONF_ID]].obj - align = align_to[CONF_ALIGN] + align = literal(align_to[CONF_ALIGN]) x = align_to[CONF_X] y = align_to[CONF_Y] lv.obj_align_to(w.obj, target, align, x, y) @@ -50,12 +49,11 @@ async def generate_triggers(lv_component): async def add_trigger(conf, event, lv_component, w): tid = conf[CONF_TRIGGER_ID] - add_line_marks(tid) trigger = cg.new_Pvariable(tid) args = w.get_args() value = w.get_value() await automation.build_automation(trigger, args, conf) - with LambdaContext([(lv_event_t_ptr, "event_data")]) as context: - add_line_marks(tid) - lv_add(trigger.trigger(value)) - lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), literal(event))) + async with LambdaContext(EVENT_ARG, where=tid) as context: + with LvConditional(w.is_selected()): + lv_add(trigger.trigger(value)) + lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), event)) diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 6997207dac..b6f65c8c1b 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,8 +1,11 @@ -from esphome import automation, codegen as cg -from esphome.core import ID -from esphome.cpp_generator import MockObjClass +import sys -from .defines import CONF_TEXT +from esphome import automation, codegen as cg +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_VALUE +from esphome.cpp_generator import MockObj, MockObjClass + +from .defines import CONF_TEXT, lvgl_ns +from .lvcode import lv_expr class LvType(cg.MockObjClass): @@ -18,36 +21,48 @@ class LvType(cg.MockObjClass): return self.args[0][0] if len(self.args) else None +class LvNumber(LvType): + def __init__(self, *args): + super().__init__( + *args, + largs=[(cg.float_, "x")], + lvalue=lambda w: w.get_number_value(), + has_on_value=True, + ) + self.value_property = CONF_VALUE + + uint16_t_ptr = cg.uint16.operator("ptr") -lvgl_ns = cg.esphome_ns.namespace("lvgl") char_ptr = cg.global_ns.namespace("char").operator("ptr") void_ptr = cg.void.operator("ptr") -LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) -LvglComponentPtr = LvglComponent.operator("ptr") -lv_event_code_t = cg.global_ns.namespace("lv_event_code_t") +lv_coord_t = cg.global_ns.namespace("lv_coord_t") +lv_event_code_t = cg.global_ns.enum("lv_event_code_t") lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t") FontEngine = lvgl_ns.class_("FontEngine") IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) LvglAction = lvgl_ns.class_("LvglAction", automation.Action) +lv_lambda_t = lvgl_ns.class_("LvLambdaType") LvCompound = lvgl_ns.class_("LvCompound") lv_font_t = cg.global_ns.class_("lv_font_t") lv_style_t = cg.global_ns.struct("lv_style_t") +# fake parent class for first class widgets and matrix buttons lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton") lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) lv_obj_t_ptr = lv_obj_base_t.operator("ptr") -lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr") +lv_disp_t = cg.global_ns.struct("lv_disp_t") lv_color_t = cg.global_ns.struct("lv_color_t") lv_group_t = cg.global_ns.struct("lv_group_t") LVTouchListener = lvgl_ns.class_("LVTouchListener") LVEncoderListener = lvgl_ns.class_("LVEncoderListener") lv_obj_t = LvType("lv_obj_t") +lv_page_t = cg.global_ns.class_("LvPageType", LvCompound) lv_img_t = LvType("lv_img_t") - -# this will be populated later, in __init__.py to avoid circular imports. -WIDGET_TYPES: dict = {} +LV_EVENT = MockObj(base="LV_EVENT_", op="") +LV_STATE = MockObj(base="LV_STATE_", op="") +LV_BTNMATRIX_CTRL = MockObj(base="LV_BTNMATRIX_CTRL_", op="") class LvText(LvType): @@ -55,7 +70,8 @@ class LvText(LvType): super().__init__( *args, largs=[(cg.std_string, "text")], - lvalue=lambda w: w.get_property("text")[0], + lvalue=lambda w: w.get_property("text"), + has_on_value=True, **kwargs, ) self.value_property = CONF_TEXT @@ -66,13 +82,21 @@ class LvBoolean(LvType): super().__init__( *args, largs=[(cg.bool_, "x")], - lvalue=lambda w: w.has_state("LV_STATE_CHECKED"), + lvalue=lambda w: w.is_checked(), has_on_value=True, **kwargs, ) -CUSTOM_EVENT = ID("lv_custom_event", False, type=lv_event_code_t) +class LvSelect(LvType): + def __init__(self, *args, **kwargs): + super().__init__( + *args, + largs=[(cg.int_, "x")], + lvalue=lambda w: w.get_property("selected"), + has_on_value=True, + **kwargs, + ) class WidgetType: @@ -80,7 +104,15 @@ class WidgetType: Describes a type of Widget, e.g. "bar" or "line" """ - def __init__(self, name, w_type, parts, schema=None, modify_schema=None): + def __init__( + self, + name: str, + w_type: LvType, + parts: tuple, + schema=None, + modify_schema=None, + lv_name=None, + ): """ :param name: The widget name, e.g. "bar" :param w_type: The C type of the widget @@ -89,6 +121,7 @@ class WidgetType: :param modify_schema: A schema to update the widget """ self.name = name + self.lv_name = lv_name or name self.w_type = w_type self.parts = parts if schema is None: @@ -98,7 +131,8 @@ class WidgetType: if modify_schema is None: self.modify_schema = self.schema else: - self.modify_schema = self.schema + self.modify_schema = modify_schema + self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") @property def animated(self): @@ -118,7 +152,7 @@ class WidgetType: :param config: Its configuration :return: Generated code as a list of text lines """ - raise NotImplementedError(f"No to_code defined for {self.name}") + return [] def obj_creator(self, parent: MockObjClass, config: dict): """ @@ -127,7 +161,7 @@ class WidgetType: :param config: Its configuration :return: Generated code as a single text line """ - return f"lv_{self.name}_create({parent})" + return lv_expr.call(f"{self.lv_name}_create", parent) def get_uses(self): """ @@ -135,3 +169,23 @@ class WidgetType: :return: """ return () + + def get_max(self, config: dict): + return sys.maxsize + + def get_min(self, config: dict): + return -sys.maxsize + + def get_step(self, config: dict): + return 1 + + def get_scale(self, config: dict): + return 1.0 + + +class NumberType(WidgetType): + def get_max(self, config: dict): + return int(config[CONF_MAX_VALUE] or 100) + + def get_min(self, config: dict): + return int(config[CONF_MIN_VALUE] or 0) diff --git a/esphome/components/lvgl/widget.py b/esphome/components/lvgl/widget.py index 83aed341e7..5734aec7dc 100644 --- a/esphome/components/lvgl/widget.py +++ b/esphome/components/lvgl/widget.py @@ -1,33 +1,63 @@ import sys -from typing import Any +from typing import Any, Union from esphome import codegen as cg, config_validation as cv from esphome.config_validation import Invalid -from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE -from esphome.core import CORE, TimePeriod +from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE +from esphome.core import ID, TimePeriod from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import MockObj, MockObjClass, VariableDeclarationExpression +from esphome.cpp_generator import AssignmentExpression, CallExpression, MockObj from .defines import ( CONF_DEFAULT, + CONF_FLEX_ALIGN_CROSS, + CONF_FLEX_ALIGN_MAIN, + CONF_FLEX_ALIGN_TRACK, + CONF_FLEX_FLOW, + CONF_GRID_COLUMN_ALIGN, + CONF_GRID_COLUMNS, + CONF_GRID_ROW_ALIGN, + CONF_GRID_ROWS, + CONF_LAYOUT, CONF_MAIN, CONF_SCROLLBAR_MODE, + CONF_STYLES, CONF_WIDGETS, OBJ_FLAGS, PARTS, STATES, - ConstantLiteral, + TYPE_FLEX, + TYPE_GRID, LValidator, join_enums, literal, ) from .helpers import add_lv_use -from .lvcode import add_group, add_line_marks, lv, lv_add, lv_assign, lv_expr, lv_obj -from .schemas import ALL_STYLES, STYLE_REMAP -from .types import WIDGET_TYPES, LvType, WidgetType, lv_obj_t, lv_obj_t_ptr +from .lvcode import ( + LvConditional, + add_line_marks, + lv, + lv_add, + lv_assign, + lv_expr, + lv_obj, + lv_Pvariable, +) +from .schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES +from .types import ( + LV_STATE, + LvType, + WidgetType, + lv_coord_t, + lv_group_t, + lv_obj_t, + lv_obj_t_ptr, +) EVENT_LAMB = "event_lamb__" +theme_widget_map = {} + class LvScrActType(WidgetType): """ @@ -37,9 +67,6 @@ class LvScrActType(WidgetType): def __init__(self): super().__init__("lv_scr_act()", lv_obj_t, ()) - def obj_creator(self, parent: MockObjClass, config: dict): - return [] - async def to_code(self, w, config: dict): return [] @@ -55,7 +82,7 @@ class Widget: def set_completed(): Widget.widgets_completed = True - def __init__(self, var, wtype: WidgetType, config: dict = None, parent=None): + def __init__(self, var, wtype: WidgetType, config: dict = None): self.var = var self.type = wtype self.config = config @@ -63,21 +90,18 @@ class Widget: self.step = 1.0 self.range_from = -sys.maxsize self.range_to = sys.maxsize - self.parent = parent + if wtype.is_compound(): + self.obj = MockObj(f"{self.var}->obj") + else: + self.obj = var @staticmethod - def create(name, var, wtype: WidgetType, config: dict = None, parent=None): - w = Widget(var, wtype, config, parent) + def create(name, var, wtype: WidgetType, config: dict = None): + w = Widget(var, wtype, config) if name is not None: widget_map[name] = w return w - @property - def obj(self): - if self.type.is_compound(): - return f"{self.var}->obj" - return self.var - def add_state(self, state): return lv_obj.add_state(self.obj, literal(state)) @@ -85,7 +109,13 @@ class Widget: return lv_obj.clear_state(self.obj, literal(state)) def has_state(self, state): - return lv_expr.obj_get_state(self.obj) & literal(state) != 0 + return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0 + + def is_pressed(self): + return self.has_state(LV_STATE.PRESSED) + + def is_checked(self): + return self.has_state(LV_STATE.CHECKED) def add_flag(self, flag): return lv_obj.add_flag(self.obj, literal(flag)) @@ -93,32 +123,37 @@ class Widget: def clear_flag(self, flag): return lv_obj.clear_flag(self.obj, literal(flag)) - def set_property(self, prop, value, animated: bool = None, ltype=None): + async def set_property(self, prop, value, animated: bool = None): if isinstance(value, dict): value = value.get(prop) + if isinstance(ALL_STYLES.get(prop), LValidator): + value = await ALL_STYLES[prop].process(value) + else: + value = literal(value) if value is None: return if isinstance(value, TimePeriod): value = value.total_milliseconds - ltype = ltype or self.__type_base() + if isinstance(value, str): + value = literal(value) if animated is None or self.type.animated is not True: - lv.call(f"{ltype}_set_{prop}", self.obj, value) + lv.call(f"{self.type.lv_name}_set_{prop}", self.obj, value) else: lv.call( - f"{ltype}_set_{prop}", + f"{self.type.lv_name}_set_{prop}", self.obj, value, - "LV_ANIM_ON" if animated else "LV_ANIM_OFF", + literal("LV_ANIM_ON" if animated else "LV_ANIM_OFF"), ) def get_property(self, prop, ltype=None): ltype = ltype or self.__type_base() - return f"lv_{ltype}_get_{prop}({self.obj})" + return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})") def set_style(self, prop, value, state): if value is None: - return [] - return lv.call(f"obj_set_style_{prop}", self.obj, value, state) + return + lv.call(f"obj_set_style_{prop}", self.obj, value, state) def __type_base(self): wtype = self.type.w_type @@ -140,6 +175,32 @@ class Widget: return self.type.w_type.value(self) return self.obj + def get_number_value(self): + value = self.type.mock_obj.get_value(self.obj) + if self.scale == 1.0: + return value + return value / float(self.scale) + + def is_selected(self): + """ + Overridable property to determine if the widget is selected. Will be None except + for matrix buttons + :return: + """ + return None + + def get_max(self): + return self.type.get_max(self.config) + + def get_min(self): + return self.type.get_min(self.config) + + def get_step(self): + return self.type.get_step(self.config) + + def get_scale(self): + return self.type.get_scale(self.config) + # Map of widgets to their config, used for trigger generation widget_map: dict[Any, Widget] = {} @@ -161,13 +222,20 @@ def get_widget_generator(wid): yield -async def get_widget(config: dict, id: str = CONF_ID) -> Widget: - wid = config[id] +async def get_widget_(wid: Widget): if obj := widget_map.get(wid): return obj return await FakeAwaitable(get_widget_generator(wid)) +async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]: + if not config: + return [] + if not isinstance(config, list): + config = [config] + return [await get_widget_(c[id]) for c in config if id in c] + + def collect_props(config): """ Collect all properties from a configuration @@ -175,7 +243,7 @@ def collect_props(config): :return: """ props = {} - for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_GROUP]: + for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_STYLES, CONF_GROUP]: if prop in config: props[prop] = config[prop] return props @@ -209,12 +277,39 @@ def collect_parts(config): async def set_obj_properties(w: Widget, config): """Generate a list of C++ statements to apply properties to an lv_obj_t""" + if layout := config.get(CONF_LAYOUT): + layout_type: str = layout[CONF_TYPE] + lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) + if layout_type == TYPE_GRID: + wid = config[CONF_ID] + rows = "{" + ",".join(layout[CONF_GRID_ROWS]) + ", LV_GRID_TEMPLATE_LAST}" + row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t) + row_array = cg.static_const_array(row_id, cg.RawExpression(rows)) + w.set_style("grid_row_dsc_array", row_array, 0) + columns = ( + "{" + ",".join(layout[CONF_GRID_COLUMNS]) + ", LV_GRID_TEMPLATE_LAST}" + ) + column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t) + column_array = cg.static_const_array(column_id, cg.RawExpression(columns)) + w.set_style("grid_column_dsc_array", column_array, 0) + w.set_style( + CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN)), 0 + ) + w.set_style( + CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN)), 0 + ) + if layout_type == TYPE_FLEX: + lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW])) + main = literal(layout[CONF_FLEX_ALIGN_MAIN]) + cross = literal(layout[CONF_FLEX_ALIGN_CROSS]) + track = literal(layout[CONF_FLEX_ALIGN_TRACK]) + lv_obj.set_flex_align(w.obj, main, cross, track) parts = collect_parts(config) for part, states in parts.items(): for state, props in states.items(): - lv_state = ConstantLiteral( - f"(int)LV_STATE_{state.upper()}|(int)LV_PART_{part.upper()}" - ) + lv_state = join_enums((f"LV_STATE_{state}", f"LV_PART_{part}")) + for style_id in props.get(CONF_STYLES, ()): + lv_obj.add_style(w.obj, MockObj(style_id), lv_state) for prop, value in { k: v for k, v in props.items() if k in ALL_STYLES }.items(): @@ -258,14 +353,12 @@ async def set_obj_properties(w: Widget, config): w.clear_state(clears) for key, value in lambs.items(): lamb = await cg.process_lambda(value, [], return_type=cg.bool_) - state = f"LV_STATE_{key.upper}" - lv.cond_if(lamb) - w.add_state(state) - lv.cond_else() - w.clear_state(state) - lv.cond_endif() - if scrollbar_mode := config.get(CONF_SCROLLBAR_MODE): - lv_obj.set_scrollbar_mode(w.obj, scrollbar_mode) + state = f"LV_STATE_{key.upper()}" + with LvConditional(f"{lamb}()") as cond: + w.add_state(state) + cond.else_() + w.clear_state(state) + await w.set_property(CONF_SCROLLBAR_MODE, config) async def add_widgets(parent: Widget, config: dict): @@ -280,7 +373,7 @@ async def add_widgets(parent: Widget, config: dict): await widget_to_code(w_cnfig, w_type, parent.obj) -async def widget_to_code(w_cnfig, w_type, parent): +async def widget_to_code(w_cnfig, w_type: WidgetType, parent): """ Converts a Widget definition to C code. :param w_cnfig: The widget configuration @@ -298,19 +391,33 @@ async def widget_to_code(w_cnfig, w_type, parent): var = cg.new_Pvariable(wid) lv_add(var.set_obj(creator)) else: - var = MockObj(wid, "->") - decl = VariableDeclarationExpression(lv_obj_t, "*", wid) - CORE.add_global(decl) - CORE.register_variable(wid, var) + var = lv_Pvariable(lv_obj_t, wid) lv_assign(var, creator) - widget = Widget.create(wid, var, spec, w_cnfig, parent) - await set_obj_properties(widget, w_cnfig) - await add_widgets(widget, w_cnfig) - await spec.to_code(widget, w_cnfig) + w = Widget.create(wid, var, spec, w_cnfig) + if theme := theme_widget_map.get(w_type): + lv_add(CallExpression(theme, w.obj)) + await set_obj_properties(w, w_cnfig) + await add_widgets(w, w_cnfig) + await spec.to_code(w, w_cnfig) lv_scr_act_spec = LvScrActType() -lv_scr_act = Widget.create( - None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None -) +lv_scr_act = Widget.create(None, literal("lv_scr_act()"), lv_scr_act_spec, {}) + +lv_groups = {} # Widget group names + + +def add_group(name): + if name is None: + return None + fullname = f"lv_esp_group_{name}" + if name not in lv_groups: + gid = ID(fullname, True, type=lv_group_t.operator("ptr")) + lv_add( + AssignmentExpression( + type_=gid.type, modifier="", name=fullname, rhs=lv_expr.group_create() + ) + ) + lv_groups[name] = literal(fullname) + return lv_groups[name] diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index fde700e0bd..0cca45d376 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -5,142 +5,229 @@ lvgl: - touchscreen_id: tft_touch long_press_repeat_time: 200ms long_press_time: 500ms - widgets: - - label: - id: hello_label - text: Hello world - text_color: 0xFF8000 - align: center - text_font: montserrat_40 - border_post: true - - - label: - text: "Hello shiny day" - text_color: 0xFFFFFF - align: bottom_mid - text_font: space16 - - obj: - align: center - arc_opa: COVER - arc_color: 0xFF0000 - arc_rounded: false - arc_width: 3 - anim_time: 1s - bg_color: light_blue - bg_grad_color: light_blue - bg_dither_mode: ordered - bg_grad_dir: hor - bg_grad_stop: 128 - bg_image_opa: transp - bg_image_recolor: light_blue - bg_image_recolor_opa: 50% - bg_main_stop: 0 - bg_opa: 20% - border_color: 0x00FF00 - border_opa: cover - border_post: true - border_side: [bottom, left] - border_width: 4 - clip_corner: false - height: 50% - image_recolor: light_blue - image_recolor_opa: cover - line_width: 10 - line_dash_width: 10 - line_dash_gap: 10 - line_rounded: false - line_color: light_blue - opa: cover - opa_layered: cover - outline_color: light_blue - outline_opa: cover - outline_pad: 10px - outline_width: 10px - pad_all: 10px - pad_bottom: 10px - pad_column: 10px - pad_left: 10px - pad_right: 10px - pad_row: 10px - pad_top: 10px - shadow_color: light_blue - shadow_ofs_x: 5 - shadow_ofs_y: 5 - shadow_opa: cover - shadow_spread: 5 - shadow_width: 10 - text_align: auto - text_color: light_blue - text_decor: [underline, strikethrough] - text_font: montserrat_18 - text_letter_space: 4 - text_line_space: 4 - text_opa: cover - transform_angle: 180 - transform_height: 100 - transform_pivot_x: 50% - transform_pivot_y: 50% - transform_zoom: 0.5 - translate_x: 10 - translate_y: 10 - max_height: 100 - max_width: 200 - min_height: 20% - min_width: 20% - radius: circle - width: 10px - x: 100 - y: 120 - - button: - width: 20% - height: 10% - pressed: - bg_color: light_blue - checkable: true - checked: - bg_color: 0x000000 - widgets: - - label: - text: Button - on_click: - lvgl.label.update: + pages: + - id: page1 + skip: true + widgets: + - label: id: hello_label - bg_color: 0x123456 - text: clicked - on_value: - logger.log: - format: "state now %d" - args: [x] - on_short_click: - lvgl.widget.hide: hello_label - on_long_press: - lvgl.widget.show: hello_label - on_cancel: - lvgl.widget.enable: hello_label - on_ready: - lvgl.widget.disable: hello_label - on_defocus: - lvgl.widget.hide: hello_label - on_focus: - logger.log: Button clicked - on_scroll: - logger.log: Button clicked - on_scroll_end: - logger.log: Button clicked - on_scroll_begin: - logger.log: Button clicked - on_release: - logger.log: Button clicked - on_long_press_repeat: - logger.log: Button clicked + text: Hello world + text_color: 0xFF8000 + align: center + text_font: montserrat_40 + border_post: true + - label: + text: "Hello shiny day" + text_color: 0xFFFFFF + align: bottom_mid + text_font: space16 + - obj: + align: center + arc_opa: COVER + arc_color: 0xFF0000 + arc_rounded: false + arc_width: 3 + anim_time: 1s + bg_color: light_blue + bg_grad_color: light_blue + bg_dither_mode: ordered + bg_grad_dir: hor + bg_grad_stop: 128 + bg_image_opa: transp + bg_image_recolor: light_blue + bg_image_recolor_opa: 50% + bg_main_stop: 0 + bg_opa: 20% + border_color: 0x00FF00 + border_opa: cover + border_post: true + border_side: [bottom, left] + border_width: 4 + clip_corner: false + height: 50% + image_recolor: light_blue + image_recolor_opa: cover + line_width: 10 + line_dash_width: 10 + line_dash_gap: 10 + line_rounded: false + line_color: light_blue + opa: cover + opa_layered: cover + outline_color: light_blue + outline_opa: cover + outline_pad: 10px + outline_width: 10px + pad_all: 10px + pad_bottom: 10px + pad_column: 10px + pad_left: 10px + pad_right: 10px + pad_row: 10px + pad_top: 10px + shadow_color: light_blue + shadow_ofs_x: 5 + shadow_ofs_y: 5 + shadow_opa: cover + shadow_spread: 5 + shadow_width: 10 + text_align: auto + text_color: light_blue + text_decor: [underline, strikethrough] + text_font: montserrat_18 + text_letter_space: 4 + text_line_space: 4 + text_opa: cover + transform_angle: 180 + transform_height: 100 + transform_pivot_x: 50% + transform_pivot_y: 50% + transform_zoom: 0.5 + translate_x: 10 + translate_y: 10 + max_height: 100 + max_width: 200 + min_height: 20% + min_width: 20% + radius: circle + width: 10px + x: 100 + y: 120 + - button: + width: 20% + height: 10% + pressed: + bg_color: light_blue + checkable: true + checked: + bg_color: 0x000000 + widgets: + - label: + text: Button + on_click: + lvgl.label.update: + id: hello_label + bg_color: 0x123456 + text: clicked + on_value: + logger.log: + format: "state now %d" + args: [x] + on_short_click: + lvgl.widget.hide: hello_label + on_long_press: + lvgl.widget.show: hello_label + on_cancel: + lvgl.widget.enable: hello_label + on_ready: + lvgl.widget.disable: hello_label + on_defocus: + lvgl.widget.hide: hello_label + on_focus: + logger.log: Button clicked + on_scroll: + logger.log: Button clicked + on_scroll_end: + logger.log: Button clicked + on_scroll_begin: + logger.log: Button clicked + on_release: + logger.log: Button clicked + on_long_press_repeat: + logger.log: Button clicked + - led: + color: 0x00FF00 + brightness: 50% + align: right_mid + - spinner: + arc_length: 120 + spin_time: 2s + align: left_mid + - image: + src: cat_image + align: top_left + y: 50 + + - id: page2 + widgets: + - arc: + align: left_mid + id: lv_arc + adjustable: true + on_value: + then: + - logger.log: + format: "Arc value is %f" + args: [x] + group: general + scroll_on_focus: true + value: 75 + min_value: 1 + max_value: 100 + arc_color: 0xFF0000 + indicator: + arc_color: 0xF000FF + pressed: + arc_color: 0xFFFF00 + focused: + arc_color: 0x808080 + - bar: + id: bar_id + align: top_mid + y: 20 + value: 30 + max_value: 100 + min_value: 10 + mode: range + on_click: + then: + - lvgl.bar.update: + id: bar_id + value: !lambda return (int)((float)rand() / RAND_MAX * 100); + - logger.log: + format: "bar value %f" + args: [x] + - line: + align: center + points: + - 5, 5 + - 70, 70 + - 120, 10 + - 180, 60 + - 240, 10 + on_click: + lvgl.page.next: + - switch: + align: right_mid + - checkbox: + text: Checkbox + align: bottom_right + - slider: + id: slider_id + align: top_mid + y: 40 + value: 30 + max_value: 100 + min_value: 10 + mode: normal + on_value: + then: + - logger.log: + format: "slider value %f" + args: [x] + on_click: + then: + - lvgl.slider.update: + id: slider_id + value: !lambda return (int)((float)rand() / RAND_MAX * 100); font: - file: "gfonts://Roboto" id: space16 bpp: 4 image: - - id: cat_img + - id: cat_image resize: 256x48 file: $component_dir/logo-text.svg - id: dog_img From e02319dcff75a003b5551866c701dd81803022c8 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 5 Aug 2024 12:09:54 -0400 Subject: [PATCH 033/160] [esp32_improv] Update Improv library to reference new repo/version (#7195) --- esphome/components/esp32_improv/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 62d9cd376c..705dff0f1b 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -1,9 +1,8 @@ import esphome.codegen as cg +from esphome.components import binary_sensor, esp32_ble_server, output import esphome.config_validation as cv -from esphome.components import binary_sensor, output, esp32_ble_server from esphome.const import CONF_ID - AUTO_LOAD = ["esp32_ble_server"] CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["wifi", "esp32"] @@ -50,7 +49,7 @@ async def to_code(config): cg.add(ble_server.register_service_component(var)) cg.add_define("USE_IMPROV") - cg.add_library("esphome/Improv", "1.2.3") + cg.add_library("improv/Improv", "1.2.4") cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) From f737ca6e286f36e243b5bae1bcb7515b9a4bf857 Mon Sep 17 00:00:00 2001 From: Daniel Kraft Date: Mon, 5 Aug 2024 23:17:02 +0200 Subject: [PATCH 034/160] hydreon_rgxx: Fix parsing of data line (#7192) --- esphome/components/hydreon_rgxx/hydreon_rgxx.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 95702fe9e8..92d7774193 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -236,7 +236,7 @@ void HydreonRGxxComponent::process_line_() { } bool is_data_line = false; for (int i = 0; i < NUM_SENSORS; i++) { - if (this->sensors_[i] != nullptr && this->buffer_starts_with_(PROTOCOL_NAMES[i])) { + if (this->sensors_[i] != nullptr && this->buffer_.find(PROTOCOL_NAMES[i]) != std::string::npos) { is_data_line = true; break; } From acaec41bb765949f37c04cf34b77e0f73df26272 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 6 Aug 2024 01:40:34 +0200 Subject: [PATCH 035/160] Remove outdated version block (#7177) --- esphome/__main__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 7237a04717..5c197ff486 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -972,13 +972,6 @@ def run_esphome(argv): args.command == "dashboard", ) - if sys.version_info < (3, 8, 0): - _LOGGER.error( - "You're running ESPHome with Python <3.8. ESPHome is no longer compatible " - "with this Python version. Please reinstall ESPHome with Python 3.8+" - ) - return 1 - if args.command in PRE_CONFIG_ACTIONS: try: return PRE_CONFIG_ACTIONS[args.command](args) From 6b141102d62930778552db1f2bdf23bdc20b1d86 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:17:29 +1000 Subject: [PATCH 036/160] [lvgl] Stage 5 (#7191) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lvgl/__init__.py | 25 +- esphome/components/lvgl/animimg.py | 2 +- esphome/components/lvgl/automation.py | 2 +- esphome/components/lvgl/{btn.py => button.py} | 8 +- esphome/components/lvgl/buttonmatrix.py | 277 ++++++++++++++++ esphome/components/lvgl/checkbox.py | 2 +- esphome/components/lvgl/defines.py | 2 +- esphome/components/lvgl/dropdown.py | 76 +++++ esphome/components/lvgl/img.py | 8 +- esphome/components/lvgl/keyboard.py | 49 +++ esphome/components/lvgl/led.py | 4 +- esphome/components/lvgl/lvgl_esphome.cpp | 4 +- esphome/components/lvgl/lvgl_esphome.h | 2 +- esphome/components/lvgl/meter.py | 302 ++++++++++++++++++ esphome/components/lvgl/msgbox.py | 127 ++++++++ esphome/components/lvgl/roller.py | 77 +++++ esphome/components/lvgl/spinbox.py | 178 +++++++++++ esphome/components/lvgl/styles.py | 2 +- esphome/components/lvgl/tabview.py | 114 +++++++ esphome/components/lvgl/textarea.py | 67 ++++ esphome/components/lvgl/tileview.py | 128 ++++++++ esphome/components/lvgl/widget.py | 8 +- esphome/core/defines.h | 3 + tests/components/lvgl/.gitattributes | 2 + tests/components/lvgl/common.yaml | 46 +++ tests/components/lvgl/helvetica.ttf | Bin 0 -> 83644 bytes tests/components/lvgl/lvgl-package.yaml | 228 ++++++++++++- .../lvgl/materialdesignicons-webfont.ttf | Bin 0 -> 1307419 bytes tests/components/lvgl/roboto.ttf | Bin 0 -> 171676 bytes 29 files changed, 1716 insertions(+), 27 deletions(-) rename esphome/components/lvgl/{btn.py => button.py} (58%) create mode 100644 esphome/components/lvgl/buttonmatrix.py create mode 100644 esphome/components/lvgl/dropdown.py create mode 100644 esphome/components/lvgl/keyboard.py create mode 100644 esphome/components/lvgl/meter.py create mode 100644 esphome/components/lvgl/msgbox.py create mode 100644 esphome/components/lvgl/roller.py create mode 100644 esphome/components/lvgl/spinbox.py create mode 100644 esphome/components/lvgl/tabview.py create mode 100644 esphome/components/lvgl/textarea.py create mode 100644 esphome/components/lvgl/tileview.py create mode 100644 tests/components/lvgl/.gitattributes create mode 100644 tests/components/lvgl/helvetica.ttf create mode 100644 tests/components/lvgl/materialdesignicons-webfont.ttf create mode 100644 tests/components/lvgl/roboto.ttf diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index c154689199..a963fca98b 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -24,10 +24,13 @@ from . import defines as df, helpers, lv_validation as lvalid from .animimg import animimg_spec from .arc import arc_spec from .automation import disp_update, update_to_code -from .btn import btn_spec +from .button import button_spec +from .buttonmatrix import buttonmatrix_spec from .checkbox import checkbox_spec from .defines import CONF_SKIP +from .dropdown import dropdown_spec from .img import img_spec +from .keyboard import keyboard_spec from .label import label_spec from .led import led_spec from .line import line_spec @@ -35,8 +38,11 @@ from .lv_bar import bar_spec from .lv_switch import switch_spec from .lv_validation import lv_bool, lv_images_used from .lvcode import LvContext, LvglComponent +from .meter import meter_spec +from .msgbox import MSGBOX_SCHEMA, msgboxes_to_code from .obj import obj_spec from .page import add_pages, page_spec +from .roller import roller_spec from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code from .schemas import ( DISP_BG_SCHEMA, @@ -52,8 +58,12 @@ from .schemas import ( obj_schema, ) from .slider import slider_spec +from .spinbox import spinbox_spec from .spinner import spinner_spec from .styles import add_top_layer, styles_to_code, theme_to_code +from .tabview import tabview_spec +from .textarea import textarea_spec +from .tileview import tileview_spec from .touchscreens import touchscreen_schema, touchscreens_to_code from .trigger import generate_triggers from .types import ( @@ -75,7 +85,7 @@ LOGGER = logging.getLogger(__name__) for w_type in ( label_spec, obj_spec, - btn_spec, + button_spec, bar_spec, slider_spec, arc_spec, @@ -86,6 +96,15 @@ for w_type in ( checkbox_spec, img_spec, switch_spec, + tabview_spec, + buttonmatrix_spec, + meter_spec, + dropdown_spec, + roller_spec, + textarea_spec, + spinbox_spec, + keyboard_spec, + tileview_spec, ): WIDGET_TYPES[w_type.name] = w_type @@ -244,6 +263,7 @@ async def to_code(config): await add_widgets(lv_scr_act, config) await add_pages(lv_component, config) await add_top_layer(config) + await msgboxes_to_code(config) await disp_update(f"{lv_component}->get_disp()", config) Widget.set_completed() await generate_triggers(lv_component) @@ -308,6 +328,7 @@ CONFIG_SCHEMA = ( cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list( container_schema(page_spec) ), + cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA), cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, diff --git a/esphome/components/lvgl/animimg.py b/esphome/components/lvgl/animimg.py index 20b85b019c..ad84713d7f 100644 --- a/esphome/components/lvgl/animimg.py +++ b/esphome/components/lvgl/animimg.py @@ -2,8 +2,8 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_DURATION, CONF_ID +from esphome.cpp_generator import MockObj -from ...cpp_generator import MockObj from .automation import action_to_code from .defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC from .helpers import lvgl_components_required diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index ffa25783ad..7a862fb58b 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -109,7 +109,7 @@ async def disp_update(disp, config: dict): if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config: return with LocalVariable("lv_disp_tmp", lv_disp_t, literal(disp)) as disp_temp: - if bg_color := config.get(CONF_DISP_BG_COLOR): + if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None: lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color)) if bg_image := config.get(CONF_DISP_BG_IMAGE): lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) diff --git a/esphome/components/lvgl/btn.py b/esphome/components/lvgl/button.py similarity index 58% rename from esphome/components/lvgl/btn.py rename to esphome/components/lvgl/button.py index 2a2a53e1e2..96329b3fa9 100644 --- a/esphome/components/lvgl/btn.py +++ b/esphome/components/lvgl/button.py @@ -3,12 +3,12 @@ from esphome.const import CONF_BUTTON from .defines import CONF_MAIN from .types import LvBoolean, WidgetType -lv_btn_t = LvBoolean("lv_btn_t") +lv_button_t = LvBoolean("lv_btn_t") -class BtnType(WidgetType): +class ButtonType(WidgetType): def __init__(self): - super().__init__(CONF_BUTTON, lv_btn_t, (CONF_MAIN,), lv_name="btn") + super().__init__(CONF_BUTTON, lv_button_t, (CONF_MAIN,), lv_name="btn") def get_uses(self): return ("btn",) @@ -17,4 +17,4 @@ class BtnType(WidgetType): return [] -btn_spec = BtnType() +button_spec = ButtonType() diff --git a/esphome/components/lvgl/buttonmatrix.py b/esphome/components/lvgl/buttonmatrix.py new file mode 100644 index 0000000000..75ed43f909 --- /dev/null +++ b/esphome/components/lvgl/buttonmatrix.py @@ -0,0 +1,277 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components.key_provider import KeyProvider +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_WIDTH +from esphome.cpp_generator import MockObj + +from .automation import action_to_code +from .button import lv_button_t +from .defines import ( + BUTTONMATRIX_CTRLS, + CONF_BUTTONS, + CONF_CONTROL, + CONF_ITEMS, + CONF_KEY_CODE, + CONF_MAIN, + CONF_ONE_CHECKED, + CONF_ROWS, + CONF_SELECTED, + CONF_TEXT, +) +from .helpers import lvgl_components_required +from .lv_validation import key_code, lv_bool +from .lvcode import lv, lv_add, lv_expr +from .schemas import automation_schema +from .types import ( + LV_BTNMATRIX_CTRL, + LV_STATE, + LvBoolean, + LvCompound, + LvType, + ObjUpdateAction, + char_ptr, + lv_pseudo_button_t, +) +from .widget import Widget, WidgetType, get_widgets, widget_map + +CONF_BUTTONMATRIX = "buttonmatrix" +CONF_BUTTON_TEXT_LIST_ID = "button_text_list_id" + +LvButtonMatrixButton = LvBoolean( + str(cg.uint16), + parents=(lv_pseudo_button_t,), +) +BUTTONMATRIX_BUTTON_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TEXT): cv.string, + cv.Optional(CONF_KEY_CODE): key_code, + cv.GenerateID(): cv.declare_id(LvButtonMatrixButton), + cv.Optional(CONF_WIDTH, default=1): cv.positive_int, + cv.Optional(CONF_CONTROL): cv.ensure_list( + cv.Schema( + {cv.Optional(k.lower()): cv.boolean for k in BUTTONMATRIX_CTRLS.choices} + ) + ), + } +).extend(automation_schema(lv_button_t)) + +BUTTONMATRIX_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ONE_CHECKED, default=False): lv_bool, + cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), + cv.Required(CONF_ROWS): cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_BUTTONS): cv.ensure_list( + BUTTONMATRIX_BUTTON_SCHEMA + ), + } + ) + ), + } +) + + +class ButtonmatrixButtonType(WidgetType): + """ + A pseudo-widget for the matrix buttons + """ + + def __init__(self): + super().__init__("btnmatrix_btn", LvButtonMatrixButton, (), {}, {}) + + async def to_code(self, w, config: dict): + return [] + + +btn_btn_spec = ButtonmatrixButtonType() + + +class MatrixButton(Widget): + """ + Describes a button within a button matrix. + """ + + @staticmethod + def create_button(id, parent, config: dict, index): + w = MatrixButton(id, parent, config, index) + widget_map[id] = w + return w + + def __init__(self, id, parent: Widget, config, index): + super().__init__(id, btn_btn_spec, config) + self.parent = parent + self.index = index + self.obj = parent.obj + + def is_selected(self): + return self.parent.var.get_selected() == MockObj(self.var) + + @staticmethod + def map_ctrls(state): + state = str(state).upper().removeprefix("LV_STATE_") + assert state in BUTTONMATRIX_CTRLS.choices + return getattr(LV_BTNMATRIX_CTRL, state) + + def has_state(self, state): + state = self.map_ctrls(state) + return lv_expr.btnmatrix_has_btn_ctrl(self.obj, self.index, state) + + def add_state(self, state): + state = self.map_ctrls(state) + return lv.btnmatrix_set_btn_ctrl(self.obj, self.index, state) + + def clear_state(self, state): + state = self.map_ctrls(state) + return lv.btnmatrix_clear_btn_ctrl(self.obj, self.index, state) + + def is_pressed(self): + return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED) + + def is_checked(self): + return self.has_state(LV_STATE.CHECKED) + + def get_value(self): + return self.is_checked() + + def check_null(self): + return None + + +async def get_button_data(config, buttonmatrix: Widget): + """ + Process a button matrix button list + :param config: The row list + :param buttonmatrix: The parent variable + :return: text array id, control list, width list + """ + text_list = [] + ctrl_list = [] + width_list = [] + key_list = [] + for row in config: + for button_conf in row.get(CONF_BUTTONS) or (): + bid = button_conf[CONF_ID] + index = len(width_list) + MatrixButton.create_button(bid, buttonmatrix, button_conf, index) + cg.new_variable(bid, index) + text_list.append(button_conf.get(CONF_TEXT) or "") + key_list.append(button_conf.get(CONF_KEY_CODE) or 0) + width_list.append(button_conf[CONF_WIDTH]) + ctrl = ["LV_BTNMATRIX_CTRL_CLICK_TRIG"] + for item in button_conf.get(CONF_CONTROL, ()): + ctrl.extend([k for k, v in item.items() if v]) + ctrl_list.append(await BUTTONMATRIX_CTRLS.process(ctrl)) + text_list.append("\n") + text_list = text_list[:-1] + text_list.append(cg.nullptr) + return text_list, ctrl_list, width_list, key_list + + +lv_buttonmatrix_t = LvType( + "LvButtonMatrixType", + parents=(KeyProvider, LvCompound), + largs=[(cg.uint16, "x")], + lvalue=lambda w: w.var.get_selected(), +) + + +class ButtonMatrixType(WidgetType): + def __init__(self): + super().__init__( + CONF_BUTTONMATRIX, + lv_buttonmatrix_t, + (CONF_MAIN, CONF_ITEMS), + BUTTONMATRIX_SCHEMA, + {}, + lv_name="btnmatrix", + ) + + async def to_code(self, w: Widget, config): + lvgl_components_required.add("BUTTONMATRIX") + if CONF_ROWS not in config: + return [] + text_list, ctrl_list, width_list, key_list = await get_button_data( + config[CONF_ROWS], w + ) + text_id = config[CONF_BUTTON_TEXT_LIST_ID] + text_id = cg.static_const_array(text_id, text_list) + lv.btnmatrix_set_map(w.obj, text_id) + set_btn_data(w.obj, ctrl_list, width_list) + lv.btnmatrix_set_one_checked(w.obj, config[CONF_ONE_CHECKED]) + for index, key in enumerate(key_list): + if key != 0: + lv_add(w.var.set_key(index, key)) + + def get_uses(self): + return ("btnmatrix",) + + +def set_btn_data(obj, ctrl_list, width_list): + for index, ctrl in enumerate(ctrl_list): + lv.btnmatrix_set_btn_ctrl(obj, index, ctrl) + for index, width in enumerate(width_list): + lv.btnmatrix_set_btn_width(obj, index, width) + + +buttonmatrix_spec = ButtonMatrixType() + + +@automation.register_action( + "lvgl.matrix.button.update", + ObjUpdateAction, + cv.Schema( + { + cv.Optional(CONF_WIDTH): cv.positive_int, + cv.Optional(CONF_CONTROL): cv.ensure_list( + cv.Schema( + { + cv.Optional(k.lower()): cv.boolean + for k in BUTTONMATRIX_CTRLS.choices + } + ), + ), + cv.Required(CONF_ID): cv.ensure_list( + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(LvButtonMatrixButton), + }, + key=CONF_ID, + ) + ), + cv.Optional(CONF_SELECTED): lv_bool, + } + ), +) +async def button_update_to_code(config, action_id, template_arg, args): + widgets = await get_widgets(config[CONF_ID]) + assert all(isinstance(w, MatrixButton) for w in widgets) + + async def do_button_update(w: MatrixButton): + if (width := config.get(CONF_WIDTH)) is not None: + lv.btnmatrix_set_btn_width(w.obj, w.index, width) + if config.get(CONF_SELECTED): + lv.btnmatrix_set_selected_btn(w.obj, w.index) + if controls := config.get(CONF_CONTROL): + adds = [] + clrs = [] + for item in controls: + adds.extend( + [f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if v] + ) + clrs.extend( + [f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if not v] + ) + if adds: + lv.btnmatrix_set_btn_ctrl( + w.obj, w.index, await BUTTONMATRIX_CTRLS.process(adds) + ) + if clrs: + lv.btnmatrix_clear_btn_ctrl( + w.obj, w.index, await BUTTONMATRIX_CTRLS.process(clrs) + ) + + return await action_to_code( + widgets, do_button_update, action_id, template_arg, args + ) diff --git a/esphome/components/lvgl/checkbox.py b/esphome/components/lvgl/checkbox.py index 7418d633cf..be7b029269 100644 --- a/esphome/components/lvgl/checkbox.py +++ b/esphome/components/lvgl/checkbox.py @@ -18,7 +18,7 @@ class CheckboxType(WidgetType): ) async def to_code(self, w: Widget, config): - if value := config.get(CONF_TEXT): + if (value := config.get(CONF_TEXT)) is not None: lv.checkbox_set_text(w.obj, await lv_text.process(value)) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 16ec45ae8a..ac28f9ed5f 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -304,7 +304,7 @@ OBJ_FLAGS = ( ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") -BTNMATRIX_CTRLS = LvConstant( +BUTTONMATRIX_CTRLS = LvConstant( "LV_BTNMATRIX_CTRL_", "HIDDEN", "NO_REPEAT", diff --git a/esphome/components/lvgl/dropdown.py b/esphome/components/lvgl/dropdown.py new file mode 100644 index 0000000000..d7bdebaade --- /dev/null +++ b/esphome/components/lvgl/dropdown.py @@ -0,0 +1,76 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_OPTIONS + +from .defines import ( + CONF_DIR, + CONF_INDICATOR, + CONF_MAIN, + CONF_SELECTED_INDEX, + CONF_SYMBOL, + DIRECTIONS, + literal, +) +from .label import CONF_LABEL +from .lv_validation import lv_int, lv_text, option_string +from .lvcode import LocalVariable, lv, lv_expr +from .schemas import part_schema +from .types import LvSelect, LvType, lv_obj_t +from .widget import Widget, WidgetType, set_obj_properties + +CONF_DROPDOWN = "dropdown" +CONF_DROPDOWN_LIST = "dropdown_list" + +lv_dropdown_t = LvSelect("lv_dropdown_t") +lv_dropdown_list_t = LvType("lv_dropdown_list_t") +dropdown_list_spec = WidgetType(CONF_DROPDOWN_LIST, lv_dropdown_list_t, (CONF_MAIN,)) + +DROPDOWN_BASE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_SYMBOL): lv_text, + cv.Optional(CONF_SELECTED_INDEX): cv.templatable(cv.int_), + cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of, + cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec), + } +) + +DROPDOWN_SCHEMA = DROPDOWN_BASE_SCHEMA.extend( + { + cv.Required(CONF_OPTIONS): cv.ensure_list(option_string), + } +) + + +class DropdownType(WidgetType): + def __init__(self): + super().__init__( + CONF_DROPDOWN, + lv_dropdown_t, + (CONF_MAIN, CONF_INDICATOR), + DROPDOWN_SCHEMA, + DROPDOWN_BASE_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + if options := config.get(CONF_OPTIONS): + text = cg.safe_exp("\n".join(options)) + lv.dropdown_set_options(w.obj, text) + if symbol := config.get(CONF_SYMBOL): + lv.dropdown_set_symbol(w.obj, await lv_text.process(symbol)) + if (selected := config.get(CONF_SELECTED_INDEX)) is not None: + value = await lv_int.process(selected) + lv.dropdown_set_selected(w.obj, value) + if dirn := config.get(CONF_DIR): + lv.dropdown_set_dir(w.obj, literal(dirn)) + if dlist := config.get(CONF_DROPDOWN_LIST): + with LocalVariable( + "dropdown_list", lv_obj_t, lv_expr.dropdown_get_list(w.obj) + ) as dlist_obj: + dwid = Widget(dlist_obj, dropdown_list_spec, dlist) + await set_obj_properties(dwid, dlist) + + def get_uses(self): + return (CONF_LABEL,) + + +dropdown_spec = DropdownType() diff --git a/esphome/components/lvgl/img.py b/esphome/components/lvgl/img.py index e9682def8c..dd962fcf31 100644 --- a/esphome/components/lvgl/img.py +++ b/esphome/components/lvgl/img.py @@ -65,16 +65,16 @@ class ImgType(WidgetType): async def to_code(self, w: Widget, config): if src := config.get(CONF_SRC): lv.img_set_src(w.obj, await lv_image.process(src)) - if cf_angle := config.get(CONF_ANGLE): + if (cf_angle := config.get(CONF_ANGLE)) is not None: pivot_x = config[CONF_PIVOT_X] pivot_y = config[CONF_PIVOT_Y] lv.img_set_pivot(w.obj, pivot_x, pivot_y) lv.img_set_angle(w.obj, cf_angle) - if img_zoom := config.get(CONF_ZOOM): + if (img_zoom := config.get(CONF_ZOOM)) is not None: lv.img_set_zoom(w.obj, img_zoom) - if offset := config.get(CONF_OFFSET_X): + if (offset := config.get(CONF_OFFSET_X)) is not None: lv.img_set_offset_x(w.obj, offset) - if offset := config.get(CONF_OFFSET_Y): + if (offset := config.get(CONF_OFFSET_Y)) is not None: lv.img_set_offset_y(w.obj, offset) if CONF_ANTIALIAS in config: lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS]) diff --git a/esphome/components/lvgl/keyboard.py b/esphome/components/lvgl/keyboard.py new file mode 100644 index 0000000000..7ce73d2170 --- /dev/null +++ b/esphome/components/lvgl/keyboard.py @@ -0,0 +1,49 @@ +from esphome.components.key_provider import KeyProvider +import esphome.config_validation as cv +from esphome.const import CONF_MODE +from esphome.cpp_types import std_string + +from .defines import CONF_ITEMS, CONF_MAIN, KEYBOARD_MODES, literal +from .helpers import add_lv_use, lvgl_components_required +from .textarea import CONF_TEXTAREA, lv_textarea_t +from .types import LvCompound, LvType +from .widget import Widget, WidgetType, get_widgets + +CONF_KEYBOARD = "keyboard" + +KEYBOARD_SCHEMA = { + cv.Optional(CONF_MODE, default="TEXT_UPPER"): KEYBOARD_MODES.one_of, + cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t), +} + +lv_keyboard_t = LvType( + "LvKeyboardType", + parents=(KeyProvider, LvCompound), + largs=[(std_string, "text")], + has_on_value=True, + lvalue=lambda w: literal(f"lv_textarea_get_text({w.obj})"), +) + + +class KeyboardType(WidgetType): + def __init__(self): + super().__init__( + CONF_KEYBOARD, + lv_keyboard_t, + (CONF_MAIN, CONF_ITEMS), + KEYBOARD_SCHEMA, + ) + + def get_uses(self): + return CONF_KEYBOARD, CONF_TEXTAREA + + async def to_code(self, w: Widget, config: dict): + lvgl_components_required.add("KEY_LISTENER") + lvgl_components_required.add(CONF_KEYBOARD) + add_lv_use("btnmatrix") + await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(config[CONF_MODE])) + if ta := await get_widgets(config, CONF_TEXTAREA): + await w.set_property(CONF_TEXTAREA, ta[0].obj) + + +keyboard_spec = KeyboardType() diff --git a/esphome/components/lvgl/led.py b/esphome/components/lvgl/led.py index f920758efb..9b6e819278 100644 --- a/esphome/components/lvgl/led.py +++ b/esphome/components/lvgl/led.py @@ -20,9 +20,9 @@ class LedType(WidgetType): super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA) async def to_code(self, w: Widget, config): - if color := config.get(CONF_COLOR): + if (color := config.get(CONF_COLOR)) is not None: lv.led_set_color(w.obj, await lv_color.process(color)) - if brightness := config.get(CONF_BRIGHTNESS): + if (brightness := config.get(CONF_BRIGHTNESS)) is not None: lv.led_set_brightness(w.obj, await lv_brightness.process(brightness)) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 1221682d28..544643d532 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -146,12 +146,12 @@ LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_ #endif // USE_LVGL_ROTARY_ENCODER #ifdef USE_LVGL_BUTTONMATRIX -void LvBtnmatrixType::set_obj(lv_obj_t *lv_obj) { +void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) { LvCompound::set_obj(lv_obj); lv_obj_add_event_cb( lv_obj, [](lv_event_t *event) { - auto *self = static_cast(event->user_data); + auto *self = static_cast(event->user_data); if (self->key_callback_.size() == 0) return; auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index b92799addd..71e0fd069f 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -246,7 +246,7 @@ class LVEncoderListener : public Parented { }; #endif // USE_LVGL_ROTARY_ENCODER #ifdef USE_LVGL_BUTTONMATRIX -class LvBtnmatrixType : public key_provider::KeyProvider, public LvCompound { +class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound { public: void set_obj(lv_obj_t *lv_obj) override; uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); } diff --git a/esphome/components/lvgl/meter.py b/esphome/components/lvgl/meter.py new file mode 100644 index 0000000000..1a6bef7c57 --- /dev/null +++ b/esphome/components/lvgl/meter.py @@ -0,0 +1,302 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_COLOR, + CONF_COUNT, + CONF_ID, + CONF_LENGTH, + CONF_LOCAL, + CONF_RANGE_FROM, + CONF_RANGE_TO, + CONF_ROTATION, + CONF_VALUE, + CONF_WIDTH, +) + +from .arc import CONF_ARC +from .automation import action_to_code +from .defines import ( + CONF_END_VALUE, + CONF_MAIN, + CONF_PIVOT_X, + CONF_PIVOT_Y, + CONF_SRC, + CONF_START_VALUE, + CONF_TICKS, +) +from .helpers import add_lv_use +from .img import CONF_IMAGE +from .line import CONF_LINE +from .lv_validation import ( + angle, + get_end_value, + get_start_value, + lv_bool, + lv_color, + lv_float, + lv_image, + requires_component, + size, +) +from .lvcode import LocalVariable, lv, lv_assign, lv_expr +from .obj import obj_spec +from .types import LvType, ObjUpdateAction +from .widget import Widget, WidgetType, get_widgets + +CONF_ANGLE_RANGE = "angle_range" +CONF_COLOR_END = "color_end" +CONF_COLOR_START = "color_start" +CONF_INDICATORS = "indicators" +CONF_LABEL_GAP = "label_gap" +CONF_MAJOR = "major" +CONF_METER = "meter" +CONF_R_MOD = "r_mod" +CONF_SCALES = "scales" +CONF_STRIDE = "stride" +CONF_TICK_STYLE = "tick_style" + +lv_meter_t = LvType("lv_meter_t") +lv_meter_indicator_t = cg.global_ns.struct("lv_meter_indicator_t") +lv_meter_indicator_t_ptr = lv_meter_indicator_t.operator("ptr") + + +def pixels(value): + """A size in one axis in pixels""" + if isinstance(value, str) and value.lower().endswith("px"): + return cv.int_(value[:-2]) + return cv.int_(value) + + +INDICATOR_LINE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_COLOR, default=0): lv_color, + cv.Optional(CONF_R_MOD, default=0): size, + cv.Optional(CONF_VALUE): lv_float, + } +) +INDICATOR_IMG_SCHEMA = cv.Schema( + { + cv.Required(CONF_SRC): lv_image, + cv.Required(CONF_PIVOT_X): pixels, + cv.Required(CONF_PIVOT_Y): pixels, + cv.Optional(CONF_VALUE): lv_float, + } +) +INDICATOR_ARC_SCHEMA = cv.Schema( + { + cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_COLOR, default=0): lv_color, + cv.Optional(CONF_R_MOD, default=0): size, + cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, + cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_END_VALUE): lv_float, + } +) +INDICATOR_TICKS_SCHEMA = cv.Schema( + { + cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_COLOR_START, default=0): lv_color, + cv.Optional(CONF_COLOR_END): lv_color, + cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, + cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_END_VALUE): lv_float, + cv.Optional(CONF_LOCAL, default=False): lv_bool, + } +) +INDICATOR_SCHEMA = cv.Schema( + { + cv.Exclusive(CONF_LINE, CONF_INDICATORS): INDICATOR_LINE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + } + ), + cv.Exclusive(CONF_IMAGE, CONF_INDICATORS): cv.All( + INDICATOR_IMG_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + } + ), + requires_component("image"), + ), + cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + } + ), + cv.Exclusive(CONF_TICK_STYLE, CONF_INDICATORS): INDICATOR_TICKS_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + } + ), + } +) + +SCALE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TICKS): cv.Schema( + { + cv.Optional(CONF_COUNT, default=12): cv.positive_int, + cv.Optional(CONF_WIDTH, default=2): size, + cv.Optional(CONF_LENGTH, default=10): size, + cv.Optional(CONF_COLOR, default=0x808080): lv_color, + cv.Optional(CONF_MAJOR): cv.Schema( + { + cv.Optional(CONF_STRIDE, default=3): cv.positive_int, + cv.Optional(CONF_WIDTH, default=5): size, + cv.Optional(CONF_LENGTH, default="15%"): size, + cv.Optional(CONF_COLOR, default=0): lv_color, + cv.Optional(CONF_LABEL_GAP, default=4): size, + } + ), + } + ), + cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, + cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, + cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), + cv.Optional(CONF_ROTATION): angle, + cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), + } +) + +METER_SCHEMA = {cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA)} + + +class MeterType(WidgetType): + def __init__(self): + super().__init__(CONF_METER, lv_meter_t, (CONF_MAIN,), METER_SCHEMA) + + async def to_code(self, w: Widget, config): + """For a meter object, create and set parameters""" + + var = w.obj + for scale_conf in config.get(CONF_SCALES) or (): + rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 + if CONF_ROTATION in scale_conf: + rotation = scale_conf[CONF_ROTATION] // 10 + with LocalVariable( + "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) + ) as meter_var: + lv.meter_set_scale_range( + var, + meter_var, + scale_conf[CONF_RANGE_FROM], + scale_conf[CONF_RANGE_TO], + scale_conf[CONF_ANGLE_RANGE], + rotation, + ) + if ticks := scale_conf.get(CONF_TICKS): + color = await lv_color.process(ticks[CONF_COLOR]) + lv.meter_set_scale_ticks( + var, + meter_var, + ticks[CONF_COUNT], + ticks[CONF_WIDTH], + ticks[CONF_LENGTH], + color, + ) + if CONF_MAJOR in ticks: + major = ticks[CONF_MAJOR] + color = await lv_color.process(major[CONF_COLOR]) + lv.meter_set_scale_major_ticks( + var, + meter_var, + major[CONF_STRIDE], + major[CONF_WIDTH], + major[CONF_LENGTH], + color, + major[CONF_LABEL_GAP], + ) + for indicator in scale_conf.get(CONF_INDICATORS) or (): + (t, v) = next(iter(indicator.items())) + iid = v[CONF_ID] + ivar = cg.new_variable( + iid, cg.nullptr, type_=lv_meter_indicator_t_ptr + ) + # Enable getting the meter to which this belongs. + wid = Widget.create(iid, var, obj_spec, v) + wid.obj = ivar + if t == CONF_LINE: + color = await lv_color.process(v[CONF_COLOR]) + lv_assign( + ivar, + lv_expr.meter_add_needle_line( + var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] + ), + ) + if t == CONF_ARC: + color = await lv_color.process(v[CONF_COLOR]) + lv_assign( + ivar, + lv_expr.meter_add_arc( + var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] + ), + ) + if t == CONF_TICK_STYLE: + color_start = await lv_color.process(v[CONF_COLOR_START]) + color_end = await lv_color.process( + v.get(CONF_COLOR_END) or color_start + ) + lv_assign( + ivar, + lv_expr.meter_add_scale_lines( + var, + meter_var, + color_start, + color_end, + v[CONF_LOCAL], + v[CONF_WIDTH], + ), + ) + if t == CONF_IMAGE: + add_lv_use("img") + lv_assign( + ivar, + lv_expr.meter_add_needle_img( + var, + meter_var, + await lv_image.process(v[CONF_SRC]), + v[CONF_PIVOT_X], + v[CONF_PIVOT_Y], + ), + ) + start_value = await get_start_value(v) + end_value = await get_end_value(v) + set_indicator_values(var, ivar, start_value, end_value) + + +meter_spec = MeterType() + + +@automation.register_action( + "lvgl.indicator.update", + ObjUpdateAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(lv_meter_indicator_t), + cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, + cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_END_VALUE): lv_float, + } + ), +) +async def indicator_update_to_code(config, action_id, template_arg, args): + widget = await get_widgets(config) + start_value = await get_start_value(config) + end_value = await get_end_value(config) + + async def set_value(w: Widget): + set_indicator_values(w.var, w.obj, start_value, end_value) + + return await action_to_code(widget, set_value, action_id, template_arg, args) + + +def set_indicator_values(meter, indicator, start_value, end_value): + if start_value is not None: + if end_value is None: + lv.meter_set_indicator_value(meter, indicator, start_value) + else: + lv.meter_set_indicator_start_value(meter, indicator, start_value) + if end_value is not None: + lv.meter_set_indicator_end_value(meter, indicator, end_value) diff --git a/esphome/components/lvgl/msgbox.py b/esphome/components/lvgl/msgbox.py new file mode 100644 index 0000000000..6dd529d77f --- /dev/null +++ b/esphome/components/lvgl/msgbox.py @@ -0,0 +1,127 @@ +from esphome import config_validation as cv +from esphome.const import CONF_BUTTON, CONF_ID +from esphome.core import ID +from esphome.cpp_generator import new_Pvariable, static_const_array +from esphome.cpp_types import nullptr + +from .button import button_spec +from .buttonmatrix import ( + BUTTONMATRIX_BUTTON_SCHEMA, + CONF_BUTTON_TEXT_LIST_ID, + buttonmatrix_spec, + get_button_data, + lv_buttonmatrix_t, + set_btn_data, +) +from .defines import ( + CONF_BODY, + CONF_BUTTONS, + CONF_CLOSE_BUTTON, + CONF_MSGBOXES, + CONF_TEXT, + CONF_TITLE, + TYPE_FLEX, + literal, +) +from .helpers import add_lv_use +from .label import CONF_LABEL +from .lv_validation import lv_bool, lv_pct, lv_text +from .lvcode import ( + EVENT_ARG, + LambdaContext, + LocalVariable, + lv_add, + lv_assign, + lv_expr, + lv_obj, + lv_Pvariable, +) +from .obj import obj_spec +from .schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema +from .styles import TOP_LAYER +from .types import LV_EVENT, char_ptr, lv_obj_t +from .widget import Widget, set_obj_properties + +CONF_MSGBOX = "msgbox" +MSGBOX_SCHEMA = container_schema( + obj_spec, + STYLE_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(lv_obj_t), + cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA, + cv.Optional(CONF_BODY): STYLED_TEXT_SCHEMA, + cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA), + cv.Optional(CONF_CLOSE_BUTTON): lv_bool, + cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), + } + ), +) + + +async def msgbox_to_code(conf): + """ + Construct a message box. This consists of a full-screen translucent background enclosing a centered container + with an optional title, body, close button and a button matrix. And any other widgets the user cares to add + :param conf: The config data + :return: code to add to the init lambda + """ + add_lv_use( + TYPE_FLEX, + CONF_BUTTON, + CONF_LABEL, + CONF_MSGBOX, + *buttonmatrix_spec.get_uses(), + *button_spec.get_uses(), + ) + mbid = conf[CONF_ID] + outer = lv_Pvariable(lv_obj_t, mbid.id) + btnm = new_Pvariable( + ID(f"{mbid.id}_btnm_", is_declaration=True, type=lv_buttonmatrix_t) + ) + msgbox = lv_Pvariable(lv_obj_t, f"{mbid.id}_msgbox") + outer_w = Widget.create(mbid, outer, obj_spec, conf) + btnm_widg = Widget.create(str(btnm), btnm, buttonmatrix_spec, conf) + text_list, ctrl_list, width_list, _ = await get_button_data((conf,), btnm_widg) + text_id = conf[CONF_BUTTON_TEXT_LIST_ID] + text_list = static_const_array(text_id, text_list) + if (text := conf.get(CONF_BODY)) is not None: + text = await lv_text.process(text.get(CONF_TEXT)) + if (title := conf.get(CONF_TITLE)) is not None: + title = await lv_text.process(title.get(CONF_TEXT)) + close_button = conf[CONF_CLOSE_BUTTON] + lv_assign(outer, lv_expr.obj_create(TOP_LAYER)) + lv_obj.set_width(outer, lv_pct(100)) + lv_obj.set_height(outer, lv_pct(100)) + lv_obj.set_style_bg_opa(outer, 128, 0) + lv_obj.set_style_bg_color(outer, literal("lv_color_black()"), 0) + lv_obj.set_style_border_width(outer, 0, 0) + lv_obj.set_style_pad_all(outer, 0, 0) + lv_obj.set_style_radius(outer, 0, 0) + outer_w.add_flag("LV_OBJ_FLAG_HIDDEN") + lv_assign( + msgbox, lv_expr.msgbox_create(outer, title, text, text_list, close_button) + ) + lv_obj.set_style_align(msgbox, literal("LV_ALIGN_CENTER"), 0) + lv_add(btnm.set_obj(lv_expr.msgbox_get_btns(msgbox))) + await set_obj_properties(outer_w, conf) + if close_button: + async with LambdaContext(EVENT_ARG, where=mbid) as context: + outer_w.add_flag("LV_OBJ_FLAG_HIDDEN") + with LocalVariable( + "close_btn_", lv_obj_t, lv_expr.msgbox_get_close_btn(msgbox) + ) as close_btn: + lv_obj.remove_event_cb(close_btn, nullptr) + lv_obj.add_event_cb( + close_btn, + await context.get_lambda(), + LV_EVENT.CLICKED, + nullptr, + ) + + if len(ctrl_list) != 0 or len(width_list) != 0: + set_btn_data(btnm.obj, ctrl_list, width_list) + + +async def msgboxes_to_code(config): + for conf in config.get(CONF_MSGBOXES, ()): + await msgbox_to_code(conf) diff --git a/esphome/components/lvgl/roller.py b/esphome/components/lvgl/roller.py new file mode 100644 index 0000000000..7af3ef3c3d --- /dev/null +++ b/esphome/components/lvgl/roller.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_MODE, CONF_OPTIONS + +from .defines import ( + CONF_ANIMATED, + CONF_MAIN, + CONF_SELECTED, + CONF_SELECTED_INDEX, + CONF_VISIBLE_ROW_COUNT, + ROLLER_MODES, + literal, +) +from .label import CONF_LABEL +from .lv_validation import animated, lv_int, option_string +from .lvcode import lv +from .types import LvSelect +from .widget import WidgetType + +CONF_ROLLER = "roller" +lv_roller_t = LvSelect("lv_roller_t") + +ROLLER_BASE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_SELECTED_INDEX): cv.templatable(cv.int_), + cv.Optional(CONF_VISIBLE_ROW_COUNT): lv_int, + } +) + +ROLLER_SCHEMA = ROLLER_BASE_SCHEMA.extend( + { + cv.Required(CONF_OPTIONS): cv.ensure_list(option_string), + cv.Optional(CONF_MODE, default="NORMAL"): ROLLER_MODES.one_of, + } +) + +ROLLER_MODIFY_SCHEMA = ROLLER_BASE_SCHEMA.extend( + { + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + + +class RollerType(WidgetType): + def __init__(self): + super().__init__( + CONF_ROLLER, + lv_roller_t, + (CONF_MAIN, CONF_SELECTED), + ROLLER_SCHEMA, + ROLLER_MODIFY_SCHEMA, + ) + + async def to_code(self, w, config): + if options := config.get(CONF_OPTIONS): + mode = await ROLLER_MODES.process(config[CONF_MODE]) + text = cg.safe_exp("\n".join(options)) + lv.roller_set_options(w.obj, text, mode) + animopt = literal(config.get(CONF_ANIMATED) or "LV_ANIM_OFF") + if CONF_SELECTED_INDEX in config: + if selected := config[CONF_SELECTED_INDEX]: + value = await lv_int.process(selected) + lv.roller_set_selected(w.obj, value, animopt) + await w.set_property( + CONF_VISIBLE_ROW_COUNT, + await lv_int.process(config.get(CONF_VISIBLE_ROW_COUNT)), + ) + + @property + def animated(self): + return True + + def get_uses(self): + return (CONF_LABEL,) + + +roller_spec = RollerType() diff --git a/esphome/components/lvgl/spinbox.py b/esphome/components/lvgl/spinbox.py new file mode 100644 index 0000000000..62c58c54a3 --- /dev/null +++ b/esphome/components/lvgl/spinbox.py @@ -0,0 +1,178 @@ +from esphome import automation +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE + +from .automation import action_to_code, update_to_code +from .defines import ( + CONF_CURSOR, + CONF_DECIMAL_PLACES, + CONF_DIGITS, + CONF_MAIN, + CONF_ROLLOVER, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_TEXTAREA_PLACEHOLDER, +) +from .label import CONF_LABEL +from .lv_validation import lv_bool, lv_float +from .lvcode import lv +from .textarea import CONF_TEXTAREA +from .types import LvNumber, ObjUpdateAction +from .widget import Widget, WidgetType, get_widgets + +CONF_SPINBOX = "spinbox" + +lv_spinbox_t = LvNumber("lv_spinbox_t") + +SPIN_ACTIONS = ( + "INCREMENT", + "DECREMENT", + "STEP_NEXT", + "STEP_PREV", + "CLEAR", +) + + +def validate_spinbox(config): + max_val = 2**31 - 1 + min_val = -1 - max_val + range_from = int(config[CONF_RANGE_FROM]) + range_to = int(config[CONF_RANGE_TO]) + step = int(config[CONF_STEP]) + if ( + range_from > max_val + or range_from < min_val + or range_to > max_val + or range_to < min_val + ): + raise cv.Invalid("Range outside allowed limits") + if step <= 0 or step >= (range_to - range_from) / 2: + raise cv.Invalid("Invalid step value") + if config[CONF_DIGITS] <= config[CONF_DECIMAL_PLACES]: + raise cv.Invalid("Number of digits must exceed number of decimal places") + return config + + +SPINBOX_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_RANGE_FROM, default=0): cv.float_, + cv.Optional(CONF_RANGE_TO, default=100): cv.float_, + cv.Optional(CONF_DIGITS, default=4): cv.int_range(1, 10), + cv.Optional(CONF_STEP, default=1.0): cv.positive_float, + cv.Optional(CONF_DECIMAL_PLACES, default=0): cv.int_range(0, 6), + cv.Optional(CONF_ROLLOVER, default=False): lv_bool, + } +).add_extra(validate_spinbox) + + +SPINBOX_MODIFY_SCHEMA = { + cv.Required(CONF_VALUE): lv_float, +} + + +class SpinboxType(WidgetType): + def __init__(self): + super().__init__( + CONF_SPINBOX, + lv_spinbox_t, + ( + CONF_MAIN, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_CURSOR, + CONF_TEXTAREA_PLACEHOLDER, + ), + SPINBOX_SCHEMA, + SPINBOX_MODIFY_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + if CONF_DIGITS in config: + digits = config[CONF_DIGITS] + scale = 10 ** config[CONF_DECIMAL_PLACES] + range_from = int(config[CONF_RANGE_FROM]) * scale + range_to = int(config[CONF_RANGE_TO]) * scale + step = int(config[CONF_STEP]) * scale + w.scale = scale + w.step = step + w.range_to = range_to + w.range_from = range_from + lv.spinbox_set_range(w.obj, range_from, range_to) + await w.set_property(CONF_STEP, step) + await w.set_property(CONF_ROLLOVER, config) + lv.spinbox_set_digit_format( + w.obj, digits, digits - config[CONF_DECIMAL_PLACES] + ) + if (value := config.get(CONF_VALUE)) is not None: + lv.spinbox_set_value(w.obj, await lv_float.process(value)) + + def get_scale(self, config): + return 10 ** config[CONF_DECIMAL_PLACES] + + def get_uses(self): + return CONF_TEXTAREA, CONF_LABEL + + def get_max(self, config: dict): + return config[CONF_RANGE_TO] + + def get_min(self, config: dict): + return config[CONF_RANGE_FROM] + + def get_step(self, config: dict): + return config[CONF_STEP] + + +spinbox_spec = SpinboxType() + + +@automation.register_action( + "lvgl.spinbox.increment", + ObjUpdateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), + }, + key=CONF_ID, + ), +) +async def spinbox_increment(config, action_id, template_arg, args): + widgets = await get_widgets(config) + + async def do_increment(w: Widget): + lv.spinbox_increment(w.obj) + + return await action_to_code(widgets, do_increment, action_id, template_arg, args) + + +@automation.register_action( + "lvgl.spinbox.decrement", + ObjUpdateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), + }, + key=CONF_ID, + ), +) +async def spinbox_decrement(config, action_id, template_arg, args): + widgets = await get_widgets(config) + + async def do_increment(w: Widget): + lv.spinbox_decrement(w.obj) + + return await action_to_code(widgets, do_increment, action_id, template_arg, args) + + +@automation.register_action( + "lvgl.spinbox.update", + ObjUpdateAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), + cv.Required(CONF_VALUE): lv_float, + } + ), +) +async def spinbox_update_to_code(config, action_id, template_arg, args): + return await update_to_code(config, action_id, template_arg, args) diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 7a795bc99d..09f1c376d0 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -26,7 +26,7 @@ async def styles_to_code(config): svar = cg.new_Pvariable(style[CONF_ID]) lv.style_init(svar) for prop, validator in ALL_STYLES.items(): - if value := style.get(prop): + if (value := style.get(prop)) is not None: if isinstance(validator, LValidator): value = await validator.process(value) if isinstance(value, list): diff --git a/esphome/components/lvgl/tabview.py b/esphome/components/lvgl/tabview.py new file mode 100644 index 0000000000..7b6a864e21 --- /dev/null +++ b/esphome/components/lvgl/tabview.py @@ -0,0 +1,114 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INDEX, CONF_NAME, CONF_POSITION, CONF_SIZE +from esphome.cpp_generator import MockObjClass + +from . import buttonmatrix_spec +from .automation import action_to_code +from .defines import ( + CONF_ANIMATED, + CONF_MAIN, + CONF_TAB_ID, + CONF_TABS, + DIRECTIONS, + TYPE_FLEX, + literal, +) +from .lv_validation import animated, lv_int, size +from .lvcode import LocalVariable, lv, lv_assign, lv_expr +from .obj import obj_spec +from .schemas import container_schema, part_schema +from .types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr +from .widget import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties + +CONF_TABVIEW = "tabview" +CONF_TAB_STYLE = "tab_style" + +lv_tab_t = LvType("lv_obj_t") + +TABVIEW_SCHEMA = cv.Schema( + { + cv.Required(CONF_TABS): cv.ensure_list( + container_schema( + obj_spec, + { + cv.Required(CONF_NAME): cv.string, + cv.GenerateID(): cv.declare_id(lv_tab_t), + }, + ) + ), + cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec), + cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of, + cv.Optional(CONF_SIZE, default="10%"): size, + } +) + + +class TabviewType(WidgetType): + def __init__(self): + super().__init__( + CONF_TABVIEW, + LvType( + "lv_tabview_t", + largs=[(lv_obj_t_ptr, "tab")], + lvalue=lambda w: lv_expr.obj_get_child( + lv_expr.tabview_get_content(w.obj), + lv_expr.tabview_get_tab_act(w.obj), + ), + has_on_value=True, + ), + parts=(CONF_MAIN,), + schema=TABVIEW_SCHEMA, + modify_schema={}, + ) + + def get_uses(self): + return "btnmatrix", TYPE_FLEX + + async def to_code(self, w: Widget, config: dict): + for tab_conf in config[CONF_TABS]: + w_id = tab_conf[CONF_ID] + tab_obj = cg.Pvariable(w_id, cg.nullptr, type_=lv_tab_t) + tab_widget = Widget.create(w_id, tab_obj, obj_spec) + lv_assign(tab_obj, lv_expr.tabview_add_tab(w.obj, tab_conf[CONF_NAME])) + await set_obj_properties(tab_widget, tab_conf) + await add_widgets(tab_widget, tab_conf) + if button_style := config.get(CONF_TAB_STYLE): + with LocalVariable( + "tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj) + ) as btnmatrix_obj: + await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style) + + def obj_creator(self, parent: MockObjClass, config: dict): + return lv_expr.call( + "tabview_create", + parent, + literal(config[CONF_POSITION]), + literal(config[CONF_SIZE]), + ) + + +tabview_spec = TabviewType() + + +@automation.register_action( + "lvgl.tabview.select", + ObjUpdateAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(tabview_spec.w_type), + cv.Optional(CONF_ANIMATED, default=False): animated, + cv.Required(CONF_INDEX): lv_int, + }, + ).add_extra(cv.has_at_least_one_key(CONF_INDEX, CONF_TAB_ID)), +) +async def tabview_select(config, action_id, template_arg, args): + widget = await get_widgets(config) + index = config[CONF_INDEX] + + async def do_select(w: Widget): + lv.tabview_set_act(w.obj, index, literal(config[CONF_ANIMATED])) + lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) + + return await action_to_code(widget, do_select, action_id, template_arg, args) diff --git a/esphome/components/lvgl/textarea.py b/esphome/components/lvgl/textarea.py new file mode 100644 index 0000000000..d383e1f098 --- /dev/null +++ b/esphome/components/lvgl/textarea.py @@ -0,0 +1,67 @@ +import esphome.config_validation as cv +from esphome.const import CONF_MAX_LENGTH + +from .defines import ( + CONF_ACCEPTED_CHARS, + CONF_CURSOR, + CONF_MAIN, + CONF_ONE_LINE, + CONF_PASSWORD_MODE, + CONF_PLACEHOLDER_TEXT, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_TEXT, + CONF_TEXTAREA_PLACEHOLDER, +) +from .lv_validation import lv_bool, lv_int, lv_text +from .schemas import TEXT_SCHEMA +from .types import LvText +from .widget import Widget, WidgetType + +CONF_TEXTAREA = "textarea" + +lv_textarea_t = LvText("lv_textarea_t") + +TEXTAREA_SCHEMA = TEXT_SCHEMA.extend( + { + cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text, + cv.Optional(CONF_ACCEPTED_CHARS): lv_text, + cv.Optional(CONF_ONE_LINE): lv_bool, + cv.Optional(CONF_PASSWORD_MODE): lv_bool, + cv.Optional(CONF_MAX_LENGTH): lv_int, + } +) + + +class TextareaType(WidgetType): + def __init__(self): + super().__init__( + CONF_TEXTAREA, + lv_textarea_t, + ( + CONF_MAIN, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_CURSOR, + CONF_TEXTAREA_PLACEHOLDER, + ), + TEXTAREA_SCHEMA, + ) + + async def to_code(self, w: Widget, config: dict): + for prop in (CONF_TEXT, CONF_PLACEHOLDER_TEXT, CONF_ACCEPTED_CHARS): + if (value := config.get(prop)) is not None: + await w.set_property(prop, await lv_text.process(value)) + await w.set_property( + CONF_MAX_LENGTH, await lv_int.process(config.get(CONF_MAX_LENGTH)) + ) + await w.set_property( + CONF_PASSWORD_MODE, + await lv_bool.process(config.get(CONF_PASSWORD_MODE)), + ) + await w.set_property( + CONF_ONE_LINE, await lv_bool.process(config.get(CONF_ONE_LINE)) + ) + + +textarea_spec = TextareaType() diff --git a/esphome/components/lvgl/tileview.py b/esphome/components/lvgl/tileview.py new file mode 100644 index 0000000000..aa841fa23e --- /dev/null +++ b/esphome/components/lvgl/tileview.py @@ -0,0 +1,128 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_ON_VALUE, CONF_ROW, CONF_TRIGGER_ID + +from .automation import action_to_code +from .defines import ( + CONF_ANIMATED, + CONF_COLUMN, + CONF_DIR, + CONF_MAIN, + CONF_TILE_ID, + CONF_TILES, + TILE_DIRECTIONS, + literal, +) +from .lv_validation import animated, lv_int +from .lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable +from .obj import obj_spec +from .schemas import container_schema +from .types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr +from .widget import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties + +CONF_TILEVIEW = "tileview" + +lv_tile_t = LvType("lv_tileview_tile_t") + +lv_tileview_t = LvType( + "lv_tileview_t", + largs=[(lv_obj_t_ptr, "tile")], + lvalue=lambda w: w.get_property("tile_act"), +) + +tile_spec = WidgetType("lv_tileview_tile_t", lv_tile_t, (CONF_MAIN,), {}) + +TILEVIEW_SCHEMA = cv.Schema( + { + cv.Required(CONF_TILES): cv.ensure_list( + container_schema( + obj_spec, + { + cv.Required(CONF_ROW): lv_int, + cv.Required(CONF_COLUMN): lv_int, + cv.GenerateID(): cv.declare_id(lv_tile_t), + cv.Optional(CONF_DIR, default="ALL"): TILE_DIRECTIONS.several_of, + }, + ) + ), + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + automation.Trigger.template(lv_obj_t_ptr) + ) + } + ), + } +) + + +class TileviewType(WidgetType): + def __init__(self): + super().__init__( + CONF_TILEVIEW, + lv_tileview_t, + (CONF_MAIN,), + schema=TILEVIEW_SCHEMA, + modify_schema={}, + ) + + async def to_code(self, w: Widget, config: dict): + for tile_conf in config.get(CONF_TILES) or (): + w_id = tile_conf[CONF_ID] + tile_obj = lv_Pvariable(lv_obj_t, w_id) + tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf) + dirs = tile_conf[CONF_DIR] + if isinstance(dirs, list): + dirs = "|".join(dirs) + lv_assign( + tile_obj, + lv_expr.tileview_add_tile( + w.obj, tile_conf[CONF_COLUMN], tile_conf[CONF_ROW], literal(dirs) + ), + ) + await set_obj_properties(tile, tile_conf) + await add_widgets(tile, tile_conf) + + +tileview_spec = TileviewType() + + +def tile_select_validate(config): + row = CONF_ROW in config + column = CONF_COLUMN in config + tile = CONF_TILE_ID in config + if tile and (row or column) or not tile and not (row and column): + raise cv.Invalid("Specify either a tile id, or both a row and a column") + return config + + +@automation.register_action( + "lvgl.tileview.select", + ObjUpdateAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(lv_tileview_t), + cv.Optional(CONF_ANIMATED, default=False): animated, + cv.Optional(CONF_ROW): lv_int, + cv.Optional(CONF_COLUMN): lv_int, + cv.Optional(CONF_TILE_ID): cv.use_id(lv_tile_t), + }, + ).add_extra(tile_select_validate), +) +async def tileview_select(config, action_id, template_arg, args): + widgets = await get_widgets(config) + + async def do_select(w: Widget): + if tile := config.get(CONF_TILE_ID): + tile = await cg.get_variable(tile) + lv_obj.set_tile(w.obj, tile, literal(config[CONF_ANIMATED])) + else: + row = await lv_int.process(config[CONF_ROW]) + column = await lv_int.process(config[CONF_COLUMN]) + lv_obj.set_tile_id( + widgets[0].obj, column, row, literal(config[CONF_ANIMATED]) + ) + lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) + + return await action_to_code(widgets, do_select, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widget.py b/esphome/components/lvgl/widget.py index 5734aec7dc..fcaee29085 100644 --- a/esphome/components/lvgl/widget.py +++ b/esphome/components/lvgl/widget.py @@ -282,13 +282,13 @@ async def set_obj_properties(w: Widget, config): lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) if layout_type == TYPE_GRID: wid = config[CONF_ID] - rows = "{" + ",".join(layout[CONF_GRID_ROWS]) + ", LV_GRID_TEMPLATE_LAST}" + rows = [str(x) for x in layout[CONF_GRID_ROWS]] + rows = "{" + ",".join(rows) + ", LV_GRID_TEMPLATE_LAST}" row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t) row_array = cg.static_const_array(row_id, cg.RawExpression(rows)) w.set_style("grid_row_dsc_array", row_array, 0) - columns = ( - "{" + ",".join(layout[CONF_GRID_COLUMNS]) + ", LV_GRID_TEMPLATE_LAST}" - ) + columns = [str(x) for x in layout[CONF_GRID_COLUMNS]] + columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}" column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t) column_array = cg.static_const_array(column_id, cg.RawExpression(columns)) w.set_style("grid_column_dsc_array", column_array, 0) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 726db24592..b7bdbb1f9d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -39,9 +39,12 @@ #define USE_LOCK #define USE_LOGGER #define USE_LVGL +#define USE_LVGL_ANIMIMG #define USE_LVGL_BINARY_SENSOR +#define USE_LVGL_BUTTONMATRIX #define USE_LVGL_FONT #define USE_LVGL_IMAGE +#define USE_LVGL_KEYBOARD #define USE_LVGL_KEY_LISTENER #define USE_LVGL_TOUCHSCREEN #define USE_LVGL_ROTARY_ENCODER diff --git a/tests/components/lvgl/.gitattributes b/tests/components/lvgl/.gitattributes new file mode 100644 index 0000000000..75e7a44254 --- /dev/null +++ b/tests/components/lvgl/.gitattributes @@ -0,0 +1,2 @@ +*.ttf -text + diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 8b92f8caa7..6d0c1967b4 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -8,3 +8,49 @@ touchscreen: x_max: 240 y_max: 320 +font: + - file: "$component_dir/roboto.ttf" + id: roboto20 + size: 20 + extras: + - file: '$component_dir/materialdesignicons-webfont.ttf' + glyphs: [ + "\U000F004B", + "\U0000f0ed", + "\U000F006E", + "\U000F012C", + "\U000F179B", + "\U000F0748", + "\U000F1A1B", + "\U000F02DC", + "\U000F0A02", + "\U000F035F", + "\U000F0156", + "\U000F0C5F", + "\U000f0084", + "\U000f0091", + ] + - file: "$component_dir/helvetica.ttf" + id: helvetica20 + - file: "$component_dir/roboto.ttf" + id: roboto10 + size: 10 + bpp: 4 + extras: + - file: '$component_dir/materialdesignicons-webfont.ttf' + glyphs: [ + "\U000F004B", + "\U0000f0ed", + "\U000F006E", + "\U000F012C", + "\U000F179B", + "\U000F0748", + "\U000F1A1B", + "\U000F02DC", + "\U000F0A02", + "\U000F035F", + "\U000F0156", + "\U000F0C5F", + "\U000f0084", + "\U000f0091", + ] diff --git a/tests/components/lvgl/helvetica.ttf b/tests/components/lvgl/helvetica.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7aec6f3f3cc74a7138ce350def27a2209b44ad89 GIT binary patch literal 83644 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

y})F$tjs}{ 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�Gn-^3|F$D~HG0GN#m0p3RY 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

f5LEo{ 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


C~-^J9kuf1f~B&vG+GBd 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
-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%0D=QdD>Z6>?g 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 ZcAgz$Pp~K2ZnnEU$(~HkLK9l*e*jr~ZsGs{ literal 0 HcmV?d00001 From 3ba9caa1184687611affb4ce5b4b7378a92c98b3 Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Tue, 6 Aug 2024 02:50:36 +0100 Subject: [PATCH 037/160] socket: socket::set_sockaddr() for IPv4 addresses in IPv6 builds (#7196) --- esphome/components/socket/socket.cpp | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index b200046d7f..5d3528dad8 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -19,24 +19,22 @@ std::unique_ptr socket_ip(int type, int protocol) { socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) { #if USE_NETWORK_IPV6 - if (addrlen < sizeof(sockaddr_in6)) { - errno = EINVAL; - return 0; - } - auto *server = reinterpret_cast(addr); - memset(server, 0, sizeof(sockaddr_in6)); - server->sin6_family = AF_INET6; - server->sin6_port = htons(port); + if (ip_address.find(':') != std::string::npos) { + if (addrlen < sizeof(sockaddr_in6)) { + errno = EINVAL; + return 0; + } + auto *server = reinterpret_cast(addr); + memset(server, 0, sizeof(sockaddr_in6)); + server->sin6_family = AF_INET6; + server->sin6_port = htons(port); - if (ip_address.find('.') != std::string::npos) { - server->sin6_addr.un.u32_addr[3] = inet_addr(ip_address.c_str()); - } else { ip6_addr_t ip6; inet6_aton(ip_address.c_str(), &ip6); memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); + return sizeof(sockaddr_in6); } - return sizeof(sockaddr_in6); -#else +#endif /* USE_NETWORK_IPV6 */ if (addrlen < sizeof(sockaddr_in)) { errno = EINVAL; return 0; @@ -47,7 +45,6 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri server->sin_addr.s_addr = inet_addr(ip_address.c_str()); server->sin_port = htons(port); return sizeof(sockaddr_in); -#endif /* USE_NETWORK_IPV6 */ } socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) { From 7074fa06ae98c8f42821269b3fcfcce7f052854e Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Mon, 5 Aug 2024 23:53:52 -0400 Subject: [PATCH 038/160] Adds MQTT component to Alarm Control panel component (#7188) --- .../alarm_control_panel/__init__.py | 141 ++++++++++-------- esphome/components/mqtt/__init__.py | 21 +-- .../mqtt/mqtt_alarm_control_panel.cpp | 128 ++++++++++++++++ .../mqtt/mqtt_alarm_control_panel.h | 39 +++++ tests/components/mqtt/common.yaml | 6 + 5 files changed, 260 insertions(+), 75 deletions(-) create mode 100644 esphome/components/mqtt/mqtt_alarm_control_panel.cpp create mode 100644 esphome/components/mqtt/mqtt_alarm_control_panel.h diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index 7ad4358011..8987d708fd 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -1,16 +1,17 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import web_server from esphome import automation from esphome.automation import maybe_simple_id -from esphome.core import CORE, coroutine_with_priority +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_CODE, CONF_ID, + CONF_MQTT_ID, CONF_ON_STATE, CONF_TRIGGER_ID, - CONF_CODE, CONF_WEB_SERVER_ID, ) +from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@grahambrown11", "@hwstar"] @@ -77,67 +78,72 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_( "AlarmControlPanelCondition", automation.Condition ) -ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( - web_server.WEBSERVER_SORTING_SCHEMA -).extend( - { - cv.GenerateID(): cv.declare_id(AlarmControlPanel), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), - } - ), - cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), - } - ), - cv.Optional(CONF_ON_ARMING): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger), - } - ), - cv.Optional(CONF_ON_PENDING): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger), - } - ), - cv.Optional(CONF_ON_DISARMED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger), - } - ), - cv.Optional(CONF_ON_CLEARED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), - } - ), - cv.Optional(CONF_ON_CHIME): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger), - } - ), - cv.Optional(CONF_ON_READY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger), - } - ), - } +ALARM_CONTROL_PANEL_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(): cv.declare_id(AlarmControlPanel), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( + mqtt.MQTTAlarmControlPanelComponent + ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), + } + ), + cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), + } + ), + cv.Optional(CONF_ON_ARMING): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger), + } + ), + cv.Optional(CONF_ON_PENDING): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger), + } + ), + cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger), + } + ), + cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger), + } + ), + cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger), + } + ), + cv.Optional(CONF_ON_DISARMED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger), + } + ), + cv.Optional(CONF_ON_CLEARED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), + } + ), + cv.Optional(CONF_ON_CHIME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger), + } + ), + cv.Optional(CONF_ON_READY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger), + } + ), + } + ) ) ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( @@ -192,6 +198,9 @@ async def setup_alarm_control_panel_core_(var, config): if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None: web_server_ = await cg.get_variable(webserver_id) web_server.add_entity_to_sorting_list(web_server_, var, config) + if mqtt_id := config.get(CONF_MQTT_ID): + mqtt_ = cg.new_Pvariable(mqtt_id, var) + await mqtt.register_mqtt_component(mqtt_, config) async def register_alarm_control_panel(var, config): diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index f4bd34bfd3..240b407819 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -1,10 +1,11 @@ import re -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition +import esphome.codegen as cg from esphome.components import logger +from esphome.components.esp32 import add_idf_sdkconfig_option +import esphome.config_validation as cv from esphome.const import ( CONF_AVAILABILITY, CONF_BIRTH_MESSAGE, @@ -13,21 +14,21 @@ from esphome.const import ( CONF_CLIENT_CERTIFICATE, CONF_CLIENT_CERTIFICATE_KEY, CONF_CLIENT_ID, - CONF_COMMAND_TOPIC, CONF_COMMAND_RETAIN, + CONF_COMMAND_TOPIC, CONF_DISCOVERY, + CONF_DISCOVERY_OBJECT_ID_GENERATOR, CONF_DISCOVERY_PREFIX, CONF_DISCOVERY_RETAIN, CONF_DISCOVERY_UNIQUE_ID_GENERATOR, - CONF_DISCOVERY_OBJECT_ID_GENERATOR, CONF_ID, CONF_KEEPALIVE, CONF_LEVEL, CONF_LOG_TOPIC, - CONF_ON_JSON_MESSAGE, - CONF_ON_MESSAGE, CONF_ON_CONNECT, CONF_ON_DISCONNECT, + CONF_ON_JSON_MESSAGE, + CONF_ON_MESSAGE, CONF_PASSWORD, CONF_PAYLOAD, CONF_PAYLOAD_AVAILABLE, @@ -45,12 +46,11 @@ from esphome.const import ( CONF_USE_ABBREVIATIONS, CONF_USERNAME, CONF_WILL_MESSAGE, + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, - PLATFORM_BK72XX, ) -from esphome.core import coroutine_with_priority, CORE -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.core import CORE, coroutine_with_priority DEPENDENCIES = ["network"] @@ -110,6 +110,9 @@ MQTTDisconnectTrigger = mqtt_ns.class_( MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component) MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition) +MQTTAlarmControlPanelComponent = mqtt_ns.class_( + "MQTTAlarmControlPanelComponent", MQTTComponent +) MQTTBinarySensorComponent = mqtt_ns.class_("MQTTBinarySensorComponent", MQTTComponent) MQTTClimateComponent = mqtt_ns.class_("MQTTClimateComponent", MQTTComponent) MQTTCoverComponent = mqtt_ns.class_("MQTTCoverComponent", MQTTComponent) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp new file mode 100644 index 0000000000..660a030d11 --- /dev/null +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -0,0 +1,128 @@ +#include "mqtt_alarm_control_panel.h" +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_ALARM_CONTROL_PANEL + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.alarm_control_panel"; + +using namespace esphome::alarm_control_panel; + +MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) + : alarm_control_panel_(alarm_control_panel) {} +void MQTTAlarmControlPanelComponent::setup() { + this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); }); + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { + auto call = this->alarm_control_panel_->make_call(); + if (strcasecmp(payload.c_str(), "ARM_AWAY") == 0) { + call.arm_away(); + } else if (strcasecmp(payload.c_str(), "ARM_HOME") == 0) { + call.arm_home(); + } else if (strcasecmp(payload.c_str(), "ARM_NIGHT") == 0) { + call.arm_night(); + } else if (strcasecmp(payload.c_str(), "ARM_VACATION") == 0) { + call.arm_vacation(); + } else if (strcasecmp(payload.c_str(), "ARM_CUSTOM_BYPASS") == 0) { + call.arm_custom_bypass(); + } else if (strcasecmp(payload.c_str(), "DISARM") == 0) { + call.disarm(); + } else if (strcasecmp(payload.c_str(), "PENDING") == 0) { + call.pending(); + } else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) { + call.triggered(); + } else { + ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name().c_str(), payload.c_str()); + } + call.perform(); + }); +} + +void MQTTAlarmControlPanelComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true) + ESP_LOGCONFIG(TAG, " Supported Features: %" PRIu32, this->alarm_control_panel_->get_supported_features()); + ESP_LOGCONFIG(TAG, " Requires Code to Disarm: %s", YESNO(this->alarm_control_panel_->get_requires_code())); + ESP_LOGCONFIG(TAG, " Requires Code To Arm: %s", YESNO(this->alarm_control_panel_->get_requires_code_to_arm())); +} + +void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES); + const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features(); + if (acp_supported_features & ACP_FEAT_ARM_AWAY) { + supported_features.add("arm_away"); + } + if (acp_supported_features & ACP_FEAT_ARM_HOME) { + supported_features.add("arm_home"); + } + if (acp_supported_features & ACP_FEAT_ARM_NIGHT) { + supported_features.add("arm_night"); + } + if (acp_supported_features & ACP_FEAT_ARM_VACATION) { + supported_features.add("arm_vacation"); + } + if (acp_supported_features & ACP_FEAT_ARM_CUSTOM_BYPASS) { + supported_features.add("arm_custom_bypass"); + } + if (acp_supported_features & ACP_FEAT_TRIGGER) { + supported_features.add("trigger"); + } + root[MQTT_CODE_DISARM_REQUIRED] = this->alarm_control_panel_->get_requires_code(); + root[MQTT_CODE_ARM_REQUIRED] = this->alarm_control_panel_->get_requires_code_to_arm(); +} + +std::string MQTTAlarmControlPanelComponent::component_type() const { return "alarm_control_panel"; } +const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return this->alarm_control_panel_; } + +bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); } +bool MQTTAlarmControlPanelComponent::publish_state() { + bool success = true; + const char *state_s = ""; + switch (this->alarm_control_panel_->get_state()) { + case ACP_STATE_DISARMED: + state_s = "disarmed"; + break; + case ACP_STATE_ARMED_HOME: + state_s = "armed_home"; + break; + case ACP_STATE_ARMED_AWAY: + state_s = "armed_away"; + break; + case ACP_STATE_ARMED_NIGHT: + state_s = "armed_night"; + break; + case ACP_STATE_ARMED_VACATION: + state_s = "armed_vacation"; + break; + case ACP_STATE_ARMED_CUSTOM_BYPASS: + state_s = "armed_custom_bypass"; + break; + case ACP_STATE_PENDING: + state_s = "pending"; + break; + case ACP_STATE_ARMING: + state_s = "arming"; + break; + case ACP_STATE_DISARMING: + state_s = "disarming"; + break; + case ACP_STATE_TRIGGERED: + state_s = "triggered"; + break; + default: + state_s = "unknown"; + } + if (!this->publish(this->get_state_topic_(), state_s)) + success = false; + return success; +} + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.h b/esphome/components/mqtt/mqtt_alarm_control_panel.h new file mode 100644 index 0000000000..4ad37b7314 --- /dev/null +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_ALARM_CONTROL_PANEL + +#include "mqtt_component.h" +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" + +namespace esphome { +namespace mqtt { + +class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent { + public: + explicit MQTTAlarmControlPanelComponent(alarm_control_panel::AlarmControlPanel *alarm_control_panel); + + void setup() override; + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + + bool publish_state(); + + void dump_config() override; + + protected: + std::string component_type() const override; + const EntityBase *get_entity() const override; + + alarm_control_panel::AlarmControlPanel *alarm_control_panel_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index a2a751df63..b7d1655ec9 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -426,3 +426,9 @@ valve: } else { return VALVE_CLOSED; } + +alarm_control_panel: + - platform: template + name: Alarm Control Panel + binary_sensors: + - input: some_binary_sensor From 71ea2cec1f21d5ea66c84f7bbe2c063a705d063b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:56:48 +1000 Subject: [PATCH 039/160] [lvgl] Final stage (#7184) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lvgl/__init__.py | 50 ++++++------- esphome/components/lvgl/automation.py | 2 +- .../components/lvgl/binary_sensor/__init__.py | 43 +++++++++++ esphome/components/lvgl/light/__init__.py | 32 +++++++++ esphome/components/lvgl/light/lvgl_light.h | 48 +++++++++++++ esphome/components/lvgl/lvgl_esphome.h | 13 ++-- esphome/components/lvgl/number/__init__.py | 52 ++++++++++++++ esphome/components/lvgl/number/lvgl_number.h | 33 +++++++++ esphome/components/lvgl/rotary_encoders.py | 2 +- esphome/components/lvgl/select/__init__.py | 46 ++++++++++++ esphome/components/lvgl/select/lvgl_select.h | 62 ++++++++++++++++ esphome/components/lvgl/sensor/__init__.py | 35 +++++++++ esphome/components/lvgl/styles.py | 4 +- esphome/components/lvgl/switch/__init__.py | 54 ++++++++++++++ esphome/components/lvgl/switch/lvgl_switch.h | 33 +++++++++ esphome/components/lvgl/text/__init__.py | 39 ++++++++++ esphome/components/lvgl/text/lvgl_text.h | 33 +++++++++ .../components/lvgl/text_sensor/__init__.py | 40 +++++++++++ esphome/components/lvgl/touchscreens.py | 2 +- esphome/components/lvgl/trigger.py | 2 +- .../lvgl/{widget.py => widgets/__init__.py} | 12 ++-- .../components/lvgl/{ => widgets}/animimg.py | 14 ++-- esphome/components/lvgl/{ => widgets}/arc.py | 10 +-- .../components/lvgl/{ => widgets}/button.py | 4 +- .../lvgl/{ => widgets}/buttonmatrix.py | 20 +++--- .../components/lvgl/{ => widgets}/checkbox.py | 12 ++-- .../components/lvgl/{ => widgets}/dropdown.py | 12 ++-- esphome/components/lvgl/{ => widgets}/img.py | 10 +-- .../components/lvgl/{ => widgets}/keyboard.py | 8 +-- .../components/lvgl/{ => widgets}/label.py | 10 +-- esphome/components/lvgl/{ => widgets}/led.py | 10 +-- esphome/components/lvgl/{ => widgets}/line.py | 11 ++- .../components/lvgl/{ => widgets}/lv_bar.py | 12 ++-- .../components/lvgl/{ => widgets}/meter.py | 24 +++---- .../components/lvgl/{ => widgets}/msgbox.py | 72 ++++++++++--------- esphome/components/lvgl/{ => widgets}/obj.py | 8 +-- esphome/components/lvgl/{ => widgets}/page.py | 12 ++-- .../components/lvgl/{ => widgets}/roller.py | 10 +-- .../components/lvgl/{ => widgets}/slider.py | 12 ++-- .../components/lvgl/{ => widgets}/spinbox.py | 12 ++-- .../components/lvgl/{ => widgets}/spinner.py | 10 +-- .../lvgl/{lv_switch.py => widgets/switch.py} | 6 +- .../components/lvgl/{ => widgets}/tabview.py | 16 ++--- .../components/lvgl/{ => widgets}/textarea.py | 10 +-- .../components/lvgl/{ => widgets}/tileview.py | 16 ++--- tests/components/lvgl/common.yaml | 72 +++++++++++++++++++ 46 files changed, 840 insertions(+), 210 deletions(-) create mode 100644 esphome/components/lvgl/binary_sensor/__init__.py create mode 100644 esphome/components/lvgl/light/__init__.py create mode 100644 esphome/components/lvgl/light/lvgl_light.h create mode 100644 esphome/components/lvgl/number/__init__.py create mode 100644 esphome/components/lvgl/number/lvgl_number.h create mode 100644 esphome/components/lvgl/select/__init__.py create mode 100644 esphome/components/lvgl/select/lvgl_select.h create mode 100644 esphome/components/lvgl/sensor/__init__.py create mode 100644 esphome/components/lvgl/switch/__init__.py create mode 100644 esphome/components/lvgl/switch/lvgl_switch.h create mode 100644 esphome/components/lvgl/text/__init__.py create mode 100644 esphome/components/lvgl/text/lvgl_text.h create mode 100644 esphome/components/lvgl/text_sensor/__init__.py rename esphome/components/lvgl/{widget.py => widgets/__init__.py} (98%) rename esphome/components/lvgl/{ => widgets}/animimg.py (89%) rename esphome/components/lvgl/{ => widgets}/arc.py (92%) rename esphome/components/lvgl/{ => widgets}/button.py (82%) rename esphome/components/lvgl/{ => widgets}/buttonmatrix.py (95%) rename esphome/components/lvgl/{ => widgets}/checkbox.py (68%) rename esphome/components/lvgl/{ => widgets}/dropdown.py (89%) rename esphome/components/lvgl/{ => widgets}/img.py (92%) rename esphome/components/lvgl/{ => widgets}/keyboard.py (86%) rename esphome/components/lvgl/{ => widgets}/label.py (85%) rename esphome/components/lvgl/{ => widgets}/led.py (80%) rename esphome/components/lvgl/{ => widgets}/line.py (85%) rename esphome/components/lvgl/{ => widgets}/lv_bar.py (81%) rename esphome/components/lvgl/{ => widgets}/meter.py (96%) rename esphome/components/lvgl/{ => widgets}/msgbox.py (72%) rename esphome/components/lvgl/{ => widgets}/obj.py (76%) rename esphome/components/lvgl/{ => widgets}/page.py (91%) rename esphome/components/lvgl/{ => widgets}/roller.py (92%) rename esphome/components/lvgl/{ => widgets}/slider.py (88%) rename esphome/components/lvgl/{ => widgets}/spinbox.py (95%) rename esphome/components/lvgl/{ => widgets}/spinner.py (82%) rename esphome/components/lvgl/{lv_switch.py => widgets/switch.py} (72%) rename esphome/components/lvgl/{ => widgets}/tabview.py (88%) rename esphome/components/lvgl/{ => widgets}/textarea.py (90%) rename esphome/components/lvgl/{ => widgets}/tileview.py (89%) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index a963fca98b..9eb4665874 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -21,28 +21,10 @@ from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid -from .animimg import animimg_spec -from .arc import arc_spec from .automation import disp_update, update_to_code -from .button import button_spec -from .buttonmatrix import buttonmatrix_spec -from .checkbox import checkbox_spec from .defines import CONF_SKIP -from .dropdown import dropdown_spec -from .img import img_spec -from .keyboard import keyboard_spec -from .label import label_spec -from .led import led_spec -from .line import line_spec -from .lv_bar import bar_spec -from .lv_switch import switch_spec from .lv_validation import lv_bool, lv_images_used from .lvcode import LvContext, LvglComponent -from .meter import meter_spec -from .msgbox import MSGBOX_SCHEMA, msgboxes_to_code -from .obj import obj_spec -from .page import add_pages, page_spec -from .roller import roller_spec from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code from .schemas import ( DISP_BG_SCHEMA, @@ -57,13 +39,7 @@ from .schemas import ( grid_alignments, obj_schema, ) -from .slider import slider_spec -from .spinbox import spinbox_spec -from .spinner import spinner_spec from .styles import add_top_layer, styles_to_code, theme_to_code -from .tabview import tabview_spec -from .textarea import textarea_spec -from .tileview import tileview_spec from .touchscreens import touchscreen_schema, touchscreens_to_code from .trigger import generate_triggers from .types import ( @@ -74,7 +50,31 @@ from .types import ( lv_style_t, lvgl_ns, ) -from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties +from .widgets import Widget, add_widgets, lv_scr_act, set_obj_properties +from .widgets.animimg import animimg_spec +from .widgets.arc import arc_spec +from .widgets.button import button_spec +from .widgets.buttonmatrix import buttonmatrix_spec +from .widgets.checkbox import checkbox_spec +from .widgets.dropdown import dropdown_spec +from .widgets.img import img_spec +from .widgets.keyboard import keyboard_spec +from .widgets.label import label_spec +from .widgets.led import led_spec +from .widgets.line import line_spec +from .widgets.lv_bar import bar_spec +from .widgets.meter import meter_spec +from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code +from .widgets.obj import obj_spec +from .widgets.page import add_pages, page_spec +from .widgets.roller import roller_spec +from .widgets.slider import slider_spec +from .widgets.spinbox import spinbox_spec +from .widgets.spinner import spinner_spec +from .widgets.switch import switch_spec +from .widgets.tabview import tabview_spec +from .widgets.textarea import textarea_spec +from .widgets.tileview import tileview_spec DOMAIN = "lvgl" DEPENDENCIES = ["display"] diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 7a862fb58b..556e286208 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -38,7 +38,7 @@ from .types import ( lv_disp_t, lv_obj_t, ) -from .widget import Widget, get_widgets, lv_scr_act, set_obj_properties +from .widgets import Widget, get_widgets, lv_scr_act, set_obj_properties async def action_to_code( diff --git a/esphome/components/lvgl/binary_sensor/__init__.py b/esphome/components/lvgl/binary_sensor/__init__.py new file mode 100644 index 0000000000..8789a06375 --- /dev/null +++ b/esphome/components/lvgl/binary_sensor/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +from esphome.components.binary_sensor import ( + BinarySensor, + binary_sensor_schema, + new_binary_sensor, +) +import esphome.config_validation as cv + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import EVENT_ARG, LambdaContext, LvContext +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, lv_pseudo_button_t +from ..widgets import Widget, get_widgets + +CONFIG_SCHEMA = ( + binary_sensor_schema(BinarySensor) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), + } + ) +) + + +async def to_code(config): + sensor = await new_binary_sensor(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + assert isinstance(widget, Widget) + async with LambdaContext(EVENT_ARG) as pressed_ctx: + pressed_ctx.add(sensor.publish_state(widget.is_pressed())) + async with LvContext(paren) as ctx: + ctx.add(sensor.publish_initial_state(widget.is_pressed())) + ctx.add( + paren.add_event_cb( + widget.obj, + await pressed_ctx.get_lambda(), + LV_EVENT.PRESSING, + LV_EVENT.RELEASED, + ) + ) diff --git a/esphome/components/lvgl/light/__init__.py b/esphome/components/lvgl/light/__init__.py new file mode 100644 index 0000000000..27c160dff6 --- /dev/null +++ b/esphome/components/lvgl/light/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +from esphome.components import light +from esphome.components.light import LightOutput +import esphome.config_validation as cv +from esphome.const import CONF_GAMMA_CORRECT, CONF_LED, CONF_OUTPUT_ID + +from ..defines import CONF_LVGL_ID +from ..lvcode import LvContext +from ..schemas import LVGL_SCHEMA +from ..types import LvType, lvgl_ns +from ..widgets import get_widgets + +lv_led_t = LvType("lv_led_t") +LVLight = lvgl_ns.class_("LVLight", LightOutput) +CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( + { + cv.Optional(CONF_GAMMA_CORRECT, default=0.0): cv.positive_float, + cv.Required(CONF_LED): cv.use_id(lv_led_t), + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LVLight), + } +).extend(LVGL_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(var, config) + + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_LED) + widget = widget[0] + async with LvContext(paren) as ctx: + ctx.add(var.set_obj(widget.obj)) diff --git a/esphome/components/lvgl/light/lvgl_light.h b/esphome/components/lvgl/light/lvgl_light.h new file mode 100644 index 0000000000..67372d89dd --- /dev/null +++ b/esphome/components/lvgl/light/lvgl_light.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/light_output.h" +#include "../lvgl_esphome.h" + +namespace esphome { +namespace lvgl { + +class LVLight : public light::LightOutput { + public: + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::RGB}); + return traits; + } + void write_state(light::LightState *state) override { + float red, green, blue; + state->current_values_as_rgb(&red, &green, &blue, false); + auto color = lv_color_make(red * 255, green * 255, blue * 255); + if (this->obj_ != nullptr) { + this->set_value_(color); + } else { + this->initial_value_ = color; + } + } + + void set_obj(lv_obj_t *obj) { + this->obj_ = obj; + if (this->initial_value_) { + lv_led_set_color(obj, this->initial_value_.value()); + lv_led_on(obj); + this->initial_value_.reset(); + } + } + + protected: + void set_value_(lv_color_t value) { + lv_led_set_color(this->obj_, value); + lv_led_on(this->obj_); + lv_event_send(this->obj_, lv_custom_event, nullptr); + } + lv_obj_t *obj_{}; + optional initial_value_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 71e0fd069f..5f2f0ea8df 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -1,13 +1,6 @@ #pragma once #include "esphome/core/defines.h" -#ifdef USE_LVGL_BINARY_SENSOR -#include "esphome/components/binary_sensor/binary_sensor.h" -#endif // USE_LVGL_BINARY_SENSOR -#ifdef USE_LVGL_ROTARY_ENCODER -#include "esphome/components/rotary_encoder/rotary_encoder.h" -#endif // USE_LVGL_ROTARY_ENCODER - // required for clang-tidy #ifndef LV_CONF_H #define LV_CONF_SKIP 1 // NOLINT @@ -19,6 +12,12 @@ #include "esphome/core/log.h" #include #include + +#ifdef USE_LVGL_ROTARY_ENCODER +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/rotary_encoder/rotary_encoder.h" +#endif // USE_LVGL_ROTARY_ENCODER + #ifdef USE_LVGL_IMAGE #include "esphome/components/image/image.h" #endif // USE_LVGL_IMAGE diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py new file mode 100644 index 0000000000..53aef2790d --- /dev/null +++ b/esphome/components/lvgl/number/__init__.py @@ -0,0 +1,52 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.cpp_generator import MockObj + +from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET +from ..lv_validation import animated +from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvNumber, lvgl_ns +from ..widgets import get_widgets + +LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number) + +CONFIG_SCHEMA = ( + number.number_schema(LVGLNumber) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + cv.Optional(CONF_ANIMATED, default=True): animated, + } + ) +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + var = await number.new_number( + config, + max_value=widget.get_max(), + min_value=widget.get_min(), + step=widget.get_step(), + ) + + async with LambdaContext([(cg.float_, "v")]) as control: + await widget.set_property( + "value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED] + ) + lv.event_send(widget.obj, CUSTOM_EVENT, cg.nullptr) + async with LambdaContext(EVENT_ARG) as event: + event.add(var.publish_state(widget.get_value())) + async with LvContext(paren): + lv_add(var.set_control_lambda(await control.get_lambda())) + lv_add( + paren.add_event_cb( + widget.obj, await event.get_lambda(), LV_EVENT.VALUE_CHANGED + ) + ) + lv_add(var.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h new file mode 100644 index 0000000000..461ea51be4 --- /dev/null +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace lvgl { + +class LVGLNumber : public number::Number { + public: + void set_control_lambda(std::function control_lambda) { + this->control_lambda_ = control_lambda; + if (this->initial_state_.has_value()) { + this->control_lambda_(this->initial_state_.value()); + this->initial_state_.reset(); + } + } + + protected: + void control(float value) { + if (this->control_lambda_ != nullptr) + this->control_lambda_(value); + else + this->initial_state_ = value; + } + std::function control_lambda_{}; + optional initial_state_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/rotary_encoders.py b/esphome/components/lvgl/rotary_encoders.py index ede6905a67..d8a82dbc78 100644 --- a/esphome/components/lvgl/rotary_encoders.py +++ b/esphome/components/lvgl/rotary_encoders.py @@ -16,7 +16,7 @@ from .helpers import lvgl_components_required from .lvcode import lv, lv_add, lv_expr from .schemas import ENCODER_SCHEMA from .types import lv_indev_type_t -from .widget import add_group +from .widgets import add_group ROTARY_ENCODER_CONFIG = cv.ensure_list( ENCODER_SCHEMA.extend( diff --git a/esphome/components/lvgl/select/__init__.py b/esphome/components/lvgl/select/__init__.py new file mode 100644 index 0000000000..34a70a23f7 --- /dev/null +++ b/esphome/components/lvgl/select/__init__.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import CONF_OPTIONS + +from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvSelect, lvgl_ns +from ..widgets import get_widgets + +LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select) + +CONFIG_SCHEMA = ( + select.select_schema(LVGLSelect) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvSelect), + cv.Optional(CONF_ANIMATED, default=False): cv.boolean, + } + ) +) + + +async def to_code(config): + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + options = widget.config.get(CONF_OPTIONS, []) + selector = await select.new_select(config, options=options) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + async with LambdaContext(EVENT_ARG) as pub_ctx: + pub_ctx.add(selector.publish_index(widget.get_value())) + async with LambdaContext([(cg.uint16, "v")]) as control: + await widget.set_property("selected", "v", animated=config[CONF_ANIMATED]) + lv.event_send(widget.obj, CUSTOM_EVENT, cg.nullptr) + async with LvContext(paren) as ctx: + lv_add(selector.set_control_lambda(await control.get_lambda())) + ctx.add( + paren.add_event_cb( + widget.obj, + await pub_ctx.get_lambda(), + LV_EVENT.VALUE_CHANGED, + ) + ) + lv_add(selector.publish_index(widget.get_value())) diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h new file mode 100644 index 0000000000..407045d605 --- /dev/null +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -0,0 +1,62 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace lvgl { + +static std::vector split_string(const std::string &str) { + std::vector strings; + auto delimiter = std::string("\n"); + + std::string::size_type pos; + std::string::size_type prev = 0; + while ((pos = str.find(delimiter, prev)) != std::string::npos) { + strings.push_back(str.substr(prev, pos - prev)); + prev = pos + delimiter.size(); + } + + // To get the last substring (or only, if delimiter is not found) + strings.push_back(str.substr(prev)); + + return strings; +} + +class LVGLSelect : public select::Select { + public: + void set_control_lambda(std::function lambda) { + this->control_lambda_ = lambda; + if (this->initial_state_.has_value()) { + this->control(this->initial_state_.value()); + this->initial_state_.reset(); + } + } + + void publish_index(size_t index) { + auto value = this->at(index); + if (value) + this->publish_state(value.value()); + } + + void set_options(const char *str) { this->traits.set_options(split_string(str)); } + + protected: + void control(const std::string &value) override { + if (this->control_lambda_ != nullptr) { + auto index = index_of(value); + if (index) + this->control_lambda_(index.value()); + } else { + this->initial_state_ = value.c_str(); + } + } + + std::function control_lambda_{}; + optional initial_state_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py new file mode 100644 index 0000000000..6e495eb685 --- /dev/null +++ b/esphome/components/lvgl/sensor/__init__.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +from esphome.components.sensor import Sensor, new_sensor, sensor_schema +import esphome.config_validation as cv + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import EVENT_ARG, LVGL_COMP_ARG, LambdaContext, LvContext, lv_add +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvNumber +from ..widgets import Widget, get_widgets + +CONFIG_SCHEMA = ( + sensor_schema(Sensor) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + } + ) +) + + +async def to_code(config): + sensor = await new_sensor(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + assert isinstance(widget, Widget) + async with LambdaContext(EVENT_ARG) as lamb: + lv_add(sensor.publish_state(widget.get_value())) + async with LvContext(paren, LVGL_COMP_ARG): + lv_add( + paren.add_event_cb( + widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED + ) + ) diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 09f1c376d0..26c2694a52 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -12,10 +12,10 @@ from .defines import ( ) from .helpers import add_lv_use from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable -from .obj import obj_spec from .schemas import ALL_STYLES from .types import lv_lambda_t, lv_obj_t, lv_obj_t_ptr -from .widget import Widget, add_widgets, set_obj_properties, theme_widget_map +from .widgets import Widget, add_widgets, set_obj_properties, theme_widget_map +from .widgets.obj import obj_spec TOP_LAYER = literal("lv_disp_get_layer_top(lv_component->get_disp())") diff --git a/esphome/components/lvgl/switch/__init__.py b/esphome/components/lvgl/switch/__init__.py new file mode 100644 index 0000000000..831fa9308b --- /dev/null +++ b/esphome/components/lvgl/switch/__init__.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +from esphome.components.switch import Switch, new_switch, switch_schema +import esphome.config_validation as cv +from esphome.cpp_generator import MockObj + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import ( + CUSTOM_EVENT, + EVENT_ARG, + LambdaContext, + LvConditional, + LvContext, + lv, + lv_add, +) +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns +from ..widgets import get_widgets + +LVGLSwitch = lvgl_ns.class_("LVGLSwitch", Switch) +CONFIG_SCHEMA = ( + switch_schema(LVGLSwitch) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), + } + ) +) + + +async def to_code(config): + switch = await new_switch(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + async with LambdaContext(EVENT_ARG) as checked_ctx: + checked_ctx.add(switch.publish_state(widget.get_value())) + async with LambdaContext([(cg.bool_, "v")]) as control: + with LvConditional(MockObj("v")) as cond: + widget.add_state(LV_STATE.CHECKED) + cond.else_() + widget.clear_state(LV_STATE.CHECKED) + lv.event_send(widget.obj, CUSTOM_EVENT, cg.nullptr) + async with LvContext(paren) as ctx: + lv_add(switch.set_control_lambda(await control.get_lambda())) + ctx.add( + paren.add_event_cb( + widget.obj, + await checked_ctx.get_lambda(), + LV_EVENT.VALUE_CHANGED, + ) + ) + lv_add(switch.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/switch/lvgl_switch.h b/esphome/components/lvgl/switch/lvgl_switch.h new file mode 100644 index 0000000000..f20f4ed960 --- /dev/null +++ b/esphome/components/lvgl/switch/lvgl_switch.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace lvgl { + +class LVGLSwitch : public switch_::Switch { + public: + void set_control_lambda(std::function state_lambda) { + this->state_lambda_ = state_lambda; + if (this->initial_state_.has_value()) { + this->state_lambda_(this->initial_state_.value()); + this->initial_state_.reset(); + } + } + + protected: + void write_state(bool value) { + if (this->state_lambda_ != nullptr) + this->state_lambda_(value); + else + this->initial_state_ = value; + } + std::function state_lambda_{}; + optional initial_state_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/text/__init__.py b/esphome/components/lvgl/text/__init__.py new file mode 100644 index 0000000000..55f1b2b3fc --- /dev/null +++ b/esphome/components/lvgl/text/__init__.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +from esphome.components import text +from esphome.components.text import new_text +import esphome.config_validation as cv + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvText, lvgl_ns +from ..widgets import get_widgets + +LVGLText = lvgl_ns.class_("LVGLText", text.Text) + +CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(LVGL_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(LVGLText), + cv.Required(CONF_WIDGET): cv.use_id(LvText), + } +) + + +async def to_code(config): + textvar = await new_text(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + async with LambdaContext([(cg.std_string, "text_value")]) as control: + await widget.set_property("text", "text_value.c_str())") + lv.event_send(widget.obj, CUSTOM_EVENT, None) + async with LambdaContext(EVENT_ARG) as lamb: + lv_add(textvar.publish_state(widget.get_value())) + async with LvContext(paren): + widget.var.set_control_lambda(await control.get_lambda()) + lv_add( + paren.add_event_cb( + widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED + ) + ) + lv_add(textvar.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/text/lvgl_text.h b/esphome/components/lvgl/text/lvgl_text.h new file mode 100644 index 0000000000..8dc0281364 --- /dev/null +++ b/esphome/components/lvgl/text/lvgl_text.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/components/text/text.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace lvgl { + +class LVGLText : public text::Text { + public: + void set_control_lambda(std::function control_lambda) { + this->control_lambda_ = control_lambda; + if (this->initial_state_.has_value()) { + this->control_lambda_(this->initial_state_.value()); + this->initial_state_.reset(); + } + } + + protected: + void control(const std::string &value) { + if (this->control_lambda_ != nullptr) + this->control_lambda_(value); + else + this->initial_state_ = value; + } + std::function control_lambda_{}; + optional initial_state_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/text_sensor/__init__.py b/esphome/components/lvgl/text_sensor/__init__.py new file mode 100644 index 0000000000..c0f0bc36a8 --- /dev/null +++ b/esphome/components/lvgl/text_sensor/__init__.py @@ -0,0 +1,40 @@ +import esphome.codegen as cg +from esphome.components.text_sensor import ( + TextSensor, + new_text_sensor, + text_sensor_schema, +) +import esphome.config_validation as cv + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import EVENT_ARG, LambdaContext, LvContext +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvText +from ..widgets import get_widgets + +CONFIG_SCHEMA = ( + text_sensor_schema(TextSensor) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvText), + } + ) +) + + +async def to_code(config): + sensor = await new_text_sensor(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + async with LambdaContext(EVENT_ARG) as pressed_ctx: + pressed_ctx.add(sensor.publish_state(widget.get_value())) + async with LvContext(paren) as ctx: + ctx.add( + paren.add_event_cb( + widget.obj, + await pressed_ctx.get_lambda(), + LV_EVENT.VALUE_CHANGED, + ) + ) diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py index 499b33aa02..292b0873f3 100644 --- a/esphome/components/lvgl/touchscreens.py +++ b/esphome/components/lvgl/touchscreens.py @@ -34,7 +34,7 @@ def touchscreen_schema(config): async def touchscreens_to_code(var, config): - for tconf in config.get(CONF_TOUCHSCREENS) or (): + for tconf in config.get(CONF_TOUCHSCREENS, ()): lvgl_components_required.add(CONF_TOUCHSCREEN) touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID]) lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index c640c8abd9..df87be718b 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -13,7 +13,7 @@ from .defines import ( ) from .lvcode import EVENT_ARG, LambdaContext, LvConditional, lv, lv_add from .types import LV_EVENT -from .widget import widget_map +from .widgets import widget_map async def generate_triggers(lv_component): diff --git a/esphome/components/lvgl/widget.py b/esphome/components/lvgl/widgets/__init__.py similarity index 98% rename from esphome/components/lvgl/widget.py rename to esphome/components/lvgl/widgets/__init__.py index fcaee29085..dff43cf257 100644 --- a/esphome/components/lvgl/widget.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -8,7 +8,7 @@ from esphome.core import ID, TimePeriod from esphome.coroutine import FakeAwaitable from esphome.cpp_generator import AssignmentExpression, CallExpression, MockObj -from .defines import ( +from ..defines import ( CONF_DEFAULT, CONF_FLEX_ALIGN_CROSS, CONF_FLEX_ALIGN_MAIN, @@ -32,8 +32,8 @@ from .defines import ( join_enums, literal, ) -from .helpers import add_lv_use -from .lvcode import ( +from ..helpers import add_lv_use +from ..lvcode import ( LvConditional, add_line_marks, lv, @@ -43,8 +43,8 @@ from .lvcode import ( lv_obj, lv_Pvariable, ) -from .schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES -from .types import ( +from ..schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES +from ..types import ( LV_STATE, LvType, WidgetType, @@ -368,7 +368,7 @@ async def add_widgets(parent: Widget, config: dict): :param config: The configuration :return: """ - for w in config.get(CONF_WIDGETS) or (): + for w in config.get(CONF_WIDGETS, ()): w_type, w_cnfig = next(iter(w.items())) await widget_to_code(w_cnfig, w_type, parent.obj) diff --git a/esphome/components/lvgl/animimg.py b/esphome/components/lvgl/widgets/animimg.py similarity index 89% rename from esphome/components/lvgl/animimg.py rename to esphome/components/lvgl/widgets/animimg.py index ad84713d7f..a973ca0702 100644 --- a/esphome/components/lvgl/animimg.py +++ b/esphome/components/lvgl/widgets/animimg.py @@ -4,15 +4,15 @@ import esphome.config_validation as cv from esphome.const import CONF_DURATION, CONF_ID from esphome.cpp_generator import MockObj -from .automation import action_to_code -from .defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC -from .helpers import lvgl_components_required +from ..automation import action_to_code +from ..defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC +from ..helpers import lvgl_components_required +from ..lv_validation import lv_image, lv_milliseconds +from ..lvcode import lv, lv_expr +from ..types import LvType, ObjUpdateAction, void_ptr +from . import Widget, WidgetType, get_widgets from .img import CONF_IMAGE from .label import CONF_LABEL -from .lv_validation import lv_image, lv_milliseconds -from .lvcode import lv, lv_expr -from .types import LvType, ObjUpdateAction, void_ptr -from .widget import Widget, WidgetType, get_widgets CONF_ANIMIMG = "animimg" CONF_SRC_LIST_ID = "src_list_id" diff --git a/esphome/components/lvgl/arc.py b/esphome/components/lvgl/widgets/arc.py similarity index 92% rename from esphome/components/lvgl/arc.py rename to esphome/components/lvgl/widgets/arc.py index d036464c7a..a6f8918e2f 100644 --- a/esphome/components/lvgl/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -8,7 +8,7 @@ from esphome.const import ( ) from esphome.cpp_types import nullptr -from .defines import ( +from ..defines import ( ARC_MODES, CONF_ADJUSTABLE, CONF_CHANGE_RATE, @@ -19,10 +19,10 @@ from .defines import ( CONF_START_ANGLE, literal, ) -from .lv_validation import angle, get_start_value, lv_float -from .lvcode import lv, lv_obj -from .types import LvNumber, NumberType -from .widget import Widget +from ..lv_validation import angle, get_start_value, lv_float +from ..lvcode import lv, lv_obj +from ..types import LvNumber, NumberType +from . import Widget CONF_ARC = "arc" ARC_SCHEMA = cv.Schema( diff --git a/esphome/components/lvgl/button.py b/esphome/components/lvgl/widgets/button.py similarity index 82% rename from esphome/components/lvgl/button.py rename to esphome/components/lvgl/widgets/button.py index 96329b3fa9..b59884ee67 100644 --- a/esphome/components/lvgl/button.py +++ b/esphome/components/lvgl/widgets/button.py @@ -1,7 +1,7 @@ from esphome.const import CONF_BUTTON -from .defines import CONF_MAIN -from .types import LvBoolean, WidgetType +from ..defines import CONF_MAIN +from ..types import LvBoolean, WidgetType lv_button_t = LvBoolean("lv_btn_t") diff --git a/esphome/components/lvgl/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py similarity index 95% rename from esphome/components/lvgl/buttonmatrix.py rename to esphome/components/lvgl/widgets/buttonmatrix.py index 75ed43f909..274b4de5ab 100644 --- a/esphome/components/lvgl/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -5,9 +5,8 @@ import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_WIDTH from esphome.cpp_generator import MockObj -from .automation import action_to_code -from .button import lv_button_t -from .defines import ( +from ..automation import action_to_code +from ..defines import ( BUTTONMATRIX_CTRLS, CONF_BUTTONS, CONF_CONTROL, @@ -19,11 +18,11 @@ from .defines import ( CONF_SELECTED, CONF_TEXT, ) -from .helpers import lvgl_components_required -from .lv_validation import key_code, lv_bool -from .lvcode import lv, lv_add, lv_expr -from .schemas import automation_schema -from .types import ( +from ..helpers import lvgl_components_required +from ..lv_validation import key_code, lv_bool +from ..lvcode import lv, lv_add, lv_expr +from ..schemas import automation_schema +from ..types import ( LV_BTNMATRIX_CTRL, LV_STATE, LvBoolean, @@ -33,7 +32,8 @@ from .types import ( char_ptr, lv_pseudo_button_t, ) -from .widget import Widget, WidgetType, get_widgets, widget_map +from . import Widget, WidgetType, get_widgets, widget_map +from .button import lv_button_t CONF_BUTTONMATRIX = "buttonmatrix" CONF_BUTTON_TEXT_LIST_ID = "button_text_list_id" @@ -151,7 +151,7 @@ async def get_button_data(config, buttonmatrix: Widget): width_list = [] key_list = [] for row in config: - for button_conf in row.get(CONF_BUTTONS) or (): + for button_conf in row.get(CONF_BUTTONS, ()): bid = button_conf[CONF_ID] index = len(width_list) MatrixButton.create_button(bid, buttonmatrix, button_conf, index) diff --git a/esphome/components/lvgl/checkbox.py b/esphome/components/lvgl/widgets/checkbox.py similarity index 68% rename from esphome/components/lvgl/checkbox.py rename to esphome/components/lvgl/widgets/checkbox.py index be7b029269..6299a2a6a2 100644 --- a/esphome/components/lvgl/checkbox.py +++ b/esphome/components/lvgl/widgets/checkbox.py @@ -1,9 +1,9 @@ -from .defines import CONF_INDICATOR, CONF_MAIN, CONF_TEXT -from .lv_validation import lv_text -from .lvcode import lv -from .schemas import TEXT_SCHEMA -from .types import LvBoolean -from .widget import Widget, WidgetType +from ..defines import CONF_INDICATOR, CONF_MAIN, CONF_TEXT +from ..lv_validation import lv_text +from ..lvcode import lv +from ..schemas import TEXT_SCHEMA +from ..types import LvBoolean +from . import Widget, WidgetType CONF_CHECKBOX = "checkbox" diff --git a/esphome/components/lvgl/dropdown.py b/esphome/components/lvgl/widgets/dropdown.py similarity index 89% rename from esphome/components/lvgl/dropdown.py rename to esphome/components/lvgl/widgets/dropdown.py index d7bdebaade..dc0346b080 100644 --- a/esphome/components/lvgl/dropdown.py +++ b/esphome/components/lvgl/widgets/dropdown.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_OPTIONS -from .defines import ( +from ..defines import ( CONF_DIR, CONF_INDICATOR, CONF_MAIN, @@ -11,12 +11,12 @@ from .defines import ( DIRECTIONS, literal, ) +from ..lv_validation import lv_int, lv_text, option_string +from ..lvcode import LocalVariable, lv, lv_expr +from ..schemas import part_schema +from ..types import LvSelect, LvType, lv_obj_t +from . import Widget, WidgetType, set_obj_properties from .label import CONF_LABEL -from .lv_validation import lv_int, lv_text, option_string -from .lvcode import LocalVariable, lv, lv_expr -from .schemas import part_schema -from .types import LvSelect, LvType, lv_obj_t -from .widget import Widget, WidgetType, set_obj_properties CONF_DROPDOWN = "dropdown" CONF_DROPDOWN_LIST = "dropdown_list" diff --git a/esphome/components/lvgl/img.py b/esphome/components/lvgl/widgets/img.py similarity index 92% rename from esphome/components/lvgl/img.py rename to esphome/components/lvgl/widgets/img.py index dd962fcf31..59b2c97c63 100644 --- a/esphome/components/lvgl/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -1,7 +1,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ANGLE, CONF_MODE -from .defines import ( +from ..defines import ( CONF_ANTIALIAS, CONF_MAIN, CONF_OFFSET_X, @@ -12,11 +12,11 @@ from .defines import ( CONF_ZOOM, LvConstant, ) +from ..lv_validation import angle, lv_bool, lv_image, size, zoom +from ..lvcode import lv +from ..types import lv_img_t +from . import Widget, WidgetType from .label import CONF_LABEL -from .lv_validation import angle, lv_bool, lv_image, size, zoom -from .lvcode import lv -from .types import lv_img_t -from .widget import Widget, WidgetType CONF_IMAGE = "image" diff --git a/esphome/components/lvgl/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py similarity index 86% rename from esphome/components/lvgl/keyboard.py rename to esphome/components/lvgl/widgets/keyboard.py index 7ce73d2170..cff322f5af 100644 --- a/esphome/components/lvgl/keyboard.py +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -3,11 +3,11 @@ import esphome.config_validation as cv from esphome.const import CONF_MODE from esphome.cpp_types import std_string -from .defines import CONF_ITEMS, CONF_MAIN, KEYBOARD_MODES, literal -from .helpers import add_lv_use, lvgl_components_required +from ..defines import CONF_ITEMS, CONF_MAIN, KEYBOARD_MODES, literal +from ..helpers import add_lv_use, lvgl_components_required +from ..types import LvCompound, LvType +from . import Widget, WidgetType, get_widgets from .textarea import CONF_TEXTAREA, lv_textarea_t -from .types import LvCompound, LvType -from .widget import Widget, WidgetType, get_widgets CONF_KEYBOARD = "keyboard" diff --git a/esphome/components/lvgl/label.py b/esphome/components/lvgl/widgets/label.py similarity index 85% rename from esphome/components/lvgl/label.py rename to esphome/components/lvgl/widgets/label.py index 6c3e1f4a00..38f688f2b0 100644 --- a/esphome/components/lvgl/label.py +++ b/esphome/components/lvgl/widgets/label.py @@ -1,6 +1,6 @@ import esphome.config_validation as cv -from .defines import ( +from ..defines import ( CONF_LONG_MODE, CONF_MAIN, CONF_RECOLOR, @@ -9,10 +9,10 @@ from .defines import ( CONF_TEXT, LV_LONG_MODES, ) -from .lv_validation import lv_bool, lv_text -from .schemas import TEXT_SCHEMA -from .types import LvText, WidgetType -from .widget import Widget +from ..lv_validation import lv_bool, lv_text +from ..schemas import TEXT_SCHEMA +from ..types import LvText, WidgetType +from . import Widget CONF_LABEL = "label" diff --git a/esphome/components/lvgl/led.py b/esphome/components/lvgl/widgets/led.py similarity index 80% rename from esphome/components/lvgl/led.py rename to esphome/components/lvgl/widgets/led.py index 9b6e819278..647973c9b7 100644 --- a/esphome/components/lvgl/led.py +++ b/esphome/components/lvgl/widgets/led.py @@ -1,11 +1,11 @@ import esphome.config_validation as cv from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED -from .defines import CONF_MAIN -from .lv_validation import lv_brightness, lv_color -from .lvcode import lv -from .types import LvType -from .widget import Widget, WidgetType +from ..defines import CONF_MAIN +from ..lv_validation import lv_brightness, lv_color +from ..lvcode import lv +from ..types import LvType +from . import Widget, WidgetType LED_SCHEMA = cv.Schema( { diff --git a/esphome/components/lvgl/line.py b/esphome/components/lvgl/widgets/line.py similarity index 85% rename from esphome/components/lvgl/line.py rename to esphome/components/lvgl/widgets/line.py index ab50832bbf..8ce4b1965f 100644 --- a/esphome/components/lvgl/line.py +++ b/esphome/components/lvgl/widgets/line.py @@ -3,11 +3,10 @@ import functools import esphome.codegen as cg import esphome.config_validation as cv -from . import defines as df -from .defines import CONF_MAIN, literal -from .lvcode import lv -from .types import LvType -from .widget import Widget, WidgetType +from ..defines import CONF_MAIN, literal +from ..lvcode import lv +from ..types import LvType +from . import Widget, WidgetType CONF_LINE = "line" CONF_POINTS = "points" @@ -32,7 +31,7 @@ def cv_point_list(value): LINE_SCHEMA = { - cv.Required(df.CONF_POINTS): cv_point_list, + cv.Required(CONF_POINTS): cv_point_list, cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t), } diff --git a/esphome/components/lvgl/lv_bar.py b/esphome/components/lvgl/widgets/lv_bar.py similarity index 81% rename from esphome/components/lvgl/lv_bar.py rename to esphome/components/lvgl/widgets/lv_bar.py index d5dcff0bf0..57209370c0 100644 --- a/esphome/components/lvgl/lv_bar.py +++ b/esphome/components/lvgl/widgets/lv_bar.py @@ -1,11 +1,13 @@ import esphome.config_validation as cv from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE -from .defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal -from .lv_validation import animated, get_start_value, lv_float -from .lvcode import lv -from .types import LvNumber, NumberType -from .widget import Widget +from ..defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal +from ..lv_validation import animated, get_start_value, lv_float +from ..lvcode import lv +from ..types import LvNumber, NumberType +from . import Widget + +# Note this file cannot be called "bar.py" because that name is disallowed. CONF_BAR = "bar" BAR_MODIFY_SCHEMA = cv.Schema( diff --git a/esphome/components/lvgl/meter.py b/esphome/components/lvgl/widgets/meter.py similarity index 96% rename from esphome/components/lvgl/meter.py rename to esphome/components/lvgl/widgets/meter.py index 1a6bef7c57..7cf154d6f3 100644 --- a/esphome/components/lvgl/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -14,9 +14,8 @@ from esphome.const import ( CONF_WIDTH, ) -from .arc import CONF_ARC -from .automation import action_to_code -from .defines import ( +from ..automation import action_to_code +from ..defines import ( CONF_END_VALUE, CONF_MAIN, CONF_PIVOT_X, @@ -25,10 +24,8 @@ from .defines import ( CONF_START_VALUE, CONF_TICKS, ) -from .helpers import add_lv_use -from .img import CONF_IMAGE -from .line import CONF_LINE -from .lv_validation import ( +from ..helpers import add_lv_use +from ..lv_validation import ( angle, get_end_value, get_start_value, @@ -39,10 +36,13 @@ from .lv_validation import ( requires_component, size, ) -from .lvcode import LocalVariable, lv, lv_assign, lv_expr +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr +from ..types import LvType, ObjUpdateAction +from . import Widget, WidgetType, get_widgets +from .arc import CONF_ARC +from .img import CONF_IMAGE +from .line import CONF_LINE from .obj import obj_spec -from .types import LvType, ObjUpdateAction -from .widget import Widget, WidgetType, get_widgets CONF_ANGLE_RANGE = "angle_range" CONF_COLOR_END = "color_end" @@ -171,7 +171,7 @@ class MeterType(WidgetType): """For a meter object, create and set parameters""" var = w.obj - for scale_conf in config.get(CONF_SCALES) or (): + for scale_conf in config.get(CONF_SCALES, ()): rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 if CONF_ROTATION in scale_conf: rotation = scale_conf[CONF_ROTATION] // 10 @@ -208,7 +208,7 @@ class MeterType(WidgetType): color, major[CONF_LABEL_GAP], ) - for indicator in scale_conf.get(CONF_INDICATORS) or (): + for indicator in scale_conf.get(CONF_INDICATORS, ()): (t, v) = next(iter(indicator.items())) iid = v[CONF_ID] ivar = cg.new_variable( diff --git a/esphome/components/lvgl/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py similarity index 72% rename from esphome/components/lvgl/msgbox.py rename to esphome/components/lvgl/widgets/msgbox.py index 6dd529d77f..4ae5be7701 100644 --- a/esphome/components/lvgl/msgbox.py +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -4,16 +4,7 @@ from esphome.core import ID from esphome.cpp_generator import new_Pvariable, static_const_array from esphome.cpp_types import nullptr -from .button import button_spec -from .buttonmatrix import ( - BUTTONMATRIX_BUTTON_SCHEMA, - CONF_BUTTON_TEXT_LIST_ID, - buttonmatrix_spec, - get_button_data, - lv_buttonmatrix_t, - set_btn_data, -) -from .defines import ( +from ..defines import ( CONF_BODY, CONF_BUTTONS, CONF_CLOSE_BUTTON, @@ -23,10 +14,9 @@ from .defines import ( TYPE_FLEX, literal, ) -from .helpers import add_lv_use -from .label import CONF_LABEL -from .lv_validation import lv_bool, lv_pct, lv_text -from .lvcode import ( +from ..helpers import add_lv_use +from ..lv_validation import lv_bool, lv_pct, lv_text +from ..lvcode import ( EVENT_ARG, LambdaContext, LocalVariable, @@ -36,11 +26,21 @@ from .lvcode import ( lv_obj, lv_Pvariable, ) +from ..schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema +from ..styles import TOP_LAYER +from ..types import LV_EVENT, char_ptr, lv_obj_t +from . import Widget, set_obj_properties +from .button import button_spec +from .buttonmatrix import ( + BUTTONMATRIX_BUTTON_SCHEMA, + CONF_BUTTON_TEXT_LIST_ID, + buttonmatrix_spec, + get_button_data, + lv_buttonmatrix_t, + set_btn_data, +) +from .label import CONF_LABEL from .obj import obj_spec -from .schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema -from .styles import TOP_LAYER -from .types import LV_EVENT, char_ptr, lv_obj_t -from .widget import Widget, set_obj_properties CONF_MSGBOX = "msgbox" MSGBOX_SCHEMA = container_schema( @@ -73,15 +73,23 @@ async def msgbox_to_code(conf): *buttonmatrix_spec.get_uses(), *button_spec.get_uses(), ) - mbid = conf[CONF_ID] - outer = lv_Pvariable(lv_obj_t, mbid.id) - btnm = new_Pvariable( - ID(f"{mbid.id}_btnm_", is_declaration=True, type=lv_buttonmatrix_t) + messagebox_id = conf[CONF_ID] + outer = lv_Pvariable(lv_obj_t, messagebox_id.id) + buttonmatrix = new_Pvariable( + ID( + f"{messagebox_id.id}_buttonmatrix_", + is_declaration=True, + type=lv_buttonmatrix_t, + ) + ) + msgbox = lv_Pvariable(lv_obj_t, f"{messagebox_id.id}_msgbox") + outer_widget = Widget.create(messagebox_id, outer, obj_spec, conf) + buttonmatrix_widget = Widget.create( + str(buttonmatrix), buttonmatrix, buttonmatrix_spec, conf + ) + text_list, ctrl_list, width_list, _ = await get_button_data( + (conf,), buttonmatrix_widget ) - msgbox = lv_Pvariable(lv_obj_t, f"{mbid.id}_msgbox") - outer_w = Widget.create(mbid, outer, obj_spec, conf) - btnm_widg = Widget.create(str(btnm), btnm, buttonmatrix_spec, conf) - text_list, ctrl_list, width_list, _ = await get_button_data((conf,), btnm_widg) text_id = conf[CONF_BUTTON_TEXT_LIST_ID] text_list = static_const_array(text_id, text_list) if (text := conf.get(CONF_BODY)) is not None: @@ -97,16 +105,16 @@ async def msgbox_to_code(conf): lv_obj.set_style_border_width(outer, 0, 0) lv_obj.set_style_pad_all(outer, 0, 0) lv_obj.set_style_radius(outer, 0, 0) - outer_w.add_flag("LV_OBJ_FLAG_HIDDEN") + outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") lv_assign( msgbox, lv_expr.msgbox_create(outer, title, text, text_list, close_button) ) lv_obj.set_style_align(msgbox, literal("LV_ALIGN_CENTER"), 0) - lv_add(btnm.set_obj(lv_expr.msgbox_get_btns(msgbox))) - await set_obj_properties(outer_w, conf) + lv_add(buttonmatrix.set_obj(lv_expr.msgbox_get_btns(msgbox))) + await set_obj_properties(outer_widget, conf) if close_button: - async with LambdaContext(EVENT_ARG, where=mbid) as context: - outer_w.add_flag("LV_OBJ_FLAG_HIDDEN") + async with LambdaContext(EVENT_ARG, where=messagebox_id) as context: + outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") with LocalVariable( "close_btn_", lv_obj_t, lv_expr.msgbox_get_close_btn(msgbox) ) as close_btn: @@ -119,7 +127,7 @@ async def msgbox_to_code(conf): ) if len(ctrl_list) != 0 or len(width_list) != 0: - set_btn_data(btnm.obj, ctrl_list, width_list) + set_btn_data(buttonmatrix.obj, ctrl_list, width_list) async def msgboxes_to_code(config): diff --git a/esphome/components/lvgl/obj.py b/esphome/components/lvgl/widgets/obj.py similarity index 76% rename from esphome/components/lvgl/obj.py rename to esphome/components/lvgl/widgets/obj.py index 40d7e55381..20a24c86f6 100644 --- a/esphome/components/lvgl/obj.py +++ b/esphome/components/lvgl/widgets/obj.py @@ -1,9 +1,9 @@ from esphome import automation -from .automation import update_to_code -from .defines import CONF_MAIN, CONF_OBJ -from .schemas import create_modify_schema -from .types import ObjUpdateAction, WidgetType, lv_obj_t +from ..automation import update_to_code +from ..defines import CONF_MAIN, CONF_OBJ +from ..schemas import create_modify_schema +from ..types import ObjUpdateAction, WidgetType, lv_obj_t class ObjType(WidgetType): diff --git a/esphome/components/lvgl/page.py b/esphome/components/lvgl/widgets/page.py similarity index 91% rename from esphome/components/lvgl/page.py rename to esphome/components/lvgl/widgets/page.py index 4566b7eea4..f80d802b33 100644 --- a/esphome/components/lvgl/page.py +++ b/esphome/components/lvgl/widgets/page.py @@ -2,7 +2,7 @@ from esphome import automation, codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME -from .defines import ( +from ..defines import ( CONF_ANIMATION, CONF_LVGL_ID, CONF_PAGE, @@ -10,11 +10,11 @@ from .defines import ( CONF_SKIP, LV_ANIM, ) -from .lv_validation import lv_bool, lv_milliseconds -from .lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp -from .schemas import LVGL_SCHEMA -from .types import LvglAction, lv_page_t -from .widget import Widget, WidgetType, add_widgets, set_obj_properties +from ..lv_validation import lv_bool, lv_milliseconds +from ..lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp +from ..schemas import LVGL_SCHEMA +from ..types import LvglAction, lv_page_t +from . import Widget, WidgetType, add_widgets, set_obj_properties class PageType(WidgetType): diff --git a/esphome/components/lvgl/roller.py b/esphome/components/lvgl/widgets/roller.py similarity index 92% rename from esphome/components/lvgl/roller.py rename to esphome/components/lvgl/widgets/roller.py index 7af3ef3c3d..50fdf6113c 100644 --- a/esphome/components/lvgl/roller.py +++ b/esphome/components/lvgl/widgets/roller.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_MODE, CONF_OPTIONS -from .defines import ( +from ..defines import ( CONF_ANIMATED, CONF_MAIN, CONF_SELECTED, @@ -11,11 +11,11 @@ from .defines import ( ROLLER_MODES, literal, ) +from ..lv_validation import animated, lv_int, option_string +from ..lvcode import lv +from ..types import LvSelect +from . import WidgetType from .label import CONF_LABEL -from .lv_validation import animated, lv_int, option_string -from .lvcode import lv -from .types import LvSelect -from .widget import WidgetType CONF_ROLLER = "roller" lv_roller_t = LvSelect("lv_roller_t") diff --git a/esphome/components/lvgl/slider.py b/esphome/components/lvgl/widgets/slider.py similarity index 88% rename from esphome/components/lvgl/slider.py rename to esphome/components/lvgl/widgets/slider.py index 1886f79b44..d5017668e4 100644 --- a/esphome/components/lvgl/slider.py +++ b/esphome/components/lvgl/widgets/slider.py @@ -1,7 +1,7 @@ import esphome.config_validation as cv from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE -from .defines import ( +from ..defines import ( BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, @@ -9,12 +9,12 @@ from .defines import ( CONF_MAIN, literal, ) -from .helpers import add_lv_use +from ..helpers import add_lv_use +from ..lv_validation import animated, get_start_value, lv_float +from ..lvcode import lv +from ..types import LvNumber, NumberType +from . import Widget from .lv_bar import CONF_BAR -from .lv_validation import animated, get_start_value, lv_float -from .lvcode import lv -from .types import LvNumber, NumberType -from .widget import Widget CONF_SLIDER = "slider" SLIDER_MODIFY_SCHEMA = cv.Schema( diff --git a/esphome/components/lvgl/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py similarity index 95% rename from esphome/components/lvgl/spinbox.py rename to esphome/components/lvgl/widgets/spinbox.py index 62c58c54a3..b84dc7cd23 100644 --- a/esphome/components/lvgl/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -2,8 +2,8 @@ from esphome import automation import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE -from .automation import action_to_code, update_to_code -from .defines import ( +from ..automation import action_to_code, update_to_code +from ..defines import ( CONF_CURSOR, CONF_DECIMAL_PLACES, CONF_DIGITS, @@ -13,12 +13,12 @@ from .defines import ( CONF_SELECTED, CONF_TEXTAREA_PLACEHOLDER, ) +from ..lv_validation import lv_bool, lv_float +from ..lvcode import lv +from ..types import LvNumber, ObjUpdateAction +from . import Widget, WidgetType, get_widgets from .label import CONF_LABEL -from .lv_validation import lv_bool, lv_float -from .lvcode import lv from .textarea import CONF_TEXTAREA -from .types import LvNumber, ObjUpdateAction -from .widget import Widget, WidgetType, get_widgets CONF_SPINBOX = "spinbox" diff --git a/esphome/components/lvgl/spinner.py b/esphome/components/lvgl/widgets/spinner.py similarity index 82% rename from esphome/components/lvgl/spinner.py rename to esphome/components/lvgl/widgets/spinner.py index 2f798d0fbf..2940feb594 100644 --- a/esphome/components/lvgl/spinner.py +++ b/esphome/components/lvgl/widgets/spinner.py @@ -1,12 +1,12 @@ import esphome.config_validation as cv from esphome.cpp_generator import MockObjClass +from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME +from ..lv_validation import angle +from ..lvcode import lv_expr +from ..types import LvType +from . import Widget, WidgetType from .arc import CONF_ARC -from .defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME -from .lv_validation import angle -from .lvcode import lv_expr -from .types import LvType -from .widget import Widget, WidgetType CONF_SPINNER = "spinner" diff --git a/esphome/components/lvgl/lv_switch.py b/esphome/components/lvgl/widgets/switch.py similarity index 72% rename from esphome/components/lvgl/lv_switch.py rename to esphome/components/lvgl/widgets/switch.py index 5db2c2ce38..a7c1356bf2 100644 --- a/esphome/components/lvgl/lv_switch.py +++ b/esphome/components/lvgl/widgets/switch.py @@ -1,6 +1,6 @@ -from .defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN -from .types import LvBoolean -from .widget import WidgetType +from ..defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN +from ..types import LvBoolean +from . import WidgetType CONF_SWITCH = "switch" diff --git a/esphome/components/lvgl/tabview.py b/esphome/components/lvgl/widgets/tabview.py similarity index 88% rename from esphome/components/lvgl/tabview.py rename to esphome/components/lvgl/widgets/tabview.py index 7b6a864e21..226fc3f286 100644 --- a/esphome/components/lvgl/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -4,9 +4,8 @@ import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_INDEX, CONF_NAME, CONF_POSITION, CONF_SIZE from esphome.cpp_generator import MockObjClass -from . import buttonmatrix_spec -from .automation import action_to_code -from .defines import ( +from ..automation import action_to_code +from ..defines import ( CONF_ANIMATED, CONF_MAIN, CONF_TAB_ID, @@ -15,12 +14,13 @@ from .defines import ( TYPE_FLEX, literal, ) -from .lv_validation import animated, lv_int, size -from .lvcode import LocalVariable, lv, lv_assign, lv_expr +from ..lv_validation import animated, lv_int, size +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr +from ..schemas import container_schema, part_schema +from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr +from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties +from .buttonmatrix import buttonmatrix_spec from .obj import obj_spec -from .schemas import container_schema, part_schema -from .types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr -from .widget import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties CONF_TABVIEW = "tabview" CONF_TAB_STYLE = "tab_style" diff --git a/esphome/components/lvgl/textarea.py b/esphome/components/lvgl/widgets/textarea.py similarity index 90% rename from esphome/components/lvgl/textarea.py rename to esphome/components/lvgl/widgets/textarea.py index d383e1f098..61d83dee9c 100644 --- a/esphome/components/lvgl/textarea.py +++ b/esphome/components/lvgl/widgets/textarea.py @@ -1,7 +1,7 @@ import esphome.config_validation as cv from esphome.const import CONF_MAX_LENGTH -from .defines import ( +from ..defines import ( CONF_ACCEPTED_CHARS, CONF_CURSOR, CONF_MAIN, @@ -13,10 +13,10 @@ from .defines import ( CONF_TEXT, CONF_TEXTAREA_PLACEHOLDER, ) -from .lv_validation import lv_bool, lv_int, lv_text -from .schemas import TEXT_SCHEMA -from .types import LvText -from .widget import Widget, WidgetType +from ..lv_validation import lv_bool, lv_int, lv_text +from ..schemas import TEXT_SCHEMA +from ..types import LvText +from . import Widget, WidgetType CONF_TEXTAREA = "textarea" diff --git a/esphome/components/lvgl/tileview.py b/esphome/components/lvgl/widgets/tileview.py similarity index 89% rename from esphome/components/lvgl/tileview.py rename to esphome/components/lvgl/widgets/tileview.py index aa841fa23e..9a426c7daf 100644 --- a/esphome/components/lvgl/tileview.py +++ b/esphome/components/lvgl/widgets/tileview.py @@ -3,8 +3,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ON_VALUE, CONF_ROW, CONF_TRIGGER_ID -from .automation import action_to_code -from .defines import ( +from ..automation import action_to_code +from ..defines import ( CONF_ANIMATED, CONF_COLUMN, CONF_DIR, @@ -14,12 +14,12 @@ from .defines import ( TILE_DIRECTIONS, literal, ) -from .lv_validation import animated, lv_int -from .lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable +from ..lv_validation import animated, lv_int +from ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable +from ..schemas import container_schema +from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr +from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties from .obj import obj_spec -from .schemas import container_schema -from .types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr -from .widget import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties CONF_TILEVIEW = "tileview" @@ -68,7 +68,7 @@ class TileviewType(WidgetType): ) async def to_code(self, w: Widget, config: dict): - for tile_conf in config.get(CONF_TILES) or (): + for tile_conf in config.get(CONF_TILES, ()): w_id = tile_conf[CONF_ID] tile_obj = lv_Pvariable(lv_obj_t, w_id) tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf) diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 6d0c1967b4..35d924d939 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -54,3 +54,75 @@ font: "\U000f0084", "\U000f0091", ] + +sensor: + - platform: lvgl + id: lvgl_sensor_id + name: "LVGL Arc Sensor" + widget: lv_arc + - platform: lvgl + widget: slider_id + name: LVGL Slider + - platform: lvgl + widget: bar_id + id: lvgl_bar_sensor + name: LVGL Bar + - platform: lvgl + widget: spinbox_id + name: LVGL Spinbox + +number: + - platform: lvgl + widget: slider_id + name: LVGL Slider + - platform: lvgl + widget: lv_arc + id: lvgl_arc_number + name: LVGL Arc + - platform: lvgl + widget: bar_id + id: lvgl_bar_number + name: LVGL Bar + - platform: lvgl + widget: spinbox_id + id: lvgl_spinbox_number + name: LVGL Spinbox + +light: + - platform: lvgl + name: LVGL LED + id: lv_light + led: lv_led + +binary_sensor: + - platform: lvgl + id: lvgl_pressbutton + name: Pressbutton + widget: spin_up + publish_initial_state: true + - platform: lvgl + name: ButtonMatrix button + widget: button_a + - platform: lvgl + id: switch_d + name: Matrix switch D + widget: button_d + on_click: + then: + - lvgl.page.previous: + animation: move_right + time: 600ms + - platform: lvgl + id: button_checker + name: LVGL button + widget: spin_up + on_state: + then: + - lvgl.checkbox.update: + id: checkbox_id + state: + checked: !lambda return x; + text: Unchecked + - platform: lvgl + name: LVGL checkbox + widget: checkbox_id From e6b1780a31d54b46628c919f90433db4e7681ec4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:39:47 +1200 Subject: [PATCH 040/160] Move ``CONF_BACKGROUND_COLOR`` and ``CONF_FOREGROUND_COLOR`` to const.py (#7202) --- .../graphical_display_menu/__init__.py | 17 ++++++++++------- esphome/components/nextion/base_component.py | 13 +++++-------- esphome/const.py | 2 ++ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/esphome/components/graphical_display_menu/__init__.py b/esphome/components/graphical_display_menu/__init__.py index 1b3ed7f8cd..d7146a7381 100644 --- a/esphome/components/graphical_display_menu/__init__.py +++ b/esphome/components/graphical_display_menu/__init__.py @@ -1,19 +1,22 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import display, font, color -from esphome.const import CONF_DISPLAY, CONF_ID, CONF_TRIGGER_ID from esphome import automation, core - +import esphome.codegen as cg +from esphome.components import color, display, font from esphome.components.display_menu_base import ( DISPLAY_MENU_BASE_SCHEMA, DisplayMenuComponent, display_menu_to_code, ) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BACKGROUND_COLOR, + CONF_DISPLAY, + CONF_FOREGROUND_COLOR, + CONF_ID, + CONF_TRIGGER_ID, +) CONF_FONT = "font" CONF_MENU_ITEM_VALUE = "menu_item_value" -CONF_FOREGROUND_COLOR = "foreground_color" -CONF_BACKGROUND_COLOR = "background_color" CONF_ON_REDRAW = "on_redraw" graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu") diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index 784da35371..d12434ec8f 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -1,12 +1,11 @@ from string import ascii_letters, digits -import esphome.config_validation as cv + import esphome.codegen as cg from esphome.components import color -from esphome.const import ( - CONF_VISIBLE, -) -from . import CONF_NEXTION_ID -from . import Nextion +import esphome.config_validation as cv +from esphome.const import CONF_BACKGROUND_COLOR, CONF_FOREGROUND_COLOR, CONF_VISIBLE + +from . import CONF_NEXTION_ID, Nextion CONF_VARIABLE_NAME = "variable_name" CONF_COMPONENT_NAME = "component_name" @@ -24,9 +23,7 @@ CONF_WAKE_UP_PAGE = "wake_up_page" CONF_START_UP_PAGE = "start_up_page" CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch" CONF_WAVE_MAX_LENGTH = "wave_max_length" -CONF_BACKGROUND_COLOR = "background_color" CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" -CONF_FOREGROUND_COLOR = "foreground_color" CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" CONF_FONT_ID = "font_id" CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" diff --git a/esphome/const.py b/esphome/const.py index fcb630badd..d7b1f558a1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -74,6 +74,7 @@ CONF_AWAY = "away" CONF_AWAY_COMMAND_TOPIC = "away_command_topic" CONF_AWAY_CONFIG = "away_config" CONF_AWAY_STATE_TOPIC = "away_state_topic" +CONF_BACKGROUND_COLOR = "background_color" CONF_BACKLIGHT_PIN = "backlight_pin" CONF_BASELINE = "baseline" CONF_BATTERY_LEVEL = "battery_level" @@ -309,6 +310,7 @@ CONF_FLOW = "flow" CONF_FLOW_CONTROL_PIN = "flow_control_pin" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" +CONF_FOREGROUND_COLOR = "foreground_color" CONF_FORMALDEHYDE = "formaldehyde" CONF_FORMAT = "format" CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy" From b0d9800817921fe98f888b296596f3a4ccf39e18 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:02:08 +1200 Subject: [PATCH 041/160] [helpers] Set default flags of ExternalRAMAllocator to ALLOW_FAILURE (#7201) --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b4ad22b083..3e6fe9433e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -680,7 +680,7 @@ template class ExternalRAMAllocator { } private: - Flags flags_{Flags::NONE}; + Flags flags_{Flags::ALLOW_FAILURE}; }; /// @} From 9188836f707db9924c95233cb604dbc51a8fa24f Mon Sep 17 00:00:00 2001 From: guillempages Date: Tue, 6 Aug 2024 13:08:06 +0200 Subject: [PATCH 042/160] Add runtime online image support (#4710) --- CODEOWNERS | 1 + esphome/components/online_image/__init__.py | 161 ++++++++++ .../components/online_image/image_decoder.cpp | 44 +++ .../components/online_image/image_decoder.h | 112 +++++++ .../components/online_image/online_image.cpp | 275 ++++++++++++++++++ .../components/online_image/online_image.h | 184 ++++++++++++ esphome/components/online_image/png_image.cpp | 68 +++++ esphome/components/online_image/png_image.h | 33 +++ esphome/core/defines.h | 1 + platformio.ini | 1 + .../components/online_image/common-esp32.yaml | 18 ++ .../online_image/common-esp8266.yaml | 18 ++ tests/components/online_image/common.yaml | 37 +++ .../online_image/test.esp32-ard.yaml | 4 + .../online_image/test.esp32-idf.yaml | 4 + 15 files changed, 961 insertions(+) create mode 100644 esphome/components/online_image/__init__.py create mode 100644 esphome/components/online_image/image_decoder.cpp create mode 100644 esphome/components/online_image/image_decoder.h create mode 100644 esphome/components/online_image/online_image.cpp create mode 100644 esphome/components/online_image/online_image.h create mode 100644 esphome/components/online_image/png_image.cpp create mode 100644 esphome/components/online_image/png_image.h create mode 100644 tests/components/online_image/common-esp32.yaml create mode 100644 tests/components/online_image/common-esp8266.yaml create mode 100644 tests/components/online_image/common.yaml create mode 100644 tests/components/online_image/test.esp32-ard.yaml create mode 100644 tests/components/online_image/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index d94c34c019..82e6e0ea4b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -276,6 +276,7 @@ esphome/components/nfc/* @jesserockz @kbx81 esphome/components/noblex/* @AGalfra esphome/components/number/* @esphome/core esphome/components/one_wire/* @ssieb +esphome/components/online_image/* @guillempages esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/pca6416a/* @Mat931 diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py new file mode 100644 index 0000000000..ee5357457a --- /dev/null +++ b/esphome/components/online_image/__init__.py @@ -0,0 +1,161 @@ +import logging + +from esphome import automation +import esphome.codegen as cg +from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent +from esphome.components.image import ( + CONF_USE_TRANSPARENCY, + IMAGE_TYPE, + Image_, + validate_cross_dependencies, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BUFFER_SIZE, + CONF_FORMAT, + CONF_ID, + CONF_ON_ERROR, + CONF_RESIZE, + CONF_TRIGGER_ID, + CONF_TYPE, + CONF_URL, +) + +AUTO_LOAD = ["image"] +DEPENDENCIES = ["display", "http_request"] +CODEOWNERS = ["@guillempages"] +MULTI_CONF = True + +CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" + +_LOGGER = logging.getLogger(__name__) + +online_image_ns = cg.esphome_ns.namespace("online_image") + +ImageFormat = online_image_ns.enum("ImageFormat") + +FORMAT_PNG = "PNG" + +IMAGE_FORMAT = {FORMAT_PNG: ImageFormat.PNG} # Add new supported formats here + +OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) + +# Actions +SetUrlAction = online_image_ns.class_( + "OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage) +) +ReleaseImageAction = online_image_ns.class_( + "OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage) +) + +# Triggers +DownloadFinishedTrigger = online_image_ns.class_( + "DownloadFinishedTrigger", automation.Trigger.template() +) +DownloadErrorTrigger = online_image_ns.class_( + "DownloadErrorTrigger", automation.Trigger.template() +) + +ONLINE_IMAGE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(OnlineImage), + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), + # + # Common image options + # + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), + # Not setting default here on purpose; the default depends on the image type, + # and thus will be set in the "validate_cross_dependencies" validator. + cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, + # + # Online Image specific options + # + cv.Required(CONF_URL): cv.url, + cv.Required(CONF_FORMAT): cv.enum(IMAGE_FORMAT, upper=True), + cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536), + cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadFinishedTrigger), + } + ), + cv.Optional(CONF_ON_ERROR): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger), + } + ), + } +).extend(cv.polling_component_schema("never")) + +CONFIG_SCHEMA = cv.Schema( + cv.All( + ONLINE_IMAGE_SCHEMA, + validate_cross_dependencies, + cv.require_framework_version( + # esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed + # esp8266_arduino=cv.Version(2, 7, 0), + esp32_arduino=cv.Version(0, 0, 0), + esp_idf=cv.Version(4, 0, 0), + ), + ) +) + +SET_URL_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(OnlineImage), + cv.Required(CONF_URL): cv.templatable(cv.url), + } +) + +RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(OnlineImage), + } +) + + +@automation.register_action("online_image.set_url", SetUrlAction, SET_URL_SCHEMA) +@automation.register_action( + "online_image.release", ReleaseImageAction, RELEASE_IMAGE_SCHEMA +) +async def online_image_action_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) + + if CONF_URL in config: + template_ = await cg.templatable(config[CONF_URL], args, cg.const_char_ptr) + cg.add(var.set_url(template_)) + return var + + +async def to_code(config): + format = config[CONF_FORMAT] + if format in [FORMAT_PNG]: + cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") + cg.add_library("pngle", "1.0.2") + + url = config[CONF_URL] + width, height = config.get(CONF_RESIZE, (0, 0)) + transparent = config[CONF_USE_TRANSPARENCY] + + var = cg.new_Pvariable( + config[CONF_ID], + url, + width, + height, + format, + config[CONF_TYPE], + config[CONF_BUFFER_SIZE], + ) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) + + cg.add(var.set_transparency(transparent)) + + for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_ERROR, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/image_decoder.cpp new file mode 100644 index 0000000000..50ec39dfcc --- /dev/null +++ b/esphome/components/online_image/image_decoder.cpp @@ -0,0 +1,44 @@ +#include "image_decoder.h" +#include "online_image.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace online_image { + +static const char *const TAG = "online_image.decoder"; + +void ImageDecoder::set_size(int width, int height) { + this->image_->resize_(width, height); + this->x_scale_ = static_cast(this->image_->buffer_width_) / width; + this->y_scale_ = static_cast(this->image_->buffer_height_) / height; +} + +void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { + auto width = std::min(this->image_->buffer_width_, static_cast(std::ceil((x + w) * this->x_scale_))); + auto height = std::min(this->image_->buffer_height_, static_cast(std::ceil((y + h) * this->y_scale_))); + for (int i = x * this->x_scale_; i < width; i++) { + for (int j = y * this->y_scale_; j < height; j++) { + this->image_->draw_pixel_(i, j, color); + } + } +} + +uint8_t *DownloadBuffer::data(size_t offset) { + if (offset > this->size_) { + ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!"); + return this->buffer_; + } + return this->buffer_ + offset; +} + +size_t DownloadBuffer::read(size_t len) { + this->unread_ -= len; + if (this->unread_ > 0) { + memmove(this->data(), this->data(len), this->unread_); + } + return this->unread_; +} + +} // namespace online_image +} // namespace esphome diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h new file mode 100644 index 0000000000..908efab987 --- /dev/null +++ b/esphome/components/online_image/image_decoder.h @@ -0,0 +1,112 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/color.h" + +namespace esphome { +namespace online_image { + +class OnlineImage; + +/** + * @brief Class to abstract decoding different image formats. + */ +class ImageDecoder { + public: + /** + * @brief Construct a new Image Decoder object + * + * @param image The image to decode the stream into. + */ + ImageDecoder(OnlineImage *image) : image_(image) {} + virtual ~ImageDecoder() = default; + + /** + * @brief Initialize the decoder. + * + * @param download_size The total number of bytes that need to be download for the image. + */ + virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; } + + /** + * @brief Decode a part of the image. It will try reading from the buffer. + * There is no guarantee that the whole available buffer will be read/decoded; + * the method will return the amount of bytes actually decoded, so that the + * unread content can be moved to the beginning. + * + * @param buffer The buffer to read from. + * @param size The maximum amount of bytes that can be read from the buffer. + * @return int The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully + * decode anything, or negative in case of a decoding error. + */ + virtual int decode(uint8_t *buffer, size_t size); + + /** + * @brief Request the image to be resized once the actual dimensions are known. + * Called by the callback functions, to be able to access the parent Image class. + * + * @param width The image's width. + * @param height The image's height. + */ + void set_size(int width, int height); + + /** + * @brief Draw a rectangle on the display_buffer using the defined color. + * Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly. + * In case of binary displays, the color will be converted to binary as well. + * Called by the callback functions, to be able to access the parent Image class. + * + * @param x The left-most coordinate of the rectangle. + * @param y The top-most coordinate of the rectangle. + * @param w The width of the rectangle. + * @param h The height of the rectangle. + * @param color The color to draw the rectangle with. + */ + void draw(int x, int y, int w, int h, const Color &color); + + bool is_finished() const { return this->decoded_bytes_ == this->download_size_; } + + protected: + OnlineImage *image_; + // Initializing to 1, to ensure it is different than initial "decoded_bytes_". + // Will be overwritten anyway once the download size is known. + uint32_t download_size_ = 1; + uint32_t decoded_bytes_ = 0; + double x_scale_ = 1.0; + double y_scale_ = 1.0; +}; + +class DownloadBuffer { + public: + DownloadBuffer(size_t size) : size_(size) { + this->buffer_ = this->allocator_.allocate(size); + this->reset(); + } + + virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } + + uint8_t *data(size_t offset = 0); + + uint8_t *append() { return this->data(this->unread_); } + + size_t unread() const { return this->unread_; } + size_t size() const { return this->size_; } + size_t free_capacity() const { return this->size_ - this->unread_; } + + size_t read(size_t len); + size_t write(size_t len) { + this->unread_ += len; + return this->unread_; + } + + void reset() { this->unread_ = 0; } + + protected: + ExternalRAMAllocator allocator_; + uint8_t *buffer_; + size_t size_; + /** Total number of downloaded bytes not yet read. */ + size_t unread_; +}; + +} // namespace online_image +} // namespace esphome diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp new file mode 100644 index 0000000000..a4cf0158aa --- /dev/null +++ b/esphome/components/online_image/online_image.cpp @@ -0,0 +1,275 @@ +#include "online_image.h" + +#include "esphome/core/log.h" + +static const char *const TAG = "online_image"; + +#include "image_decoder.h" + +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "png_image.h" +#endif + +namespace esphome { +namespace online_image { + +using image::ImageType; + +inline bool is_color_on(const Color &color) { + // This produces the most accurate monochrome conversion, but is slightly slower. + // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; + + // Approximation using fast integer computations; produces acceptable results + // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B + return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80; +} + +OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, + uint32_t download_buffer_size) + : Image(nullptr, 0, 0, type), + buffer_(nullptr), + download_buffer_(download_buffer_size), + format_(format), + fixed_width_(width), + fixed_height_(height) { + this->set_url(url); +} + +void OnlineImage::release() { + if (this->buffer_) { + ESP_LOGD(TAG, "Deallocating old buffer..."); + this->allocator_.deallocate(this->buffer_, this->get_buffer_size_()); + this->data_start_ = nullptr; + this->buffer_ = nullptr; + this->width_ = 0; + this->height_ = 0; + this->buffer_width_ = 0; + this->buffer_height_ = 0; + this->end_connection_(); + } +} + +bool OnlineImage::resize_(int width_in, int height_in) { + int width = this->fixed_width_; + int height = this->fixed_height_; + if (this->auto_resize_()) { + width = width_in; + height = height_in; + if (this->width_ != width && this->height_ != height) { + this->release(); + } + } + if (this->buffer_) { + return false; + } + auto new_size = this->get_buffer_size_(width, height); + ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size); + delay_microseconds_safe(2000); + this->buffer_ = this->allocator_.allocate(new_size); + if (this->buffer_) { + this->buffer_width_ = width; + this->buffer_height_ = height; + this->width_ = width; + ESP_LOGD(TAG, "New size: (%d, %d)", width, height); + } else { +#if defined(USE_ESP8266) + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + int max_block = ESP.getMaxFreeBlockSize(); +#elif defined(USE_ESP32) + int max_block = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); +#else + int max_block = -1; +#endif + ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %d Bytes", max_block); + this->end_connection_(); + return false; + } + return true; +} + +void OnlineImage::update() { + if (this->decoder_) { + ESP_LOGW(TAG, "Image already being updated."); + return; + } else { + ESP_LOGI(TAG, "Updating image"); + } + + this->downloader_ = this->parent_->get(this->url_); + + if (this->downloader_ == nullptr) { + ESP_LOGE(TAG, "Download failed."); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + + int http_code = this->downloader_->status_code; + if (http_code == HTTP_CODE_NOT_MODIFIED) { + // Image hasn't changed on server. Skip download. + this->end_connection_(); + return; + } + if (http_code != HTTP_CODE_OK) { + ESP_LOGE(TAG, "HTTP result: %d", http_code); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + + ESP_LOGD(TAG, "Starting download"); + size_t total_size = this->downloader_->content_length; + +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT + if (this->format_ == ImageFormat::PNG) { + this->decoder_ = esphome::make_unique(this); + } +#endif // ONLINE_IMAGE_PNG_SUPPORT + + if (!this->decoder_) { + ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported."); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + this->decoder_->prepare(total_size); + ESP_LOGI(TAG, "Downloading image"); +} + +void OnlineImage::loop() { + if (!this->decoder_) { + // Not decoding at the moment => nothing to do. + return; + } + if (!this->downloader_ || this->decoder_->is_finished()) { + ESP_LOGD(TAG, "Image fully downloaded"); + this->data_start_ = buffer_; + this->width_ = buffer_width_; + this->height_ = buffer_height_; + this->end_connection_(); + this->download_finished_callback_.call(); + return; + } + if (this->downloader_ == nullptr) { + ESP_LOGE(TAG, "Downloader not instantiated; cannot download"); + return; + } + size_t available = this->download_buffer_.free_capacity(); + if (available) { + auto len = this->downloader_->read(this->download_buffer_.append(), available); + if (len > 0) { + this->download_buffer_.write(len); + auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread()); + if (fed < 0) { + ESP_LOGE(TAG, "Error when decoding image."); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + this->download_buffer_.read(fed); + } + } +} + +void OnlineImage::draw_pixel_(int x, int y, Color color) { + if (!this->buffer_) { + ESP_LOGE(TAG, "Buffer not allocated!"); + return; + } + if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) { + ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y); + return; + } + uint32_t pos = this->get_position_(x, y); + switch (this->type_) { + case ImageType::IMAGE_TYPE_BINARY: { + const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; + const uint32_t pos = x + y * width_8; + if ((this->has_transparency() && color.w > 127) || is_color_on(color)) { + this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u)); + } else { + this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u)); + } + break; + } + case ImageType::IMAGE_TYPE_GRAYSCALE: { + uint8_t gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); + if (this->has_transparency()) { + if (gray == 1) { + gray = 0; + } + if (color.w < 0x80) { + gray = 1; + } + } + this->buffer_[pos] = gray; + break; + } + case ImageType::IMAGE_TYPE_RGB565: { + uint16_t col565 = display::ColorUtil::color_to_565(color); + if (this->has_transparency()) { + if (col565 == 0x0020) { + col565 = 0; + } + if (color.w < 0x80) { + col565 = 0x0020; + } + } + this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); + this->buffer_[pos + 1] = static_cast(col565 & 0xFF); + 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; + } + } + this->buffer_[pos + 0] = color.r; + this->buffer_[pos + 1] = color.g; + this->buffer_[pos + 2] = color.b; + break; + } + } +} + +void OnlineImage::end_connection_() { + if (this->downloader_) { + this->downloader_->end(); + this->downloader_ = nullptr; + } + this->decoder_.reset(); + this->download_buffer_.reset(); +} + +bool OnlineImage::validate_url_(const std::string &url) { + if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { + ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); + return false; + } + return true; +} + +void OnlineImage::add_on_finished_callback(std::function &&callback) { + this->download_finished_callback_.add(std::move(callback)); +} + +void OnlineImage::add_on_error_callback(std::function &&callback) { + this->download_error_callback_.add(std::move(callback)); +} + +} // namespace online_image +} // namespace esphome diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h new file mode 100644 index 0000000000..30e97760ea --- /dev/null +++ b/esphome/components/online_image/online_image.h @@ -0,0 +1,184 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/components/http_request/http_request.h" +#include "esphome/components/image/image.h" + +#include "image_decoder.h" + +namespace esphome { +namespace online_image { + +using t_http_codes = enum { + HTTP_CODE_OK = 200, + HTTP_CODE_NOT_MODIFIED = 304, + HTTP_CODE_NOT_FOUND = 404, +}; + +/** + * @brief Format that the image is encoded with. + */ +enum ImageFormat { + /** Automatically detect from MIME type. Not supported yet. */ + AUTO, + /** JPEG format. Not supported yet. */ + JPEG, + /** PNG format. */ + PNG, +}; + +/** + * @brief Download an image from a given URL, and decode it using the specified decoder. + * The image will then be stored in a buffer, so that it can be re-displayed without the + * need to re-download or re-decode. + */ +class OnlineImage : public PollingComponent, + public image::Image, + public Parented { + public: + /** + * @brief Construct a new OnlineImage object. + * + * @param url URL to download the image from. + * @param width Desired width of the target image area. + * @param height Desired height of the target image area. + * @param format Format that the image is encoded in (@see ImageFormat). + * @param buffer_size Size of the buffer used to download the image. + */ + OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, + uint32_t buffer_size); + + void update() override; + void loop() override; + + /** Set the URL to download the image from. */ + void set_url(const std::string &url) { + if (this->validate_url_(url)) { + this->url_ = url; + } + } + + /** + * Release the buffer storing the image. The image will need to be downloaded again + * to be able to be displayed. + */ + void release(); + + void add_on_finished_callback(std::function &&callback); + void add_on_error_callback(std::function &&callback); + + protected: + bool validate_url_(const std::string &url); + + using Allocator = ExternalRAMAllocator; + Allocator allocator_{Allocator::Flags::ALLOW_FAILURE}; + + uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); } + int get_buffer_size_(int width, int height) const { + return std::ceil(image::image_type_to_bpp(this->type_) * width * height / 8.0); + } + + int get_position_(int x, int y) const { + return ((x + y * this->buffer_width_) * image::image_type_to_bpp(this->type_)) / 8; + } + + ESPHOME_ALWAYS_INLINE bool auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; } + + bool resize_(int width, int height); + + /** + * @brief Draw a pixel into the buffer. + * + * This is used by the decoder to fill the buffer that will later be displayed + * by the `draw` method. This will internally convert the supplied 32 bit RGBA + * color into the requested image storage format. + * + * @param x Horizontal pixel position. + * @param y Vertical pixel position. + * @param color 32 bit color to put into the pixel. + */ + void draw_pixel_(int x, int y, Color color); + + void end_connection_(); + + CallbackManager download_finished_callback_{}; + CallbackManager download_error_callback_{}; + + std::shared_ptr downloader_{nullptr}; + std::unique_ptr decoder_{nullptr}; + + uint8_t *buffer_; + DownloadBuffer download_buffer_; + + const ImageFormat format_; + + std::string url_{""}; + + /** width requested on configuration, or 0 if non specified. */ + const int fixed_width_; + /** height requested on configuration, or 0 if non specified. */ + const int fixed_height_; + /** + * Actual width of the current image. If fixed_width_ is specified, + * this will be equal to it; otherwise it will be set once the decoding + * starts and the original size is known. + * This needs to be separate from "BaseImage::get_width()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). + */ + int buffer_width_; + /** + * Actual height of the current image. If fixed_height_ is specified, + * this will be equal to it; otherwise it will be set once the decoding + * starts and the original size is known. + * This needs to be separate from "BaseImage::get_height()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). + */ + int buffer_height_; + + friend void ImageDecoder::set_size(int width, int height); + friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color); +}; + +template class OnlineImageSetUrlAction : public Action { + public: + OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(const char *, url) + void play(Ts... x) override { + this->parent_->set_url(this->url_.value(x...)); + this->parent_->update(); + } + + protected: + OnlineImage *parent_; +}; + +template class OnlineImageReleaseAction : public Action { + public: + OnlineImageReleaseAction(OnlineImage *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(const char *, url) + void play(Ts... x) override { this->parent_->release(); } + + protected: + OnlineImage *parent_; +}; + +class DownloadFinishedTrigger : public Trigger<> { + public: + explicit DownloadFinishedTrigger(OnlineImage *parent) { + parent->add_on_finished_callback([this]() { this->trigger(); }); + } +}; + +class DownloadErrorTrigger : public Trigger<> { + public: + explicit DownloadErrorTrigger(OnlineImage *parent) { + parent->add_on_error_callback([this]() { this->trigger(); }); + } +}; + +} // namespace online_image +} // namespace esphome diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp new file mode 100644 index 0000000000..c8e215a91d --- /dev/null +++ b/esphome/components/online_image/png_image.cpp @@ -0,0 +1,68 @@ +#include "png_image.h" +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT + +#include "esphome/components/display/display_buffer.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +static const char *const TAG = "online_image.png"; + +namespace esphome { +namespace online_image { + +/** + * @brief Callback method that will be called by the PNGLE engine when the basic + * data of the image is received (i.e. width and height); + * + * @param pngle The PNGLE object, including the context data. + * @param w The width of the image. + * @param h The height of the image. + */ +static void init_callback(pngle_t *pngle, uint32_t w, uint32_t h) { + PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); + decoder->set_size(w, h); +} + +/** + * @brief Callback method that will be called by the PNGLE engine when a chunk + * of the image is decoded. + * + * @param pngle The PNGLE object, including the context data. + * @param x The X coordinate to draw the rectangle on. + * @param y The Y coordinate to draw the rectangle on. + * @param w The width of the rectangle to draw. + * @param h The height of the rectangle to draw. + * @param rgba The color to paint the rectangle in. + */ +static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) { + PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); + Color color(rgba[0], rgba[1], rgba[2], rgba[3]); + decoder->draw(x, y, w, h, color); +} + +void PngDecoder::prepare(uint32_t download_size) { + ImageDecoder::prepare(download_size); + pngle_set_user_data(this->pngle_, this); + pngle_set_init_callback(this->pngle_, init_callback); + pngle_set_draw_callback(this->pngle_, draw_callback); +} + +int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { + if (size < 256 && size < this->download_size_ - this->decoded_bytes_) { + ESP_LOGD(TAG, "Waiting for data"); + return 0; + } + auto fed = pngle_feed(this->pngle_, buffer, size); + if (fed < 0) { + ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_)); + } else { + this->decoded_bytes_ += fed; + } + return fed; +} + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_PNG_SUPPORT diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h new file mode 100644 index 0000000000..a928276dcc --- /dev/null +++ b/esphome/components/online_image/png_image.h @@ -0,0 +1,33 @@ +#pragma once + +#include "image_decoder.h" +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include + +namespace esphome { +namespace online_image { + +/** + * @brief Image decoder specialization for PNG images. + */ +class PngDecoder : public ImageDecoder { + public: + /** + * @brief Construct a new PNG Decoder object. + * + * @param display The image to decode the stream into. + */ + PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {} + ~PngDecoder() override { pngle_destroy(this->pngle_); } + + void prepare(uint32_t download_size) override; + int HOT decode(uint8_t *buffer, size_t size) override; + + protected: + pngle_t *pngle_; +}; + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_PNG_SUPPORT diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b7bdbb1f9d..61a4940d01 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -53,6 +53,7 @@ #define USE_MQTT #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER +#define USE_ONLINE_IMAGE_PNG_SUPPORT #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK diff --git a/platformio.ini b/platformio.ini index e4f363d650..87a239207f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -40,6 +40,7 @@ lib_deps = wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 pavlodn/HaierProtocol@0.9.31 ; haier + kikuchan98/pngle@1.0.2 ; online_image ; This is using the repository until a new release is published to PlatformIO https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library lvgl/lvgl@8.4.0 ; lvgl diff --git a/tests/components/online_image/common-esp32.yaml b/tests/components/online_image/common-esp32.yaml new file mode 100644 index 0000000000..8cc50fc3e0 --- /dev/null +++ b/tests/components/online_image/common-esp32.yaml @@ -0,0 +1,18 @@ +<<: !include common.yaml + +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 12 + dc_pin: 13 + reset_pin: 21 + lambda: |- + it.fill(Color(0, 0, 0)); + it.image(0, 0, id(online_rgba_image)); diff --git a/tests/components/online_image/common-esp8266.yaml b/tests/components/online_image/common-esp8266.yaml new file mode 100644 index 0000000000..01e3467413 --- /dev/null +++ b/tests/components/online_image/common-esp8266.yaml @@ -0,0 +1,18 @@ +<<: !include common.yaml + +spi: + - id: spi_main_lcd + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 15 + dc_pin: 3 + reset_pin: 1 + lambda: |- + it.fill(Color(0, 0, 0)); + it.image(0, 0, id(online_rgba_image)); diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml new file mode 100644 index 0000000000..8f7ea6238b --- /dev/null +++ b/tests/components/online_image/common.yaml @@ -0,0 +1,37 @@ +wifi: + ssid: MySSID + password: password1 + +# Purposely test that `online_image:` does auto-load `image:` +# Keep the `image:` undefined. +# image: +online_image: + - id: online_binary_image + url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png + format: PNG + type: BINARY + resize: 50x50 + - id: online_binary_transparent_image + url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png + type: TRANSPARENT_BINARY + format: png + - id: online_rgba_image + url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png + format: PNG + type: RGBA + - id: online_rgb24_image + url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png + format: PNG + type: RGB24 + use_transparency: true + +# Check the set_url action +time: + - platform: sntp + on_time: + - at: "13:37:42" + then: + - online_image.set_url: + id: online_rgba_image + url: http://www.example.org/example.png + diff --git a/tests/components/online_image/test.esp32-ard.yaml b/tests/components/online_image/test.esp32-ard.yaml new file mode 100644 index 0000000000..4111cbd0ad --- /dev/null +++ b/tests/components/online_image/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +<<: !include common-esp32.yaml + +http_request: + verify_ssl: false diff --git a/tests/components/online_image/test.esp32-idf.yaml b/tests/components/online_image/test.esp32-idf.yaml new file mode 100644 index 0000000000..3f01009812 --- /dev/null +++ b/tests/components/online_image/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +<<: !include common-esp32.yaml + +http_request: + From 455df35e50c5c83cdaf46e79156ee203b03dce3a Mon Sep 17 00:00:00 2001 From: Mimoja Date: Tue, 6 Aug 2024 13:17:02 +0200 Subject: [PATCH 043/160] Update i2s_audio_speaker.cppi2s_audio/speaker: Fix fallthrough compiler warning (#7167) --- esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 6b07ecb1b6..1c6c50d8c9 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -233,6 +233,7 @@ void I2SAudioSpeaker::loop() { switch (this->state_) { case speaker::STATE_STARTING: this->start_(); + [[fallthrough]]; case speaker::STATE_RUNNING: case speaker::STATE_STOPPING: this->watch_(); From 8667f51cf08f1147cf02f56bd882beab837317ba Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 7 Aug 2024 07:15:15 +1200 Subject: [PATCH 044/160] Move CONF_ITEMS/CONF_FONT/CONF_TEXT to const.py (#7204) --- .../components/display_menu_base/__init__.py | 29 ++++++++++--------- .../graphical_display_menu/__init__.py | 2 +- esphome/components/lvgl/defines.py | 3 +- esphome/components/lvgl/schemas.py | 5 ++-- esphome/components/lvgl/types.py | 4 +-- .../components/lvgl/widgets/buttonmatrix.py | 4 +-- esphome/components/lvgl/widgets/checkbox.py | 4 ++- esphome/components/lvgl/widgets/keyboard.py | 4 +-- esphome/components/lvgl/widgets/label.py | 2 +- esphome/components/lvgl/widgets/msgbox.py | 3 +- esphome/components/lvgl/widgets/textarea.py | 3 +- esphome/const.py | 3 ++ 12 files changed, 34 insertions(+), 32 deletions(-) diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py index 0c738ba838..8ae9cbc2a4 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -1,23 +1,26 @@ import re -import esphome.codegen as cg -import esphome.config_validation as cv + from esphome import automation, core +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components.number import Number +from esphome.components.select import Select +from esphome.components.switch import Switch +import esphome.config_validation as cv from esphome.const import ( - CONF_ID, - CONF_TYPE, - CONF_TRIGGER_ID, - CONF_ON_VALUE, + CONF_ACTIVE, CONF_COMMAND, CONF_CUSTOM, - CONF_NUMBER, CONF_FORMAT, + CONF_ID, + CONF_ITEMS, CONF_MODE, - CONF_ACTIVE, + CONF_NUMBER, + CONF_ON_VALUE, + CONF_TEXT, + CONF_TRIGGER_ID, + CONF_TYPE, ) -from esphome.automation import maybe_simple_id -from esphome.components.select import Select -from esphome.components.number import Number -from esphome.components.switch import Switch CODEOWNERS = ["@numo68"] @@ -29,10 +32,8 @@ CONF_JOYSTICK = "joystick" CONF_LABEL = "label" CONF_MENU = "menu" CONF_BACK = "back" -CONF_TEXT = "text" CONF_SELECT = "select" CONF_SWITCH = "switch" -CONF_ITEMS = "items" CONF_ON_TEXT = "on_text" CONF_OFF_TEXT = "off_text" CONF_VALUE_LAMBDA = "value_lambda" diff --git a/esphome/components/graphical_display_menu/__init__.py b/esphome/components/graphical_display_menu/__init__.py index d7146a7381..f4d59b22b8 100644 --- a/esphome/components/graphical_display_menu/__init__.py +++ b/esphome/components/graphical_display_menu/__init__.py @@ -10,12 +10,12 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BACKGROUND_COLOR, CONF_DISPLAY, + CONF_FONT, CONF_FOREGROUND_COLOR, CONF_ID, CONF_TRIGGER_ID, ) -CONF_FONT = "font" CONF_MENU_ITEM_VALUE = "menu_item_value" CONF_ON_REDRAW = "on_redraw" diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index ac28f9ed5f..1b41b32c90 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -5,6 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i """ from esphome import codegen as cg, config_validation as cv +from esphome.const import CONF_ITEMS from esphome.core import ID, Lambda from esphome.cpp_generator import MockObj from esphome.cpp_types import uint32 @@ -115,7 +116,6 @@ CONF_SCROLLBAR = "scrollbar" CONF_INDICATOR = "indicator" CONF_KNOB = "knob" CONF_SELECTED = "selected" -CONF_ITEMS = "items" CONF_TICKS = "ticks" CONF_CURSOR = "cursor" CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder" @@ -460,7 +460,6 @@ CONF_SKIP = "skip" CONF_SYMBOL = "symbol" CONF_TAB_ID = "tab_id" CONF_TABS = "tabs" -CONF_TEXT = "text" CONF_TILE = "tile" CONF_TILE_ID = "tile_id" CONF_TILES = "tiles" diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 796783890d..62536bf4d5 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_ID, CONF_ON_VALUE, CONF_STATE, + CONF_TEXT, CONF_TRIGGER_ID, CONF_TYPE, ) @@ -25,7 +26,7 @@ WIDGET_TYPES: dict = {} # A schema for text properties TEXT_SCHEMA = cv.Schema( { - cv.Optional(df.CONF_TEXT): cv.Any( + cv.Optional(CONF_TEXT): cv.Any( cv.All( cv.Schema( { @@ -330,7 +331,7 @@ DISP_BG_SCHEMA = cv.Schema( # A style schema that can include text STYLED_TEXT_SCHEMA = cv.maybe_simple_value( - STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT + STYLE_SCHEMA.extend(TEXT_SCHEMA), key=CONF_TEXT ) # For use by platform components diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index b6f65c8c1b..be17cf62c2 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,10 +1,10 @@ import sys from esphome import automation, codegen as cg -from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_VALUE +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE from esphome.cpp_generator import MockObj, MockObjClass -from .defines import CONF_TEXT, lvgl_ns +from .defines import lvgl_ns from .lvcode import lv_expr diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index 274b4de5ab..e61c5e3477 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_WIDTH +from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH from esphome.cpp_generator import MockObj from ..automation import action_to_code @@ -10,13 +10,11 @@ from ..defines import ( BUTTONMATRIX_CTRLS, CONF_BUTTONS, CONF_CONTROL, - CONF_ITEMS, CONF_KEY_CODE, CONF_MAIN, CONF_ONE_CHECKED, CONF_ROWS, CONF_SELECTED, - CONF_TEXT, ) from ..helpers import lvgl_components_required from ..lv_validation import key_code, lv_bool diff --git a/esphome/components/lvgl/widgets/checkbox.py b/esphome/components/lvgl/widgets/checkbox.py index 6299a2a6a2..79c60a8669 100644 --- a/esphome/components/lvgl/widgets/checkbox.py +++ b/esphome/components/lvgl/widgets/checkbox.py @@ -1,4 +1,6 @@ -from ..defines import CONF_INDICATOR, CONF_MAIN, CONF_TEXT +from esphome.const import CONF_TEXT + +from ..defines import CONF_INDICATOR, CONF_MAIN from ..lv_validation import lv_text from ..lvcode import lv from ..schemas import TEXT_SCHEMA diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py index cff322f5af..ba7edb302e 100644 --- a/esphome/components/lvgl/widgets/keyboard.py +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -1,9 +1,9 @@ from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv -from esphome.const import CONF_MODE +from esphome.const import CONF_ITEMS, CONF_MODE from esphome.cpp_types import std_string -from ..defines import CONF_ITEMS, CONF_MAIN, KEYBOARD_MODES, literal +from ..defines import CONF_MAIN, KEYBOARD_MODES, literal from ..helpers import add_lv_use, lvgl_components_required from ..types import LvCompound, LvType from . import Widget, WidgetType, get_widgets diff --git a/esphome/components/lvgl/widgets/label.py b/esphome/components/lvgl/widgets/label.py index 38f688f2b0..6b04235674 100644 --- a/esphome/components/lvgl/widgets/label.py +++ b/esphome/components/lvgl/widgets/label.py @@ -1,4 +1,5 @@ import esphome.config_validation as cv +from esphome.const import CONF_TEXT from ..defines import ( CONF_LONG_MODE, @@ -6,7 +7,6 @@ from ..defines import ( CONF_RECOLOR, CONF_SCROLLBAR, CONF_SELECTED, - CONF_TEXT, LV_LONG_MODES, ) from ..lv_validation import lv_bool, lv_text diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py index 4ae5be7701..63c4326c7c 100644 --- a/esphome/components/lvgl/widgets/msgbox.py +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -1,5 +1,5 @@ from esphome import config_validation as cv -from esphome.const import CONF_BUTTON, CONF_ID +from esphome.const import CONF_BUTTON, CONF_ID, CONF_TEXT from esphome.core import ID from esphome.cpp_generator import new_Pvariable, static_const_array from esphome.cpp_types import nullptr @@ -9,7 +9,6 @@ from ..defines import ( CONF_BUTTONS, CONF_CLOSE_BUTTON, CONF_MSGBOXES, - CONF_TEXT, CONF_TITLE, TYPE_FLEX, literal, diff --git a/esphome/components/lvgl/widgets/textarea.py b/esphome/components/lvgl/widgets/textarea.py index 61d83dee9c..23d50b3894 100644 --- a/esphome/components/lvgl/widgets/textarea.py +++ b/esphome/components/lvgl/widgets/textarea.py @@ -1,5 +1,5 @@ import esphome.config_validation as cv -from esphome.const import CONF_MAX_LENGTH +from esphome.const import CONF_MAX_LENGTH, CONF_TEXT from ..defines import ( CONF_ACCEPTED_CHARS, @@ -10,7 +10,6 @@ from ..defines import ( CONF_PLACEHOLDER_TEXT, CONF_SCROLLBAR, CONF_SELECTED, - CONF_TEXT, CONF_TEXTAREA_PLACEHOLDER, ) from ..lv_validation import lv_bool, lv_int, lv_text diff --git a/esphome/const.py b/esphome/const.py index d7b1f558a1..13559ecf95 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -308,6 +308,7 @@ CONF_FLASH_LENGTH = "flash_length" CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length" CONF_FLOW = "flow" CONF_FLOW_CONTROL_PIN = "flow_control_pin" +CONF_FONT = "font" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" CONF_FOREGROUND_COLOR = "foreground_color" @@ -407,6 +408,7 @@ CONF_INVERTED = "inverted" CONF_IP_ADDRESS = "ip_address" CONF_IRQ_PIN = "irq_pin" CONF_IS_RGBW = "is_rgbw" +CONF_ITEMS = "items" CONF_JS_INCLUDE = "js_include" CONF_JS_URL = "js_url" CONF_JVC = "jvc" @@ -841,6 +843,7 @@ CONF_TEMPERATURE = "temperature" CONF_TEMPERATURE_OFFSET = "temperature_offset" CONF_TEMPERATURE_SOURCE = "temperature_source" CONF_TEMPERATURE_STEP = "temperature_step" +CONF_TEXT = "text" CONF_TEXT_SENSORS = "text_sensors" CONF_THEN = "then" CONF_THRESHOLD = "threshold" From eccc5a3ea31853986aeae1b66ceaca438aede161 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 7 Aug 2024 05:15:28 +1000 Subject: [PATCH 045/160] [lvgl] Fix compile error when using encoder with buttons only. (#7203) --- esphome/components/lvgl/__init__.py | 6 ++-- esphome/components/lvgl/defines.py | 2 +- .../lvgl/{rotary_encoders.py => encoders.py} | 24 +++++++------- esphome/components/lvgl/lvgl_esphome.cpp | 4 +-- esphome/components/lvgl/lvgl_esphome.h | 21 +++++++----- esphome/components/lvgl/schemas.py | 8 ++--- esphome/components/lvgl/widgets/__init__.py | 32 +++---------------- tests/components/lvgl/test.esp32-ard.yaml | 27 ++++++++++++++++ tests/components/lvgl/test.esp32-idf.yaml | 2 +- 9 files changed, 68 insertions(+), 58 deletions(-) rename esphome/components/lvgl/{rotary_encoders.py => encoders.py} (77%) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 9eb4665874..87fbcab4dc 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -23,9 +23,9 @@ from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid from .automation import disp_update, update_to_code from .defines import CONF_SKIP +from .encoders import ENCODERS_CONFIG, encoders_to_code from .lv_validation import lv_bool, lv_images_used from .lvcode import LvContext, LvglComponent -from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code from .schemas import ( DISP_BG_SCHEMA, FLEX_OBJ_SCHEMA, @@ -256,7 +256,7 @@ async def to_code(config): async with LvContext(lv_component): await touchscreens_to_code(lv_component, config) - await rotary_encoders_to_code(lv_component, config) + await encoders_to_code(lv_component, config) await theme_to_code(config) await styles_to_code(config) await set_obj_properties(lv_scr_act, config) @@ -336,7 +336,7 @@ CONFIG_SCHEMA = ( {cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()} ), cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema, - cv.GenerateID(df.CONF_ROTARY_ENCODERS): ROTARY_ENCODER_CONFIG, + cv.GenerateID(df.CONF_ENCODERS): ENCODERS_CONFIG, } ) .extend(DISP_BG_SCHEMA) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 1b41b32c90..d0047b59f7 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -388,6 +388,7 @@ CONF_DEFAULT = "default" CONF_DEFAULT_FONT = "default_font" CONF_DIR = "dir" CONF_DISPLAYS = "displays" +CONF_ENCODERS = "encoders" CONF_END_ANGLE = "end_angle" CONF_END_VALUE = "end_value" CONF_ENTER_BUTTON = "enter_button" @@ -441,7 +442,6 @@ CONF_RECOLOR = "recolor" CONF_RIGHT_BUTTON = "right_button" CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" -CONF_ROTARY_ENCODERS = "rotary_encoders" CONF_ROWS = "rows" CONF_SCALE_LINES = "scale_lines" CONF_SCROLLBAR_MODE = "scrollbar_mode" diff --git a/esphome/components/lvgl/rotary_encoders.py b/esphome/components/lvgl/encoders.py similarity index 77% rename from esphome/components/lvgl/rotary_encoders.py rename to esphome/components/lvgl/encoders.py index d8a82dbc78..caddc2e47f 100644 --- a/esphome/components/lvgl/rotary_encoders.py +++ b/esphome/components/lvgl/encoders.py @@ -5,25 +5,26 @@ import esphome.config_validation as cv from esphome.const import CONF_GROUP, CONF_ID, CONF_SENSOR from .defines import ( + CONF_ENCODERS, CONF_ENTER_BUTTON, CONF_LEFT_BUTTON, CONF_LONG_PRESS_REPEAT_TIME, CONF_LONG_PRESS_TIME, CONF_RIGHT_BUTTON, - CONF_ROTARY_ENCODERS, ) -from .helpers import lvgl_components_required -from .lvcode import lv, lv_add, lv_expr +from .helpers import lvgl_components_required, requires_component +from .lvcode import lv, lv_add, lv_assign, lv_expr, lv_Pvariable from .schemas import ENCODER_SCHEMA -from .types import lv_indev_type_t -from .widgets import add_group +from .types import lv_group_t, lv_indev_type_t -ROTARY_ENCODER_CONFIG = cv.ensure_list( +ENCODERS_CONFIG = cv.ensure_list( ENCODER_SCHEMA.extend( { cv.Required(CONF_ENTER_BUTTON): cv.use_id(BinarySensor), cv.Required(CONF_SENSOR): cv.Any( - cv.use_id(RotaryEncoderSensor), + cv.All( + cv.use_id(RotaryEncoderSensor), requires_component("rotary_encoder") + ), cv.Schema( { cv.Required(CONF_LEFT_BUTTON): cv.use_id(BinarySensor), @@ -36,10 +37,9 @@ ROTARY_ENCODER_CONFIG = cv.ensure_list( ) -async def rotary_encoders_to_code(var, config): - for enc_conf in config.get(CONF_ROTARY_ENCODERS, ()): +async def encoders_to_code(var, config): + for enc_conf in config.get(CONF_ENCODERS, ()): lvgl_components_required.add("KEY_LISTENER") - lvgl_components_required.add("ROTARY_ENCODER") lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds listener = cg.new_Pvariable( @@ -57,7 +57,9 @@ async def rotary_encoders_to_code(var, config): lv_add(listener.set_sensor(sensor_config)) b_sensor = await cg.get_variable(enc_conf[CONF_ENTER_BUTTON]) cg.add(listener.set_enter_button(b_sensor)) - if group := add_group(enc_conf.get(CONF_GROUP)): + if group := enc_conf.get(CONF_GROUP): + group = lv_Pvariable(lv_group_t, group) + lv_assign(group, lv_expr.group_create()) lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) else: lv.indev_drv_register(listener.get_drv()) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 544643d532..6f23c2421b 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -127,7 +127,7 @@ void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { } #endif // USE_LVGL_TOUCHSCREEN -#ifdef USE_LVGL_ROTARY_ENCODER +#ifdef USE_LVGL_KEY_LISTENER LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) { lv_indev_drv_init(&this->drv_); this->drv_.type = type; @@ -143,7 +143,7 @@ LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_ data->continue_reading = false; }; } -#endif // USE_LVGL_ROTARY_ENCODER +#endif // USE_LVGL_KEY_LISTENER #ifdef USE_LVGL_BUTTONMATRIX void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) { diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 5f2f0ea8df..45841b99d9 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -1,6 +1,13 @@ #pragma once #include "esphome/core/defines.h" +#ifdef USE_LVGL_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif // USE_LVGL_BINARY_SENSOR +#ifdef USE_LVGL_ROTARY_ENCODER +#include "esphome/components/rotary_encoder/rotary_encoder.h" +#endif // USE_LVGL_ROTARY_ENCODER + // required for clang-tidy #ifndef LV_CONF_H #define LV_CONF_SKIP 1 // NOLINT @@ -12,12 +19,7 @@ #include "esphome/core/log.h" #include #include - -#ifdef USE_LVGL_ROTARY_ENCODER -#include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/rotary_encoder/rotary_encoder.h" -#endif // USE_LVGL_ROTARY_ENCODER - +#include #ifdef USE_LVGL_IMAGE #include "esphome/components/image/image.h" #endif // USE_LVGL_IMAGE @@ -202,7 +204,7 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented { public: LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt); @@ -218,9 +220,11 @@ class LVEncoderListener : public Parented { enter_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_ENTER, state); }); } +#ifdef USE_LVGL_ROTARY_ENCODER void set_sensor(rotary_encoder::RotaryEncoderSensor *sensor) { sensor->register_listener([this](int32_t count) { this->set_count(count); }); } +#endif // USE_LVGL_ROTARY_ENCODER void event(int key, bool pressed) { if (!this->parent_->is_paused()) { @@ -243,7 +247,8 @@ class LVEncoderListener : public Parented { int32_t last_count_{}; int key_{}; }; -#endif // USE_LVGL_ROTARY_ENCODER +#endif // USE_LVGL_KEY_LISTENER + #ifdef USE_LVGL_BUTTONMATRIX class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound { public: diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 62536bf4d5..f172ba9f2b 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -16,9 +16,9 @@ from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid, types as ty from .helpers import add_lv_use, requires_component, validate_printf -from .lv_validation import id_name, lv_color, lv_font, lv_image +from .lv_validation import lv_color, lv_font, lv_image from .lvcode import LvglComponent -from .types import WidgetType +from .types import WidgetType, lv_group_t # this will be populated later, in __init__.py to avoid circular imports. WIDGET_TYPES: dict = {} @@ -61,7 +61,7 @@ ENCODER_SCHEMA = cv.Schema( cv.GenerateID(): cv.All( cv.declare_id(ty.LVEncoderListener), requires_component("binary_sensor") ), - cv.Optional(CONF_GROUP): lvalid.id_name, + cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t), cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, } @@ -249,7 +249,7 @@ def obj_schema(widget_type: WidgetType): cv.Schema( { cv.Optional(CONF_STATE): SET_STATE_SCHEMA, - cv.Optional(CONF_GROUP): id_name, + cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), } ) ) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index dff43cf257..f1946015bc 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -6,7 +6,7 @@ from esphome.config_validation import Invalid from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE from esphome.core import ID, TimePeriod from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import AssignmentExpression, CallExpression, MockObj +from esphome.cpp_generator import CallExpression, MockObj from ..defines import ( CONF_DEFAULT, @@ -44,15 +44,7 @@ from ..lvcode import ( lv_Pvariable, ) from ..schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES -from ..types import ( - LV_STATE, - LvType, - WidgetType, - lv_coord_t, - lv_group_t, - lv_obj_t, - lv_obj_t_ptr, -) +from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr EVENT_LAMB = "event_lamb__" @@ -317,7 +309,8 @@ async def set_obj_properties(w: Widget, config): value = await ALL_STYLES[prop].process(value) prop_r = STYLE_REMAP.get(prop, prop) w.set_style(prop_r, value, lv_state) - if group := add_group(config.get(CONF_GROUP)): + if group := config.get(CONF_GROUP): + group = await cg.get_variable(group) lv.group_add_obj(group, w.obj) flag_clr = set() flag_set = set() @@ -404,20 +397,3 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): lv_scr_act_spec = LvScrActType() lv_scr_act = Widget.create(None, literal("lv_scr_act()"), lv_scr_act_spec, {}) - -lv_groups = {} # Widget group names - - -def add_group(name): - if name is None: - return None - fullname = f"lv_esp_group_{name}" - if name not in lv_groups: - gid = ID(fullname, True, type=lv_group_t.operator("ptr")) - lv_add( - AssignmentExpression( - type_=gid.type, modifier="", name=fullname, rhs=lv_expr.group_create() - ) - ) - lv_groups[name] = literal(fullname) - return lv_groups[name] diff --git a/tests/components/lvgl/test.esp32-ard.yaml b/tests/components/lvgl/test.esp32-ard.yaml index abfb324ea5..2d6a6871ba 100644 --- a/tests/components/lvgl/test.esp32-ard.yaml +++ b/tests/components/lvgl/test.esp32-ard.yaml @@ -24,6 +24,33 @@ display: invert_colors: false update_interval: never +binary_sensor: + - platform: gpio + internal: true + id: up_button + pin: + number: GPIO38 + inverted: true + - platform: gpio + internal: true + id: down_button + pin: + number: GPIO37 + inverted: true + - platform: gpio + internal: true + id: select_button + pin: + number: GPIO39 + inverted: true +lvgl: + encoders: + group: switches + enter_button: select_button + sensor: + left_button: up_button + right_button: down_button + packages: lvgl: !include lvgl-package.yaml diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index 0f740db980..927d72d15c 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -67,7 +67,7 @@ lvgl: displays: - tft_display - second_display - rotary_encoders: + encoders: sensor: encoder enter_button: pushbutton group: general From da0dbe8753645b753a9dc64fb6ad21ccd29a867c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 7 Aug 2024 07:29:05 +1200 Subject: [PATCH 046/160] Revert "Add null GPIO pin " (#6621) --- .../cst226/touchscreen/cst226_touchscreen.cpp | 18 +++++++++++------- .../cst226/touchscreen/cst226_touchscreen.h | 2 +- esphome/core/gpio.h | 18 ------------------ 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp b/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp index 69728dc666..d4e43d30f5 100644 --- a/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp +++ b/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp @@ -5,13 +5,17 @@ namespace cst226 { void CST226Touchscreen::setup() { esph_log_config(TAG, "Setting up CST226 Touchscreen..."); - this->reset_pin_->setup(); - this->reset_pin_->digital_write(true); - delay(5); - this->reset_pin_->digital_write(false); - delay(5); - this->reset_pin_->digital_write(true); - this->set_timeout(30, [this] { this->continue_setup_(); }); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(5); + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + this->set_timeout(30, [this] { this->continue_setup_(); }); + } else { + this->continue_setup_(); + } } void CST226Touchscreen::update_touches() { diff --git a/esphome/components/cst226/touchscreen/cst226_touchscreen.h b/esphome/components/cst226/touchscreen/cst226_touchscreen.h index 1b15b952c4..9f518e5068 100644 --- a/esphome/components/cst226/touchscreen/cst226_touchscreen.h +++ b/esphome/components/cst226/touchscreen/cst226_touchscreen.h @@ -35,7 +35,7 @@ class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice void continue_setup_(); InternalGPIOPin *interrupt_pin_{}; - GPIOPin *reset_pin_{NULL_PIN}; + GPIOPin *reset_pin_{}; uint8_t chip_id_{}; bool setup_complete_{}; }; diff --git a/esphome/core/gpio.h b/esphome/core/gpio.h index b3f6b00196..1b6f2ba1e6 100644 --- a/esphome/core/gpio.h +++ b/esphome/core/gpio.h @@ -62,24 +62,6 @@ class GPIOPin { virtual bool is_internal() { return false; } }; -/** - * A pin to replace those that don't exist. - */ -class NullPin : public GPIOPin { - public: - void setup() override {} - - void pin_mode(gpio::Flags _) override {} - - bool digital_read() override { return false; } - - void digital_write(bool _) override {} - - std::string dump_summary() const override { return {"Not used"}; } -}; - -static GPIOPin *const NULL_PIN = new NullPin(); - /// Copy of GPIOPin that is safe to use from ISRs (with no virtual functions) class ISRInternalGPIOPin { public: From 1e63fddf36390794c4aa70e6a9e508b23b4fa852 Mon Sep 17 00:00:00 2001 From: iannisimo <11798717+iannisimo@users.noreply.github.com> Date: Wed, 7 Aug 2024 01:02:30 +0200 Subject: [PATCH 047/160] [remote_transmitter] Change default carrier_frequency to valid value (#7176) set current_carrier_frequency_ default value to esp-idf's default (38000) --- esphome/components/remote_transmitter/remote_transmitter.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index b897fa8fab..a5896796c0 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -49,7 +49,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #ifdef USE_ESP32 void configure_rmt_(); - uint32_t current_carrier_frequency_{UINT32_MAX}; + uint32_t current_carrier_frequency_{38000}; bool initialized_{false}; std::vector rmt_temp_; esp_err_t error_code_{ESP_OK}; From 73f786c606dd3ad913e4d27553acd620548a0349 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:46:37 +1200 Subject: [PATCH 048/160] [code-quality] Organise script imports (#7198) --- script/build_codeowners.py | 8 ++++---- script/build_language_schema.py | 14 +++++++------- script/bump-version.py | 2 +- script/clang-format | 12 +++--------- script/clang-tidy | 29 ++++++++++++++--------------- script/helpers.py | 2 +- script/lint-python | 19 ++++++++++--------- script/list-components.py | 9 +++++---- 8 files changed, 45 insertions(+), 50 deletions(-) diff --git a/script/build_codeowners.py b/script/build_codeowners.py index 6bc558d351..db34ad7702 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 -from pathlib import Path -import sys import argparse from collections import defaultdict +from pathlib import Path +import sys -from esphome.helpers import write_file_if_changed from esphome.config import get_component, get_platform -from esphome.core import CORE from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK +from esphome.core import CORE +from esphome.helpers import write_file_if_changed parser = argparse.ArgumentParser() parser.add_argument( diff --git a/script/build_language_schema.py b/script/build_language_schema.py index cb3dc1832d..8b2c28b06b 100644 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -1,9 +1,10 @@ +import argparse +import glob import inspect import json -import argparse import os -import glob import re + import voluptuous as vol # NOTE: Cannot import other esphome components globally as a modification in vol_schema @@ -94,13 +95,12 @@ load_components() # Import esphome after loading components (so schema is tracked) # pylint: disable=wrong-import-position -import esphome.core as esphome_core -import esphome.config_validation as cv -from esphome import automation -from esphome import pins +from esphome import automation, pins from esphome.components import remote_base -from esphome.loader import get_platform, CORE_COMPONENTS_PATH +import esphome.config_validation as cv +import esphome.core as esphome_core from esphome.helpers import write_file_if_changed +from esphome.loader import CORE_COMPONENTS_PATH, get_platform from esphome.util import Registry # pylint: enable=wrong-import-position diff --git a/script/bump-version.py b/script/bump-version.py index a55bb65cd6..8389d482b8 100755 --- a/script/bump-version.py +++ b/script/bump-version.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 import argparse -import re from dataclasses import dataclass +import re import sys diff --git a/script/clang-format b/script/clang-format index b065d80795..d922c5b6f1 100755 --- a/script/clang-format +++ b/script/clang-format @@ -1,15 +1,6 @@ #!/usr/bin/env python3 -from helpers import ( - print_error_for_file, - get_output, - git_ls_files, - filter_changed, - get_binary, -) import argparse -import click -import colorama import multiprocessing import os import queue @@ -18,6 +9,9 @@ import subprocess import sys import threading +import click +import colorama +from helpers import filter_changed, get_binary, git_ls_files, print_error_for_file def run_format(executable, args, queue, lock, failed_files): diff --git a/script/clang-tidy b/script/clang-tidy index bd919825fd..ea522157c5 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,21 +1,6 @@ #!/usr/bin/env python3 -from helpers import ( - print_error_for_file, - get_output, - filter_grep, - build_all_include, - temp_header_file, - git_ls_files, - filter_changed, - load_idedata, - root_path, - basepath, - get_binary, -) import argparse -import click -import colorama import multiprocessing import os import queue @@ -26,6 +11,20 @@ import sys import tempfile import threading +import click +import colorama +from helpers import ( + basepath, + build_all_include, + filter_changed, + filter_grep, + get_binary, + git_ls_files, + load_idedata, + print_error_for_file, + root_path, + temp_header_file, +) def clang_options(idedata): diff --git a/script/helpers.py b/script/helpers.py index 52b0658fb6..56349b6052 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1,8 +1,8 @@ import json import os.path +from pathlib import Path import re import subprocess -from pathlib import Path import colorama diff --git a/script/lint-python b/script/lint-python index 7de1de80b0..01e5e76190 100755 --- a/script/lint-python +++ b/script/lint-python @@ -1,19 +1,20 @@ #!/usr/bin/env python3 -from helpers import ( - styled, - print_error_for_file, - get_output, - get_err, - git_ls_files, - filter_changed, -) import argparse -import colorama import os import re import sys +import colorama +from helpers import ( + filter_changed, + get_err, + get_output, + git_ls_files, + print_error_for_file, + styled, +) + curfile = None diff --git a/script/list-components.py b/script/list-components.py index 559919bb8a..0d4777436b 100755 --- a/script/list-components.py +++ b/script/list-components.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 +import argparse from pathlib import Path import sys -import argparse -from helpers import git_ls_files, changed_files -from esphome.loader import get_component, get_platform -from esphome.core import CORE +from helpers import changed_files, git_ls_files + from esphome.const import ( KEY_CORE, KEY_TARGET_FRAMEWORK, @@ -13,6 +12,8 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, ) +from esphome.core import CORE +from esphome.loader import get_component, get_platform def filter_component_files(str): From 9b0c2234d89c4f6a642a9fefc77f2e7f0a1c5ef7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:47:46 +1200 Subject: [PATCH 049/160] [max31856] Use cv.frequency as validator (#7212) --- esphome/components/max31856/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py index 71f1f3bfa5..bf9741aeed 100644 --- a/esphome/components/max31856/sensor.py +++ b/esphome/components/max31856/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, spi +import esphome.config_validation as cv from esphome.const import ( CONF_MAINS_FILTER, DEVICE_CLASS_TEMPERATURE, @@ -15,8 +15,8 @@ MAX31856Sensor = max31856_ns.class_( MAX31865ConfigFilter = max31856_ns.enum("MAX31856ConfigFilter") FILTER = { - "50HZ": MAX31865ConfigFilter.FILTER_50HZ, - "60HZ": MAX31865ConfigFilter.FILTER_60HZ, + 50: MAX31865ConfigFilter.FILTER_50HZ, + 60: MAX31865ConfigFilter.FILTER_60HZ, } CONFIG_SCHEMA = ( @@ -29,8 +29,8 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.Optional(CONF_MAINS_FILTER, default="60HZ"): cv.enum( - FILTER, upper=True, space="" + cv.Optional(CONF_MAINS_FILTER, default="60Hz"): cv.All( + cv.frequency, cv.enum(FILTER, int=True) ), } ) From c348efa401ea64b6a07ae6671fc16f62f3dcb1e2 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 7 Aug 2024 05:49:51 +0200 Subject: [PATCH 050/160] [code-quality] Organise base entities imports (#7208) --- esphome/components/binary_sensor/__init__.py | 10 +++--- esphome/components/button/__init__.py | 8 ++--- esphome/components/climate/__init__.py | 10 +++--- esphome/components/cover/__init__.py | 16 +++++----- esphome/components/datetime/__init__.py | 26 +++++++-------- esphome/components/event/__init__.py | 10 +++--- esphome/components/fan/__init__.py | 28 ++++++++--------- esphome/components/light/__init__.py | 33 ++++++++++---------- esphome/components/lock/__init__.py | 6 ++-- esphome/components/media_player/__init__.py | 8 ++--- esphome/components/number/__init__.py | 17 +++++----- esphome/components/select/__init__.py | 14 ++++----- esphome/components/sensor/__init__.py | 20 ++++++------ esphome/components/switch/__init__.py | 6 ++-- esphome/components/text/__init__.py | 10 +++--- esphome/components/text_sensor/__init__.py | 14 ++++----- esphome/components/valve/__init__.py | 6 ++-- 17 files changed, 119 insertions(+), 123 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 11a1887206..95fd17bcc0 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -1,10 +1,8 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from esphome import automation, core from esphome.automation import Condition, maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DELAY, CONF_DEVICE_CLASS, @@ -16,6 +14,7 @@ from esphome.const import ( CONF_INVERTED, CONF_MAX_LENGTH, CONF_MIN_LENGTH, + CONF_MQTT_ID, CONF_ON_CLICK, CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, @@ -26,7 +25,6 @@ from esphome.const import ( CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, - CONF_MQTT_ID, CONF_WEB_SERVER_ID, DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY_CHARGING, @@ -59,6 +57,8 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity from esphome.util import Registry CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 773ab9d37f..3010d3006a 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -1,16 +1,16 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, + CONF_MQTT_ID, CONF_ON_PRESS, CONF_TRIGGER_ID, - CONF_MQTT_ID, CONF_WEB_SERVER_ID, DEVICE_CLASS_EMPTY, DEVICE_CLASS_IDENTIFY, @@ -18,8 +18,8 @@ from esphome.const import ( DEVICE_CLASS_UPDATE, ) from esphome.core import CORE, coroutine_with_priority -from esphome.cpp_helpers import setup_entity from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index ccd7a3da4e..c7e4ce7745 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -1,8 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.cpp_helpers import setup_entity from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_ACTION_STATE_TOPIC, CONF_AWAY, @@ -21,6 +20,7 @@ from esphome.const import ( CONF_MODE, CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, + CONF_MQTT_ID, CONF_ON_CONTROL, CONF_ON_STATE, CONF_PRESET, @@ -33,20 +33,20 @@ from esphome.const import ( CONF_TARGET_HUMIDITY_STATE_TOPIC, CONF_TARGET_TEMPERATURE, CONF_TARGET_TEMPERATURE_COMMAND_TOPIC, - CONF_TARGET_TEMPERATURE_STATE_TOPIC, CONF_TARGET_TEMPERATURE_HIGH, CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC, CONF_TARGET_TEMPERATURE_HIGH_STATE_TOPIC, CONF_TARGET_TEMPERATURE_LOW, CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC, CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC, + CONF_TARGET_TEMPERATURE_STATE_TOPIC, CONF_TEMPERATURE_STEP, CONF_TRIGGER_ID, CONF_VISUAL, - CONF_MQTT_ID, CONF_WEB_SERVER_ID, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 313b2c5928..d25dd91148 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -1,23 +1,23 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.automation import maybe_simple_id, Condition +from esphome.automation import Condition, maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( - CONF_ID, CONF_DEVICE_CLASS, - CONF_STATE, + CONF_ID, + CONF_MQTT_ID, CONF_ON_OPEN, CONF_POSITION, CONF_POSITION_COMMAND_TOPIC, CONF_POSITION_STATE_TOPIC, + CONF_STATE, + CONF_STOP, CONF_TILT, CONF_TILT_COMMAND_TOPIC, CONF_TILT_STATE_TOPIC, - CONF_STOP, - CONF_MQTT_ID, - CONF_WEB_SERVER_ID, CONF_TRIGGER_ID, + CONF_WEB_SERVER_ID, DEVICE_CLASS_AWNING, DEVICE_CLASS_BLIND, DEVICE_CLASS_CURTAIN, diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index c118216a2d..4fda97c5bc 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -1,32 +1,30 @@ -import esphome.codegen as cg - -import esphome.config_validation as cv from esphome import automation -from esphome.components import mqtt, web_server, time +import esphome.codegen as cg +from esphome.components import mqtt, time, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_DATE, + CONF_DATETIME, + CONF_DAY, + CONF_HOUR, CONF_ID, + CONF_MINUTE, + CONF_MONTH, + CONF_MQTT_ID, CONF_ON_TIME, CONF_ON_VALUE, + CONF_SECOND, + CONF_TIME, CONF_TIME_ID, CONF_TRIGGER_ID, CONF_TYPE, - CONF_MQTT_ID, CONF_WEB_SERVER_ID, - CONF_DATE, - CONF_DATETIME, - CONF_TIME, CONF_YEAR, - CONF_MONTH, - CONF_DAY, - CONF_SECOND, - CONF_HOUR, - CONF_MINUTE, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity - CODEOWNERS = ["@rfdarter", "@jesserockz"] DEPENDENCIES = ["time"] diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 241e884386..031a4c0de8 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -1,24 +1,24 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt +import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, + CONF_EVENT_TYPE, CONF_ICON, CONF_ID, + CONF_MQTT_ID, CONF_ON_EVENT, CONF_TRIGGER_ID, - CONF_MQTT_ID, - CONF_EVENT_TYPE, DEVICE_CLASS_BUTTON, DEVICE_CLASS_DOORBELL, DEVICE_CLASS_EMPTY, DEVICE_CLASS_MOTION, ) from esphome.core import CORE, coroutine_with_priority -from esphome.cpp_helpers import setup_entity from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@nohat"] IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 847a59baa1..62624ec6e3 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -1,31 +1,31 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_DIRECTION, CONF_ID, CONF_MQTT_ID, - CONF_WEB_SERVER_ID, - CONF_OSCILLATING, - CONF_OSCILLATION_COMMAND_TOPIC, - CONF_OSCILLATION_STATE_TOPIC, - CONF_SPEED, - CONF_SPEED_LEVEL_COMMAND_TOPIC, - CONF_SPEED_LEVEL_STATE_TOPIC, - CONF_SPEED_COMMAND_TOPIC, - CONF_SPEED_STATE_TOPIC, CONF_OFF_SPEED_CYCLE, CONF_ON_DIRECTION_SET, CONF_ON_OSCILLATING_SET, + CONF_ON_PRESET_SET, CONF_ON_SPEED_SET, CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, - CONF_ON_PRESET_SET, - CONF_TRIGGER_ID, - CONF_DIRECTION, + CONF_OSCILLATING, + CONF_OSCILLATION_COMMAND_TOPIC, + CONF_OSCILLATION_STATE_TOPIC, CONF_RESTORE_MODE, + CONF_SPEED, + CONF_SPEED_COMMAND_TOPIC, + CONF_SPEED_LEVEL_COMMAND_TOPIC, + CONF_SPEED_LEVEL_STATE_TOPIC, + CONF_SPEED_STATE_TOPIC, + CONF_TRIGGER_ID, + CONF_WEB_SERVER_ID, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 161b4d8cd9..d9f139d2f4 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -1,8 +1,9 @@ -import esphome.codegen as cg -import esphome.config_validation as cv import esphome.automation as auto +import esphome.codegen as cg from esphome.components import mqtt, power_supply, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_COLOR_CORRECT, CONF_DEFAULT_TRANSITION_LENGTH, CONF_EFFECTS, @@ -10,36 +11,36 @@ from esphome.const import ( CONF_GAMMA_CORRECT, CONF_ID, CONF_MQTT_ID, - CONF_WEB_SERVER_ID, - CONF_POWER_SUPPLY, - CONF_RESTORE_MODE, + CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, - CONF_ON_STATE, + CONF_POWER_SUPPLY, + CONF_RESTORE_MODE, CONF_TRIGGER_ID, - CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE, + CONF_WEB_SERVER_ID, ) from esphome.core import coroutine_with_priority from esphome.cpp_helpers import setup_entity + from .automation import light_control_to_code # noqa from .effects import ( - validate_effects, + ADDRESSABLE_EFFECTS, BINARY_EFFECTS, + EFFECTS_REGISTRY, MONOCHROMATIC_EFFECTS, RGB_EFFECTS, - ADDRESSABLE_EFFECTS, - EFFECTS_REGISTRY, + validate_effects, ) from .types import ( # noqa - LightState, - AddressableLightState, - light_ns, - LightOutput, AddressableLight, - LightTurnOnTrigger, - LightTurnOffTrigger, + AddressableLightState, + LightOutput, + LightState, LightStateTrigger, + LightTurnOffTrigger, + LightTurnOnTrigger, + light_ns, ) CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index c2d6054ed9..6b92bc264b 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -1,14 +1,14 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition, maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_MQTT_ID, CONF_ON_LOCK, CONF_ON_UNLOCK, CONF_TRIGGER_ID, - CONF_MQTT_ID, CONF_WEB_SERVER_ID, ) from esphome.core import CORE, coroutine_with_priority diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 320014e355..423cb065dc 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -1,20 +1,18 @@ from esphome import automation -import esphome.config_validation as cv -import esphome.codegen as cg - from esphome.automation import maybe_simple_id +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_ON_IDLE, CONF_ON_STATE, CONF_TRIGGER_ID, CONF_VOLUME, - CONF_ON_IDLE, ) from esphome.core import CORE from esphome.coroutine import coroutine_with_priority from esphome.cpp_helpers import setup_entity - CODEOWNERS = ["@jesserockz"] IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index d9c16fd7a9..ece738af49 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -1,24 +1,23 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.components import mqtt -from esphome.components import web_server +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_ABOVE, CONF_BELOW, + CONF_CYCLE, CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, - CONF_ID, CONF_ICON, + CONF_ID, CONF_MODE, + CONF_MQTT_ID, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, + CONF_OPERATION, CONF_TRIGGER_ID, CONF_UNIT_OF_MEASUREMENT, - CONF_MQTT_ID, CONF_VALUE, - CONF_OPERATION, - CONF_CYCLE, CONF_WEB_SERVER_ID, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, @@ -72,8 +71,8 @@ from esphome.const import ( DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, coroutine_with_priority -from esphome.cpp_helpers import setup_entity from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 073fbef1d4..2bc68d43ec 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -1,20 +1,20 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_CYCLE, CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, + CONF_INDEX, + CONF_MODE, + CONF_MQTT_ID, CONF_ON_VALUE, + CONF_OPERATION, CONF_OPTION, CONF_TRIGGER_ID, - CONF_MQTT_ID, CONF_WEB_SERVER_ID, - CONF_CYCLE, - CONF_MODE, - CONF_OPERATION, - CONF_INDEX, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_generator import MockObjClass diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 3b76466dec..867cdc1f48 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -1,22 +1,27 @@ import math -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( - CONF_DEVICE_CLASS, CONF_ABOVE, CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_BELOW, + CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_EXPIRE_AFTER, CONF_FILTERS, + CONF_FORCE_UPDATE, CONF_FROM, CONF_ICON, CONF_ID, CONF_IGNORE_OUT_OF_RANGE, + CONF_MAX_VALUE, + CONF_METHOD, + CONF_MIN_VALUE, + CONF_MQTT_ID, CONF_MULTIPLE, CONF_ON_RAW_VALUE, CONF_ON_VALUE, @@ -30,14 +35,9 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, - CONF_WINDOW_SIZE, - CONF_MQTT_ID, - CONF_WEB_SERVER_ID, - CONF_FORCE_UPDATE, CONF_VALUE, - CONF_MIN_VALUE, - CONF_MAX_VALUE, - CONF_METHOD, + CONF_WEB_SERVER_ID, + CONF_WINDOW_SIZE, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, DEVICE_CLASS_ATMOSPHERIC_PRESSURE, diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 3539d0e34e..fef4f7f007 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -1,8 +1,8 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition, maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, @@ -10,11 +10,11 @@ from esphome.const import ( CONF_ID, CONF_INVERTED, CONF_MQTT_ID, - CONF_WEB_SERVER_ID, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_RESTORE_MODE, CONF_TRIGGER_ID, + CONF_WEB_SERVER_ID, DEVICE_CLASS_EMPTY, DEVICE_CLASS_OUTLET, DEVICE_CLASS_SWITCH, diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 5a8e763495..386baaf756 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -1,18 +1,18 @@ from typing import Optional -import esphome.codegen as cg -import esphome.config_validation as cv + from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_MODE, + CONF_MQTT_ID, CONF_ON_VALUE, CONF_TRIGGER_ID, - CONF_MQTT_ID, - CONF_WEB_SERVER_ID, CONF_VALUE, + CONF_WEB_SERVER_ID, ) - from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index f4e795924c..ba8a2def41 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -1,21 +1,21 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_FILTERS, + CONF_FROM, CONF_ICON, CONF_ID, - CONF_ON_VALUE, - CONF_ON_RAW_VALUE, - CONF_TRIGGER_ID, CONF_MQTT_ID, - CONF_WEB_SERVER_ID, + CONF_ON_RAW_VALUE, + CONF_ON_VALUE, CONF_STATE, - CONF_FROM, CONF_TO, + CONF_TRIGGER_ID, + CONF_WEB_SERVER_ID, DEVICE_CLASS_DATE, DEVICE_CLASS_EMPTY, DEVICE_CLASS_TIMESTAMP, diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index c03d13fec8..3c03bab857 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -1,8 +1,8 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.automation import maybe_simple_id, Condition +from esphome.automation import Condition, maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, CONF_ID, From ddd80272386c923db6043e3659997c1e34398b3f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:33:12 +1200 Subject: [PATCH 051/160] [spi] Remove ``SPIDelegateDummy`` (#7215) --- esphome/components/spi/spi.cpp | 6 ------ esphome/components/spi/spi.h | 21 ++------------------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index b13826c443..f9435b0424 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -7,10 +7,6 @@ namespace spi { const char *const TAG = "spi"; -SPIDelegate *const SPIDelegate::NULL_DELEGATE = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - new SPIDelegateDummy(); -// https://bugs.llvm.org/show_bug.cgi?id=48040 - bool SPIDelegate::is_ready() { return true; } GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -79,8 +75,6 @@ void SPIComponent::dump_config() { } } -void SPIDelegateDummy::begin_transaction() { ESP_LOGE(TAG, "SPIDevice not initialised - did you call spi_setup()?"); } - uint8_t SPIDelegateBitBash::transfer(uint8_t data) { return this->transfer_(data, 8); } void SPIDelegateBitBash::write(uint16_t data, size_t num_bits) { this->transfer_(data, num_bits); } diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index f581dc3f56..4cd8d3383c 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -163,8 +163,6 @@ class Utility { } }; -class SPIDelegateDummy; - // represents a device attached to an SPI bus, with a defined clock rate, mode and bit order. On Arduino this is // a thin wrapper over SPIClass. class SPIDelegate { @@ -250,21 +248,6 @@ class SPIDelegate { uint32_t data_rate_{1000000}; SPIMode mode_{MODE0}; GPIOPin *cs_pin_{NullPin::NULL_PIN}; - static SPIDelegate *const NULL_DELEGATE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -}; - -/** - * A dummy SPIDelegate that complains if it's used. - */ - -class SPIDelegateDummy : public SPIDelegate { - public: - SPIDelegateDummy() = default; - - uint8_t transfer(uint8_t data) override { return 0; } - void end_transaction() override{}; - - void begin_transaction() override; }; /** @@ -382,7 +365,7 @@ class SPIClient { virtual void spi_teardown() { this->parent_->unregister_device(this); - this->delegate_ = SPIDelegate::NULL_DELEGATE; + this->delegate_ = nullptr; } bool spi_is_ready() { return this->delegate_->is_ready(); } @@ -393,7 +376,7 @@ class SPIClient { uint32_t data_rate_{1000000}; SPIComponent *parent_{nullptr}; GPIOPin *cs_{nullptr}; - SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE}; + SPIDelegate *delegate_{nullptr}; }; /** From 132269c5b84c6c7502db239094bb14b27615de7e Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 7 Aug 2024 09:31:44 +0200 Subject: [PATCH 052/160] [code-quality] Apply ruff linting suggestions (#7206) --- esphome/config_validation.py | 2 +- esphome/mqtt.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 6e1d3ba2f9..ef60d6e0d6 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -91,7 +91,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=consider-using-f-string VARIABLE_PROG = re.compile( - "\\$([{0}]+|\\{{[{0}]*\\}})".format(VALID_SUBSTITUTIONS_CHARACTERS) + f"\\$([{VALID_SUBSTITUTIONS_CHARACTERS}]+|\\{{[{VALID_SUBSTITUTIONS_CHARACTERS}]*\\}})" ) # pylint: disable=invalid-name diff --git a/esphome/mqtt.py b/esphome/mqtt.py index d7e14a1d08..c1c45799cc 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -3,7 +3,6 @@ import hashlib import json import logging import ssl -import sys import time import paho.mqtt.client as mqtt @@ -103,10 +102,7 @@ def prepare( if config[CONF_MQTT].get(CONF_SSL_FINGERPRINTS) or config[CONF_MQTT].get( CONF_CERTIFICATE_AUTHORITY ): - if sys.version_info >= (2, 7, 13): - tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member - else: - tls_version = ssl.PROTOCOL_SSLv23 + tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member client.tls_set( ca_certs=None, certfile=None, From 2a8424a7f2022dced130a80139729015792b7f39 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 7 Aug 2024 09:32:06 +0200 Subject: [PATCH 053/160] [code-quality] Organise logger imports (#7205) --- esphome/components/logger/__init__.py | 33 ++++++++++++--------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 99aa39c4ba..f30bc23e38 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -1,9 +1,21 @@ import re -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import LambdaAction +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) +from esphome.components.libretiny import get_libretiny_component, get_libretiny_family +from esphome.components.libretiny.const import COMPONENT_BK72XX, COMPONENT_RTL87XX +import esphome.config_validation as cv from esphome.const import ( CONF_ARGS, CONF_BAUD_RATE, @@ -18,27 +30,12 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_TX_BUFFER_SIZE, PLATFORM_BK72XX, - PLATFORM_RTL87XX, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + PLATFORM_RTL87XX, ) from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority -from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant -from esphome.components.esp32.const import ( - VARIANT_ESP32, - VARIANT_ESP32S2, - VARIANT_ESP32C3, - VARIANT_ESP32S3, - VARIANT_ESP32C2, - VARIANT_ESP32C6, - VARIANT_ESP32H2, -) -from esphome.components.libretiny import get_libretiny_component, get_libretiny_family -from esphome.components.libretiny.const import ( - COMPONENT_BK72XX, - COMPONENT_RTL87XX, -) CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") From 4b91ef5123e064d33975c928fa40e7cffaa5b468 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 7 Aug 2024 09:33:41 +0200 Subject: [PATCH 054/160] [code-quality] Apply ruff linting suggestions to core (#7207) --- esphome/core/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 9d3d14492e..a97c3b18c9 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -336,7 +336,7 @@ class ID: else: self.is_manual = is_manual self.is_declaration = is_declaration - self.type: Optional["MockObjClass"] = type + self.type: Optional[MockObjClass] = type def resolve(self, registered_ids): from esphome.config_validation import RESERVED_IDS @@ -500,7 +500,7 @@ class EsphomeCore: # The relative path to where all build files are stored self.build_path: Optional[str] = None # The validated configuration, this is None until the config has been validated - self.config: Optional["ConfigType"] = None + self.config: Optional[ConfigType] = None # The pending tasks in the task queue (mostly for C++ generation) # This is a priority queue (with heapq) # Each item is a tuple of form: (-priority, unique number, task) @@ -508,17 +508,17 @@ class EsphomeCore: # Task counter for pending tasks self.task_counter = 0 # The variable cache, for each ID this holds a MockObj of the variable obj - self.variables: dict[str, "MockObj"] = {} + self.variables: dict[str, MockObj] = {} # A list of statements that go in the main setup() block - self.main_statements: list["Statement"] = [] + self.main_statements: list[Statement] = [] # A list of statements to insert in the global block (includes and global variables) - self.global_statements: list["Statement"] = [] + self.global_statements: list[Statement] = [] # A set of platformio libraries to add to the project self.libraries: list[Library] = [] # A set of build flags to set in the platformio project self.build_flags: set[str] = set() # A set of defines to set for the compile process in esphome/core/defines.h - self.defines: set["Define"] = set() + self.defines: set[Define] = set() # A map of all platformio options to apply self.platformio_options: dict[str, Union[str, list[str]]] = {} # A set of strings of names of loaded integrations, used to find namespace ID conflicts From 9a9757ddebfc025d19c65c6c0c3467eeb887061f Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 8 Aug 2024 02:29:32 +0200 Subject: [PATCH 055/160] [code-quality] fix clang-tidy sprinkler (#7222) * fix clang-tidy * fix build error * clang-tidy * clang-tidy --- esphome/components/sprinkler/sprinkler.cpp | 4 ++-- esphome/components/sprinkler/sprinkler.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 982d9add1a..59565251c3 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -647,7 +647,7 @@ void Sprinkler::set_valve_run_duration(const optional valve_number, cons return; } auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); - if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == min_str) { + if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { call.set_value(run_duration.value() / 60.0); } else { call.set_value(run_duration.value()); @@ -729,7 +729,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { return 0; } if (this->valve_[valve_number].run_duration_number != nullptr) { - if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == min_str) { + if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); } else { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 5311ae4c05..c4a8b8aeb8 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -11,7 +11,7 @@ namespace esphome { namespace sprinkler { -const std::string min_str = "min"; +const std::string MIN_STR = "min"; enum SprinklerState : uint8_t { // NOTE: these states are used by both SprinklerValveOperator and Sprinkler (the controller)! From 24b6c1d3eb85e1c50552a2aff040e0250d749e26 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 8 Aug 2024 02:30:49 +0200 Subject: [PATCH 056/160] [code-quality] __attribute__((packed)) (#7221) --- esphome/components/climate/climate.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 7c2a0b1ed3..d81702fb0c 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -141,7 +141,7 @@ struct ClimateDeviceRestoreState { float target_temperature_low; float target_temperature_high; }; - }; + } __attribute__((packed)); float target_humidity; /// Convert this struct to a climate call that can be performed. From 7fd65987d33f21433975ef252c29f20a33ef33a0 Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Thu, 8 Aug 2024 03:29:49 +0100 Subject: [PATCH 057/160] hx711: Check for DOUT going high after a reading (#7214) --- esphome/components/hx711/hx711.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index dbbf4c91f4..1a7169eed7 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -39,8 +39,8 @@ bool HX711Sensor::read_sensor_(uint32_t *result) { return false; } - this->status_clear_warning(); uint32_t data = 0; + bool final_dout; { InterruptLock lock; @@ -59,8 +59,17 @@ bool HX711Sensor::read_sensor_(uint32_t *result) { this->sck_pin_->digital_write(false); delayMicroseconds(1); } + final_dout = this->dout_pin_->digital_read(); } + if (!final_dout) { + ESP_LOGW(TAG, "HX711 DOUT pin not high after reading (data 0x%" PRIx32 ")!", data); + this->status_set_warning(); + return false; + } + + this->status_clear_warning(); + if (data & 0x800000ULL) { data |= 0xFF000000ULL; } From 3f1d2c0cafe246c88e4d257b36e70c5e3be4ec71 Mon Sep 17 00:00:00 2001 From: dentra Date: Thu, 8 Aug 2024 07:49:37 +0300 Subject: [PATCH 058/160] [mqtt] Add extended device info (#7194) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/mqtt/mqtt_component.cpp | 36 +++++++++++++++++++--- esphome/components/mqtt/mqtt_const.h | 2 ++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index bb46ce732d..295fbba5e5 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -150,12 +150,40 @@ bool MQTTComponent::send_discovery_() { const std::string &node_area = App.get_area(); JsonObject device_info = root.createNestedObject(MQTT_DEVICE); - device_info[MQTT_DEVICE_IDENTIFIERS] = get_mac_address(); + const auto mac = get_mac_address(); + device_info[MQTT_DEVICE_IDENTIFIERS] = mac; device_info[MQTT_DEVICE_NAME] = node_friendly_name; - device_info[MQTT_DEVICE_SW_VERSION] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time(); +#ifdef ESPHOME_PROJECT_NAME + device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")"; + const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.'); + if (model == nullptr) { // must never happen but check anyway + device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; + device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME; + } else { + device_info[MQTT_DEVICE_MODEL] = model + 1; + device_info[MQTT_DEVICE_MANUFACTURER] = std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME); + } +#else + device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time() + ")"; device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; - device_info[MQTT_DEVICE_MANUFACTURER] = "espressif"; - device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area; +#if defined(USE_ESP8266) || defined(USE_ESP32) + device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif"; +#elif defined(USE_RP2040) + device_info[MQTT_DEVICE_MANUFACTURER] = "Raspberry Pi"; +#elif defined(USE_BK72XX) + device_info[MQTT_DEVICE_MANUFACTURER] = "Beken"; +#elif defined(USE_RTL87XX) + device_info[MQTT_DEVICE_MANUFACTURER] = "Realtek"; +#elif defined(USE_HOST) + device_info[MQTT_DEVICE_MANUFACTURER] = "Host"; +#endif +#endif + if (!node_area.empty()) { + device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area; + } + + device_info[MQTT_DEVICE_CONNECTIONS][0][0] = "mac"; + device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac; }, this->qos_, discovery_info.retain); } diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 0e063c66d2..71f169fbe8 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -62,6 +62,7 @@ constexpr const char *const MQTT_DEVICE_MODEL = "mdl"; constexpr const char *const MQTT_DEVICE_NAME = "name"; constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "sa"; constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw"; +constexpr const char *const MQTT_DEVICE_HW_VERSION = "hw"; constexpr const char *const MQTT_DOCKED_TEMPLATE = "dock_tpl"; constexpr const char *const MQTT_DOCKED_TOPIC = "dock_t"; constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "fx_cmd_t"; @@ -322,6 +323,7 @@ constexpr const char *const MQTT_DEVICE_MODEL = "model"; constexpr const char *const MQTT_DEVICE_NAME = "name"; constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "suggested_area"; constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw_version"; +constexpr const char *const MQTT_DEVICE_HW_VERSION = "hw_version"; constexpr const char *const MQTT_DOCKED_TEMPLATE = "docked_template"; constexpr const char *const MQTT_DOCKED_TOPIC = "docked_topic"; constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "effect_command_topic"; From a3d5b69a9c7690efcfb3441e2d69166bb8cc703e Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 8 Aug 2024 07:02:41 +0200 Subject: [PATCH 059/160] [code-quality] NOLINT readability-identifier-naming (#7220) --- esphome/core/entity_base.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 434111de79..4ca21f9ee5 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -63,7 +63,7 @@ class EntityBase { EntityCategory entity_category_{ENTITY_CATEGORY_NONE}; }; -class EntityBase_DeviceClass { +class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) public: /// Get the device class, using the manual override if set. std::string get_device_class(); @@ -74,7 +74,7 @@ class EntityBase_DeviceClass { const char *device_class_{nullptr}; ///< Device class override }; -class EntityBase_UnitOfMeasurement { +class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) public: /// Get the unit of measurement, using the manual override if set. std::string get_unit_of_measurement(); From b71c03424ecf1aaa74a12bdc86e28bdbed9488dd Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 8 Aug 2024 07:02:55 +0200 Subject: [PATCH 060/160] [code-quality] Organise time imports (#7219) --- esphome/components/time/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index c888705ba2..6a3368ca73 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -1,32 +1,32 @@ -import logging from importlib import resources +import logging from typing import Optional import tzlocal +from esphome import automation +from esphome.automation import Condition import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation from esphome.const import ( - CONF_ID, + CONF_AT, CONF_CRON, CONF_DAYS_OF_MONTH, CONF_DAYS_OF_WEEK, + CONF_HOUR, CONF_HOURS, + CONF_ID, + CONF_MINUTE, CONF_MINUTES, CONF_MONTHS, CONF_ON_TIME, CONF_ON_TIME_SYNC, + CONF_SECOND, CONF_SECONDS, CONF_TIMEZONE, CONF_TRIGGER_ID, - CONF_AT, - CONF_SECOND, - CONF_HOUR, - CONF_MINUTE, ) from esphome.core import coroutine_with_priority -from esphome.automation import Condition _LOGGER = logging.getLogger(__name__) From a47a17d7e7d4649d7a92c3fb96c921374b14d2d6 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 8 Aug 2024 18:24:10 +1000 Subject: [PATCH 061/160] [lvgl] Fix set state on updates (#7227) --- esphome/components/lvgl/lvgl_esphome.h | 4 ++-- esphome/components/lvgl/widgets/__init__.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 45841b99d9..1497e1004a 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -1,9 +1,9 @@ #pragma once #include "esphome/core/defines.h" -#ifdef USE_LVGL_BINARY_SENSOR +#ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" -#endif // USE_LVGL_BINARY_SENSOR +#endif // USE_BINARY_SENSOR #ifdef USE_LVGL_ROTARY_ENCODER #include "esphome/components/rotary_encoder/rotary_encoder.h" #endif // USE_LVGL_ROTARY_ENCODER diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index f1946015bc..603de6aa3e 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -271,6 +271,7 @@ async def set_obj_properties(w: Widget, config): """Generate a list of C++ statements to apply properties to an lv_obj_t""" if layout := config.get(CONF_LAYOUT): layout_type: str = layout[CONF_TYPE] + add_lv_use(layout_type) lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) if layout_type == TYPE_GRID: wid = config[CONF_ID] @@ -334,7 +335,7 @@ async def set_obj_properties(w: Widget, config): for key, value in states.items(): if isinstance(value, cv.Lambda): lambs[key] = value - elif value == "true": + elif value: adds.add(key) else: clears.add(key) From b43c5b851aaaf15f170dbaf23a6ec5feaf4ee664 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 9 Aug 2024 13:15:25 +0200 Subject: [PATCH 062/160] add missing overrides (#7231) --- esphome/components/lvgl/number/lvgl_number.h | 2 +- esphome/components/lvgl/switch/lvgl_switch.h | 2 +- esphome/components/lvgl/text/lvgl_text.h | 2 +- esphome/components/spi_led_strip/spi_led_strip.h | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index 461ea51be4..659fc615c9 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -19,7 +19,7 @@ class LVGLNumber : public number::Number { } protected: - void control(float value) { + void control(float value) override { if (this->control_lambda_ != nullptr) this->control_lambda_(value); else diff --git a/esphome/components/lvgl/switch/lvgl_switch.h b/esphome/components/lvgl/switch/lvgl_switch.h index f20f4ed960..67be11faba 100644 --- a/esphome/components/lvgl/switch/lvgl_switch.h +++ b/esphome/components/lvgl/switch/lvgl_switch.h @@ -19,7 +19,7 @@ class LVGLSwitch : public switch_::Switch { } protected: - void write_state(bool value) { + void write_state(bool value) override { if (this->state_lambda_ != nullptr) this->state_lambda_(value); else diff --git a/esphome/components/lvgl/text/lvgl_text.h b/esphome/components/lvgl/text/lvgl_text.h index 8dc0281364..4bd5b76744 100644 --- a/esphome/components/lvgl/text/lvgl_text.h +++ b/esphome/components/lvgl/text/lvgl_text.h @@ -19,7 +19,7 @@ class LVGLText : public text::Text { } protected: - void control(const std::string &value) { + void control(const std::string &value) override { if (this->control_lambda_ != nullptr) this->control_lambda_(value); else diff --git a/esphome/components/spi_led_strip/spi_led_strip.h b/esphome/components/spi_led_strip/spi_led_strip.h index 0d8c1c1e1c..8b713378ec 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.h +++ b/esphome/components/spi_led_strip/spi_led_strip.h @@ -13,7 +13,7 @@ class SpiLedStrip : public light::AddressableLight, public spi::SPIDevice { public: - void setup() { this->spi_setup(); } + void setup() override { this->spi_setup(); } int32_t size() const override { return this->num_leds_; } @@ -43,7 +43,7 @@ class SpiLedStrip : public light::AddressableLight, memset(this->buf_, 0, 4); } - void dump_config() { + void dump_config() override { esph_log_config(TAG, "SPI LED Strip:"); esph_log_config(TAG, " LEDs: %d", this->num_leds_); if (this->data_rate_ >= spi::DATA_RATE_1MHZ) From 15602b0664b71ac6362e5cf4ff3c0e94cf066c38 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Mon, 12 Aug 2024 06:06:29 +1000 Subject: [PATCH 063/160] Add text_align_to_string (#7243) --- esphome/components/display/display.cpp | 31 ++++++++++++++++++++++++++ esphome/components/display/display.h | 3 +++ 2 files changed, 34 insertions(+) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 75205292f7..63c74e09ca 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -675,5 +675,36 @@ void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; } void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; } const display_writer_t &DisplayPage::get_writer() const { return this->writer_; } +const LogString *text_align_to_string(TextAlign textalign) { + switch (textalign) { + case TextAlign::TOP_LEFT: + return LOG_STR("TOP_LEFT"); + case TextAlign::TOP_CENTER: + return LOG_STR("TOP_CENTER"); + case TextAlign::TOP_RIGHT: + return LOG_STR("TOP_RIGHT"); + case TextAlign::CENTER_LEFT: + return LOG_STR("CENTER_LEFT"); + case TextAlign::CENTER: + return LOG_STR("CENTER"); + case TextAlign::CENTER_RIGHT: + return LOG_STR("CENTER_RIGHT"); + case TextAlign::BASELINE_LEFT: + return LOG_STR("BASELINE_LEFT"); + case TextAlign::BASELINE_CENTER: + return LOG_STR("BASELINE_CENTER"); + case TextAlign::BASELINE_RIGHT: + return LOG_STR("BASELINE_RIGHT"); + case TextAlign::BOTTOM_LEFT: + return LOG_STR("BOTTOM_LEFT"); + case TextAlign::BOTTOM_CENTER: + return LOG_STR("BOTTOM_CENTER"); + case TextAlign::BOTTOM_RIGHT: + return LOG_STR("BOTTOM_RIGHT"); + default: + return LOG_STR("UNKNOWN"); + } +} + } // namespace display } // namespace esphome diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 4ee7ef93cb..34feafea6e 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -8,6 +8,7 @@ #include "esphome/core/color.h" #include "esphome/core/automation.h" #include "esphome/core/time.h" +#include "esphome/core/log.h" #include "display_color_utils.h" #ifdef USE_GRAPH @@ -737,5 +738,7 @@ class DisplayOnPageChangeTrigger : public Trigger DisplayPage *to_{nullptr}; }; +const LogString *text_align_to_string(TextAlign textalign); + } // namespace display } // namespace esphome From 442e765187e73aeff3e24f727de5662359943493 Mon Sep 17 00:00:00 2001 From: Nis Wechselberg Date: Mon, 12 Aug 2024 04:18:11 +0200 Subject: [PATCH 064/160] [sml] Fixed crashing sml parser (#7235) --- esphome/components/sml/sml_parser.cpp | 72 ++++++++++++++++++--------- 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/esphome/components/sml/sml_parser.cpp b/esphome/components/sml/sml_parser.cpp index c782c0fc5e..2cc71e87fa 100644 --- a/esphome/components/sml/sml_parser.cpp +++ b/esphome/components/sml/sml_parser.cpp @@ -10,7 +10,7 @@ SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) { this->pos_ = 0; while (this->pos_ < this->buffer_.size()) { if (this->buffer_[this->pos_] == 0x00) - break; // fill byte detected -> no more messages + break; // EndOfSmlMsg SmlNode message = SmlNode(); if (!this->setup_node(&message)) @@ -20,40 +20,66 @@ SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) { } bool SmlFile::setup_node(SmlNode *node) { - uint8_t type = this->buffer_[this->pos_] >> 4; // type including overlength info - uint8_t length = this->buffer_[this->pos_] & 0x0f; // length including TL bytes - bool is_list = (type & 0x07) == SML_LIST; - bool has_extended_length = type & 0x08; // we have a long list/value (>15 entries) - uint8_t parse_length = length; - if (has_extended_length) { - length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); - parse_length = length; + // If the TL field is 0x00, this is the end of the message + // (see 6.3.1 of SML protocol definition) + if (this->buffer_[this->pos_] == 0x00) { + // Increment past this byte and signal that the message is done this->pos_ += 1; + return true; } - if (this->pos_ + parse_length >= this->buffer_.size()) + // Extract data from initial TL field + uint8_t type = (this->buffer_[this->pos_] >> 4) & 0x07; // type without overlength info + bool overlength = (this->buffer_[this->pos_] >> 4) & 0x08; // overlength information + uint8_t length = this->buffer_[this->pos_] & 0x0f; // length (including TL bytes) + + // Check if we need additional length bytes + if (overlength) { + // Shift the current length to the higher nibble + // and add the lower nibble of the next byte to the length + length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); + // We are basically done with the first TL field now, + // so increment past that, we now point to the second TL field + this->pos_ += 1; + // Decrement the length for value fields (not lists), + // since the byte we just handled is counted as part of the field + // in case of values but not for lists + if (type != SML_LIST) + length -= 1; + + // Technically, this is not enough, the standard allows for more than two length fields. + // However I don't think it is very common to have more than 255 entries in a list + } + + // We are done with the last TL field(s), so advance the position + this->pos_ += 1; + // and decrement the length for non-list fields + if (type != SML_LIST) + length -= 1; + + // Check if the buffer length is long enough + if (this->pos_ + length > this->buffer_.size()) return false; - node->type = type & 0x07; + node->type = type; node->nodes.clear(); node->value_bytes.clear(); - // if the list is a has_extended_length list with e.g. 16 elements this is a 0x00 byte but not the end of message - if (!has_extended_length && this->buffer_[this->pos_] == 0x00) { // end of message - this->pos_ += 1; - } else if (is_list) { // list - this->pos_ += 1; - node->nodes.reserve(parse_length); - for (size_t i = 0; i != parse_length; i++) { + if (type == SML_LIST) { + node->nodes.reserve(length); + for (size_t i = 0; i != length; i++) { SmlNode child_node = SmlNode(); if (!this->setup_node(&child_node)) return false; node->nodes.emplace_back(child_node); } - } else { // value - node->value_bytes = - bytes(this->buffer_.begin() + this->pos_ + 1, this->buffer_.begin() + this->pos_ + parse_length); - this->pos_ += parse_length; + } else { + // Value starts at the current position + // Value ends "length" bytes later, + // (since the TL field is counted but already subtracted from length) + node->value_bytes = bytes(this->buffer_.begin() + this->pos_, this->buffer_.begin() + this->pos_ + length); + // Increment the pointer past all consumed bytes + this->pos_ += length; } return true; } @@ -101,7 +127,7 @@ int64_t bytes_to_int(const bytes &buffer) { // see https://stackoverflow.com/questions/42534749/signed-extension-from-24-bit-to-32-bit-in-c if (buffer.size() < 8) { const int bits = buffer.size() * 8; - const uint64_t m = 1u << (bits - 1); + const uint64_t m = 1ull << (bits - 1); tmp = (tmp ^ m) - m; } From d04e706295a495c0b8dbbf08c6ad95bbfa623712 Mon Sep 17 00:00:00 2001 From: Flo Date: Mon, 12 Aug 2024 04:20:51 +0200 Subject: [PATCH 065/160] Allow project name and version as improv_serial identity (#7248) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/improv_serial/__init__.py | 14 ++++---------- .../improv_serial/improv_serial_component.cpp | 4 ++++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 2b377d77b8..544af212e0 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -1,12 +1,10 @@ +import esphome.codegen as cg from esphome.components import improv_base from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import ( - VARIANT_ESP32S3, -) +from esphome.components.esp32.const import VARIANT_ESP32S3 from esphome.components.logger import USB_CDC -from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER -import esphome.codegen as cg import esphome.config_validation as cv +from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER from esphome.core import CORE import esphome.final_validate as fv @@ -19,11 +17,7 @@ improv_serial_ns = cg.esphome_ns.namespace("improv_serial") ImprovSerialComponent = improv_serial_ns.class_("ImprovSerialComponent", cg.Component) CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(ImprovSerialComponent), - } - ) + cv.Schema({cv.GenerateID(): cv.declare_id(ImprovSerialComponent)}) .extend(improv_base.IMPROV_SCHEMA) .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 12809e38cb..425a5c8576 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -170,7 +170,11 @@ std::vector ImprovSerialComponent::build_rpc_settings_response_(improv: } std::vector ImprovSerialComponent::build_version_info_() { +#ifdef ESPHOME_PROJECT_NAME + std::vector infos = {ESPHOME_PROJECT_NAME, ESPHOME_PROJECT_VERSION, ESPHOME_VARIANT, App.get_name()}; +#else std::vector infos = {"ESPHome", ESPHOME_VERSION, ESPHOME_VARIANT, App.get_name()}; +#endif std::vector data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false); return data; }; From 34d435c99643280c297e0741a14a58574e7c879e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:56:54 +1000 Subject: [PATCH 066/160] [lvgl] Implement default group for encoders (#7242) Co-authored-by: clydeps --- esphome/components/lvgl/__init__.py | 6 ++++-- esphome/components/lvgl/defines.py | 1 + esphome/components/lvgl/encoders.py | 10 +++++++--- esphome/components/lvgl/touchscreens.py | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 87fbcab4dc..6bf6e287f8 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -47,6 +47,7 @@ from .types import ( IdleTrigger, ObjUpdateAction, lv_font_t, + lv_group_t, lv_style_t, lvgl_ns, ) @@ -335,8 +336,9 @@ CONFIG_SCHEMA = ( cv.Optional(df.CONF_THEME): cv.Schema( {cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()} ), - cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema, - cv.GenerateID(df.CONF_ENCODERS): ENCODERS_CONFIG, + cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema, + cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG, + cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t), } ) .extend(DISP_BG_SCHEMA) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index d0047b59f7..1c6fd2678c 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -386,6 +386,7 @@ CONF_COLOR_DEPTH = "color_depth" CONF_CONTROL = "control" CONF_DEFAULT = "default" CONF_DEFAULT_FONT = "default_font" +CONF_DEFAULT_GROUP = "default_group" CONF_DIR = "dir" CONF_DISPLAYS = "displays" CONF_ENCODERS = "encoders" diff --git a/esphome/components/lvgl/encoders.py b/esphome/components/lvgl/encoders.py index caddc2e47f..cfd0e42996 100644 --- a/esphome/components/lvgl/encoders.py +++ b/esphome/components/lvgl/encoders.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import CONF_GROUP, CONF_ID, CONF_SENSOR from .defines import ( + CONF_DEFAULT_GROUP, CONF_ENCODERS, CONF_ENTER_BUTTON, CONF_LEFT_BUTTON, @@ -38,7 +39,10 @@ ENCODERS_CONFIG = cv.ensure_list( async def encoders_to_code(var, config): - for enc_conf in config.get(CONF_ENCODERS, ()): + default_group = lv_Pvariable(lv_group_t, config[CONF_DEFAULT_GROUP]) + lv_assign(default_group, lv_expr.group_create()) + lv.group_set_default(default_group) + for enc_conf in config[CONF_ENCODERS]: lvgl_components_required.add("KEY_LISTENER") lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds @@ -60,6 +64,6 @@ async def encoders_to_code(var, config): if group := enc_conf.get(CONF_GROUP): group = lv_Pvariable(lv_group_t, group) lv_assign(group, lv_expr.group_create()) - lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) else: - lv.indev_drv_register(listener.get_drv()) + group = default_group + lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py index 292b0873f3..4d430a428e 100644 --- a/esphome/components/lvgl/touchscreens.py +++ b/esphome/components/lvgl/touchscreens.py @@ -34,7 +34,7 @@ def touchscreen_schema(config): async def touchscreens_to_code(var, config): - for tconf in config.get(CONF_TOUCHSCREENS, ()): + for tconf in config[CONF_TOUCHSCREENS]: lvgl_components_required.add(CONF_TOUCHSCREEN) touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID]) lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds From f2e99fa3192f3792189c73326be5fa6563cdf032 Mon Sep 17 00:00:00 2001 From: "David K." <142583+neffs@users.noreply.github.com> Date: Mon, 12 Aug 2024 06:14:58 +0200 Subject: [PATCH 067/160] [bme68x_bsec2_i2c] BME68X Temperature+Pressure+Humidity+Gas Sensor via BSEC2 (#4585) * Added initial bme68x component * Initialize all child sensors to nullptr This was added to all other sensors in #3808 * Update BSEC2 and BME68x Libraries Current versions from Bosch Sensortec * Add myself to codeowners for bme68x_bsec * Move constants to const.py, according to ci-custom checks Move constants to const.py, according to ci-custom checks * Update library dependencies We'll stick with 1.4.2200 for now. 1.4.2200 is not on platform.io registry, use tag instead. Update to 1.5.2400 needs some work due to multi instance support. * Update BSEC2 to 1.6.2400 * Add consts to bme680x_bsec Enable inclusion with external_components * Update device class for pressure * Update to use multisensor API * Tidy up some constants * Add tests * Remove scd30 changes * Import CONF_SAMPLE_RATE * Pull BSEC config blob from repo based on config * Rename component to `bme68x_bsec_i2c` * Fix tests + codeowners * Cleanup for review * Rename using `bsec2` * Apply suggestions from code review Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Download file during validation stage, instead * Make `dump_config()` only dump stuff * Compile safely without sensor and text sensor headers * Use `intf_ptr` * Save state if measuring static IAQ, too * Update CODEOWNERS * Simplify esphome/components/bme68x_bsec2_i2c/__init__.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Remove extraneous colon & imports * Track & save the maximum accuracy value * Polish up accuracy sensor handling * Log static sensor, update `defines.h` * Walruses make it better * Add some logging of setup failures * Update esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp Co-authored-by: Trevor North * Break out some things * Update CODEOWNERS * Update CODEOWNERS take 2 * Use `add_extra` in base schema * Another walrus in the sensor Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --------- Co-authored-by: Keith Burzinski Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Trevor North --- CODEOWNERS | 2 + esphome/components/bme68x_bsec2/__init__.py | 196 +++++++ .../components/bme68x_bsec2/bme68x_bsec2.cpp | 523 ++++++++++++++++++ .../components/bme68x_bsec2/bme68x_bsec2.h | 163 ++++++ esphome/components/bme68x_bsec2/sensor.py | 130 +++++ .../components/bme68x_bsec2/text_sensor.py | 33 ++ .../components/bme68x_bsec2_i2c/__init__.py | 28 + .../bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp | 53 ++ .../bme68x_bsec2_i2c/bme68x_bsec2_i2c.h | 28 + esphome/core/defines.h | 3 +- tests/components/bme68x_bsec2_i2c/common.yaml | 34 ++ .../bme68x_bsec2_i2c/test.esp32-ard.yaml | 5 + .../bme68x_bsec2_i2c/test.esp32-c3-ard.yaml | 5 + .../bme68x_bsec2_i2c/test.esp32-s2-ard.yaml | 5 + .../bme68x_bsec2_i2c/test.esp32-s3-ard.yaml | 5 + .../bme68x_bsec2_i2c/test.esp8266-ard.yaml | 5 + 16 files changed, 1217 insertions(+), 1 deletion(-) create mode 100644 esphome/components/bme68x_bsec2/__init__.py create mode 100644 esphome/components/bme68x_bsec2/bme68x_bsec2.cpp create mode 100644 esphome/components/bme68x_bsec2/bme68x_bsec2.h create mode 100644 esphome/components/bme68x_bsec2/sensor.py create mode 100644 esphome/components/bme68x_bsec2/text_sensor.py create mode 100644 esphome/components/bme68x_bsec2_i2c/__init__.py create mode 100644 esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp create mode 100644 esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h create mode 100644 tests/components/bme68x_bsec2_i2c/common.yaml create mode 100644 tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml create mode 100644 tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml create mode 100644 tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml create mode 100644 tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml create mode 100644 tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 82e6e0ea4b..999449a3df 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -65,6 +65,8 @@ esphome/components/bluetooth_proxy/* @jesserockz esphome/components/bme280_base/* @esphome/core esphome/components/bme280_spi/* @apbodrov esphome/components/bme680_bsec/* @trvrnrth +esphome/components/bme68x_bsec2/* @kbx81 @neffs +esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs esphome/components/bmi160/* @flaviut esphome/components/bmp3xx/* @latonita esphome/components/bmp3xx_base/* @latonita @martgras diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py new file mode 100644 index 0000000000..1930c7c9e3 --- /dev/null +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -0,0 +1,196 @@ +import hashlib +from pathlib import Path + +from esphome import core, external_files +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MODEL, + CONF_RAW_DATA_ID, + CONF_SAMPLE_RATE, + CONF_TEMPERATURE_OFFSET, +) + +CODEOWNERS = ["@neffs", "@kbx81"] + +DOMAIN = "bme68x_bsec2" + +BSEC2_LIBRARY_VERSION = "v1.7.2502" + +CONF_ALGORITHM_OUTPUT = "algorithm_output" +CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id" +CONF_IAQ_MODE = "iaq_mode" +CONF_OPERATING_AGE = "operating_age" +CONF_STATE_SAVE_INTERVAL = "state_save_interval" +CONF_SUPPLY_VOLTAGE = "supply_voltage" + +bme68x_bsec2_ns = cg.esphome_ns.namespace("bme68x_bsec2") +BME68xBSEC2Component = bme68x_bsec2_ns.class_("BME68xBSEC2Component", cg.Component) + + +MODEL_OPTIONS = ["bme680", "bme688"] + +AlgorithmOutput = bme68x_bsec2_ns.enum("AlgorithmOutput") +ALGORITHM_OUTPUT_OPTIONS = { + "classification": AlgorithmOutput.ALGORITHM_OUTPUT_CLASSIFICATION, + "regression": AlgorithmOutput.ALGORITHM_OUTPUT_REGRESSION, +} + +OperatingAge = bme68x_bsec2_ns.enum("OperatingAge") +OPERATING_AGE_OPTIONS = { + "4d": OperatingAge.OPERATING_AGE_4D, + "28d": OperatingAge.OPERATING_AGE_28D, +} + +SampleRate = bme68x_bsec2_ns.enum("SampleRate") +SAMPLE_RATE_OPTIONS = { + "LP": SampleRate.SAMPLE_RATE_LP, + "ULP": SampleRate.SAMPLE_RATE_ULP, +} + +Voltage = bme68x_bsec2_ns.enum("Voltage") +VOLTAGE_OPTIONS = { + "1.8V": Voltage.VOLTAGE_1_8V, + "3.3V": Voltage.VOLTAGE_3_3V, +} + +ALGORITHM_OUTPUT_FILE_NAME = { + "classification": "sel", + "regression": "reg", +} + +SAMPLE_RATE_FILE_NAME = { + "LP": "3s", + "ULP": "300s", +} + +VOLTAGE_FILE_NAME = { + "1.8V": "18v", + "3.3V": "33v", +} + + +def _compute_local_file_path(url: str) -> Path: + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + return base_dir / key + + +def _compute_url(config: dict) -> str: + model = config.get(CONF_MODEL) + operating_age = config.get(CONF_OPERATING_AGE) + sample_rate = SAMPLE_RATE_FILE_NAME[config.get(CONF_SAMPLE_RATE)] + volts = VOLTAGE_FILE_NAME[config.get(CONF_SUPPLY_VOLTAGE)] + if model == "bme688": + algo = ALGORITHM_OUTPUT_FILE_NAME[ + config.get(CONF_ALGORITHM_OUTPUT, "classification") + ] + filename = "bsec_selectivity" + else: + algo = "iaq" + filename = "bsec_iaq" + return f"https://raw.githubusercontent.com/boschsensortec/Bosch-BSEC2-Library/{BSEC2_LIBRARY_VERSION}/src/config/{model}/{model}_{algo}_{volts}_{sample_rate}_{operating_age}/{filename}.txt" + + +def download_bme68x_blob(config): + url = _compute_url(config) + path = _compute_local_file_path(url) + external_files.download_content(url, path) + + return config + + +def validate_bme68x(config): + if CONF_ALGORITHM_OUTPUT not in config: + return config + + if config[CONF_MODEL] != "bme688": + raise cv.Invalid(f"{CONF_ALGORITHM_OUTPUT} is only valid for BME688") + + if config[CONF_ALGORITHM_OUTPUT] == "regression" and ( + config[CONF_OPERATING_AGE] != "4d" + or config[CONF_SAMPLE_RATE] != "ULP" + or config[CONF_SUPPLY_VOLTAGE] != "1.8V" + ): + raise cv.Invalid( + f" To use '{CONF_ALGORITHM_OUTPUT}: regression', {CONF_OPERATING_AGE} must be '4d', {CONF_SAMPLE_RATE} must be 'ULP' and {CONF_SUPPLY_VOLTAGE} must be '1.8V'" + ) + return config + + +CONFIG_SCHEMA_BASE = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BME68xBSEC2Component), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + cv.Required(CONF_MODEL): cv.one_of(*MODEL_OPTIONS, lower=True), + cv.Optional(CONF_ALGORITHM_OUTPUT): cv.enum( + ALGORITHM_OUTPUT_OPTIONS, lower=True + ), + cv.Optional(CONF_OPERATING_AGE, default="28d"): cv.enum( + OPERATING_AGE_OPTIONS, lower=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum( + SAMPLE_RATE_OPTIONS, upper=True + ), + cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum( + VOLTAGE_OPTIONS, upper=True + ), + cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature, + cv.Optional( + CONF_STATE_SAVE_INTERVAL, default="6hours" + ): cv.positive_time_period_minutes, + }, + ) + .add_extra(cv.only_with_arduino) + .add_extra(validate_bme68x) + .add_extra(download_bme68x_blob) +) + + +async def to_code_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if algo_output := config.get(CONF_ALGORITHM_OUTPUT): + cg.add(var.set_algorithm_output(algo_output)) + cg.add(var.set_operating_age(config[CONF_OPERATING_AGE])) + cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) + cg.add(var.set_voltage(config[CONF_SUPPLY_VOLTAGE])) + cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) + cg.add( + var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) + ) + + path = _compute_local_file_path(_compute_url(config)) + + try: + with open(path, encoding="utf-8") as f: + bsec2_iaq_config = f.read() + except Exception as e: + raise core.EsphomeError(f"Could not open binary configuration file {path}: {e}") + + # Convert retrieved BSEC2 config to an array of ints + rhs = [int(x) for x in bsec2_iaq_config.split(",")] + # Create an array which will reside in program memory and configure the sensor instance to use it + bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs))) + + # Although this component does not use SPI, the BSEC2 library requires the SPI library + cg.add_library("SPI", None) + cg.add_library( + "BME68x Sensor library", + "1.1.40407", + ) + cg.add_library( + "BSEC2 Software Library", + None, + f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}", + ) + + cg.add_define("USE_BSEC2") + + return var diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp new file mode 100644 index 0000000000..5425bbd5b7 --- /dev/null +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -0,0 +1,523 @@ +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_BSEC2 +#include "bme68x_bsec2.h" + +#include + +namespace esphome { +namespace bme68x_bsec2 { + +#define BME68X_BSEC2_ALGORITHM_OUTPUT_LOG(a) (a == ALGORITHM_OUTPUT_CLASSIFICATION ? "Classification" : "Regression") +#define BME68X_BSEC2_OPERATING_AGE_LOG(o) (o == OPERATING_AGE_4D ? "4 days" : "28 days") +#define BME68X_BSEC2_SAMPLE_RATE_LOG(r) (r == SAMPLE_RATE_DEFAULT ? "Default" : (r == SAMPLE_RATE_ULP ? "ULP" : "LP")) +#define BME68X_BSEC2_VOLTAGE_LOG(v) (v == VOLTAGE_3_3V ? "3.3V" : "1.8V") + +static const char *const TAG = "bme68x_bsec2.sensor"; + +static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; + +void BME68xBSEC2Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up BME68X via BSEC2..."); + + this->bsec_status_ = bsec_init_m(&this->bsec_instance_); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "bsec_init_m failed: status %d", this->bsec_status_); + return; + } + + bsec_get_version_m(&this->bsec_instance_, &this->version_); + + this->bme68x_status_ = bme68x_init(&this->bme68x_); + if (this->bme68x_status_ != BME68X_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "bme68x_init failed: status %d", this->bme68x_status_); + return; + } + if (this->bsec2_configuration_ != nullptr && this->bsec2_configuration_length_) { + this->set_config_(this->bsec2_configuration_, this->bsec2_configuration_length_); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "bsec_set_configuration_m failed: status %d", this->bsec_status_); + return; + } + } + + this->update_subscription_(); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "bsec_update_subscription_m failed: status %d", this->bsec_status_); + return; + } + + this->load_state_(); +} + +void BME68xBSEC2Component::dump_config() { + ESP_LOGCONFIG(TAG, "BME68X via BSEC2:"); + + ESP_LOGCONFIG(TAG, " BSEC2 version: %d.%d.%d.%d", this->version_.major, this->version_.minor, + this->version_.major_bugfix, this->version_.minor_bugfix); + + ESP_LOGCONFIG(TAG, " BSEC2 configuration blob:"); + ESP_LOGCONFIG(TAG, " Configured: %s", YESNO(this->bsec2_blob_configured_)); + if (this->bsec2_configuration_ != nullptr && this->bsec2_configuration_length_) { + ESP_LOGCONFIG(TAG, " Size: %" PRIu32, this->bsec2_configuration_length_); + } + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_, + this->bme68x_status_); + } + + if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) { + ESP_LOGCONFIG(TAG, " Algorithm output: %s", BME68X_BSEC2_ALGORITHM_OUTPUT_LOG(this->algorithm_output_)); + } + ESP_LOGCONFIG(TAG, " Operating age: %s", BME68X_BSEC2_OPERATING_AGE_LOG(this->operating_age_)); + ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->sample_rate_)); + ESP_LOGCONFIG(TAG, " Voltage: %s", BME68X_BSEC2_VOLTAGE_LOG(this->voltage_)); + ESP_LOGCONFIG(TAG, " State save interval: %ims", this->state_save_interval_ms_); + ESP_LOGCONFIG(TAG, " Temperature offset: %.2f", this->temperature_offset_); + +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->temperature_sample_rate_)); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->pressure_sample_rate_)); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->humidity_sample_rate_)); + LOG_SENSOR(" ", "Gas resistance", this->gas_resistance_sensor_); + LOG_SENSOR(" ", "CO2 equivalent", this->co2_equivalent_sensor_); + LOG_SENSOR(" ", "Breath VOC equivalent", this->breath_voc_equivalent_sensor_); + LOG_SENSOR(" ", "IAQ", this->iaq_sensor_); + LOG_SENSOR(" ", "IAQ static", this->iaq_static_sensor_); + LOG_SENSOR(" ", "Numeric IAQ accuracy", this->iaq_accuracy_sensor_); +#endif +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR(" ", "IAQ accuracy", this->iaq_accuracy_text_sensor_); +#endif +} + +float BME68xBSEC2Component::get_setup_priority() const { return setup_priority::DATA; } + +void BME68xBSEC2Component::loop() { + this->run_(); + + if (this->bsec_status_ < BSEC_OK || this->bme68x_status_ < BME68X_OK) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + if (this->bsec_status_ > BSEC_OK || this->bme68x_status_ > BME68X_OK) { + this->status_set_warning(); + } else { + this->status_clear_warning(); + } + // Process a single action from the queue. These are primarily sensor state publishes + // that in totality take too long to send in a single call. + if (this->queue_.size()) { + auto action = std::move(this->queue_.front()); + this->queue_.pop(); + action(); + } +} + +void BME68xBSEC2Component::set_config_(const uint8_t *config, uint32_t len) { + if (len > BSEC_MAX_PROPERTY_BLOB_SIZE) { + ESP_LOGE(TAG, "Configuration is larger than BSEC_MAX_PROPERTY_BLOB_SIZE"); + this->mark_failed(); + return; + } + uint8_t work_buffer[BSEC_MAX_PROPERTY_BLOB_SIZE]; + this->bsec_status_ = bsec_set_configuration_m(&this->bsec_instance_, config, len, work_buffer, sizeof(work_buffer)); + if (this->bsec_status_ == BSEC_OK) { + this->bsec2_blob_configured_ = true; + } +} + +float BME68xBSEC2Component::calc_sensor_sample_rate_(SampleRate sample_rate) { + if (sample_rate == SAMPLE_RATE_DEFAULT) { + sample_rate = this->sample_rate_; + } + return sample_rate == SAMPLE_RATE_ULP ? BSEC_SAMPLE_RATE_ULP : BSEC_SAMPLE_RATE_LP; +} + +void BME68xBSEC2Component::update_subscription_() { + bsec_sensor_configuration_t virtual_sensors[BSEC_NUMBER_OUTPUTS]; + uint8_t num_virtual_sensors = 0; +#ifdef USE_SENSOR + if (this->iaq_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_IAQ; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->iaq_static_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_STATIC_IAQ; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->co2_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_CO2_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->breath_voc_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_BREATH_VOC_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->pressure_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_PRESSURE; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->pressure_sample_rate_); + num_virtual_sensors++; + } + + if (this->gas_resistance_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_GAS; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->temperature_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->temperature_sample_rate_); + num_virtual_sensors++; + } + + if (this->humidity_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->humidity_sample_rate_); + num_virtual_sensors++; + } +#endif + bsec_sensor_configuration_t sensor_settings[BSEC_MAX_PHYSICAL_SENSOR]; + uint8_t num_sensor_settings = BSEC_MAX_PHYSICAL_SENSOR; + this->bsec_status_ = bsec_update_subscription_m(&this->bsec_instance_, virtual_sensors, num_virtual_sensors, + sensor_settings, &num_sensor_settings); +} + +void BME68xBSEC2Component::run_() { + int64_t curr_time_ns = this->get_time_ns_(); + if (curr_time_ns < this->next_call_ns_) { + return; + } + this->op_mode_ = this->bsec_settings_.op_mode; + uint8_t status; + + ESP_LOGV(TAG, "Performing sensor run"); + + struct bme68x_conf bme68x_conf; + this->bsec_status_ = bsec_sensor_control_m(&this->bsec_instance_, curr_time_ns, &this->bsec_settings_); + if (this->bsec_status_ < BSEC_OK) { + ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC2 error code %d)", this->bsec_status_); + return; + } + this->next_call_ns_ = this->bsec_settings_.next_call; + + if (this->bsec_settings_.trigger_measurement) { + bme68x_get_conf(&bme68x_conf, &this->bme68x_); + + bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling; + bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling; + bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling; + bme68x_set_conf(&bme68x_conf, &this->bme68x_); + + switch (this->bsec_settings_.op_mode) { + case BME68X_FORCED_MODE: + this->bme68x_heatr_conf_.enable = BME68X_ENABLE; + this->bme68x_heatr_conf_.heatr_temp = this->bsec_settings_.heater_temperature; + this->bme68x_heatr_conf_.heatr_dur = this->bsec_settings_.heater_duration; + + status = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_); + status = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); + status = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_); + this->op_mode_ = BME68X_FORCED_MODE; + this->sleep_mode_ = false; + ESP_LOGV(TAG, "Using forced mode"); + + break; + case BME68X_PARALLEL_MODE: + if (this->op_mode_ != this->bsec_settings_.op_mode) { + this->bme68x_heatr_conf_.enable = BME68X_ENABLE; + this->bme68x_heatr_conf_.heatr_temp_prof = this->bsec_settings_.heater_temperature_profile; + this->bme68x_heatr_conf_.heatr_dur_prof = this->bsec_settings_.heater_duration_profile; + this->bme68x_heatr_conf_.profile_len = this->bsec_settings_.heater_profile_len; + this->bme68x_heatr_conf_.shared_heatr_dur = + BSEC_TOTAL_HEAT_DUR - + (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &bme68x_conf, &this->bme68x_) / INT64_C(1000)); + + status = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); + + status = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_); + this->op_mode_ = BME68X_PARALLEL_MODE; + this->sleep_mode_ = false; + ESP_LOGV(TAG, "Using parallel mode"); + } + break; + case BME68X_SLEEP_MODE: + if (!this->sleep_mode_) { + bme68x_set_op_mode(BME68X_SLEEP_MODE, &this->bme68x_); + this->sleep_mode_ = true; + ESP_LOGV(TAG, "Using sleep mode"); + } + break; + } + + uint32_t meas_dur = 0; + meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); + ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); + this->set_timeout("read", meas_dur / 1000, [this, curr_time_ns]() { this->read_(curr_time_ns); }); + } else { + ESP_LOGV(TAG, "Measurement not required"); + this->read_(curr_time_ns); + } +} + +void BME68xBSEC2Component::read_(int64_t trigger_time_ns) { + ESP_LOGV(TAG, "Reading data"); + + if (this->bsec_settings_.trigger_measurement) { + uint8_t current_op_mode; + this->bme68x_status_ = bme68x_get_op_mode(¤t_op_mode, &this->bme68x_); + + if (current_op_mode == BME68X_SLEEP_MODE) { + ESP_LOGV(TAG, "Still in sleep mode, doing nothing"); + return; + } + } + + if (!this->bsec_settings_.process_data) { + ESP_LOGV(TAG, "Data processing not required"); + return; + } + + struct bme68x_data data[3]; + uint8_t nFields = 0; + this->bme68x_status_ = bme68x_get_data(this->op_mode_, &data[0], &nFields, &this->bme68x_); + + if (this->bme68x_status_ != BME68X_OK) { + ESP_LOGW(TAG, "Failed to get sensor data (BME68X error code %d)", this->bme68x_status_); + return; + } + if (nFields < 1) { + ESP_LOGD(TAG, "BME68X did not provide new data"); + return; + } + + for (uint8_t i = 0; i < nFields; i++) { + bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR]; // Temperature, Pressure, Humidity & Gas Resistance + uint8_t num_inputs = 0; + + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_TEMPERATURE)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_TEMPERATURE; + inputs[num_inputs].signal = data[i].temperature; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_HEATSOURCE)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_HEATSOURCE; + inputs[num_inputs].signal = this->temperature_offset_; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_HUMIDITY)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_HUMIDITY; + inputs[num_inputs].signal = data[i].humidity; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_PRESSURE)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_PRESSURE; + inputs[num_inputs].signal = data[i].pressure; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_GASRESISTOR)) { + if (data[i].status & BME68X_GASM_VALID_MSK) { + inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR; + inputs[num_inputs].signal = data[i].gas_resistance; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } else { + ESP_LOGD(TAG, "BME68X did not report gas data"); + } + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_PROFILE_PART) && + (data[i].status & BME68X_GASM_VALID_MSK)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_PROFILE_PART; + inputs[num_inputs].signal = (this->op_mode_ == BME68X_FORCED_MODE) ? 0 : data[i].gas_index; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + + if (num_inputs < 1) { + ESP_LOGD(TAG, "No signal inputs available for BSEC2"); + return; + } + + bsec_output_t outputs[BSEC_NUMBER_OUTPUTS]; + uint8_t num_outputs = BSEC_NUMBER_OUTPUTS; + this->bsec_status_ = bsec_do_steps_m(&this->bsec_instance_, inputs, num_inputs, outputs, &num_outputs); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "BSEC2 failed to process signals (BSEC2 error code %d)", this->bsec_status_); + return; + } + if (num_outputs < 1) { + ESP_LOGD(TAG, "No signal outputs provided by BSEC2"); + return; + } + + this->publish_(outputs, num_outputs); + } +} + +void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { + ESP_LOGV(TAG, "Publishing sensor states"); + bool update_accuracy = false; + uint8_t max_accuracy = 0; + for (uint8_t i = 0; i < num_outputs; i++) { + float signal = outputs[i].signal; + switch (outputs[i].sensor_id) { + case BSEC_OUTPUT_IAQ: + max_accuracy = std::max(outputs[i].accuracy, max_accuracy); + update_accuracy = true; +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_STATIC_IAQ: + max_accuracy = std::max(outputs[i].accuracy, max_accuracy); + update_accuracy = true; +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_static_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_CO2_EQUIVALENT: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_RAW_PRESSURE: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); }); +#endif + break; + case BSEC_OUTPUT_RAW_GAS: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); }); +#endif + break; + } + } + if (update_accuracy) { +#ifdef USE_SENSOR + this->queue_push_( + [this, max_accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, max_accuracy, true); }); +#endif +#ifdef USE_TEXT_SENSOR + this->queue_push_([this, max_accuracy]() { + this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[max_accuracy]); + }); +#endif + // Queue up an opportunity to save state + this->queue_push_([this, max_accuracy]() { this->save_state_(max_accuracy); }); + } +} + +int64_t BME68xBSEC2Component::get_time_ns_() { + int64_t time_ms = millis(); + if (this->last_time_ms_ > time_ms) { + this->millis_overflow_counter_++; + } + this->last_time_ms_ = time_ms; + + return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000); +} + +#ifdef USE_SENSOR +void BME68xBSEC2Component::publish_sensor_(sensor::Sensor *sensor, float value, bool change_only) { + if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} +#endif + +#ifdef USE_TEXT_SENSOR +void BME68xBSEC2Component::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) { + if (!sensor || (sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} +#endif + +void BME68xBSEC2Component::load_state_() { + uint32_t hash = this->get_hash(); + this->bsec_state_ = global_preferences->make_preference(hash, true); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + if (this->bsec_state_.load(&state)) { + ESP_LOGV(TAG, "Loading state"); + uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; + this->bsec_status_ = + bsec_set_state_m(&this->bsec_instance_, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, sizeof(work_buffer)); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed to load state (BSEC2 error code %d)", this->bsec_status_); + } + ESP_LOGI(TAG, "Loaded state"); + } +} + +void BME68xBSEC2Component::save_state_(uint8_t accuracy) { + if (accuracy < 3 || (millis() - this->last_state_save_ms_ < this->state_save_interval_ms_)) { + return; + } + + ESP_LOGV(TAG, "Saving state"); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + uint8_t work_buffer[BSEC_MAX_STATE_BLOB_SIZE]; + uint32_t num_serialized_state = BSEC_MAX_STATE_BLOB_SIZE; + + this->bsec_status_ = bsec_get_state_m(&this->bsec_instance_, 0, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, + BSEC_MAX_STATE_BLOB_SIZE, &num_serialized_state); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed fetch state for save (BSEC2 error code %d)", this->bsec_status_); + return; + } + + if (!this->bsec_state_.save(&state)) { + ESP_LOGW(TAG, "Failed to save state"); + return; + } + this->last_state_save_ms_ = millis(); + + ESP_LOGI(TAG, "Saved state"); +} + +} // namespace bme68x_bsec2 +} // namespace esphome +#endif diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h new file mode 100644 index 0000000000..7b9db2b7bf --- /dev/null +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h @@ -0,0 +1,163 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" + +#ifdef USE_BSEC2 + +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif + +#include +#include + +#include + +namespace esphome { +namespace bme68x_bsec2 { + +enum AlgorithmOutput { + ALGORITHM_OUTPUT_IAQ, + ALGORITHM_OUTPUT_CLASSIFICATION, + ALGORITHM_OUTPUT_REGRESSION, +}; + +enum OperatingAge { + OPERATING_AGE_4D, + OPERATING_AGE_28D, +}; + +enum SampleRate { + SAMPLE_RATE_LP = 0, + SAMPLE_RATE_ULP = 1, + SAMPLE_RATE_DEFAULT = 2, +}; + +enum Voltage { + VOLTAGE_1_8V, + VOLTAGE_3_3V, +}; + +class BME68xBSEC2Component : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + void set_algorithm_output(AlgorithmOutput algorithm_output) { this->algorithm_output_ = algorithm_output; } + void set_operating_age(OperatingAge operating_age) { this->operating_age_ = operating_age; } + void set_temperature_offset(float offset) { this->temperature_offset_ = offset; } + void set_voltage(Voltage voltage) { this->voltage_ = voltage; } + + void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; } + void set_temperature_sample_rate(SampleRate sample_rate) { this->temperature_sample_rate_ = sample_rate; } + void set_pressure_sample_rate(SampleRate sample_rate) { this->pressure_sample_rate_ = sample_rate; } + void set_humidity_sample_rate(SampleRate sample_rate) { this->humidity_sample_rate_ = sample_rate; } + + void set_bsec2_configuration(const uint8_t *data, const uint32_t len) { + this->bsec2_configuration_ = data; + this->bsec2_configuration_length_ = len; + } + + void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; } + +#ifdef USE_SENSOR + void set_temperature_sensor(sensor::Sensor *sensor) { this->temperature_sensor_ = sensor; } + void set_pressure_sensor(sensor::Sensor *sensor) { this->pressure_sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } + void set_gas_resistance_sensor(sensor::Sensor *sensor) { this->gas_resistance_sensor_ = sensor; } + void set_iaq_sensor(sensor::Sensor *sensor) { this->iaq_sensor_ = sensor; } + void set_iaq_static_sensor(sensor::Sensor *sensor) { this->iaq_static_sensor_ = sensor; } + void set_iaq_accuracy_sensor(sensor::Sensor *sensor) { this->iaq_accuracy_sensor_ = sensor; } + void set_co2_equivalent_sensor(sensor::Sensor *sensor) { this->co2_equivalent_sensor_ = sensor; } + void set_breath_voc_equivalent_sensor(sensor::Sensor *sensor) { this->breath_voc_equivalent_sensor_ = sensor; } +#endif +#ifdef USE_TEXT_SENSOR + void set_iaq_accuracy_text_sensor(text_sensor::TextSensor *sensor) { this->iaq_accuracy_text_sensor_ = sensor; } +#endif + virtual uint32_t get_hash() = 0; + + protected: + void set_config_(const uint8_t *config, u_int32_t len); + float calc_sensor_sample_rate_(SampleRate sample_rate); + void update_subscription_(); + + void run_(); + void read_(int64_t trigger_time_ns); + void publish_(const bsec_output_t *outputs, uint8_t num_outputs); + int64_t get_time_ns_(); + +#ifdef USE_SENSOR + void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false); +#endif +#ifdef USE_TEXT_SENSOR + void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value); +#endif + + void load_state_(); + void save_state_(uint8_t accuracy); + + void queue_push_(std::function &&f) { this->queue_.push(std::move(f)); } + + struct bme68x_dev bme68x_; + bsec_bme_settings_t bsec_settings_; + bsec_version_t version_; + uint8_t bsec_instance_[BSEC_INSTANCE_SIZE]; + + struct bme68x_heatr_conf bme68x_heatr_conf_; + uint8_t op_mode_; // operating mode of sensor + bool sleep_mode_; + bsec_library_return_t bsec_status_{BSEC_OK}; + int8_t bme68x_status_{BME68X_OK}; + + int64_t last_time_ms_{0}; + uint32_t millis_overflow_counter_{0}; + int64_t next_call_ns_{0}; + + std::queue> queue_; + + uint8_t const *bsec2_configuration_{nullptr}; + uint32_t bsec2_configuration_length_{0}; + bool bsec2_blob_configured_{false}; + + ESPPreferenceObject bsec_state_; + uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day + uint32_t last_state_save_ms_ = 0; + + float temperature_offset_{0}; + + AlgorithmOutput algorithm_output_{ALGORITHM_OUTPUT_IAQ}; + OperatingAge operating_age_{OPERATING_AGE_28D}; + Voltage voltage_{VOLTAGE_3_3V}; + + SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate + SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT}; + SampleRate pressure_sample_rate_{SAMPLE_RATE_DEFAULT}; + SampleRate humidity_sample_rate_{SAMPLE_RATE_DEFAULT}; + +#ifdef USE_SENSOR + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *gas_resistance_sensor_{nullptr}; + sensor::Sensor *iaq_sensor_{nullptr}; + sensor::Sensor *iaq_static_sensor_{nullptr}; + sensor::Sensor *iaq_accuracy_sensor_{nullptr}; + sensor::Sensor *co2_equivalent_sensor_{nullptr}; + sensor::Sensor *breath_voc_equivalent_sensor_{nullptr}; +#endif +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *iaq_accuracy_text_sensor_{nullptr}; +#endif +}; + +} // namespace bme68x_bsec2 +} // namespace esphome +#endif diff --git a/esphome/components/bme68x_bsec2/sensor.py b/esphome/components/bme68x_bsec2/sensor.py new file mode 100644 index 0000000000..419f47b248 --- /dev/null +++ b/esphome/components/bme68x_bsec2/sensor.py @@ -0,0 +1,130 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_GAS_RESISTANCE, + CONF_HUMIDITY, + CONF_IAQ_ACCURACY, + CONF_PRESSURE, + CONF_SAMPLE_RATE, + CONF_TEMPERATURE, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + ICON_GAS_CYLINDER, + ICON_GAUGE, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + UNIT_OHM, + UNIT_PARTS_PER_MILLION, + UNIT_PERCENT, +) + +from . import CONF_BME68X_BSEC2_ID, SAMPLE_RATE_OPTIONS, BME68xBSEC2Component + +DEPENDENCIES = ["bme68x_bsec2"] + +CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent" +CONF_CO2_EQUIVALENT = "co2_equivalent" +CONF_IAQ = "iaq" +CONF_IAQ_STATIC = "iaq_static" +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" +ICON_TEST_TUBE = "mdi:test-tube" +UNIT_IAQ = "IAQ" + +TYPES = [ + CONF_TEMPERATURE, + CONF_PRESSURE, + CONF_HUMIDITY, + CONF_GAS_RESISTANCE, + CONF_IAQ, + CONF_IAQ_STATIC, + CONF_IAQ_ACCURACY, + CONF_CO2_EQUIVALENT, + CONF_BREATH_VOC_EQUIVALENT, +] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + icon=ICON_GAUGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} + ), + cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_OHM, + icon=ICON_GAS_CYLINDER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IAQ): sensor.sensor_schema( + unit_of_measurement=UNIT_IAQ, + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IAQ_STATIC): sensor.sensor_schema( + unit_of_measurement=UNIT_IAQ, + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema( + icon=ICON_ACCURACY, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +) + + +async def setup_conf(config, key, hub): + if conf := config.get(key): + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) + if sample_rate := conf.get(CONF_SAMPLE_RATE): + cg.add(getattr(hub, f"set_{key}_sample_rate")(sample_rate)) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_BME68X_BSEC2_ID]) + for key in TYPES: + await setup_conf(config, key, hub) diff --git a/esphome/components/bme68x_bsec2/text_sensor.py b/esphome/components/bme68x_bsec2/text_sensor.py new file mode 100644 index 0000000000..fce00afe34 --- /dev/null +++ b/esphome/components/bme68x_bsec2/text_sensor.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_IAQ_ACCURACY + +from . import CONF_BME68X_BSEC2_ID, BME68xBSEC2Component + +DEPENDENCIES = ["bme68x_bsec2"] + +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" + +TYPES = [CONF_IAQ_ACCURACY] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component), + cv.Optional(CONF_IAQ_ACCURACY): text_sensor.text_sensor_schema( + icon=ICON_ACCURACY + ), + } +) + + +async def setup_conf(config, key, hub): + if conf := config.get(key): + sens = await text_sensor.new_text_sensor(conf) + cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_BME68X_BSEC2_ID]) + for key in TYPES: + await setup_conf(config, key, hub) diff --git a/esphome/components/bme68x_bsec2_i2c/__init__.py b/esphome/components/bme68x_bsec2_i2c/__init__.py new file mode 100644 index 0000000000..d6fb7fa9be --- /dev/null +++ b/esphome/components/bme68x_bsec2_i2c/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +from esphome.components import i2c +from esphome.components.bme68x_bsec2 import ( + CONFIG_SCHEMA_BASE, + BME68xBSEC2Component, + to_code_base, +) +import esphome.config_validation as cv + +CODEOWNERS = ["@neffs", "@kbx81"] + +AUTO_LOAD = ["bme68x_bsec2"] +DEPENDENCIES = ["i2c"] + +bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c") +BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_( + "BME68xBSEC2I2CComponent", BME68xBSEC2Component, i2c.I2CDevice +) + + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + cv.Schema({cv.GenerateID(): cv.declare_id(BME68xBSEC2I2CComponent)}) +).extend(i2c.i2c_device_schema(0x76)) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp new file mode 100644 index 0000000000..874c8bf388 --- /dev/null +++ b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp @@ -0,0 +1,53 @@ +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_BSEC2 +#include "bme68x_bsec2_i2c.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace bme68x_bsec2_i2c { + +static const char *const TAG = "bme68x_bsec2_i2c.sensor"; + +void BME68xBSEC2I2CComponent::setup() { + // must set up our bme68x_dev instance before calling setup() + this->bme68x_.intf_ptr = (void *) this; + this->bme68x_.intf = BME68X_I2C_INTF; + this->bme68x_.read = BME68xBSEC2I2CComponent::read_bytes_wrapper; + this->bme68x_.write = BME68xBSEC2I2CComponent::write_bytes_wrapper; + this->bme68x_.delay_us = BME68xBSEC2I2CComponent::delay_us; + this->bme68x_.amb_temp = 25; + + BME68xBSEC2Component::setup(); +} + +void BME68xBSEC2I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + BME68xBSEC2Component::dump_config(); +} + +uint32_t BME68xBSEC2I2CComponent::get_hash() { return fnv1_hash("bme68x_bsec_state_" + to_string(this->address_)); } + +int8_t BME68xBSEC2I2CComponent::read_bytes_wrapper(uint8_t a_register, uint8_t *data, uint32_t len, void *intfPtr) { + ESP_LOGVV(TAG, "read_bytes_wrapper: reg = %u", a_register); + return static_cast(intfPtr)->read_bytes(a_register, data, len) ? 0 : -1; +} + +int8_t BME68xBSEC2I2CComponent::write_bytes_wrapper(uint8_t a_register, const uint8_t *data, uint32_t len, + void *intfPtr) { + ESP_LOGVV(TAG, "write_bytes_wrapper: reg = %u", a_register); + return static_cast(intfPtr)->write_bytes(a_register, data, len) ? 0 : -1; +} + +void BME68xBSEC2I2CComponent::delay_us(uint32_t period, void *intfPtr) { + ESP_LOGVV(TAG, "Delaying for %" PRIu32 "us", period); + delayMicroseconds(period); +} + +} // namespace bme68x_bsec2_i2c +} // namespace esphome +#endif diff --git a/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h new file mode 100644 index 0000000000..a21a123f7b --- /dev/null +++ b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" + +#ifdef USE_BSEC2 + +#include "esphome/components/bme68x_bsec2/bme68x_bsec2.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bme68x_bsec2_i2c { + +class BME68xBSEC2I2CComponent : public bme68x_bsec2::BME68xBSEC2Component, public i2c::I2CDevice { + void setup() override; + void dump_config() override; + + uint32_t get_hash() override; + + static int8_t read_bytes_wrapper(uint8_t a_register, uint8_t *data, uint32_t len, void *intfPtr); + static int8_t write_bytes_wrapper(uint8_t a_register, const uint8_t *data, uint32_t len, void *intfPtr); + static void delay_us(uint32_t period, void *intfPtr); +}; + +} // namespace bme68x_bsec2_i2c +} // namespace esphome +#endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 61a4940d01..a711148ec8 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -158,6 +158,7 @@ #endif // Disabled feature flags -// #define USE_BSEC // Requires a library with proprietary license. +// #define USE_BSEC // Requires a library with proprietary license +// #define USE_BSEC2 // Requires a library with proprietary license #define USE_DASHBOARD_IMPORT diff --git a/tests/components/bme68x_bsec2_i2c/common.yaml b/tests/components/bme68x_bsec2_i2c/common.yaml new file mode 100644 index 0000000000..b8a16ee7bb --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/common.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_bme68x + scl: ${scl_pin} + sda: ${sda_pin} + +bme68x_bsec2_i2c: + address: 0x76 + model: bme688 + algorithm_output: classification + operating_age: 28d + sample_rate: LP + supply_voltage: 3.3V + +sensor: + - platform: bme68x_bsec2 + temperature: + name: BME68X Temperature + pressure: + name: BME68X Pressure + humidity: + name: BME68X Humidity + gas_resistance: + name: BME68X Gas Sensor + iaq: + name: BME68X IAQ + co2_equivalent: + name: BME68X eCO2 + breath_voc_equivalent: + name: BME68X Breath eVOC + +text_sensor: + - platform: bme68x_bsec2 + iaq_accuracy: + name: BME68X Accuracy diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..84a9dd4bb4 --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO6 + sda_pin: GPIO7 + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml From e769804fe6f2502e98ddd8ced9db78898ac8ff1e Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 12 Aug 2024 06:27:22 +0200 Subject: [PATCH 068/160] [code-quality] clang-tidy media_player (#7238) --- esphome/components/media_player/automation.h | 58 +++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index fc3ce7a764..f0e0a5dd31 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -7,30 +7,24 @@ namespace esphome { namespace media_player { -#define MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ACTION_CLASS, ACTION_COMMAND) \ - template class ACTION_CLASS : public Action, public Parented { \ - void play(Ts... x) override { \ - this->parent_->make_call().set_command(MediaPlayerCommand::MEDIA_PLAYER_COMMAND_##ACTION_COMMAND).perform(); \ - } \ - }; +template +class MediaPlayerCommandAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->make_call().set_command(Command).perform(); } +}; -#define MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(TRIGGER_CLASS, TRIGGER_STATE) \ - class TRIGGER_CLASS : public Trigger<> { \ - public: \ - explicit TRIGGER_CLASS(MediaPlayer *player) { \ - player->add_on_state_callback([this, player]() { \ - if (player->state == MediaPlayerState::MEDIA_PLAYER_STATE_##TRIGGER_STATE) \ - this->trigger(); \ - }); \ - } \ - }; - -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PlayAction, PLAY) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PauseAction, PAUSE) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(StopAction, STOP) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ToggleAction, TOGGLE) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeUpAction, VOLUME_UP) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeDownAction, VOLUME_DOWN) +template +using PlayAction = MediaPlayerCommandAction; +template +using PauseAction = MediaPlayerCommandAction; +template +using StopAction = MediaPlayerCommandAction; +template +using ToggleAction = MediaPlayerCommandAction; +template +using VolumeUpAction = MediaPlayerCommandAction; +template +using VolumeDownAction = MediaPlayerCommandAction; template class PlayMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) @@ -49,10 +43,20 @@ class StateTrigger : public Trigger<> { } }; -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(IdleTrigger, IDLE) -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(PlayTrigger, PLAYING) -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(PauseTrigger, PAUSED) -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(AnnouncementTrigger, ANNOUNCING) +template class MediaPlayerStateTrigger : public Trigger<> { + public: + explicit MediaPlayerStateTrigger(MediaPlayer *player) { + player->add_on_state_callback([this, player]() { + if (player->state == State) + this->trigger(); + }); + } +}; + +using IdleTrigger = MediaPlayerStateTrigger; +using PlayTrigger = MediaPlayerStateTrigger; +using PauseTrigger = MediaPlayerStateTrigger; +using AnnouncementTrigger = MediaPlayerStateTrigger; template class IsIdleCondition : public Condition, public Parented { public: From 82c5cd18de86307cd6a047abc0f5489caf1af137 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:30:27 +1200 Subject: [PATCH 069/160] Bump docker/build-push-action from 6.5.0 to 6.6.1 in /.github/actions/build-image (#7232) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index bd9ceb8072..f9c44cfb63 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -46,7 +46,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v6.5.0 + uses: docker/build-push-action@v6.6.1 with: context: . file: ./docker/Dockerfile @@ -69,7 +69,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v6.5.0 + uses: docker/build-push-action@v6.6.1 with: context: . file: ./docker/Dockerfile From 8a076cc9064612f51e2a356259544890c66d82a5 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 12 Aug 2024 06:49:35 +0200 Subject: [PATCH 070/160] fix build error (#7229) --- esphome/components/api/api_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 81fa4cb339..8e4c6faaee 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1336,7 +1336,7 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { update->check(); break; default: - ESP_LOGW(TAG, "Unknown update command: %d", msg.command); + ESP_LOGW(TAG, "Unknown update command: %" PRIu32, msg.command); break; } } From f13cf1f7a02c6fbe3d867151aac276b306e0820a Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 12 Aug 2024 06:52:09 +0200 Subject: [PATCH 071/160] adjust to new python pre-commit hooks (#7178) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- script/clang-tidy | 35 +++++++++++++++++++++++++---------- script/helpers.py | 9 ++++----- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/script/clang-tidy b/script/clang-tidy index ea522157c5..5bb93846b2 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -115,9 +115,10 @@ def clang_options(idedata): pids = set() -def run_tidy(executable, args, options, tmpdir, queue, lock, failed_files): + +def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files): while True: - path = queue.get() + path = path_queue.get() invocation = [executable] if tmpdir is not None: @@ -139,17 +140,20 @@ def run_tidy(executable, args, options, tmpdir, queue, lock, failed_files): invocation.append("--") invocation.extend(options) - proc = subprocess.run(invocation, capture_output=True, encoding="utf-8") + proc = subprocess.run( + invocation, capture_output=True, encoding="utf-8", check=False + ) if proc.returncode != 0: with lock: print_error_for_file(path, proc.stdout) failed_files.append(path) - queue.task_done() + path_queue.task_done() def progress_bar_show(value): if value is None: return "" + return None def split_list(a, n): @@ -237,7 +241,15 @@ def main(): for _ in range(args.jobs): t = threading.Thread( target=run_tidy, - args=(executable, args, options, tmpdir, task_queue, lock, failed_files), + args=( + executable, + args, + options, + tmpdir, + task_queue, + lock, + failed_files, + ), ) t.daemon = True t.start() @@ -245,14 +257,14 @@ def main(): # Fill the queue with files. with click.progressbar( files, width=30, file=sys.stderr, item_show_func=progress_bar_show - ) as bar: - for name in bar: + ) as progress_bar: + for name in progress_bar: task_queue.put(name) # Wait for all threads to be done. task_queue.join() - except FileNotFoundError as ex: + except FileNotFoundError: return 1 except KeyboardInterrupt: print() @@ -262,7 +274,7 @@ def main(): # Kill subprocesses (and ourselves!) # No simple, clean alternative appears to be available. os.kill(0, 9) - return 2 # Will not execute. + return 2 # Will not execute. if args.fix and failed_files: print("Applying fixes ...") @@ -272,7 +284,10 @@ def main(): except FileNotFoundError: subprocess.call(["clang-apply-replacements", tmpdir]) except FileNotFoundError: - print("Error please install clang-apply-replacements-14 or clang-apply-replacements.\n", file=sys.stderr) + print( + "Error please install clang-apply-replacements-14 or clang-apply-replacements.\n", + file=sys.stderr, + ) except: print("Error applying fixes.\n", file=sys.stderr) raise diff --git a/script/helpers.py b/script/helpers.py index 56349b6052..6f36faaeb1 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -159,20 +159,19 @@ def get_binary(name: str, version: str) -> str: binary_file = f"{name}-{version}" try: result = subprocess.check_output([binary_file, "-version"]) - if result.returncode == 0: - return binary_file - except Exception: + return binary_file + except FileNotFoundError: pass binary_file = name try: result = subprocess.run( - [binary_file, "-version"], text=True, capture_output=True + [binary_file, "-version"], text=True, capture_output=True, check=False ) if result.returncode == 0 and (f"version {version}") in result.stdout: return binary_file raise FileNotFoundError(f"{name} not found") - except FileNotFoundError as ex: + except FileNotFoundError: print( f""" Oops. It looks like {name} is not installed. It should be available under venv/bin From 8148eae1340c5de43c1333761b27d01f8e9d9ba9 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Tue, 13 Aug 2024 01:16:42 +0200 Subject: [PATCH 072/160] add windows script/setup.bat (#7140) Co-authored-by: Keith Burzinski --- script/setup.bat | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 script/setup.bat diff --git a/script/setup.bat b/script/setup.bat new file mode 100644 index 0000000000..0b49768139 --- /dev/null +++ b/script/setup.bat @@ -0,0 +1,28 @@ +@echo off + +if defined DEVCONTAINER goto :install +if defined VIRTUAL_ENV goto :install +if defined ESPHOME_NO_VENV goto :install + +echo Starting the Virtual Environment +python -m venv venv +call venv/Scripts/activate +echo Running the Virtual Environment + +:install + +echo Installing required packages... + +python.exe -m pip install --upgrade pip + +pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt -r requirements_dev.txt +pip3 install setuptools wheel +pip3 install -e ".[dev,test,displays]" --config-settings editable_mode=compat + +pre-commit install + +python script/platformio_install_deps.py platformio.ini --libraries --tools --platforms + +echo . +echo . +echo Virtual environment created. Run 'venv/Scripts/activate' to use it. From 5f3f10628318d01d4a01e8fc23161f2ce075052b Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 13 Aug 2024 01:29:09 +0200 Subject: [PATCH 073/160] [code-quality] add NOLINT haier_base (#7236) --- esphome/components/haier/haier_base.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index c0bf878519..7d92a6611c 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -80,8 +80,8 @@ class HaierClimateBase : public esphome::Component, const char *phase_to_string_(ProtocolPhases phase); virtual void set_handlers() = 0; virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; - virtual haier_protocol::HaierMessage get_control_message() = 0; - virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; + virtual haier_protocol::HaierMessage get_control_message() = 0; // NOLINT(readability-identifier-naming) + virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; // NOLINT(readability-identifier-naming) virtual void initialization(){}; virtual bool prepare_pending_action(); virtual void process_protocol_reset(); From 64ee40d3704a4e40ffea6f962ada64fc803d4f62 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 13 Aug 2024 01:33:51 +0200 Subject: [PATCH 074/160] [code-quality] clang-tidy bedjet (#7251) --- esphome/components/bedjet/bedjet_codec.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bedjet/bedjet_codec.h b/esphome/components/bedjet/bedjet_codec.h index 527e757d7f..07aee32d54 100644 --- a/esphome/components/bedjet/bedjet_codec.h +++ b/esphome/components/bedjet/bedjet_codec.h @@ -90,7 +90,7 @@ struct BedjetStatusPacket { int unused_6 : 1; // 0x4 bool is_dual_zone : 1; /// Is part of a Dual Zone configuration int unused_7 : 1; // 0x1 - } dual_zone_flags; + } dual_zone_flags; // NOLINT(clang-diagnostic-unaligned-access) uint8_t unused_4 : 8; // Unknown 23-24 = 0x1310 uint8_t unused_5 : 8; // Unknown 23-24 = 0x1310 From f24fd34d860ab39fa66d100aff96dc0f0ec43ee2 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 13 Aug 2024 01:38:13 +0200 Subject: [PATCH 075/160] fix name conflict with zephyr macro (#7252) --- esphome/components/fingerprint_grow/fingerprint_grow.cpp | 2 +- esphome/components/fingerprint_grow/fingerprint_grow.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index c2cab368c9..0dfea49b8b 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -307,7 +307,7 @@ void FingerprintGrowComponent::delete_fingerprint(uint16_t finger_id) { void FingerprintGrowComponent::delete_all_fingerprints() { ESP_LOGI(TAG, "Deleting all stored fingerprints"); - this->data_ = {EMPTY}; + this->data_ = {DELETE_ALL}; switch (this->send_command_()) { case OK: ESP_LOGI(TAG, "Deleted all fingerprints"); diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 20ff60997b..1c3098ef14 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -36,7 +36,7 @@ enum GrowCommand { LOAD = 0x07, UPLOAD = 0x08, DELETE = 0x0C, - EMPTY = 0x0D, + DELETE_ALL = 0x0D, // aka EMPTY READ_SYS_PARAM = 0x0F, SET_PASSWORD = 0x12, VERIFY_PASSWORD = 0x13, From 8d5be27746ed6a510b87f53cff6811a277a2589d Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 13 Aug 2024 02:47:18 +0200 Subject: [PATCH 076/160] [code-quality] Apply ruff linting suggestions (#7239) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/network/__init__.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 9ef75e0fb9..f5ddbc0da7 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,8 +1,6 @@ -from esphome.core import CORE import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components.esp32 import add_idf_sdkconfig_option - +import esphome.config_validation as cv from esphome.const import ( CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT, @@ -10,6 +8,7 @@ from esphome.const import ( PLATFORM_ESP8266, PLATFORM_RP2040, ) +from esphome.core import CORE CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] @@ -42,11 +41,10 @@ async def to_code(config): if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6) add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6) - else: - if enable_ipv6: - cg.add_build_flag("-DCONFIG_LWIP_IPV6") - cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG") - if CORE.is_rp2040: - cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6") - if CORE.is_esp8266: - cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY") + elif enable_ipv6: + cg.add_build_flag("-DCONFIG_LWIP_IPV6") + cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG") + if CORE.is_rp2040: + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6") + if CORE.is_esp8266: + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY") From fc146dabed431ff1c7f314d329cad62ea9d06731 Mon Sep 17 00:00:00 2001 From: juanluss31 <40864809+juanluss31@users.noreply.github.com> Date: Tue, 13 Aug 2024 03:12:48 +0200 Subject: [PATCH 077/160] Add support for LYWSD02MMC Xiaomi device (#7080) --- CODEOWNERS | 1 + esphome/components/xiaomi_ble/xiaomi_ble.cpp | 23 +++++- esphome/components/xiaomi_ble/xiaomi_ble.h | 1 + .../components/xiaomi_lywsd02mmc/__init__.py | 0 .../components/xiaomi_lywsd02mmc/sensor.py | 77 +++++++++++++++++++ .../xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp | 73 ++++++++++++++++++ .../xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h | 37 +++++++++ .../components/xiaomi_lywsd02mmc/common.yaml | 12 +++ .../xiaomi_lywsd02mmc/test.esp32-ard.yaml | 1 + .../xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml | 1 + .../xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml | 1 + .../xiaomi_lywsd02mmc/test.esp32-idf.yaml | 1 + 12 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 esphome/components/xiaomi_lywsd02mmc/__init__.py create mode 100644 esphome/components/xiaomi_lywsd02mmc/sensor.py create mode 100644 esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp create mode 100644 esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h create mode 100644 tests/components/xiaomi_lywsd02mmc/common.yaml create mode 100644 tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml create mode 100644 tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml create mode 100644 tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml create mode 100644 tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 999449a3df..9865e51f11 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -453,6 +453,7 @@ esphome/components/wl_134/* @hobbypunk90 esphome/components/x9c/* @EtienneMD esphome/components/xgzp68xx/* @gcormier esphome/components/xiaomi_hhccjcy10/* @fariouche +esphome/components/xiaomi_lywsd02mmc/* @juanluss31 esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 95faea0446..85434341cc 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -49,8 +49,8 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_ const uint16_t conductivity = encode_uint16(data[1], data[0]); result.conductivity = conductivity; } - // battery, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x100A) && (value_length == 1)) { + // battery / MiaoMiaoce battery, 1 byte, 8-bit unsigned integer, 1 % + else if ((value_type == 0x100A || value_type == 0x4803) && (value_length == 1)) { result.battery_level = data[0]; } // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % @@ -80,6 +80,17 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_ result.has_motion = !idle_time; } else if ((value_type == 0x1018) && (value_length == 1)) { result.is_light = data[0]; + } + // MiaoMiaoce temperature, 4 bytes, float, 0.1 °C + else if ((value_type == 0x4C01) && (value_length == 4)) { + const uint32_t int_number = encode_uint32(data[3], data[2], data[1], data[0]); + float temperature; + std::memcpy(&temperature, &int_number, sizeof(temperature)); + result.temperature = temperature; + } + // MiaoMiaoce humidity, 1 byte, 8-bit unsigned integer, 1 % + else if ((value_type == 0x4C02) && (value_length == 1)) { + result.humidity = data[0]; } else { return false; } @@ -111,7 +122,8 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult } while (payload_length > 3) { - if (payload[payload_offset + 1] != 0x10 && payload[payload_offset + 1] != 0x00) { + if (payload[payload_offset + 1] != 0x10 && payload[payload_offset + 1] != 0x00 && + payload[payload_offset + 1] != 0x4C && payload[payload_offset + 1] != 0x48) { ESP_LOGVV(TAG, "parse_xiaomi_message(): fixed byte not found, stop parsing residual data."); break; } @@ -190,6 +202,11 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service } else if (device_uuid == 0x045b) { // rectangular body, e-ink display result.type = XiaomiParseResult::TYPE_LYWSD02; result.name = "LYWSD02"; + } else if (device_uuid == 0x2542) { // rectangular body, e-ink display — with bindkeys + result.type = XiaomiParseResult::TYPE_LYWSD02MMC; + result.name = "LYWSD02MMC"; + if (raw.size() == 19) + result.raw_offset -= 6; } else if (device_uuid == 0x040a) { // Mosquito Repellent Smart Version result.type = XiaomiParseResult::TYPE_WX08ZM; result.name = "WX08ZM"; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index c1086605d1..6978be97f4 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -17,6 +17,7 @@ struct XiaomiParseResult { TYPE_HHCCPOT002, TYPE_LYWSDCGQ, TYPE_LYWSD02, + TYPE_LYWSD02MMC, TYPE_CGG1, TYPE_LYWSD03MMC, TYPE_CGD1, diff --git a/esphome/components/xiaomi_lywsd02mmc/__init__.py b/esphome/components/xiaomi_lywsd02mmc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_lywsd02mmc/sensor.py b/esphome/components/xiaomi_lywsd02mmc/sensor.py new file mode 100644 index 0000000000..43784ef698 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02mmc/sensor.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_HUMIDITY, + CONF_MAC_ADDRESS, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_BATTERY, + CONF_ID, + CONF_BINDKEY, +) + +AUTO_LOAD = ["xiaomi_ble"] +CODEOWNERS = ["@juanluss31"] +DEPENDENCIES = ["esp32_ble_tracker"] + +xiaomi_lywsd02mmc_ns = cg.esphome_ns.namespace("xiaomi_lywsd02mmc") +XiaomiLYWSD02MMC = xiaomi_lywsd02mmc_ns.class_( + "XiaomiLYWSD02MMC", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XiaomiLYWSD02MMC), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) + if battery_level_config := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(battery_level_config) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp new file mode 100644 index 0000000000..cc122f2264 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp @@ -0,0 +1,73 @@ +#include "xiaomi_lywsd02mmc.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_lywsd02mmc { + +static const char *const TAG = "xiaomi_lywsd02mmc"; + +void XiaomiLYWSD02MMC::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi LYWSD02MMC"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiLYWSD02MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + return success; +} + +void XiaomiLYWSD02MMC::set_bindkey(const std::string &bindkey) { + memset(this->bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + this->bindkey_[i] = std::strtoul(temp, nullptr, 16); + } +} + +} // namespace xiaomi_lywsd02mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h new file mode 100644 index 0000000000..19092aa2a9 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_lywsd02mmc { + +class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { this->address_ = address; } + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_lywsd02mmc +} // namespace esphome + +#endif diff --git a/tests/components/xiaomi_lywsd02mmc/common.yaml b/tests/components/xiaomi_lywsd02mmc/common.yaml new file mode 100644 index 0000000000..e63f585830 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/common.yaml @@ -0,0 +1,12 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_lywsd02mmc + mac_address: A4:C1:38:54:5E:18 + bindkey: 2529d8e0d23150a588675cc54ad48400 + temperature: + name: Xiaomi LYWSD02MMC Temperature + humidity: + name: Xiaomi LYWSD02MMC Humidity + battery_level: + name: Xiaomi LYWSD02MMC Battery Level diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 8d106e97a2bd03239e11609fb6a53b1e493c634b Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 13 Aug 2024 03:14:25 +0200 Subject: [PATCH 078/160] [code-quality] fix clang-tidy web server (#7230) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/web_server/web_server.h | 2 +- esphome/components/web_server_base/web_server_base.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 5b98806af1..d4ab592b7b 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -334,7 +334,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Override the web handler's handleRequest method. void handleRequest(AsyncWebServerRequest *request) override; /// This web handle is not trivial. - bool isRequestHandlerTrivial() override; + bool isRequestHandlerTrivial() override; // NOLINT(readability-identifier-naming) void add_entity_to_sorting_list(EntityBase *entity, float weight); diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index c312126472..2282d55ec1 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -134,6 +134,7 @@ class OTARequestHandler : public AsyncWebHandler { return request->url() == "/update" && request->method() == HTTP_POST; } + // NOLINTNEXTLINE(readability-identifier-naming) bool isRequestHandlerTrivial() override { return false; } protected: From 390d5f2f9361c00c558d64fb00c9ce66b92703e1 Mon Sep 17 00:00:00 2001 From: RFDarter Date: Tue, 13 Aug 2024 03:26:39 +0200 Subject: [PATCH 079/160] [test][web_server] Rejig test for v3 (#7110) --- tests/components/web_server/common_v1.yaml | 3 ++- tests/components/web_server/common_v2.yaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/web_server/common_v1.yaml b/tests/components/web_server/common_v1.yaml index bf5aab4ce6..3c51f894b8 100644 --- a/tests/components/web_server/common_v1.yaml +++ b/tests/components/web_server/common_v1.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + device_base: !include common.yaml web_server: port: 8080 diff --git a/tests/components/web_server/common_v2.yaml b/tests/components/web_server/common_v2.yaml index 564c43e553..2af5ceca44 100644 --- a/tests/components/web_server/common_v2.yaml +++ b/tests/components/web_server/common_v2.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + device_base: !include common.yaml web_server: port: 8080 From ab51bbd8f7a9ca8648f7a245cdec02f8fa08e14b Mon Sep 17 00:00:00 2001 From: Olivier ARCHER Date: Tue, 13 Aug 2024 03:52:31 +0200 Subject: [PATCH 080/160] [api] Error log when NONE Update command is sent (#7247) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8e4c6faaee..bd438265d4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1335,6 +1335,9 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { case enums::UPDATE_COMMAND_CHECK: update->check(); break; + case enums::UPDATE_COMMAND_NONE: + ESP_LOGE(TAG, "UPDATE_COMMAND_NONE not handled. Check client is sending the correct command"); + break; default: ESP_LOGW(TAG, "Unknown update command: %" PRIu32, msg.command); break; From 2b25daa199b175e1f2d3f1db94b6516f37b90748 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:12:06 +1200 Subject: [PATCH 081/160] [api] Add new flag to request state/attribute once from HA only (#7258) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.cpp | 15 +++++++++++++++ esphome/components/api/api_pb2.h | 2 ++ esphome/components/api/api_server.cpp | 10 ++++++++++ esphome/components/api/api_server.h | 3 +++ 5 files changed, 31 insertions(+) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index b62fddf815..72eaeed6d7 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -686,6 +686,7 @@ message SubscribeHomeAssistantStateResponse { option (source) = SOURCE_SERVER; string entity_id = 1; string attribute = 2; + bool once = 3; } message HomeAssistantStateResponse { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index a57627a66c..bb37824403 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3109,6 +3109,16 @@ void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeAssistantStatesRequest {}"); } #endif +bool SubscribeHomeAssistantStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->once = value.as_bool(); + return true; + } + default: + return false; + } +} bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -3126,6 +3136,7 @@ bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, Proto void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->entity_id); buffer.encode_string(2, this->attribute); + buffer.encode_bool(3, this->once); } #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { @@ -3138,6 +3149,10 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { out.append(" attribute: "); out.append("'").append(this->attribute).append("'"); out.append("\n"); + + out.append(" once: "); + out.append(YESNO(this->once)); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index bb5263cffa..3eb945fd8d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -836,6 +836,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { public: std::string entity_id{}; std::string attribute{}; + bool once{false}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -843,6 +844,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class HomeAssistantStateResponse : public ProtoMessage { public: diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a61ae89243..0fde3e47af 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -359,8 +359,18 @@ void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, + std::function f) { + this->state_subs_.push_back(HomeAssistantStateSubscription{ + .entity_id = std::move(entity_id), + .attribute = std::move(attribute), + .callback = std::move(f), + .once = true, + }); +}; const std::vector &APIServer::get_state_subs() const { return this->state_subs_; } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 43bc8a7348..899eaede49 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -112,10 +112,13 @@ class APIServer : public Component, public Controller { std::string entity_id; optional attribute; std::function callback; + bool once; }; void subscribe_home_assistant_state(std::string entity_id, optional attribute, std::function f); + void get_home_assistant_state(std::string entity_id, optional attribute, + std::function f); const std::vector &get_state_subs() const; const std::vector &get_user_services() const { return this->user_services_; } From 8696f922d120e787f7231d9d4b8a00f74eec0125 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:33:16 +1200 Subject: [PATCH 082/160] [homeassistant] Add ``HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA`` (#7259) --- CODEOWNERS | 2 +- esphome/components/homeassistant/__init__.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9865e51f11..663a942cb4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -168,7 +168,7 @@ esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode -esphome/components/homeassistant/* @OttoWinter +esphome/components/homeassistant/* @OttoWinter @esphome/core esphome/components/honeywell_hih_i2c/* @Benichou34 esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp2_i2c/* @jpfaff diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index 776aa7fd7b..6d997e48ca 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@OttoWinter", "@esphome/core"] homeassistant_ns = cg.esphome_ns.namespace("homeassistant") HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema( @@ -13,6 +13,13 @@ HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema( } ) +HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA = cv.Schema( + { + cv.Required(CONF_ENTITY_ID): cv.entity_id, + cv.Optional(CONF_INTERNAL, default=True): cv.boolean, + } +) + def setup_home_assistant_entity(var, config): cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) From 2a70ef05d17c0645fd4e9023d6b1bc5c2ea078ca Mon Sep 17 00:00:00 2001 From: nkinnan Date: Mon, 12 Aug 2024 23:48:12 -0700 Subject: [PATCH 083/160] [const] Add some units for future use and adjust case (#7260) --- esphome/const.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/const.py b/esphome/const.py index 13559ecf95..c5d0e8f838 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1034,8 +1034,10 @@ UNIT_KELVIN = "K" UNIT_KILOGRAM = "kg" UNIT_KILOMETER = "km" UNIT_KILOMETER_PER_HOUR = "km/h" -UNIT_KILOVOLT_AMPS_REACTIVE = "kVAr" -UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVArh" +UNIT_KILOVOLT_AMPS = "kVA" +UNIT_KILOVOLT_AMPS_HOURS = "kVAh" +UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR" +UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" UNIT_LUX = "lx" @@ -1066,6 +1068,7 @@ UNIT_SECOND = "s" UNIT_STEPS = "steps" UNIT_VOLT = "V" UNIT_VOLT_AMPS = "VA" +UNIT_VOLT_AMPS_HOURS = "VAh" UNIT_VOLT_AMPS_REACTIVE = "VAR" UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh" UNIT_WATT = "W" From 506e69addf81fc8cc542520d7db17937a8b74c5b Mon Sep 17 00:00:00 2001 From: guillempages Date: Tue, 13 Aug 2024 09:44:43 +0200 Subject: [PATCH 084/160] [online_image] add option to show placeholder while downloading (#7083) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/online_image/__init__.py | 6 ++++++ esphome/components/online_image/online_image.cpp | 8 ++++++++ esphome/components/online_image/online_image.h | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index ee5357457a..d9a7609543 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -27,6 +27,7 @@ CODEOWNERS = ["@guillempages"] MULTI_CONF = True CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" +CONF_PLACEHOLDER = "placeholder" _LOGGER = logging.getLogger(__name__) @@ -73,6 +74,7 @@ ONLINE_IMAGE_SCHEMA = cv.Schema( # 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( { @@ -152,6 +154,10 @@ async def to_code(config): cg.add(var.set_transparency(transparent)) + if placeholder_id := config.get(CONF_PLACEHOLDER): + placeholder = await cg.get_variable(placeholder_id) + cg.add(var.set_placeholder(placeholder)) + for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index a4cf0158aa..480bad6aca 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -35,6 +35,14 @@ OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFor this->set_url(url); } +void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { + if (this->data_start_) { + Image::draw(x, y, display, color_on, color_off); + } else if (this->placeholder_) { + this->placeholder_->draw(x, y, display, color_on, color_off); + } +} + void OnlineImage::release() { if (this->buffer_) { ESP_LOGD(TAG, "Deallocating old buffer..."); diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 30e97760ea..775cc46e0b 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -50,6 +50,8 @@ class OnlineImage : public PollingComponent, OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, uint32_t buffer_size); + void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + void update() override; void loop() override; @@ -60,6 +62,14 @@ class OnlineImage : public PollingComponent, } } + /** + * @brief Set the image that needs to be shown as long as the downloaded image + * is not available. + * + * @param placeholder Pointer to the (@link Image) to show as placeholder. + */ + void set_placeholder(image::Image *placeholder) { this->placeholder_ = placeholder; } + /** * Release the buffer storing the image. The image will need to be downloaded again * to be able to be displayed. @@ -113,6 +123,7 @@ class OnlineImage : public PollingComponent, DownloadBuffer download_buffer_; const ImageFormat format_; + image::Image *placeholder_{nullptr}; std::string url_{""}; From 3598560472b844c172a1ed2783f13ba258be378d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 13 Aug 2024 18:06:01 +1000 Subject: [PATCH 085/160] [lvgl] Add initial_focus for encoders (#7256) --- esphome/components/lvgl/__init__.py | 3 ++- esphome/components/lvgl/defines.py | 1 + esphome/components/lvgl/encoders.py | 8 ++++++++ esphome/components/lvgl/schemas.py | 25 +++++++++++++++-------- tests/components/lvgl/test.esp32-ard.yaml | 1 + 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 6bf6e287f8..7c51d9c70d 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -23,7 +23,7 @@ from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid from .automation import disp_update, update_to_code from .defines import CONF_SKIP -from .encoders import ENCODERS_CONFIG, encoders_to_code +from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code from .lv_validation import lv_bool, lv_images_used from .lvcode import LvContext, LvglComponent from .schemas import ( @@ -272,6 +272,7 @@ async def to_code(config): templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ) await build_automation(idle_trigger, [], conf) + await initial_focus_to_code(config) for comp in helpers.lvgl_components_required: CORE.add_define(f"USE_LVGL_{comp.upper()}") diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 1c6fd2678c..e48679996b 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -413,6 +413,7 @@ CONF_GRID_ROW_ALIGN = "grid_row_align" CONF_GRID_ROWS = "grid_rows" CONF_HEADER_MODE = "header_mode" CONF_HOME = "home" +CONF_INITIAL_FOCUS = "initial_focus" CONF_KEY_CODE = "key_code" CONF_LAYOUT = "layout" CONF_LEFT_BUTTON = "left_button" diff --git a/esphome/components/lvgl/encoders.py b/esphome/components/lvgl/encoders.py index cfd0e42996..81bcda95b4 100644 --- a/esphome/components/lvgl/encoders.py +++ b/esphome/components/lvgl/encoders.py @@ -8,6 +8,7 @@ from .defines import ( CONF_DEFAULT_GROUP, CONF_ENCODERS, CONF_ENTER_BUTTON, + CONF_INITIAL_FOCUS, CONF_LEFT_BUTTON, CONF_LONG_PRESS_REPEAT_TIME, CONF_LONG_PRESS_TIME, @@ -67,3 +68,10 @@ async def encoders_to_code(var, config): else: group = default_group lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) + + +async def initial_focus_to_code(config): + for enc_conf in config[CONF_ENCODERS]: + if default_focus := enc_conf.get(CONF_INITIAL_FOCUS): + obj = await cg.get_variable(default_focus) + lv.group_focus_obj(obj) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index f172ba9f2b..e4b1c3f8fa 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -14,11 +14,19 @@ from esphome.const import ( from esphome.core import TimePeriod from esphome.schema_extractors import SCHEMA_EXTRACT -from . import defines as df, lv_validation as lvalid, types as ty +from . import defines as df, lv_validation as lvalid from .helpers import add_lv_use, requires_component, validate_printf from .lv_validation import lv_color, lv_font, lv_image from .lvcode import LvglComponent -from .types import WidgetType, lv_group_t +from .types import ( + LVEncoderListener, + LvType, + WidgetType, + lv_group_t, + lv_obj_t, + lv_pseudo_button_t, + lv_style_t, +) # this will be populated later, in __init__.py to avoid circular imports. WIDGET_TYPES: dict = {} @@ -46,7 +54,7 @@ TEXT_SCHEMA = cv.Schema( LIST_ACTION_SCHEMA = cv.ensure_list( cv.maybe_simple_value( { - cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t), + cv.Required(CONF_ID): cv.use_id(lv_pseudo_button_t), }, key=CONF_ID, ) @@ -59,9 +67,10 @@ PRESS_TIME = cv.All( ENCODER_SCHEMA = cv.Schema( { cv.GenerateID(): cv.All( - cv.declare_id(ty.LVEncoderListener), requires_component("binary_sensor") + cv.declare_id(LVEncoderListener), requires_component("binary_sensor") ), cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t), + cv.Optional(df.CONF_INITIAL_FOCUS): cv.use_id(lv_obj_t), cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, } @@ -161,7 +170,7 @@ STYLE_REMAP = { # Complete object style schema STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( { - cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(ty.lv_style_t)), + cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(lv_style_t)), cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" ).one_of, @@ -193,12 +202,12 @@ def part_schema(widget_type: WidgetType): ) -def automation_schema(typ: ty.LvType): +def automation_schema(typ: LvType): if typ.has_on_value: events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,) else: events = df.LV_EVENT_TRIGGERS - if isinstance(typ, ty.LvType): + if isinstance(typ, LvType): template = Trigger.template(typ.get_arg_type()) else: template = Trigger.template() @@ -261,7 +270,7 @@ LAYOUT_SCHEMAS = {} ALIGN_TO_SCHEMA = { cv.Optional(df.CONF_ALIGN_TO): cv.Schema( { - cv.Required(CONF_ID): cv.use_id(ty.lv_obj_t), + cv.Required(CONF_ID): cv.use_id(lv_obj_t), cv.Required(df.CONF_ALIGN): df.ALIGN_ALIGNMENTS.one_of, cv.Optional(df.CONF_X, default=0): lvalid.pixels_or_percent, cv.Optional(df.CONF_Y, default=0): lvalid.pixels_or_percent, diff --git a/tests/components/lvgl/test.esp32-ard.yaml b/tests/components/lvgl/test.esp32-ard.yaml index 2d6a6871ba..51593e7967 100644 --- a/tests/components/lvgl/test.esp32-ard.yaml +++ b/tests/components/lvgl/test.esp32-ard.yaml @@ -46,6 +46,7 @@ binary_sensor: lvgl: encoders: group: switches + initial_focus: button_button enter_button: select_button sensor: left_button: up_button From c9979ad90c950198d3cc624e88d43975e5af497a Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 13 Aug 2024 21:46:23 +0200 Subject: [PATCH 086/160] [code-quality] fix order in esphome/const.py (#7267) --- esphome/const.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/const.py b/esphome/const.py index c5d0e8f838..55f1c23b40 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -7,22 +7,22 @@ VALID_SUBSTITUTIONS_CHARACTERS = ( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" ) +PLATFORM_BK72XX = "bk72xx" PLATFORM_ESP32 = "esp32" PLATFORM_ESP8266 = "esp8266" -PLATFORM_RP2040 = "rp2040" PLATFORM_HOST = "host" -PLATFORM_BK72XX = "bk72xx" -PLATFORM_RTL87XX = "rtl87xx" PLATFORM_LIBRETINY_OLDSTYLE = "libretiny" +PLATFORM_RP2040 = "rp2040" +PLATFORM_RTL87XX = "rtl87xx" TARGET_PLATFORMS = [ + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, - PLATFORM_RP2040, PLATFORM_HOST, - PLATFORM_BK72XX, - PLATFORM_RTL87XX, PLATFORM_LIBRETINY_OLDSTYLE, + PLATFORM_RP2040, + PLATFORM_RTL87XX, ] SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} From b082a64d3248d4a272924f5887915109427b5d68 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 13 Aug 2024 21:48:27 +0200 Subject: [PATCH 087/160] [code-quality] fix clang-tidy network (#7266) --- esphome/components/network/__init__.py | 1 + esphome/components/network/ip_address.h | 3 +++ esphome/components/network/util.cpp | 3 ++- esphome/components/network/util.h | 4 +++- esphome/core/defines.h | 1 + 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index f5ddbc0da7..96db322bde 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -32,6 +32,7 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): + cg.add_define("USE_NETWORK") if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None: cg.add_define("USE_NETWORK_IPV6", enable_ipv6) if enable_ipv6: diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 30a426e458..941934cf0a 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -1,4 +1,6 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include #include #include @@ -140,3 +142,4 @@ using IPAddresses = std::array; } // namespace network } // namespace esphome +#endif diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 445485b644..ed519f738a 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -1,6 +1,6 @@ #include "util.h" #include "esphome/core/defines.h" - +#ifdef USE_NETWORK #ifdef USE_WIFI #include "esphome/components/wifi/wifi_component.h" #endif @@ -63,3 +63,4 @@ std::string get_use_address() { } // namespace network } // namespace esphome +#endif diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index 5377d44f2f..b518696e68 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include #include "ip_address.h" @@ -16,3 +17,4 @@ IPAddresses get_ip_addresses(); } // namespace network } // namespace esphome +#endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a711148ec8..a4d473b76e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -51,6 +51,7 @@ #define USE_MDNS #define USE_MEDIA_PLAYER #define USE_MQTT +#define USE_NETWORK #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER #define USE_ONLINE_IMAGE_PNG_SUPPORT From 9663b7d67ccf2c2e188a709a512ffe0aaae45865 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 13 Aug 2024 21:53:42 +0200 Subject: [PATCH 088/160] [code-quality] fix clang-tidy core optional (#7265) --- esphome/core/optional.h | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/core/optional.h b/esphome/core/optional.h index 5b96781e63..770b77081e 100644 --- a/esphome/core/optional.h +++ b/esphome/core/optional.h @@ -104,7 +104,6 @@ template class optional { // NOLINT has_value_ = true; } - private: bool has_value_{false}; // NOLINT value_type value_; // NOLINT }; From 4bd7ba0d30dca6cb3c13cdd7a0cfab64253e9720 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 13 Aug 2024 21:54:37 +0200 Subject: [PATCH 089/160] [code-quality] Fix variable naming in base_light_effects (#7237) --- esphome/components/light/base_light_effects.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index f7829a3f44..9e02e889c9 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -25,7 +25,7 @@ class PulseLightEffect : public LightEffect { return; } auto call = this->state_->turn_on(); - float out = this->on_ ? this->max_brightness : this->min_brightness; + float out = this->on_ ? this->max_brightness_ : this->min_brightness_; call.set_brightness_if_supported(out); call.set_transition_length_if_supported(this->on_ ? this->transition_on_length_ : this->transition_off_length_); this->on_ = !this->on_; @@ -43,8 +43,8 @@ class PulseLightEffect : public LightEffect { void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } void set_min_max_brightness(float min, float max) { - this->min_brightness = min; - this->max_brightness = max; + this->min_brightness_ = min; + this->max_brightness_ = max; } protected: @@ -53,8 +53,8 @@ class PulseLightEffect : public LightEffect { uint32_t transition_on_length_{}; uint32_t transition_off_length_{}; uint32_t update_interval_{}; - float min_brightness{0.0}; - float max_brightness{1.0}; + float min_brightness_{0.0}; + float max_brightness_{1.0}; }; /// Random effect. Sets random colors every 10 seconds and slowly transitions between them. From f81ce2c70743098796493ce703c7c388de10f783 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 13 Aug 2024 21:56:09 +0200 Subject: [PATCH 090/160] [code-quality] fix clang-tidy mqtt (#7253) --- esphome/components/mqtt/mqtt_backend.h | 4 +++- esphome/components/mqtt/mqtt_backend_esp32.cpp | 5 ++++- esphome/components/mqtt/mqtt_backend_esp32.h | 4 +++- esphome/components/mqtt/mqtt_backend_esp8266.h | 4 +++- esphome/components/mqtt/mqtt_backend_libretiny.h | 4 +++- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/esphome/components/mqtt/mqtt_backend.h b/esphome/components/mqtt/mqtt_backend.h index d23cda578d..3962c40a42 100644 --- a/esphome/components/mqtt/mqtt_backend.h +++ b/esphome/components/mqtt/mqtt_backend.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_MQTT #include #include #include "esphome/components/network/ip_address.h" @@ -67,3 +68,4 @@ class MQTTBackend { } // namespace mqtt } // namespace esphome +#endif diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 9c2e487ae7..ed500c6d44 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -1,7 +1,9 @@ +#include "mqtt_backend_esp32.h" + +#ifdef USE_MQTT #ifdef USE_ESP32 #include -#include "mqtt_backend_esp32.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" @@ -189,3 +191,4 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b } // namespace mqtt } // namespace esphome #endif // USE_ESP32 +#endif diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index b1f672da10..9054702115 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -1,5 +1,7 @@ #pragma once +#include "mqtt_backend.h" +#ifdef USE_MQTT #ifdef USE_ESP32 #include @@ -7,7 +9,6 @@ #include #include "esphome/components/network/ip_address.h" #include "esphome/core/helpers.h" -#include "mqtt_backend.h" namespace esphome { namespace mqtt { @@ -174,3 +175,4 @@ class MQTTBackendESP32 final : public MQTTBackend { } // namespace esphome #endif +#endif diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.h b/esphome/components/mqtt/mqtt_backend_esp8266.h index 06d4993bdf..a979634bf4 100644 --- a/esphome/components/mqtt/mqtt_backend_esp8266.h +++ b/esphome/components/mqtt/mqtt_backend_esp8266.h @@ -1,8 +1,9 @@ #pragma once +#include "mqtt_backend.h" +#ifdef USE_MQTT #ifdef USE_ESP8266 -#include "mqtt_backend.h" #include namespace esphome { @@ -70,3 +71,4 @@ class MQTTBackendESP8266 final : public MQTTBackend { } // namespace esphome #endif // defined(USE_ESP8266) +#endif diff --git a/esphome/components/mqtt/mqtt_backend_libretiny.h b/esphome/components/mqtt/mqtt_backend_libretiny.h index ac4d4298fc..2578ae9941 100644 --- a/esphome/components/mqtt/mqtt_backend_libretiny.h +++ b/esphome/components/mqtt/mqtt_backend_libretiny.h @@ -1,8 +1,9 @@ #pragma once +#include "mqtt_backend.h" +#ifdef USE_MQTT #ifdef USE_LIBRETINY -#include "mqtt_backend.h" #include namespace esphome { @@ -70,3 +71,4 @@ class MQTTBackendLibreTiny final : public MQTTBackend { } // namespace esphome #endif // defined(USE_LIBRETINY) +#endif From 2e58297a16e2ea0e94235b5268fa98e19744f954 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 13 Aug 2024 21:58:30 +0200 Subject: [PATCH 091/160] [code-quality] fix clang-tidy wifi related (#7254) --- esphome/components/wifi/wifi_component.cpp | 2 ++ esphome/components/wifi/wifi_component.h | 4 +++- esphome/components/wifi/wifi_component_esp32_arduino.cpp | 2 ++ esphome/components/wifi/wifi_component_esp8266.cpp | 2 ++ esphome/components/wifi/wifi_component_esp_idf.cpp | 2 ++ esphome/components/wifi/wifi_component_libretiny.cpp | 2 ++ esphome/components/wifi/wifi_component_pico_w.cpp | 2 ++ esphome/components/wifi_info/wifi_info_text_sensor.cpp | 2 ++ esphome/components/wifi_info/wifi_info_text_sensor.h | 2 ++ esphome/components/wifi_signal/wifi_signal_sensor.cpp | 2 ++ esphome/components/wifi_signal/wifi_signal_sensor.h | 3 ++- 11 files changed, 23 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8c40f87879..583a27466a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,4 +1,5 @@ #include "wifi_component.h" +#ifdef USE_WIFI #include #include @@ -856,3 +857,4 @@ WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-con } // namespace wifi } // namespace esphome +#endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d79cde0b18..dde0d1d5a5 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -1,9 +1,10 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_WIFI #include "esphome/components/network/ip_address.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include @@ -442,3 +443,4 @@ template class WiFiDisableAction : public Action { } // namespace wifi } // namespace esphome +#endif diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 71548b7a3e..b8724838c8 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -1,5 +1,6 @@ #include "wifi_component.h" +#ifdef USE_WIFI #ifdef USE_ESP32_FRAMEWORK_ARDUINO #include @@ -802,3 +803,4 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddr } // namespace esphome #endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 997457e2d2..92f80c1e52 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -1,6 +1,7 @@ #include "wifi_component.h" #include "esphome/core/defines.h" +#ifdef USE_WIFI #ifdef USE_ESP8266 #include @@ -834,3 +835,4 @@ void WiFiComponent::wifi_loop_() {} } // namespace esphome #endif +#endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index a8d67ed44d..6008acb95d 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1,5 +1,6 @@ #include "wifi_component.h" +#ifdef USE_WIFI #ifdef USE_ESP_IDF #include @@ -1010,3 +1011,4 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { } // namespace esphome #endif // USE_ESP_IDF +#endif diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index f6b0fb2699..19ade84a88 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -1,5 +1,6 @@ #include "wifi_component.h" +#ifdef USE_WIFI #ifdef USE_LIBRETINY #include @@ -468,3 +469,4 @@ void WiFiComponent::wifi_loop_() {} } // namespace esphome #endif // USE_LIBRETINY +#endif diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 4afcf2d78b..bac986d899 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -1,6 +1,7 @@ #include "wifi_component.h" +#ifdef USE_WIFI #ifdef USE_RP2040 #include "lwip/dns.h" @@ -218,3 +219,4 @@ void WiFiComponent::wifi_pre_setup_() {} } // namespace esphome #endif +#endif diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index eeb4985398..150c7229f8 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -1,4 +1,5 @@ #include "wifi_info_text_sensor.h" +#ifdef USE_WIFI #include "esphome/core/log.h" namespace esphome { @@ -15,3 +16,4 @@ void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo DNS Addre } // namespace wifi_info } // namespace esphome +#endif diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 0f31a57cc5..0aa44a0894 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/wifi/wifi_component.h" +#ifdef USE_WIFI #include namespace esphome { @@ -131,3 +132,4 @@ class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { } // namespace wifi_info } // namespace esphome +#endif diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.cpp b/esphome/components/wifi_signal/wifi_signal_sensor.cpp index ba22138e2a..4347295421 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.cpp +++ b/esphome/components/wifi_signal/wifi_signal_sensor.cpp @@ -1,4 +1,5 @@ #include "wifi_signal_sensor.h" +#ifdef USE_WIFI #include "esphome/core/log.h" namespace esphome { @@ -10,3 +11,4 @@ void WiFiSignalSensor::dump_config() { LOG_SENSOR("", "WiFi Signal", this); } } // namespace wifi_signal } // namespace esphome +#endif diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.h b/esphome/components/wifi_signal/wifi_signal_sensor.h index f797aaa590..fbe03a6404 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.h +++ b/esphome/components/wifi_signal/wifi_signal_sensor.h @@ -4,7 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/wifi/wifi_component.h" - +#ifdef USE_WIFI namespace esphome { namespace wifi_signal { @@ -19,3 +19,4 @@ class WiFiSignalSensor : public sensor::Sensor, public PollingComponent { } // namespace wifi_signal } // namespace esphome +#endif From 9ec61cbff3ec79a7fb3d5fe7720ced35dd69989f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 08:12:56 +1200 Subject: [PATCH 092/160] Bump docker/build-push-action from 6.6.1 to 6.7.0 in /.github/actions/build-image (#7269) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index f9c44cfb63..56be20bd87 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -46,7 +46,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v6.6.1 + uses: docker/build-push-action@v6.7.0 with: context: . file: ./docker/Dockerfile @@ -69,7 +69,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v6.6.1 + uses: docker/build-push-action@v6.7.0 with: context: . file: ./docker/Dockerfile From 0c567adf639585474881d8ee8aff8e316b501a80 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 Aug 2024 08:13:09 +1200 Subject: [PATCH 093/160] [CI] Dont run full CI on ``build-image`` action changes (#7270) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8e93248bb..126a541b3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: paths: - "**" - "!.github/workflows/*.yml" + - "!.github/actions/build-image/*" - ".github/workflows/ci.yml" - "!.yamllint" - "!.github/dependabot.yml" From 68c56b3e03bffb47c4f4481ff534d8d3ac3bc38e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 14 Aug 2024 07:29:31 +1000 Subject: [PATCH 094/160] Implement ByteBuffer (#6878) --- esphome/core/bytebuffer.cpp | 134 ++++++++++++++++++++++++++++++++++++ esphome/core/bytebuffer.h | 96 ++++++++++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 esphome/core/bytebuffer.cpp create mode 100644 esphome/core/bytebuffer.h diff --git a/esphome/core/bytebuffer.cpp b/esphome/core/bytebuffer.cpp new file mode 100644 index 0000000000..fb2ade3166 --- /dev/null +++ b/esphome/core/bytebuffer.cpp @@ -0,0 +1,134 @@ +#include "bytebuffer.h" +#include + +namespace esphome { + +ByteBuffer ByteBuffer::create(size_t capacity) { + std::vector data(capacity); + return {data}; +} + +ByteBuffer ByteBuffer::wrap(uint8_t *ptr, size_t len) { + std::vector data(ptr, ptr + len); + return {data}; +} + +ByteBuffer ByteBuffer::wrap(std::vector data) { return {std::move(data)}; } + +void ByteBuffer::set_limit(size_t limit) { + assert(limit <= this->get_capacity()); + this->limit_ = limit; +} +void ByteBuffer::set_position(size_t position) { + assert(position <= this->get_limit()); + this->position_ = position; +} +void ByteBuffer::clear() { + this->limit_ = this->get_capacity(); + this->position_ = 0; +} +uint16_t ByteBuffer::get_uint16() { + assert(this->get_remaining() >= 2); + uint16_t value; + if (endianness_ == LITTLE) { + value = this->data_[this->position_++]; + value |= this->data_[this->position_++] << 8; + } else { + value = this->data_[this->position_++] << 8; + value |= this->data_[this->position_++]; + } + return value; +} + +uint32_t ByteBuffer::get_uint32() { + assert(this->get_remaining() >= 4); + uint32_t value; + if (endianness_ == LITTLE) { + value = this->data_[this->position_++]; + value |= this->data_[this->position_++] << 8; + value |= this->data_[this->position_++] << 16; + value |= this->data_[this->position_++] << 24; + } else { + value = this->data_[this->position_++] << 24; + value |= this->data_[this->position_++] << 16; + value |= this->data_[this->position_++] << 8; + value |= this->data_[this->position_++]; + } + return value; +} +uint32_t ByteBuffer::get_uint24() { + assert(this->get_remaining() >= 3); + uint32_t value; + if (endianness_ == LITTLE) { + value = this->data_[this->position_++]; + value |= this->data_[this->position_++] << 8; + value |= this->data_[this->position_++] << 16; + } else { + value = this->data_[this->position_++] << 16; + value |= this->data_[this->position_++] << 8; + value |= this->data_[this->position_++]; + } + return value; +} +uint32_t ByteBuffer::get_int24() { + auto value = this->get_uint24(); + uint32_t mask = (~(uint32_t) 0) << 23; + if ((value & mask) != 0) + value |= mask; + return value; +} +uint8_t ByteBuffer::get_uint8() { + assert(this->get_remaining() >= 1); + return this->data_[this->position_++]; +} +float ByteBuffer::get_float() { + auto value = this->get_uint32(); + return *(float *) &value; +} +void ByteBuffer::put_uint8(uint8_t value) { + assert(this->get_remaining() >= 1); + this->data_[this->position_++] = value; +} + +void ByteBuffer::put_uint16(uint16_t value) { + assert(this->get_remaining() >= 2); + if (this->endianness_ == LITTLE) { + this->data_[this->position_++] = (uint8_t) value; + this->data_[this->position_++] = (uint8_t) (value >> 8); + } else { + this->data_[this->position_++] = (uint8_t) (value >> 8); + this->data_[this->position_++] = (uint8_t) value; + } +} +void ByteBuffer::put_uint24(uint32_t value) { + assert(this->get_remaining() >= 3); + if (this->endianness_ == LITTLE) { + this->data_[this->position_++] = (uint8_t) value; + this->data_[this->position_++] = (uint8_t) (value >> 8); + this->data_[this->position_++] = (uint8_t) (value >> 16); + } else { + this->data_[this->position_++] = (uint8_t) (value >> 16); + this->data_[this->position_++] = (uint8_t) (value >> 8); + this->data_[this->position_++] = (uint8_t) value; + } +} +void ByteBuffer::put_uint32(uint32_t value) { + assert(this->get_remaining() >= 4); + if (this->endianness_ == LITTLE) { + this->data_[this->position_++] = (uint8_t) value; + this->data_[this->position_++] = (uint8_t) (value >> 8); + this->data_[this->position_++] = (uint8_t) (value >> 16); + this->data_[this->position_++] = (uint8_t) (value >> 24); + } else { + this->data_[this->position_++] = (uint8_t) (value >> 24); + this->data_[this->position_++] = (uint8_t) (value >> 16); + this->data_[this->position_++] = (uint8_t) (value >> 8); + this->data_[this->position_++] = (uint8_t) value; + } +} +void ByteBuffer::put_float(float value) { this->put_uint32(*(uint32_t *) &value); } +void ByteBuffer::flip() { + this->limit_ = this->position_; + this->position_ = 0; +} +} // namespace esphome diff --git a/esphome/core/bytebuffer.h b/esphome/core/bytebuffer.h new file mode 100644 index 0000000000..f242e5e333 --- /dev/null +++ b/esphome/core/bytebuffer.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include +#include + +namespace esphome { + +enum Endian { LITTLE, BIG }; + +/** + * A class modelled on the Java ByteBuffer class. It wraps a vector of bytes and permits putting and getting + * items of various sizes, with an automatically incremented position. + * + * There are three variables maintained pointing into the buffer: + * + * 0 <= position <= limit <= capacity + * + * capacity: the maximum amount of data that can be stored + * limit: the limit of the data currently available to get or put + * position: the current insert or extract position + * + * In addition a mark can be set to the current position with mark(). A subsequent call to reset() will restore + * the position to the mark. + * + * The buffer can be marked to be little-endian (default) or big-endian. All subsequent operations will use that order. + * + */ +class ByteBuffer { + public: + /** + * Create a new Bytebuffer with the given capacity + */ + static ByteBuffer create(size_t capacity); + /** + * Wrap an existing vector in a Bytebufffer + */ + static ByteBuffer wrap(std::vector data); + /** + * Wrap an existing array in a Bytebufffer + */ + static ByteBuffer wrap(uint8_t *ptr, size_t len); + + // Get one byte from the buffer, increment position by 1 + uint8_t get_uint8(); + // Get a 16 bit unsigned value, increment by 2 + uint16_t get_uint16(); + // Get a 24 bit unsigned value, increment by 3 + uint32_t get_uint24(); + // Get a 32 bit unsigned value, increment by 4 + uint32_t get_uint32(); + // signed versions of the get functions + uint8_t get_int8() { return (int8_t) this->get_uint8(); }; + int16_t get_int16() { return (int16_t) this->get_uint16(); } + uint32_t get_int24(); + int32_t get_int32() { return (int32_t) this->get_uint32(); } + // Get a float value, increment by 4 + float get_float(); + + // put values into the buffer, increment the position accordingly + void put_uint8(uint8_t value); + void put_uint16(uint16_t value); + void put_uint24(uint32_t value); + void put_uint32(uint32_t value); + void put_float(float value); + + inline size_t get_capacity() const { return this->data_.size(); } + inline size_t get_position() const { return this->position_; } + inline size_t get_limit() const { return this->limit_; } + inline size_t get_remaining() const { return this->get_limit() - this->get_position(); } + inline Endian get_endianness() const { return this->endianness_; } + inline void mark() { this->mark_ = this->position_; } + inline void big_endian() { this->endianness_ = BIG; } + inline void little_endian() { this->endianness_ = LITTLE; } + void set_limit(size_t limit); + void set_position(size_t position); + // set position to 0, limit to capacity. + void clear(); + // set limit to current position, postition to zero. Used when swapping from write to read operations. + void flip(); + // retrieve a pointer to the underlying data. + uint8_t *array() { return this->data_.data(); }; + void rewind() { this->position_ = 0; } + void reset() { this->position_ = this->mark_; } + + protected: + ByteBuffer(std::vector data) : data_(std::move(data)) { this->limit_ = this->get_capacity(); } + std::vector data_; + Endian endianness_{LITTLE}; + size_t position_{0}; + size_t mark_{0}; + size_t limit_{0}; +}; + +} // namespace esphome From c5b1a8eb81c4187c3c0f7ca4da40232425845113 Mon Sep 17 00:00:00 2001 From: PaoloTK <60204407+PaoloTK@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:29:55 +0200 Subject: [PATCH 095/160] Add min and max brightness parameters for Light dim_relative Action (#6971) --- esphome/components/light/automation.h | 17 ++++++++++++++- esphome/components/light/automation.py | 21 +++++++++++++++++++ esphome/components/light/types.py | 7 +++++++ esphome/const.py | 2 ++ tests/components/light/test.esp32-ard.yaml | 2 ++ tests/components/light/test.esp32-c3-ard.yaml | 2 ++ tests/components/light/test.esp32-c3-idf.yaml | 2 ++ tests/components/light/test.esp32-idf.yaml | 2 ++ tests/components/light/test.esp8266-ard.yaml | 2 ++ tests/components/light/test.rp2040-ard.yaml | 2 ++ 10 files changed, 58 insertions(+), 1 deletion(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index b63fc93dc5..6e055741da 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -7,6 +7,8 @@ namespace esphome { namespace light { +enum class LimitMode { CLAMP, DO_NOTHING }; + template class ToggleAction : public Action { public: explicit ToggleAction(LightState *state) : state_(state) {} @@ -77,7 +79,10 @@ template class DimRelativeAction : public Action { float rel = this->relative_brightness_.value(x...); float cur; this->parent_->remote_values.as_brightness(&cur); - float new_brightness = clamp(cur + rel, 0.0f, 1.0f); + if ((limit_mode_ == LimitMode::DO_NOTHING) && ((cur < min_brightness_) || (cur > max_brightness_))) { + return; + } + float new_brightness = clamp(cur + rel, min_brightness_, max_brightness_); call.set_state(new_brightness != 0.0f); call.set_brightness(new_brightness); @@ -85,8 +90,18 @@ template class DimRelativeAction : public Action { call.perform(); } + void set_min_max_brightness(float min, float max) { + this->min_brightness_ = min; + this->max_brightness_ = max; + } + + void set_limit_mode(LimitMode limit_mode) { this->limit_mode_ = limit_mode; } + protected: LightState *parent_; + float min_brightness_{0.0}; + float max_brightness_{1.0}; + LimitMode limit_mode_{LimitMode::CLAMP}; }; template class LightIsOnCondition : public Condition { diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index cfba273565..ec0375f54a 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -19,10 +19,15 @@ from esphome.const import ( CONF_WARM_WHITE, CONF_RANGE_FROM, CONF_RANGE_TO, + CONF_BRIGHTNESS_LIMITS, + CONF_LIMIT_MODE, + CONF_MIN_BRIGHTNESS, + CONF_MAX_BRIGHTNESS, ) from .types import ( ColorMode, COLOR_MODES, + LIMIT_MODES, DimRelativeAction, ToggleAction, LightState, @@ -167,6 +172,15 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable( cv.positive_time_period_milliseconds ), + cv.Optional(CONF_BRIGHTNESS_LIMITS): cv.Schema( + { + cv.Optional(CONF_MIN_BRIGHTNESS, default="0%"): cv.percentage, + cv.Optional(CONF_MAX_BRIGHTNESS, default="100%"): cv.percentage, + cv.Optional(CONF_LIMIT_MODE, default="CLAMP"): cv.enum( + LIMIT_MODES, upper=True, space="_" + ), + } + ), } ) @@ -182,6 +196,13 @@ async def light_dim_relative_to_code(config, action_id, template_arg, args): if CONF_TRANSITION_LENGTH in config: templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) cg.add(var.set_transition_length(templ)) + if conf := config.get(CONF_BRIGHTNESS_LIMITS): + cg.add( + var.set_min_max_brightness( + conf[CONF_MIN_BRIGHTNESS], conf[CONF_MAX_BRIGHTNESS] + ) + ) + cg.add(var.set_limit_mode(conf[CONF_LIMIT_MODE])) return var diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index a453debd94..64483bcc9c 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -26,6 +26,13 @@ COLOR_MODES = { "RGB_COLD_WARM_WHITE": ColorMode.RGB_COLD_WARM_WHITE, } +# Limit modes +LimitMode = light_ns.enum("LimitMode", is_class=True) +LIMIT_MODES = { + "CLAMP": LimitMode.CLAMP, + "DO_NOTHING": LimitMode.DO_NOTHING, +} + # Actions ToggleAction = light_ns.class_("ToggleAction", automation.Action) LightControlAction = light_ns.class_("LightControlAction", automation.Action) diff --git a/esphome/const.py b/esphome/const.py index 55f1c23b40..37f20796b5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -95,6 +95,7 @@ CONF_BOARD_FLASH_MODE = "board_flash_mode" CONF_BORDER = "border" CONF_BRANCH = "branch" CONF_BRIGHTNESS = "brightness" +CONF_BRIGHTNESS_LIMITS = "brightness_limits" CONF_BROKER = "broker" CONF_BSSID = "bssid" CONF_BUFFER_SIZE = "buffer_size" @@ -429,6 +430,7 @@ CONF_LIGHT = "light" CONF_LIGHT_ID = "light_id" CONF_LIGHTNING_ENERGY = "lightning_energy" CONF_LIGHTNING_THRESHOLD = "lightning_threshold" +CONF_LIMIT_MODE = "limit_mode" CONF_LINE_THICKNESS = "line_thickness" CONF_LINE_TYPE = "line_type" CONF_LOADED_INTEGRATIONS = "loaded_integrations" diff --git a/tests/components/light/test.esp32-ard.yaml b/tests/components/light/test.esp32-ard.yaml index 7e5718d8d4..1d0b4cd8f0 100644 --- a/tests/components/light/test.esp32-ard.yaml +++ b/tests/components/light/test.esp32-ard.yaml @@ -15,6 +15,8 @@ esphome: - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% + brightness_limits: + max_brightness: 90% output: - platform: gpio diff --git a/tests/components/light/test.esp32-c3-ard.yaml b/tests/components/light/test.esp32-c3-ard.yaml index 8e1709838a..79171805a6 100644 --- a/tests/components/light/test.esp32-c3-ard.yaml +++ b/tests/components/light/test.esp32-c3-ard.yaml @@ -15,6 +15,8 @@ esphome: - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% + brightness_limits: + max_brightness: 90% output: - platform: gpio diff --git a/tests/components/light/test.esp32-c3-idf.yaml b/tests/components/light/test.esp32-c3-idf.yaml index 8e1709838a..79171805a6 100644 --- a/tests/components/light/test.esp32-c3-idf.yaml +++ b/tests/components/light/test.esp32-c3-idf.yaml @@ -15,6 +15,8 @@ esphome: - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% + brightness_limits: + max_brightness: 90% output: - platform: gpio diff --git a/tests/components/light/test.esp32-idf.yaml b/tests/components/light/test.esp32-idf.yaml index 7e5718d8d4..1d0b4cd8f0 100644 --- a/tests/components/light/test.esp32-idf.yaml +++ b/tests/components/light/test.esp32-idf.yaml @@ -15,6 +15,8 @@ esphome: - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% + brightness_limits: + max_brightness: 90% output: - platform: gpio diff --git a/tests/components/light/test.esp8266-ard.yaml b/tests/components/light/test.esp8266-ard.yaml index 4611fb374a..555e1a1b67 100644 --- a/tests/components/light/test.esp8266-ard.yaml +++ b/tests/components/light/test.esp8266-ard.yaml @@ -15,6 +15,8 @@ esphome: - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% + brightness_limits: + max_brightness: 90% output: - platform: gpio diff --git a/tests/components/light/test.rp2040-ard.yaml b/tests/components/light/test.rp2040-ard.yaml index 0215a17e71..a509bc85c9 100644 --- a/tests/components/light/test.rp2040-ard.yaml +++ b/tests/components/light/test.rp2040-ard.yaml @@ -15,6 +15,8 @@ esphome: - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% + brightness_limits: + max_brightness: 90% output: - platform: gpio From 1d25db491c26b7400a8483ab2199fa8d05badfbc Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Wed, 14 Aug 2024 04:03:12 +0200 Subject: [PATCH 096/160] [homeassistant] Native switch entity import and control (#7018) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../homeassistant/switch/__init__.py | 30 ++++++++++ .../switch/homeassistant_switch.cpp | 59 +++++++++++++++++++ .../switch/homeassistant_switch.h | 22 +++++++ tests/components/homeassistant/common.yaml | 5 ++ 5 files changed, 117 insertions(+) create mode 100644 esphome/components/homeassistant/switch/__init__.py create mode 100644 esphome/components/homeassistant/switch/homeassistant_switch.cpp create mode 100644 esphome/components/homeassistant/switch/homeassistant_switch.h diff --git a/CODEOWNERS b/CODEOWNERS index 663a942cb4..0c36cda527 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,6 +169,7 @@ esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode esphome/components/homeassistant/* @OttoWinter @esphome/core +esphome/components/homeassistant/switch/* @Links2004 esphome/components/honeywell_hih_i2c/* @Benichou34 esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp2_i2c/* @jpfaff diff --git a/esphome/components/homeassistant/switch/__init__.py b/esphome/components/homeassistant/switch/__init__.py new file mode 100644 index 0000000000..3d7c80682a --- /dev/null +++ b/esphome/components/homeassistant/switch/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import CONF_ID + +from .. import ( + HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, +) + +CODEOWNERS = ["@Links2004"] +DEPENDENCIES = ["api"] + +HomeassistantSwitch = homeassistant_ns.class_( + "HomeassistantSwitch", switch.Switch, cg.Component +) + +CONFIG_SCHEMA = ( + switch.switch_schema(HomeassistantSwitch) + .extend(cv.COMPONENT_SCHEMA) + .extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await switch.register_switch(var, config) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.cpp b/esphome/components/homeassistant/switch/homeassistant_switch.cpp new file mode 100644 index 0000000000..05ef46e30e --- /dev/null +++ b/esphome/components/homeassistant/switch/homeassistant_switch.cpp @@ -0,0 +1,59 @@ +#include "homeassistant_switch.h" +#include "esphome/components/api/api_server.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace homeassistant { + +static const char *const TAG = "homeassistant.switch"; + +using namespace esphome::switch_; + +void HomeassistantSwitch::setup() { + api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullopt, [this](const std::string &state) { + auto val = parse_on_off(state.c_str()); + switch (val) { + case PARSE_NONE: + case PARSE_TOGGLE: + ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str()); + break; + case PARSE_ON: + case PARSE_OFF: + bool new_state = val == PARSE_ON; + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state)); + this->publish_state(new_state); + break; + } + }); +} + +void HomeassistantSwitch::dump_config() { + LOG_SWITCH("", "Homeassistant Switch", this); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); +} + +float HomeassistantSwitch::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } + +void HomeassistantSwitch::write_state(bool state) { + if (!api::global_api_server->is_connected()) { + ESP_LOGE(TAG, "No clients connected to API server"); + return; + } + + api::HomeassistantServiceResponse resp; + if (state) { + resp.service = "switch.turn_on"; + } else { + resp.service = "switch.turn_off"; + } + + api::HomeassistantServiceMap entity_id_kv; + entity_id_kv.key = "entity_id"; + entity_id_kv.value = this->entity_id_; + resp.data.push_back(entity_id_kv); + + api::global_api_server->send_homeassistant_service_call(resp); +} + +} // namespace homeassistant +} // namespace esphome diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.h b/esphome/components/homeassistant/switch/homeassistant_switch.h new file mode 100644 index 0000000000..a4da257960 --- /dev/null +++ b/esphome/components/homeassistant/switch/homeassistant_switch.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace homeassistant { + +class HomeassistantSwitch : public switch_::Switch, public Component { + public: + void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + protected: + void write_state(bool state) override; + std::string entity_id_; +}; + +} // namespace homeassistant +} // namespace esphome diff --git a/tests/components/homeassistant/common.yaml b/tests/components/homeassistant/common.yaml index 07a6e8090c..2b2805d06e 100644 --- a/tests/components/homeassistant/common.yaml +++ b/tests/components/homeassistant/common.yaml @@ -32,6 +32,11 @@ wifi: api: +switch: + - platform: homeassistant + entity_id: switch.my_cool_switch + id: my_cool_switch + binary_sensor: - platform: homeassistant entity_id: binary_sensor.hello_world From a5fdcb31fc56d928aa7a3b340829970f6ce0bbc0 Mon Sep 17 00:00:00 2001 From: Landon Rohatensky Date: Tue, 13 Aug 2024 19:04:12 -0700 Subject: [PATCH 097/160] [homeassistant] Native number entity import and control (#6455) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../homeassistant/number/__init__.py | 33 ++++++ .../number/homeassistant_number.cpp | 100 ++++++++++++++++++ .../number/homeassistant_number.h | 31 ++++++ tests/components/homeassistant/common.yaml | 5 + 5 files changed, 170 insertions(+) create mode 100644 esphome/components/homeassistant/number/__init__.py create mode 100644 esphome/components/homeassistant/number/homeassistant_number.cpp create mode 100644 esphome/components/homeassistant/number/homeassistant_number.h diff --git a/CODEOWNERS b/CODEOWNERS index 0c36cda527..3ea9c75ac2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,6 +169,7 @@ esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode esphome/components/homeassistant/* @OttoWinter @esphome/core +esphome/components/homeassistant/number/* @landonr esphome/components/homeassistant/switch/* @Links2004 esphome/components/honeywell_hih_i2c/* @Benichou34 esphome/components/honeywellabp/* @RubyBailey diff --git a/esphome/components/homeassistant/number/__init__.py b/esphome/components/homeassistant/number/__init__.py new file mode 100644 index 0000000000..a6cc615a64 --- /dev/null +++ b/esphome/components/homeassistant/number/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv + +from .. import ( + HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, +) + +CODEOWNERS = ["@landonr"] +DEPENDENCIES = ["api"] + +HomeassistantNumber = homeassistant_ns.class_( + "HomeassistantNumber", number.Number, cg.Component +) + +CONFIG_SCHEMA = ( + number.number_schema(HomeassistantNumber) + .extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await number.new_number( + config, + min_value=0, + max_value=0, + step=0, + ) + await cg.register_component(var, config) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp new file mode 100644 index 0000000000..d3e285f4ac --- /dev/null +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -0,0 +1,100 @@ +#include "homeassistant_number.h" + +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_server.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace homeassistant { + +static const char *const TAG = "homeassistant.number"; + +void HomeassistantNumber::state_changed_(const std::string &state) { + auto number_value = parse_number(state); + if (!number_value.has_value()) { + ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str()); + this->publish_state(NAN); + return; + } + if (this->state == number_value.value()) { + return; + } + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), state.c_str()); + this->publish_state(number_value.value()); +} + +void HomeassistantNumber::min_retrieved_(const std::string &min) { + auto min_value = parse_number(min); + if (!min_value.has_value()) { + ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_.c_str(), min.c_str()); + } + ESP_LOGD(TAG, "'%s': Min retrieved: %s", get_name().c_str(), min.c_str()); + this->traits.set_min_value(min_value.value()); +} + +void HomeassistantNumber::max_retrieved_(const std::string &max) { + auto max_value = parse_number(max); + if (!max_value.has_value()) { + ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_.c_str(), max.c_str()); + } + ESP_LOGD(TAG, "'%s': Max retrieved: %s", get_name().c_str(), max.c_str()); + this->traits.set_max_value(max_value.value()); +} + +void HomeassistantNumber::step_retrieved_(const std::string &step) { + auto step_value = parse_number(step); + if (!step_value.has_value()) { + ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_.c_str(), step.c_str()); + } + ESP_LOGD(TAG, "'%s': Step Retrieved %s", get_name().c_str(), step.c_str()); + this->traits.set_step(step_value.value()); +} + +void HomeassistantNumber::setup() { + api::global_api_server->subscribe_home_assistant_state( + this->entity_id_, nullopt, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1)); + + api::global_api_server->get_home_assistant_state( + this->entity_id_, optional("min"), + std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1)); + api::global_api_server->get_home_assistant_state( + this->entity_id_, optional("max"), + std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1)); + api::global_api_server->get_home_assistant_state( + this->entity_id_, optional("step"), + std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1)); +} + +void HomeassistantNumber::dump_config() { + LOG_NUMBER("", "Homeassistant Number", this); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); +} + +float HomeassistantNumber::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } + +void HomeassistantNumber::control(float value) { + if (!api::global_api_server->is_connected()) { + ESP_LOGE(TAG, "No clients connected to API server"); + return; + } + + this->publish_state(value); + + api::HomeassistantServiceResponse resp; + resp.service = "number.set_value"; + + api::HomeassistantServiceMap entity_id; + entity_id.key = "entity_id"; + entity_id.value = this->entity_id_; + resp.data.push_back(entity_id); + + api::HomeassistantServiceMap entity_value; + entity_value.key = "value"; + entity_value.value = to_string(value); + resp.data.push_back(entity_value); + + api::global_api_server->send_homeassistant_service_call(resp); +} + +} // namespace homeassistant +} // namespace esphome diff --git a/esphome/components/homeassistant/number/homeassistant_number.h b/esphome/components/homeassistant/number/homeassistant_number.h new file mode 100644 index 0000000000..0860b4e91c --- /dev/null +++ b/esphome/components/homeassistant/number/homeassistant_number.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "esphome/components/number/number.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace homeassistant { + +class HomeassistantNumber : public number::Number, public Component { + public: + void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + protected: + void state_changed_(const std::string &state); + void min_retrieved_(const std::string &min); + void max_retrieved_(const std::string &max); + void step_retrieved_(const std::string &step); + + void control(float value) override; + + std::string entity_id_; +}; +} // namespace homeassistant +} // namespace esphome diff --git a/tests/components/homeassistant/common.yaml b/tests/components/homeassistant/common.yaml index 2b2805d06e..8c9a4ad75f 100644 --- a/tests/components/homeassistant/common.yaml +++ b/tests/components/homeassistant/common.yaml @@ -46,6 +46,11 @@ binary_sensor: attribute: world id: ha_hello_world_binary_attribute +number: + - platform: homeassistant + entity_id: number.hello_world + id: ha_hello_world_number + sensor: - platform: homeassistant entity_id: sensor.hello_world From a0eff08f39c69059d74929e6c91b8b61fb7ee51f Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:05:25 +1000 Subject: [PATCH 098/160] [lvgl] Rework events to avoid feedback loops (#7262) --- esphome/components/lvgl/automation.py | 4 +-- esphome/components/lvgl/defines.py | 1 + esphome/components/lvgl/light/lvgl_light.h | 2 +- esphome/components/lvgl/lvcode.py | 6 ++++- esphome/components/lvgl/lvgl_esphome.cpp | 15 +++++++---- esphome/components/lvgl/lvgl_esphome.h | 5 +++- esphome/components/lvgl/number/__init__.py | 22 +++++++++++++--- esphome/components/lvgl/select/__init__.py | 13 ++++++++-- esphome/components/lvgl/sensor/__init__.py | 16 ++++++++++-- esphome/components/lvgl/switch/__init__.py | 6 +++-- esphome/components/lvgl/text/__init__.py | 17 ++++++++++--- .../components/lvgl/text_sensor/__init__.py | 4 ++- esphome/components/lvgl/trigger.py | 25 +++++++++++++++---- tests/components/lvgl/common.yaml | 1 + 14 files changed, 108 insertions(+), 29 deletions(-) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 556e286208..a39f589136 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -17,6 +17,7 @@ from .defines import ( from .lv_validation import lv_bool, lv_color, lv_image from .lvcode import ( LVGL_COMP_ARG, + UPDATE_EVENT, LambdaContext, LocalVariable, LvConditional, @@ -30,7 +31,6 @@ from .lvcode import ( ) from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA from .types import ( - LV_EVENT, LV_STATE, LvglAction, LvglCondition, @@ -64,7 +64,7 @@ async def update_to_code(config, action_id, template_arg, args): widget.type.w_type.value_property is not None and widget.type.w_type.value_property in config ): - lv.event_send(widget.obj, LV_EVENT.VALUE_CHANGED, nullptr) + lv.event_send(widget.obj, UPDATE_EVENT, nullptr) widgets = await get_widgets(config[CONF_ID]) return await action_to_code(widgets, do_update, action_id, template_arg, args) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index e48679996b..8f7a973722 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -470,6 +470,7 @@ CONF_TOP_LAYER = "top_layer" CONF_TOUCHSCREENS = "touchscreens" CONF_TRANSPARENCY_KEY = "transparency_key" CONF_THEME = "theme" +CONF_UPDATE_ON_RELEASE = "update_on_release" CONF_VISIBLE_ROW_COUNT = "visible_row_count" CONF_WIDGET = "widget" CONF_WIDGETS = "widgets" diff --git a/esphome/components/lvgl/light/lvgl_light.h b/esphome/components/lvgl/light/lvgl_light.h index 67372d89dd..50ae4c5327 100644 --- a/esphome/components/lvgl/light/lvgl_light.h +++ b/esphome/components/lvgl/light/lvgl_light.h @@ -38,7 +38,7 @@ class LVLight : public light::LightOutput { void set_value_(lv_color_t value) { lv_led_set_color(this->obj_, value); lv_led_on(this->obj_); - lv_event_send(this->obj_, lv_custom_event, nullptr); + lv_event_send(this->obj_, lv_api_event, nullptr); } lv_obj_t *obj_{}; optional initial_value_{}; diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index f54a032de2..6d7e364e5d 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -29,7 +29,11 @@ LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)] lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr") EVENT_ARG = [(lv_event_t_ptr, "ev")] -CUSTOM_EVENT = literal("lvgl::lv_custom_event") +# Two custom events; API_EVENT is fired when an entity is updated remotely by an API interaction; +# UPDATE_EVENT is fired when an entity is programmatically updated locally. +# VALUE_CHANGED is the event generated by LVGL when an entity's value changes through user interaction. +API_EVENT = literal("lvgl::lv_api_event") +UPDATE_EVENT = literal("lvgl::lv_update_event") def get_line_marks(value) -> list: diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 6f23c2421b..92f7a880c3 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -27,7 +27,8 @@ static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { area->y2++; } -lv_event_code_t lv_custom_event; // NOLINT +lv_event_code_t lv_api_event; // NOLINT +lv_event_code_t lv_update_event; // NOLINT void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); } void LvglComponent::set_paused(bool paused, bool show_snow) { this->paused_ = paused; @@ -40,15 +41,18 @@ void LvglComponent::set_paused(bool paused, bool show_snow) { } void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { lv_obj_add_event_cb(obj, callback, event, this); - if (event == LV_EVENT_VALUE_CHANGED) { - lv_obj_add_event_cb(obj, callback, lv_custom_event, this); - } } void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2) { this->add_event_cb(obj, callback, event1); this->add_event_cb(obj, callback, event2); } +void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, + lv_event_code_t event2, lv_event_code_t event3) { + this->add_event_cb(obj, callback, event1); + this->add_event_cb(obj, callback, event2); + this->add_event_cb(obj, callback, event3); +} void LvglComponent::add_page(LvPageType *page) { this->pages_.push_back(page); page->setup(this->pages_.size() - 1); @@ -228,7 +232,8 @@ void LvglComponent::setup() { lv_log_register_print_cb(log_cb); #endif lv_init(); - lv_custom_event = static_cast(lv_event_register_id()); + lv_update_event = static_cast(lv_event_register_id()); + lv_api_event = static_cast(lv_event_register_id()); auto *display = this->displays_[0]; size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_; auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 1497e1004a..3a3d1aa6c5 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -38,7 +38,8 @@ namespace esphome { namespace lvgl { -extern lv_event_code_t lv_custom_event; // NOLINT +extern lv_event_code_t lv_api_event; // NOLINT +extern lv_event_code_t lv_update_event; // NOLINT #ifdef USE_LVGL_COLOR inline lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); } #endif // USE_LVGL_COLOR @@ -133,6 +134,8 @@ class LvglComponent : public PollingComponent { void set_paused(bool paused, bool show_snow); void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event); void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); + void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, + lv_event_code_t event3); bool is_paused() const { return this->paused_; } void add_page(LvPageType *page); void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time); diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py index 53aef2790d..6336bb0632 100644 --- a/esphome/components/lvgl/number/__init__.py +++ b/esphome/components/lvgl/number/__init__.py @@ -3,9 +3,17 @@ from esphome.components import number import esphome.config_validation as cv from esphome.cpp_generator import MockObj -from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET +from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_UPDATE_ON_RELEASE, CONF_WIDGET from ..lv_validation import animated -from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lv, + lv_add, +) from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LvNumber, lvgl_ns from ..widgets import get_widgets @@ -19,6 +27,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_WIDGET): cv.use_id(LvNumber), cv.Optional(CONF_ANIMATED, default=True): animated, + cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean, } ) ) @@ -39,14 +48,19 @@ async def to_code(config): await widget.set_property( "value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED] ) - lv.event_send(widget.obj, CUSTOM_EVENT, cg.nullptr) + lv.event_send(widget.obj, API_EVENT, cg.nullptr) async with LambdaContext(EVENT_ARG) as event: event.add(var.publish_state(widget.get_value())) + event_code = ( + LV_EVENT.VALUE_CHANGED + if not config[CONF_UPDATE_ON_RELEASE] + else LV_EVENT.RELEASED + ) async with LvContext(paren): lv_add(var.set_control_lambda(await control.get_lambda())) lv_add( paren.add_event_cb( - widget.obj, await event.get_lambda(), LV_EVENT.VALUE_CHANGED + widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code ) ) lv_add(var.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/select/__init__.py b/esphome/components/lvgl/select/__init__.py index 34a70a23f7..b55bde13bc 100644 --- a/esphome/components/lvgl/select/__init__.py +++ b/esphome/components/lvgl/select/__init__.py @@ -4,7 +4,15 @@ import esphome.config_validation as cv from esphome.const import CONF_OPTIONS from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET -from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lv, + lv_add, +) from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LvSelect, lvgl_ns from ..widgets import get_widgets @@ -33,7 +41,7 @@ async def to_code(config): pub_ctx.add(selector.publish_index(widget.get_value())) async with LambdaContext([(cg.uint16, "v")]) as control: await widget.set_property("selected", "v", animated=config[CONF_ANIMATED]) - lv.event_send(widget.obj, CUSTOM_EVENT, cg.nullptr) + lv.event_send(widget.obj, API_EVENT, cg.nullptr) async with LvContext(paren) as ctx: lv_add(selector.set_control_lambda(await control.get_lambda())) ctx.add( @@ -41,6 +49,7 @@ async def to_code(config): widget.obj, await pub_ctx.get_lambda(), LV_EVENT.VALUE_CHANGED, + UPDATE_EVENT, ) ) lv_add(selector.publish_index(widget.get_value())) diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py index 6e495eb685..82e21d5e95 100644 --- a/esphome/components/lvgl/sensor/__init__.py +++ b/esphome/components/lvgl/sensor/__init__.py @@ -3,7 +3,15 @@ from esphome.components.sensor import Sensor, new_sensor, sensor_schema import esphome.config_validation as cv from ..defines import CONF_LVGL_ID, CONF_WIDGET -from ..lvcode import EVENT_ARG, LVGL_COMP_ARG, LambdaContext, LvContext, lv_add +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + LVGL_COMP_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lv_add, +) from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LvNumber from ..widgets import Widget, get_widgets @@ -30,6 +38,10 @@ async def to_code(config): async with LvContext(paren, LVGL_COMP_ARG): lv_add( paren.add_event_cb( - widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED + widget.obj, + await lamb.get_lambda(), + LV_EVENT.VALUE_CHANGED, + API_EVENT, + UPDATE_EVENT, ) ) diff --git a/esphome/components/lvgl/switch/__init__.py b/esphome/components/lvgl/switch/__init__.py index 831fa9308b..957fce17ff 100644 --- a/esphome/components/lvgl/switch/__init__.py +++ b/esphome/components/lvgl/switch/__init__.py @@ -5,8 +5,9 @@ from esphome.cpp_generator import MockObj from ..defines import CONF_LVGL_ID, CONF_WIDGET from ..lvcode import ( - CUSTOM_EVENT, + API_EVENT, EVENT_ARG, + UPDATE_EVENT, LambdaContext, LvConditional, LvContext, @@ -41,7 +42,7 @@ async def to_code(config): widget.add_state(LV_STATE.CHECKED) cond.else_() widget.clear_state(LV_STATE.CHECKED) - lv.event_send(widget.obj, CUSTOM_EVENT, cg.nullptr) + lv.event_send(widget.obj, API_EVENT, cg.nullptr) async with LvContext(paren) as ctx: lv_add(switch.set_control_lambda(await control.get_lambda())) ctx.add( @@ -49,6 +50,7 @@ async def to_code(config): widget.obj, await checked_ctx.get_lambda(), LV_EVENT.VALUE_CHANGED, + UPDATE_EVENT, ) ) lv_add(switch.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/text/__init__.py b/esphome/components/lvgl/text/__init__.py index 55f1b2b3fc..9ee494d8a0 100644 --- a/esphome/components/lvgl/text/__init__.py +++ b/esphome/components/lvgl/text/__init__.py @@ -4,7 +4,15 @@ from esphome.components.text import new_text import esphome.config_validation as cv from ..defines import CONF_LVGL_ID, CONF_WIDGET -from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lv, + lv_add, +) from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LvText, lvgl_ns from ..widgets import get_widgets @@ -26,14 +34,17 @@ async def to_code(config): widget = widget[0] async with LambdaContext([(cg.std_string, "text_value")]) as control: await widget.set_property("text", "text_value.c_str())") - lv.event_send(widget.obj, CUSTOM_EVENT, None) + lv.event_send(widget.obj, API_EVENT, None) async with LambdaContext(EVENT_ARG) as lamb: lv_add(textvar.publish_state(widget.get_value())) async with LvContext(paren): widget.var.set_control_lambda(await control.get_lambda()) lv_add( paren.add_event_cb( - widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED + widget.obj, + await lamb.get_lambda(), + LV_EVENT.VALUE_CHANGED, + UPDATE_EVENT, ) ) lv_add(textvar.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/text_sensor/__init__.py b/esphome/components/lvgl/text_sensor/__init__.py index c0f0bc36a8..cab715dce0 100644 --- a/esphome/components/lvgl/text_sensor/__init__.py +++ b/esphome/components/lvgl/text_sensor/__init__.py @@ -7,7 +7,7 @@ from esphome.components.text_sensor import ( import esphome.config_validation as cv from ..defines import CONF_LVGL_ID, CONF_WIDGET -from ..lvcode import EVENT_ARG, LambdaContext, LvContext +from ..lvcode import API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LvText from ..widgets import get_widgets @@ -36,5 +36,7 @@ async def to_code(config): widget.obj, await pressed_ctx.get_lambda(), LV_EVENT.VALUE_CHANGED, + API_EVENT, + UPDATE_EVENT, ) ) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index df87be718b..ba93aabb2d 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -11,7 +11,15 @@ from .defines import ( LV_EVENT_TRIGGERS, literal, ) -from .lvcode import EVENT_ARG, LambdaContext, LvConditional, lv, lv_add +from .lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvConditional, + lv, + lv_add, +) from .types import LV_EVENT from .widgets import widget_map @@ -34,9 +42,16 @@ async def generate_triggers(lv_component): conf = conf[0] w.add_flag("LV_OBJ_FLAG_CLICKABLE") event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()]) - await add_trigger(conf, event, lv_component, w) + await add_trigger(conf, lv_component, w, event) for conf in w.config.get(CONF_ON_VALUE, ()): - await add_trigger(conf, LV_EVENT.VALUE_CHANGED, lv_component, w) + await add_trigger( + conf, + lv_component, + w, + LV_EVENT.VALUE_CHANGED, + API_EVENT, + UPDATE_EVENT, + ) # Generate align to directives while we're here if align_to := w.config.get(CONF_ALIGN_TO): @@ -47,7 +62,7 @@ async def generate_triggers(lv_component): lv.obj_align_to(w.obj, target, align, x, y) -async def add_trigger(conf, event, lv_component, w): +async def add_trigger(conf, lv_component, w, *events): tid = conf[CONF_TRIGGER_ID] trigger = cg.new_Pvariable(tid) args = w.get_args() @@ -56,4 +71,4 @@ async def add_trigger(conf, event, lv_component, w): async with LambdaContext(EVENT_ARG, where=tid) as context: with LvConditional(w.is_selected()): lv_add(trigger.trigger(value)) - lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), event)) + lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), *events)) diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 35d924d939..002c7a118d 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -75,6 +75,7 @@ number: - platform: lvgl widget: slider_id name: LVGL Slider + update_on_release: true - platform: lvgl widget: lv_arc id: lvgl_arc_number From bec2d42c793c59b18e15a0fbb5cbe22761858290 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:06:13 +1000 Subject: [PATCH 099/160] Add `color_filter_opa` style property (#7276) --- esphome/components/lvgl/schemas.py | 1 + tests/components/lvgl/lvgl-package.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index e4b1c3f8fa..f1c7ff4df6 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -103,6 +103,7 @@ STYLE_PROPS = { ).several_of, "border_width": cv.positive_int, "clip_corner": lvalid.lv_bool, + "color_filter_opa": lvalid.opacity, "height": lvalid.size, "image_recolor": lvalid.lv_color, "image_recolor_opa": lvalid.opacity, diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 09ff9c9d39..54022354f5 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -101,6 +101,7 @@ lvgl: border_side: [bottom, left] border_width: 4 clip_corner: false + color_filter_opa: transp height: 50% image_recolor: light_blue image_recolor_opa: cover From 56e05998efb14b7e91fea7e4ef3cfe76a7128c61 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 14 Aug 2024 04:08:10 +0200 Subject: [PATCH 100/160] [code-quality] fix clang-tidy wake_on_lan (#7275) --- esphome/components/wake_on_lan/wake_on_lan.cpp | 2 ++ esphome/components/wake_on_lan/wake_on_lan.h | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index 080e1bbac8..d18cdf89c8 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -1,4 +1,5 @@ #include "wake_on_lan.h" +#ifdef USE_NETWORK #include "esphome/core/log.h" #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" @@ -85,3 +86,4 @@ void WakeOnLanButton::setup() { } // namespace wake_on_lan } // namespace esphome +#endif diff --git a/esphome/components/wake_on_lan/wake_on_lan.h b/esphome/components/wake_on_lan/wake_on_lan.h index 42cb3a9268..f516c4d669 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.h +++ b/esphome/components/wake_on_lan/wake_on_lan.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include "esphome/components/button/button.h" #include "esphome/core/component.h" #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) @@ -32,3 +33,4 @@ class WakeOnLanButton : public button::Button, public Component { } // namespace wake_on_lan } // namespace esphome +#endif From 4cb174585c0521801d6e8ffda189d37b5a40aa30 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 14 Aug 2024 04:14:29 +0200 Subject: [PATCH 101/160] [code-quality] fix readability-braces-around-statements (#7273) --- .../binary/light/binary_light_output.h | 5 +- esphome/components/demo/demo_sensor.h | 5 +- .../light/addressable_light_effect.h | 20 +++--- esphome/components/lvgl/number/lvgl_number.h | 5 +- esphome/components/lvgl/switch/lvgl_switch.h | 5 +- esphome/components/lvgl/text/lvgl_text.h | 5 +- esphome/components/rgbct/rgbct_light_output.h | 5 +- esphome/components/rgbw/rgbw_light_output.h | 5 +- esphome/components/rgbww/rgbww_light_output.h | 5 +- .../components/spi_led_strip/spi_led_strip.h | 5 +- esphome/core/application.h | 63 ++++++++++++------- esphome/core/base_automation.h | 5 +- esphome/core/color.h | 40 +++++++----- 13 files changed, 108 insertions(+), 65 deletions(-) diff --git a/esphome/components/binary/light/binary_light_output.h b/esphome/components/binary/light/binary_light_output.h index 86c83aff5c..8346a82cf0 100644 --- a/esphome/components/binary/light/binary_light_output.h +++ b/esphome/components/binary/light/binary_light_output.h @@ -18,10 +18,11 @@ class BinaryLightOutput : public light::LightOutput { void write_state(light::LightState *state) override { bool binary; state->current_values_as_binary(&binary); - if (binary) + if (binary) { this->output_->turn_on(); - else + } else { this->output_->turn_off(); + } } protected: diff --git a/esphome/components/demo/demo_sensor.h b/esphome/components/demo/demo_sensor.h index b4afa03e11..d965d987de 100644 --- a/esphome/components/demo/demo_sensor.h +++ b/esphome/components/demo/demo_sensor.h @@ -16,10 +16,11 @@ class DemoSensor : public sensor::Sensor, public PollingComponent { float base = std::isnan(this->state) ? 0.0f : this->state; this->publish_state(base + val * 10); } else { - if (val < 0.1) + if (val < 0.1) { this->publish_state(NAN); - else + } else { this->publish_state(val * 100); + } } } }; diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 73083a58b7..d622ec0375 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -114,10 +114,11 @@ class AddressableColorWipeEffect : public AddressableLightEffect { if (now - this->last_add_ < this->add_led_interval_) return; this->last_add_ = now; - if (this->reverse_) + if (this->reverse_) { it.shift_left(1); - else + } else { it.shift_right(1); + } const AddressableColorWipeEffectColor &color = this->colors_[this->at_color_]; Color esp_color = Color(color.r, color.g, color.b, color.w); if (color.gradient) { @@ -127,10 +128,11 @@ class AddressableColorWipeEffect : public AddressableLightEffect { uint8_t gradient = 255 * ((float) this->leds_added_ / color.num_leds); esp_color = esp_color.gradient(next_esp_color, gradient); } - if (this->reverse_) + if (this->reverse_) { it[-1] = esp_color; - else + } else { it[0] = esp_color; + } if (++this->leds_added_ >= color.num_leds) { this->leds_added_ = 0; this->at_color_ = (this->at_color_ + 1) % this->colors_.size(); @@ -207,10 +209,11 @@ class AddressableTwinkleEffect : public AddressableLightEffect { const uint8_t sine = half_sin8(view.get_effect_data()); view = current_color * sine; const uint8_t new_pos = view.get_effect_data() + pos_add; - if (new_pos < view.get_effect_data()) + if (new_pos < view.get_effect_data()) { view.set_effect_data(0); - else + } else { view.set_effect_data(new_pos); + } } else { view = Color::BLACK; } @@ -254,10 +257,11 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { view = Color(((color >> 2) & 1) * sine, ((color >> 1) & 1) * sine, ((color >> 0) & 1) * sine); } const uint8_t new_x = x + pos_add; - if (new_x > 0b11111) + if (new_x > 0b11111) { view.set_effect_data(0); - else + } else { view.set_effect_data((new_x << 3) | color); + } } else { view = Color(0, 0, 0, 0); } diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index 659fc615c9..a70c9eab9c 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -20,10 +20,11 @@ class LVGLNumber : public number::Number { protected: void control(float value) override { - if (this->control_lambda_ != nullptr) + if (this->control_lambda_ != nullptr) { this->control_lambda_(value); - else + } else { this->initial_state_ = value; + } } std::function control_lambda_{}; optional initial_state_{}; diff --git a/esphome/components/lvgl/switch/lvgl_switch.h b/esphome/components/lvgl/switch/lvgl_switch.h index 67be11faba..dbc885219b 100644 --- a/esphome/components/lvgl/switch/lvgl_switch.h +++ b/esphome/components/lvgl/switch/lvgl_switch.h @@ -20,10 +20,11 @@ class LVGLSwitch : public switch_::Switch { protected: void write_state(bool value) override { - if (this->state_lambda_ != nullptr) + if (this->state_lambda_ != nullptr) { this->state_lambda_(value); - else + } else { this->initial_state_ = value; + } } std::function state_lambda_{}; optional initial_state_{}; diff --git a/esphome/components/lvgl/text/lvgl_text.h b/esphome/components/lvgl/text/lvgl_text.h index 4bd5b76744..d3513c3697 100644 --- a/esphome/components/lvgl/text/lvgl_text.h +++ b/esphome/components/lvgl/text/lvgl_text.h @@ -20,10 +20,11 @@ class LVGLText : public text::Text { protected: void control(const std::string &value) override { - if (this->control_lambda_ != nullptr) + if (this->control_lambda_ != nullptr) { this->control_lambda_(value); - else + } else { this->initial_state_ = value; + } } std::function control_lambda_{}; optional initial_state_{}; diff --git a/esphome/components/rgbct/rgbct_light_output.h b/esphome/components/rgbct/rgbct_light_output.h index 9257d67cd1..9e23f783ae 100644 --- a/esphome/components/rgbct/rgbct_light_output.h +++ b/esphome/components/rgbct/rgbct_light_output.h @@ -23,10 +23,11 @@ class RGBCTLightOutput : public light::LightOutput { light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->color_interlock_) + if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLOR_TEMPERATURE}); - else + } else { traits.set_supported_color_modes({light::ColorMode::RGB_COLOR_TEMPERATURE, light::ColorMode::COLOR_TEMPERATURE}); + } traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; diff --git a/esphome/components/rgbw/rgbw_light_output.h b/esphome/components/rgbw/rgbw_light_output.h index 0f55775608..a2ab17b75d 100644 --- a/esphome/components/rgbw/rgbw_light_output.h +++ b/esphome/components/rgbw/rgbw_light_output.h @@ -16,10 +16,11 @@ class RGBWLightOutput : public light::LightOutput { void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->color_interlock_) + if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); - else + } else { traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); + } return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/rgbww/rgbww_light_output.h b/esphome/components/rgbww/rgbww_light_output.h index 5a86b88595..9687360059 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -20,10 +20,11 @@ class RGBWWLightOutput : public light::LightOutput { void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->color_interlock_) + if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLD_WARM_WHITE}); - else + } else { traits.set_supported_color_modes({light::ColorMode::RGB_COLD_WARM_WHITE}); + } traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; diff --git a/esphome/components/spi_led_strip/spi_led_strip.h b/esphome/components/spi_led_strip/spi_led_strip.h index 8b713378ec..1b317cdd69 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.h +++ b/esphome/components/spi_led_strip/spi_led_strip.h @@ -46,10 +46,11 @@ class SpiLedStrip : public light::AddressableLight, void dump_config() override { esph_log_config(TAG, "SPI LED Strip:"); esph_log_config(TAG, " LEDs: %d", this->num_leds_); - if (this->data_rate_ >= spi::DATA_RATE_1MHZ) + if (this->data_rate_ >= spi::DATA_RATE_1MHZ) { esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000)); - else + } else { esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000)); + } } void write_state(light::LightState *state) override { diff --git a/esphome/core/application.h b/esphome/core/application.h index 2697357456..462beb1f25 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -246,162 +246,180 @@ class Application { #ifdef USE_BINARY_SENSOR const std::vector &get_binary_sensors() { return this->binary_sensors_; } binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->binary_sensors_) + for (auto *obj : this->binary_sensors_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_SWITCH const std::vector &get_switches() { return this->switches_; } switch_::Switch *get_switch_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->switches_) + for (auto *obj : this->switches_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_BUTTON const std::vector &get_buttons() { return this->buttons_; } button::Button *get_button_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->buttons_) + for (auto *obj : this->buttons_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_SENSOR const std::vector &get_sensors() { return this->sensors_; } sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->sensors_) + for (auto *obj : this->sensors_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_TEXT_SENSOR const std::vector &get_text_sensors() { return this->text_sensors_; } text_sensor::TextSensor *get_text_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->text_sensors_) + for (auto *obj : this->text_sensors_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_FAN const std::vector &get_fans() { return this->fans_; } fan::Fan *get_fan_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->fans_) + for (auto *obj : this->fans_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_COVER const std::vector &get_covers() { return this->covers_; } cover::Cover *get_cover_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->covers_) + for (auto *obj : this->covers_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_LIGHT const std::vector &get_lights() { return this->lights_; } light::LightState *get_light_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->lights_) + for (auto *obj : this->lights_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_CLIMATE const std::vector &get_climates() { return this->climates_; } climate::Climate *get_climate_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->climates_) + for (auto *obj : this->climates_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_NUMBER const std::vector &get_numbers() { return this->numbers_; } number::Number *get_number_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->numbers_) + for (auto *obj : this->numbers_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_DATETIME_DATE const std::vector &get_dates() { return this->dates_; } datetime::DateEntity *get_date_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->dates_) + for (auto *obj : this->dates_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_DATETIME_TIME const std::vector &get_times() { return this->times_; } datetime::TimeEntity *get_time_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->times_) + for (auto *obj : this->times_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_DATETIME_DATETIME const std::vector &get_datetimes() { return this->datetimes_; } datetime::DateTimeEntity *get_datetime_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->datetimes_) + for (auto *obj : this->datetimes_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_TEXT const std::vector &get_texts() { return this->texts_; } text::Text *get_text_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->texts_) + for (auto *obj : this->texts_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_SELECT const std::vector &get_selects() { return this->selects_; } select::Select *get_select_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->selects_) + for (auto *obj : this->selects_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_LOCK const std::vector &get_locks() { return this->locks_; } lock::Lock *get_lock_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->locks_) + for (auto *obj : this->locks_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_VALVE const std::vector &get_valves() { return this->valves_; } valve::Valve *get_valve_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->valves_) + for (auto *obj : this->valves_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_MEDIA_PLAYER const std::vector &get_media_players() { return this->media_players_; } media_player::MediaPlayer *get_media_player_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->media_players_) + for (auto *obj : this->media_players_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif @@ -411,9 +429,10 @@ class Application { return this->alarm_control_panels_; } alarm_control_panel::AlarmControlPanel *get_alarm_control_panel_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->alarm_control_panels_) + for (auto *obj : this->alarm_control_panels_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif @@ -421,9 +440,10 @@ class Application { #ifdef USE_EVENT const std::vector &get_events() { return this->events_; } event::Event *get_event_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->events_) + for (auto *obj : this->events_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif @@ -431,9 +451,10 @@ class Application { #ifdef USE_UPDATE const std::vector &get_updates() { return this->updates_; } update::UpdateEntity *get_update_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->updates_) + for (auto *obj : this->updates_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 1bf0efb9a4..dcf7da2f21 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -278,10 +278,11 @@ template class RepeatAction : public Action { this->then_.add_actions(actions); this->then_.add_action(new LambdaAction([this](uint32_t iteration, Ts... x) { iteration++; - if (iteration >= this->count_.value(x...)) + if (iteration >= this->count_.value(x...)) { this->play_next_tuple_(this->var_); - else + } else { this->then_.play(iteration, x...); + } })); } diff --git a/esphome/core/color.h b/esphome/core/color.h index 8965d9fc83..1c43fd9d3e 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -85,22 +85,26 @@ struct Color { } inline Color operator+(const Color &add) const ESPHOME_ALWAYS_INLINE { Color ret; - if (uint8_t(add.r + this->r) < this->r) + if (uint8_t(add.r + this->r) < this->r) { ret.r = 255; - else + } else { ret.r = this->r + add.r; - if (uint8_t(add.g + this->g) < this->g) + } + if (uint8_t(add.g + this->g) < this->g) { ret.g = 255; - else + } else { ret.g = this->g + add.g; - if (uint8_t(add.b + this->b) < this->b) + } + if (uint8_t(add.b + this->b) < this->b) { ret.b = 255; - else + } else { ret.b = this->b + add.b; - if (uint8_t(add.w + this->w) < this->w) + } + if (uint8_t(add.w + this->w) < this->w) { ret.w = 255; - else + } else { ret.w = this->w + add.w; + } return ret; } inline Color &operator+=(const Color &add) ESPHOME_ALWAYS_INLINE { return *this = (*this) + add; } @@ -108,22 +112,26 @@ struct Color { inline Color &operator+=(uint8_t add) ESPHOME_ALWAYS_INLINE { return *this = (*this) + add; } inline Color operator-(const Color &subtract) const ESPHOME_ALWAYS_INLINE { Color ret; - if (subtract.r > this->r) + if (subtract.r > this->r) { ret.r = 0; - else + } else { ret.r = this->r - subtract.r; - if (subtract.g > this->g) + } + if (subtract.g > this->g) { ret.g = 0; - else + } else { ret.g = this->g - subtract.g; - if (subtract.b > this->b) + } + if (subtract.b > this->b) { ret.b = 0; - else + } else { ret.b = this->b - subtract.b; - if (subtract.w > this->w) + } + if (subtract.w > this->w) { ret.w = 0; - else + } else { ret.w = this->w - subtract.w; + } return ret; } inline Color &operator-=(const Color &subtract) ESPHOME_ALWAYS_INLINE { return *this = (*this) - subtract; } From 8756b41b6344a61a49a3b4cdb66437b1b6064036 Mon Sep 17 00:00:00 2001 From: Olivier ARCHER Date: Wed, 14 Aug 2024 04:19:46 +0200 Subject: [PATCH 102/160] [mqtt] fix missing initializer in MQTTClientComponent::disable_discovery (#7271) --- esphome/components/mqtt/mqtt_client.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 876367aaea..c19b24c0cf 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -632,6 +632,7 @@ void MQTTClientComponent::disable_discovery() { this->discovery_info_ = MQTTDiscoveryInfo{ .prefix = "", .retain = false, + .discover_ip = false, .clean = false, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR, From b2b23f2a4f2fbbba0ded4b77104df871a2f27260 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 14 Aug 2024 04:21:19 +0200 Subject: [PATCH 103/160] [code-quality] fix readability-named-parameter (#7272) --- esphome/core/automation.h | 8 +++++--- esphome/core/optional.h | 30 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 5a0a17ea1a..e77e453431 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -82,7 +82,7 @@ template class Condition { } protected: - template bool check_tuple_(const std::tuple &tuple, seq) { + template bool check_tuple_(const std::tuple &tuple, seq /*unused*/) { return this->check(std::get(tuple)...); } }; @@ -156,7 +156,7 @@ template class Action { } } } - template void play_next_tuple_(const std::tuple &tuple, seq) { + template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { this->play_next_(std::get(tuple)...); } void play_next_tuple_(const std::tuple &tuple) { @@ -223,7 +223,9 @@ template class ActionList { } protected: - template void play_tuple_(const std::tuple &tuple, seq) { this->play(std::get(tuple)...); } + template void play_tuple_(const std::tuple &tuple, seq /*unused*/) { + this->play(std::get(tuple)...); + } Action *actions_begin_{nullptr}; Action *actions_end_{nullptr}; diff --git a/esphome/core/optional.h b/esphome/core/optional.h index 770b77081e..1e28ef1354 100644 --- a/esphome/core/optional.h +++ b/esphome/core/optional.h @@ -24,7 +24,7 @@ namespace esphome { struct nullopt_t { // NOLINT struct init {}; // NOLINT - nullopt_t(init) {} + nullopt_t(init /*unused*/) {} }; // extra parenthesis to prevent the most vexing parse: @@ -42,13 +42,13 @@ template class optional { // NOLINT optional() {} - optional(nullopt_t) {} + optional(nullopt_t /*unused*/) {} optional(T const &arg) : has_value_(true), value_(arg) {} // NOLINT template optional(optional const &other) : has_value_(other.has_value()), value_(other.value()) {} - optional &operator=(nullopt_t) { + optional &operator=(nullopt_t /*unused*/) { reset(); return *this; } @@ -130,29 +130,29 @@ template inline bool operator>=(optional const &x, op // Comparison with nullopt -template inline bool operator==(optional const &x, nullopt_t) { return (!x); } +template inline bool operator==(optional const &x, nullopt_t /*unused*/) { return (!x); } -template inline bool operator==(nullopt_t, optional const &x) { return (!x); } +template inline bool operator==(nullopt_t /*unused*/, optional const &x) { return (!x); } -template inline bool operator!=(optional const &x, nullopt_t) { return bool(x); } +template inline bool operator!=(optional const &x, nullopt_t /*unused*/) { return bool(x); } -template inline bool operator!=(nullopt_t, optional const &x) { return bool(x); } +template inline bool operator!=(nullopt_t /*unused*/, optional const &x) { return bool(x); } -template inline bool operator<(optional const &, nullopt_t) { return false; } +template inline bool operator<(optional const & /*unused*/, nullopt_t /*unused*/) { return false; } -template inline bool operator<(nullopt_t, optional const &x) { return bool(x); } +template inline bool operator<(nullopt_t /*unused*/, optional const &x) { return bool(x); } -template inline bool operator<=(optional const &x, nullopt_t) { return (!x); } +template inline bool operator<=(optional const &x, nullopt_t /*unused*/) { return (!x); } -template inline bool operator<=(nullopt_t, optional const &) { return true; } +template inline bool operator<=(nullopt_t /*unused*/, optional const & /*unused*/) { return true; } -template inline bool operator>(optional const &x, nullopt_t) { return bool(x); } +template inline bool operator>(optional const &x, nullopt_t /*unused*/) { return bool(x); } -template inline bool operator>(nullopt_t, optional const &) { return false; } +template inline bool operator>(nullopt_t /*unused*/, optional const & /*unused*/) { return false; } -template inline bool operator>=(optional const &, nullopt_t) { return true; } +template inline bool operator>=(optional const & /*unused*/, nullopt_t /*unused*/) { return true; } -template inline bool operator>=(nullopt_t, optional const &x) { return (!x); } +template inline bool operator>=(nullopt_t /*unused*/, optional const &x) { return (!x); } // Comparison with T From 8f093823672a49d63d9276c4072d2a3189d194d6 Mon Sep 17 00:00:00 2001 From: Philippe Wechsler <29612400+MadMonkey87@users.noreply.github.com> Date: Wed, 14 Aug 2024 04:25:45 +0200 Subject: [PATCH 104/160] support illuminance for airthings wave plus device (#5203) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../airthings_wave_plus/airthings_wave_plus.cpp | 7 +++++-- .../airthings_wave_plus/airthings_wave_plus.h | 2 ++ esphome/components/airthings_wave_plus/sensor.py | 12 ++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp index a32128e992..8c8c514fdb 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp @@ -14,8 +14,6 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { ESP_LOGD(TAG, "version = %d", value->version); if (value->version == 1) { - ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); - if (this->humidity_sensor_ != nullptr) { this->humidity_sensor_->publish_state(value->humidity / 2.0f); } @@ -43,6 +41,10 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) { this->tvoc_sensor_->publish_state(value->voc); } + + if (this->illuminance_sensor_ != nullptr) { + this->illuminance_sensor_->publish_state(value->ambientLight); + } } else { ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); } @@ -68,6 +70,7 @@ void AirthingsWavePlus::dump_config() { LOG_SENSOR(" ", "Radon", this->radon_sensor_); LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_); LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_); } AirthingsWavePlus::AirthingsWavePlus() { diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.h b/esphome/components/airthings_wave_plus/airthings_wave_plus.h index 23c8cbb166..bd7a40ef8b 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.h +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.h @@ -22,6 +22,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; } protected: bool is_valid_radon_value_(uint16_t radon); @@ -32,6 +33,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { sensor::Sensor *radon_sensor_{nullptr}; sensor::Sensor *radon_long_term_sensor_{nullptr}; sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *illuminance_sensor_{nullptr}; struct WavePlusReadings { uint8_t version; diff --git a/esphome/components/airthings_wave_plus/sensor.py b/esphome/components/airthings_wave_plus/sensor.py index 643a2bfb68..d28c7e2abc 100644 --- a/esphome/components/airthings_wave_plus/sensor.py +++ b/esphome/components/airthings_wave_plus/sensor.py @@ -12,6 +12,9 @@ from esphome.const import ( CONF_CO2, UNIT_BECQUEREL_PER_CUBIC_METER, UNIT_PARTS_PER_MILLION, + CONF_ILLUMINANCE, + UNIT_LUX, + DEVICE_CLASS_ILLUMINANCE, ) DEPENDENCIES = airthings_wave_base.DEPENDENCIES @@ -45,6 +48,12 @@ CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), } ) @@ -62,3 +71,6 @@ async def to_code(config): if config_co2 := config.get(CONF_CO2): sens = await sensor.new_sensor(config_co2) cg.add(var.set_co2(sens)) + if config_illuminance := config.get(CONF_ILLUMINANCE): + sens = await sensor.new_sensor(config_illuminance) + cg.add(var.set_illuminance(sens)) From d6f130e35ab2ecd0d2c09ef31459c205852aa751 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 13 Aug 2024 23:40:07 -0400 Subject: [PATCH 105/160] [micro_wake_word] Bump ESPMicroSpeechFeatures version to 1.1.0 (#7249) --- .../components/micro_wake_word/__init__.py | 47 +++++++++---------- platformio.ini | 2 +- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index c2faca25f4..cd45f75b01 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -1,39 +1,34 @@ -import logging - -import json import hashlib -from urllib.parse import urljoin +import json +import logging from pathlib import Path +from urllib.parse import urljoin + import requests -import esphome.config_validation as cv -import esphome.codegen as cg - -from esphome.core import CORE, HexInt - -from esphome.components import esp32, microphone -from esphome import automation, git, external_files +from esphome import automation, external_files, git from esphome.automation import register_action, register_condition - - +import esphome.codegen as cg +from esphome.components import esp32, microphone +import esphome.config_validation as cv from esphome.const import ( - __version__, + CONF_FILE, CONF_ID, CONF_MICROPHONE, CONF_MODEL, - CONF_URL, - CONF_FILE, + CONF_PASSWORD, CONF_PATH, + CONF_RAW_DATA_ID, CONF_REF, CONF_REFRESH, CONF_TYPE, + CONF_URL, CONF_USERNAME, - CONF_PASSWORD, - CONF_RAW_DATA_ID, TYPE_GIT, TYPE_LOCAL, + __version__, ) - +from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) @@ -174,12 +169,12 @@ def _convert_manifest_v1_to_v2(v1_manifest): CONF_SLIDING_WINDOW_AVERAGE_SIZE ] del v2_manifest[KEY_MICRO][CONF_SLIDING_WINDOW_AVERAGE_SIZE] - v2_manifest[KEY_MICRO][ - CONF_TENSOR_ARENA_SIZE - ] = 45672 # Original Inception-based V1 manifest models require a minimum of 45672 bytes - v2_manifest[KEY_MICRO][ - CONF_FEATURE_STEP_SIZE - ] = 20 # Original Inception-based V1 manifest models use a 20 ms feature step size + + # Original Inception-based V1 manifest models require a minimum of 45672 bytes + v2_manifest[KEY_MICRO][CONF_TENSOR_ARENA_SIZE] = 45672 + + # Original Inception-based V1 manifest models use a 20 ms feature step size + v2_manifest[KEY_MICRO][CONF_FEATURE_STEP_SIZE] = 20 return v2_manifest @@ -502,7 +497,7 @@ async def to_code(config): ) cg.add(var.set_features_step_size(manifest[KEY_MICRO][CONF_FEATURE_STEP_SIZE])) - cg.add_library("kahrendt/ESPMicroSpeechFeatures", "1.0.0") + cg.add_library("kahrendt/ESPMicroSpeechFeatures", "1.1.0") MICRO_WAKE_WORD_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(MicroWakeWord)}) diff --git a/platformio.ini b/platformio.ini index 87a239207f..4a0a3f2ef4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -145,7 +145,7 @@ framework = espidf lib_deps = ${common:idf.lib_deps} droscy/esp_wireguard@0.4.2 ; wireguard - kahrendt/ESPMicroSpeechFeatures@1.0.0 ; micro_wake_word + kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word build_flags = ${common:idf.build_flags} -Wno-nonnull-compare From cf6ea7cb2cf1636cb18e60ecf010fa6bb559c464 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Wed, 14 Aug 2024 05:42:43 +0200 Subject: [PATCH 106/160] Implement the finish() method and action. implement the is_stopped condition (#7255) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 12 ++++++++++-- .../i2s_audio/speaker/i2s_audio_speaker.h | 2 ++ esphome/components/speaker/__init__.py | 19 ++++++++++++++----- esphome/components/speaker/automation.h | 10 ++++++++++ esphome/components/speaker/speaker.h | 5 +++++ tests/components/speaker/test.esp32-ard.yaml | 11 +++++++++-- .../components/speaker/test.esp32-c3-ard.yaml | 11 +++++++++-- .../components/speaker/test.esp32-c3-idf.yaml | 11 +++++++++-- tests/components/speaker/test.esp32-idf.yaml | 11 +++++++++-- 9 files changed, 77 insertions(+), 15 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 1c6c50d8c9..cf5a2c2766 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -180,7 +180,11 @@ void I2SAudioSpeaker::player_task(void *params) { } } -void I2SAudioSpeaker::stop() { +void I2SAudioSpeaker::stop() { this->stop_(false); } + +void I2SAudioSpeaker::finish() { this->stop_(true); } + +void I2SAudioSpeaker::stop_(bool wait_on_empty) { if (this->is_failed()) return; if (this->state_ == speaker::STATE_STOPPED) @@ -192,7 +196,11 @@ void I2SAudioSpeaker::stop() { this->state_ = speaker::STATE_STOPPING; DataEvent data; data.stop = true; - xQueueSendToFront(this->buffer_queue_, &data, portMAX_DELAY); + if (wait_on_empty) { + xQueueSend(this->buffer_queue_, &data, portMAX_DELAY); + } else { + xQueueSendToFront(this->buffer_queue_, &data, portMAX_DELAY); + } } void I2SAudioSpeaker::watch_() { diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 1800feaeec..0bdb67ceba 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -53,6 +53,7 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud void start() override; void stop() override; + void finish() override; size_t play(const uint8_t *data, size_t length) override; @@ -60,6 +61,7 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud protected: void start_(); + void stop_(bool wait_on_empty); void watch_(); static void player_task(void *params); diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 79d5df8c5a..d28b726d1f 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -1,13 +1,11 @@ from esphome import automation -import esphome.config_validation as cv -import esphome.codegen as cg - from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID, CONF_DATA +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_DATA, CONF_ID from esphome.core import CORE from esphome.coroutine import coroutine_with_priority - CODEOWNERS = ["@jesserockz"] IS_PLATFORM_COMPONENT = True @@ -22,8 +20,12 @@ PlayAction = speaker_ns.class_( StopAction = speaker_ns.class_( "StopAction", automation.Action, cg.Parented.template(Speaker) ) +FinishAction = speaker_ns.class_( + "FinishAction", automation.Action, cg.Parented.template(Speaker) +) IsPlayingCondition = speaker_ns.class_("IsPlayingCondition", automation.Condition) +IsStoppedCondition = speaker_ns.class_("IsStoppedCondition", automation.Condition) async def setup_speaker_core_(var, config): @@ -75,11 +77,18 @@ async def speaker_play_action(config, action_id, template_arg, args): automation.register_action("speaker.stop", StopAction, SPEAKER_AUTOMATION_SCHEMA)( speaker_action ) +automation.register_action("speaker.finish", FinishAction, SPEAKER_AUTOMATION_SCHEMA)( + speaker_action +) automation.register_condition( "speaker.is_playing", IsPlayingCondition, SPEAKER_AUTOMATION_SCHEMA )(speaker_action) +automation.register_condition( + "speaker.is_stopped", IsStoppedCondition, SPEAKER_AUTOMATION_SCHEMA +)(speaker_action) + @coroutine_with_priority(100.0) async def to_code(config): diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index e28991a0d1..2716fe6100 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -39,10 +39,20 @@ template class StopAction : public Action, public Parente void play(Ts... x) override { this->parent_->stop(); } }; +template class FinishAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->finish(); } +}; + template class IsPlayingCondition : public Condition, public Parented { public: bool check(Ts... x) override { return this->parent_->is_running(); } }; +template class IsStoppedCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_stopped(); } +}; + } // namespace speaker } // namespace esphome diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index b494873160..142231881c 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -17,10 +17,15 @@ class Speaker { virtual void start() = 0; virtual void stop() = 0; + // In compare between *STOP()* and *FINISH()*; *FINISH()* will stop after emptying the play buffer, + // while *STOP()* will break directly. + // When finish() is not implemented on the plateform component it should just do a normal stop. + virtual void finish() { this->stop(); } virtual bool has_buffered_data() const = 0; bool is_running() const { return this->state_ == STATE_RUNNING; } + bool is_stopped() const { return this->state_ == STATE_STOPPED; } protected: State state_{STATE_STOPPED}; diff --git a/tests/components/speaker/test.esp32-ard.yaml b/tests/components/speaker/test.esp32-ard.yaml index 416e203d7b..e10c3e88c1 100644 --- a/tests/components/speaker/test.esp32-ard.yaml +++ b/tests/components/speaker/test.esp32-ard.yaml @@ -1,8 +1,15 @@ esphome: on_boot: then: - - speaker.play: [0, 1, 2, 3] - - speaker.stop + - if: + condition: speaker.is_stopped + then: + - speaker.play: [0, 1, 2, 3] + - if: + condition: speaker.is_playing + then: + - speaker.finish: + - speaker.stop: i2s_audio: i2s_lrclk_pin: 16 diff --git a/tests/components/speaker/test.esp32-c3-ard.yaml b/tests/components/speaker/test.esp32-c3-ard.yaml index c7809baace..08699d8b22 100644 --- a/tests/components/speaker/test.esp32-c3-ard.yaml +++ b/tests/components/speaker/test.esp32-c3-ard.yaml @@ -1,8 +1,15 @@ esphome: on_boot: then: - - speaker.play: [0, 1, 2, 3] - - speaker.stop + - if: + condition: speaker.is_stopped + then: + - speaker.play: [0, 1, 2, 3] + - if: + condition: speaker.is_playing + then: + - speaker.finish: + - speaker.stop: i2s_audio: i2s_lrclk_pin: 6 diff --git a/tests/components/speaker/test.esp32-c3-idf.yaml b/tests/components/speaker/test.esp32-c3-idf.yaml index c7809baace..08699d8b22 100644 --- a/tests/components/speaker/test.esp32-c3-idf.yaml +++ b/tests/components/speaker/test.esp32-c3-idf.yaml @@ -1,8 +1,15 @@ esphome: on_boot: then: - - speaker.play: [0, 1, 2, 3] - - speaker.stop + - if: + condition: speaker.is_stopped + then: + - speaker.play: [0, 1, 2, 3] + - if: + condition: speaker.is_playing + then: + - speaker.finish: + - speaker.stop: i2s_audio: i2s_lrclk_pin: 6 diff --git a/tests/components/speaker/test.esp32-idf.yaml b/tests/components/speaker/test.esp32-idf.yaml index 416e203d7b..e10c3e88c1 100644 --- a/tests/components/speaker/test.esp32-idf.yaml +++ b/tests/components/speaker/test.esp32-idf.yaml @@ -1,8 +1,15 @@ esphome: on_boot: then: - - speaker.play: [0, 1, 2, 3] - - speaker.stop + - if: + condition: speaker.is_stopped + then: + - speaker.play: [0, 1, 2, 3] + - if: + condition: speaker.is_playing + then: + - speaker.finish: + - speaker.stop: i2s_audio: i2s_lrclk_pin: 16 From ccf57488c5432372dcdb2e87072ed15a9c7953a4 Mon Sep 17 00:00:00 2001 From: Mike La Spina Date: Tue, 13 Aug 2024 23:43:35 -0500 Subject: [PATCH 107/160] Correct offset calibration (#7228) Co-authored-by: descipher Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/atm90e32/__init__.py | 7 ++ esphome/components/atm90e32/atm90e32.cpp | 84 +++++++++++++++---- esphome/components/atm90e32/atm90e32.h | 27 ++++-- esphome/components/atm90e32/atm90e32_reg.h | 2 + .../components/atm90e32/button/__init__.py | 43 ++++++++++ .../atm90e32/button/atm90e32_button.cpp | 20 +++++ .../atm90e32/button/atm90e32_button.h | 27 ++++++ esphome/components/atm90e32/sensor.py | 26 +++--- tests/components/atm90e32/test.esp32-ard.yaml | 9 ++ .../atm90e32/test.esp32-c3-ard.yaml | 9 ++ .../atm90e32/test.esp32-c3-idf.yaml | 9 ++ tests/components/atm90e32/test.esp32-idf.yaml | 9 ++ .../components/atm90e32/test.esp8266-ard.yaml | 40 +++++++++ .../components/atm90e32/test.rp2040-ard.yaml | 9 ++ 15 files changed, 288 insertions(+), 34 deletions(-) create mode 100644 esphome/components/atm90e32/button/__init__.py create mode 100644 esphome/components/atm90e32/button/atm90e32_button.cpp create mode 100644 esphome/components/atm90e32/button/atm90e32_button.h diff --git a/CODEOWNERS b/CODEOWNERS index 3ea9c75ac2..1236c8d842 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -46,6 +46,7 @@ esphome/components/async_tcp/* @OttoWinter esphome/components/at581x/* @X-Ryl669 esphome/components/atc_mithermometer/* @ahpohl esphome/components/atm90e26/* @danieltwagner +esphome/components/atm90e32/* @circuitsetup @descipher esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter diff --git a/esphome/components/atm90e32/__init__.py b/esphome/components/atm90e32/__init__.py index e69de29bb2..8ce95be489 100644 --- a/esphome/components/atm90e32/__init__.py +++ b/esphome/components/atm90e32/__init__.py @@ -0,0 +1,7 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@circuitsetup", "@descipher"] + +atm90e32_ns = cg.esphome_ns.namespace("atm90e32") + +CONF_ATM90E32_ID = "atm90e32_id" diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index e27459b18a..43647b1855 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -132,10 +132,77 @@ void ATM90E32Component::update() { this->status_clear_warning(); } +void ATM90E32Component::restore_calibrations_() { + if (enable_offset_calibration_) { + this->pref_.load(&this->offset_phase_); + } +}; + +void ATM90E32Component::run_offset_calibrations() { + // Run the calibrations and + // Setup voltage and current calibration offsets for PHASE A + this->offset_phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA); + this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA); + this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset + // Setup voltage and current calibration offsets for PHASE B + this->offset_phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB); + this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB); + this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset + // Setup voltage and current calibration offsets for PHASE C + this->offset_phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC); + this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC); + this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset + this->pref_.save(&this->offset_phase_); + ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_, + this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_); + ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_, + this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_); +} + +void ATM90E32Component::clear_offset_calibrations() { + // Clear the calibrations and + this->offset_phase_[PHASEA].voltage_offset_ = 0; + this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEA].current_offset_ = 0; + this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset + this->offset_phase_[PHASEB].voltage_offset_ = 0; + this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEB].current_offset_ = 0; + this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset + this->offset_phase_[PHASEC].voltage_offset_ = 0; + this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEC].current_offset_ = 0; + this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset + this->pref_.save(&this->offset_phase_); + ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_, + this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_); + ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_, + this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_); +} + void ATM90E32Component::setup() { ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component..."); this->spi_setup(); - + if (this->enable_offset_calibration_) { + uint32_t hash = fnv1_hash(App.get_friendly_name()); + this->pref_ = global_preferences->make_preference(hash, true); + this->restore_calibrations_(); + } uint16_t mmode0 = 0x87; // 3P4W 50Hz if (line_freq_ == 60) { mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz @@ -167,27 +234,12 @@ void ATM90E32Component::setup() { this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750 this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10% - // Setup voltage and current calibration offsets for PHASE A - this->phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA); - this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // A Voltage offset - this->phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA); - this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // A Current offset // Setup voltage and current gain for PHASE A this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_); // A Voltage rms gain this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_); // A line current gain - // Setup voltage and current calibration offsets for PHASE B - this->phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB); - this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // B Voltage offset - this->phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB); - this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // B Current offset // Setup voltage and current gain for PHASE B this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_); // B Voltage rms gain this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_); // B line current gain - // Setup voltage and current calibration offsets for PHASE C - this->phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC); - this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset - this->phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC); - this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset // Setup voltage and current gain for PHASE C this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_); // C Voltage rms gain this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_); // C line current gain diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 0a334dbe8b..35c61d1e05 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -1,9 +1,12 @@ #pragma once -#include "esphome/core/component.h" +#include "atm90e32_reg.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/spi/spi.h" -#include "atm90e32_reg.h" +#include "esphome/core/application.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" namespace esphome { namespace atm90e32 { @@ -20,7 +23,6 @@ class ATM90E32Component : public PollingComponent, void dump_config() override; float get_setup_priority() const override; void update() override; - void set_voltage_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].voltage_sensor_ = obj; } void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; } void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; } @@ -48,9 +50,11 @@ class ATM90E32Component : public PollingComponent, void set_line_freq(int freq) { line_freq_ = freq; } void set_current_phases(int phases) { current_phases_ = phases; } void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } + void run_offset_calibrations(); + void clear_offset_calibrations(); + void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; } uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/); uint16_t calibrate_current_offset_phase(uint8_t /*phase*/); - int32_t last_periodic_millis = millis(); protected: @@ -83,10 +87,11 @@ class ATM90E32Component : public PollingComponent, float get_chip_temperature_(); bool get_publish_interval_flag_() { return publish_interval_flag_; }; void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; }; + void restore_calibrations_(); struct ATM90E32Phase { - uint16_t voltage_gain_{7305}; - uint16_t ct_gain_{27961}; + uint16_t voltage_gain_{0}; + uint16_t ct_gain_{0}; uint16_t voltage_offset_{0}; uint16_t current_offset_{0}; float voltage_{0}; @@ -114,13 +119,21 @@ class ATM90E32Component : public PollingComponent, uint32_t cumulative_reverse_active_energy_{0}; } phase_[3]; + struct Calibration { + uint16_t voltage_offset_{0}; + uint16_t current_offset_{0}; + } offset_phase_[3]; + + ESPPreferenceObject pref_; + sensor::Sensor *freq_sensor_{nullptr}; sensor::Sensor *chip_temperature_sensor_{nullptr}; uint16_t pga_gain_{0x15}; int line_freq_{60}; int current_phases_{3}; - bool publish_interval_flag_{true}; + bool publish_interval_flag_{false}; bool peak_current_signed_{false}; + bool enable_offset_calibration_{false}; }; } // namespace atm90e32 diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h index dac62aa6b4..954fb42e79 100644 --- a/esphome/components/atm90e32/atm90e32_reg.h +++ b/esphome/components/atm90e32/atm90e32_reg.h @@ -1,5 +1,7 @@ #pragma once +#include + namespace esphome { namespace atm90e32 { diff --git a/esphome/components/atm90e32/button/__init__.py b/esphome/components/atm90e32/button/__init__.py new file mode 100644 index 0000000000..931346b386 --- /dev/null +++ b/esphome/components/atm90e32/button/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_CHIP, ICON_SCALE + +from .. import atm90e32_ns +from ..sensor import ATM90E32Component + +CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration" +CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration" + +ATM90E32CalibrationButton = atm90e32_ns.class_( + "ATM90E32CalibrationButton", + button.Button, +) +ATM90E32ClearCalibrationButton = atm90e32_ns.class_( + "ATM90E32ClearCalibrationButton", + button.Button, +) + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component), + cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema( + ATM90E32CalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), + cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema( + ATM90E32ClearCalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_CHIP, + ), +} + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION): + b = await button.new_button(run_offset) + await cg.register_parented(b, parent) + if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION): + b = await button.new_button(clear_offset) + await cg.register_parented(b, parent) diff --git a/esphome/components/atm90e32/button/atm90e32_button.cpp b/esphome/components/atm90e32/button/atm90e32_button.cpp new file mode 100644 index 0000000000..00715b61dd --- /dev/null +++ b/esphome/components/atm90e32/button/atm90e32_button.cpp @@ -0,0 +1,20 @@ +#include "atm90e32_button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace atm90e32 { + +static const char *const TAG = "atm90e32.button"; + +void ATM90E32CalibrationButton::press_action() { + ESP_LOGI(TAG, "Running offset calibrations, Note: CTs and ACVs must be 0 during this process..."); + this->parent_->run_offset_calibrations(); +} + +void ATM90E32ClearCalibrationButton::press_action() { + ESP_LOGI(TAG, "Offset calibrations cleared."); + this->parent_->clear_offset_calibrations(); +} + +} // namespace atm90e32 +} // namespace esphome diff --git a/esphome/components/atm90e32/button/atm90e32_button.h b/esphome/components/atm90e32/button/atm90e32_button.h new file mode 100644 index 0000000000..0617099457 --- /dev/null +++ b/esphome/components/atm90e32/button/atm90e32_button.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/atm90e32/atm90e32.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace atm90e32 { + +class ATM90E32CalibrationButton : public button::Button, public Parented { + public: + ATM90E32CalibrationButton() = default; + + protected: + void press_action() override; +}; + +class ATM90E32ClearCalibrationButton : public button::Button, public Parented { + public: + ATM90E32ClearCalibrationButton() = default; + + protected: + void press_action() override; +}; + +} // namespace atm90e32 +} // namespace esphome diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 2bc7f0498d..be2196223c 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -1,21 +1,21 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, spi +import esphome.config_validation as cv from esphome.const import ( - CONF_ID, - CONF_REACTIVE_POWER, - CONF_VOLTAGE, + CONF_APPARENT_POWER, CONF_CURRENT, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_FREQUENCY, + CONF_ID, CONF_PHASE_A, + CONF_PHASE_ANGLE, CONF_PHASE_B, CONF_PHASE_C, - CONF_PHASE_ANGLE, CONF_POWER, CONF_POWER_FACTOR, - CONF_APPARENT_POWER, - CONF_FREQUENCY, - CONF_FORWARD_ACTIVE_ENERGY, + CONF_REACTIVE_POWER, CONF_REVERSE_ACTIVE_ENERGY, + CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -23,13 +23,13 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, - ICON_LIGHTBULB, ICON_CURRENT_AC, + ICON_LIGHTBULB, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, - UNIT_DEGREES, UNIT_CELSIUS, + UNIT_DEGREES, UNIT_HERTZ, UNIT_VOLT, UNIT_VOLT_AMPS_REACTIVE, @@ -37,6 +37,8 @@ from esphome.const import ( UNIT_WATT_HOURS, ) +from . import atm90e32_ns + CONF_LINE_FREQUENCY = "line_frequency" CONF_CHIP_TEMPERATURE = "chip_temperature" CONF_GAIN_PGA = "gain_pga" @@ -46,6 +48,7 @@ CONF_GAIN_CT = "gain_ct" CONF_HARMONIC_POWER = "harmonic_power" CONF_PEAK_CURRENT = "peak_current" CONF_PEAK_CURRENT_SIGNED = "peak_current_signed" +CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration" UNIT_DEG = "degrees" LINE_FREQS = { "50HZ": 50, @@ -61,7 +64,6 @@ PGA_GAINS = { "4X": 0x2A, } -atm90e32_ns = cg.esphome_ns.namespace("atm90e32") ATM90E32Component = atm90e32_ns.class_( "ATM90E32Component", cg.PollingComponent, spi.SPIDevice ) @@ -164,6 +166,7 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True), cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean, + cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("60s")) @@ -227,3 +230,4 @@ async def to_code(config): cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED])) + cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION])) diff --git a/tests/components/atm90e32/test.esp32-ard.yaml b/tests/components/atm90e32/test.esp32-ard.yaml index 131270f8ad..3bdc2bcec6 100644 --- a/tests/components/atm90e32/test.esp32-ard.yaml +++ b/tests/components/atm90e32/test.esp32-ard.yaml @@ -7,6 +7,7 @@ spi: sensor: - platform: atm90e32 cs_pin: 13 + id: chip1 phase_a: voltage: name: EMON Line Voltage A @@ -49,3 +50,11 @@ sensor: line_frequency: 60Hz current_phases: 3 gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.esp32-c3-ard.yaml b/tests/components/atm90e32/test.esp32-c3-ard.yaml index 263fb6d24e..9ec0037a61 100644 --- a/tests/components/atm90e32/test.esp32-c3-ard.yaml +++ b/tests/components/atm90e32/test.esp32-c3-ard.yaml @@ -7,6 +7,7 @@ spi: sensor: - platform: atm90e32 cs_pin: 8 + id: chip1 phase_a: voltage: name: EMON Line Voltage A @@ -49,3 +50,11 @@ sensor: line_frequency: 60Hz current_phases: 3 gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.esp32-c3-idf.yaml b/tests/components/atm90e32/test.esp32-c3-idf.yaml index 263fb6d24e..9ec0037a61 100644 --- a/tests/components/atm90e32/test.esp32-c3-idf.yaml +++ b/tests/components/atm90e32/test.esp32-c3-idf.yaml @@ -7,6 +7,7 @@ spi: sensor: - platform: atm90e32 cs_pin: 8 + id: chip1 phase_a: voltage: name: EMON Line Voltage A @@ -49,3 +50,11 @@ sensor: line_frequency: 60Hz current_phases: 3 gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.esp32-idf.yaml b/tests/components/atm90e32/test.esp32-idf.yaml index 131270f8ad..3bdc2bcec6 100644 --- a/tests/components/atm90e32/test.esp32-idf.yaml +++ b/tests/components/atm90e32/test.esp32-idf.yaml @@ -7,6 +7,7 @@ spi: sensor: - platform: atm90e32 cs_pin: 13 + id: chip1 phase_a: voltage: name: EMON Line Voltage A @@ -49,3 +50,11 @@ sensor: line_frequency: 60Hz current_phases: 3 gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.esp8266-ard.yaml b/tests/components/atm90e32/test.esp8266-ard.yaml index e8e2abc1a9..fbb3368efa 100644 --- a/tests/components/atm90e32/test.esp8266-ard.yaml +++ b/tests/components/atm90e32/test.esp8266-ard.yaml @@ -7,6 +7,7 @@ spi: sensor: - platform: atm90e32 cs_pin: 5 + id: chip1 phase_a: voltage: name: EMON Line Voltage A @@ -49,3 +50,42 @@ sensor: line_frequency: 60Hz current_phases: 3 gain_pga: 2X + enable_offset_calibration: True + - platform: atm90e32 + cs_pin: 4 + id: chip2 + phase_a: + voltage: + name: EMON Line Voltage A + current: + name: EMON CT1 Current + power: + name: EMON Active Power CT1 + reactive_power: + name: EMON Reactive Power CT1 + power_factor: + name: EMON Power Factor CT1 + gain_voltage: 7305 + gain_ct: 27961 + phase_c: + voltage: + name: EMON Line Voltage C + current: + name: EMON CT2 Current + power: + name: EMON Active Power CT2 + reactive_power: + name: EMON Reactive Power CT2 + power_factor: + name: EMON Power Factor CT2 + gain_voltage: 7305 + gain_ct: 27961 + line_frequency: 60Hz + current_phases: 2 +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.rp2040-ard.yaml b/tests/components/atm90e32/test.rp2040-ard.yaml index 525e0b801a..a6b7956da7 100644 --- a/tests/components/atm90e32/test.rp2040-ard.yaml +++ b/tests/components/atm90e32/test.rp2040-ard.yaml @@ -7,6 +7,7 @@ spi: sensor: - platform: atm90e32 cs_pin: 5 + id: chip1 phase_a: voltage: name: EMON Line Voltage A @@ -49,3 +50,11 @@ sensor: line_frequency: 60Hz current_phases: 3 gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" From 350f17e48f60cdd7100945c82a14e284adf517b5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:56:53 +1200 Subject: [PATCH 108/160] Bump version to 2024.9.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 37f20796b5..6157ce32f7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.8.0-dev" +__version__ = "2024.9.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 7b233d6871eef2a9860b994940ee6669560e576c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:56:53 +1200 Subject: [PATCH 109/160] Bump version to 2024.8.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 37f20796b5..47aacd6452 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.8.0-dev" +__version__ = "2024.8.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 7133e08755fb9c65abc62af9dc0df47900caa0a3 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 14 Aug 2024 00:55:23 -0700 Subject: [PATCH 110/160] remove extra number from pronto (#7263) --- esphome/components/remote_base/pronto_protocol.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 625af76235..35fd782248 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -201,9 +201,6 @@ std::string ProntoProtocol::compensate_and_dump_sequence_(const RawTimings &data out += dump_duration_(t_duration, timebase); } - // append minimum gap - out += dump_duration_(PRONTO_DEFAULT_GAP, timebase, true); - return out; } From 80a0f137224c82fdc3cf17dfdd5dd3683ad9e796 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 14 Aug 2024 23:05:16 +0200 Subject: [PATCH 111/160] [code-quality] fix performance-unnecessary-value-param (#7274) --- esphome/components/lvgl/number/lvgl_number.h | 4 +++- esphome/components/lvgl/select/lvgl_select.h | 4 +++- esphome/components/lvgl/switch/lvgl_switch.h | 4 +++- esphome/components/lvgl/text/lvgl_text.h | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index a70c9eab9c..77fadd2a29 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/components/number/number.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" @@ -11,7 +13,7 @@ namespace lvgl { class LVGLNumber : public number::Number { public: void set_control_lambda(std::function control_lambda) { - this->control_lambda_ = control_lambda; + this->control_lambda_ = std::move(control_lambda); if (this->initial_state_.has_value()) { this->control_lambda_(this->initial_state_.value()); this->initial_state_.reset(); diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index 407045d605..97cc8697eb 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/components/select/select.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" @@ -28,7 +30,7 @@ static std::vector split_string(const std::string &str) { class LVGLSelect : public select::Select { public: void set_control_lambda(std::function lambda) { - this->control_lambda_ = lambda; + this->control_lambda_ = std::move(lambda); if (this->initial_state_.has_value()) { this->control(this->initial_state_.value()); this->initial_state_.reset(); diff --git a/esphome/components/lvgl/switch/lvgl_switch.h b/esphome/components/lvgl/switch/lvgl_switch.h index dbc885219b..af839b8892 100644 --- a/esphome/components/lvgl/switch/lvgl_switch.h +++ b/esphome/components/lvgl/switch/lvgl_switch.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/components/switch/switch.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" @@ -11,7 +13,7 @@ namespace lvgl { class LVGLSwitch : public switch_::Switch { public: void set_control_lambda(std::function state_lambda) { - this->state_lambda_ = state_lambda; + this->state_lambda_ = std::move(state_lambda); if (this->initial_state_.has_value()) { this->state_lambda_(this->initial_state_.value()); this->initial_state_.reset(); diff --git a/esphome/components/lvgl/text/lvgl_text.h b/esphome/components/lvgl/text/lvgl_text.h index d3513c3697..4c380d69a2 100644 --- a/esphome/components/lvgl/text/lvgl_text.h +++ b/esphome/components/lvgl/text/lvgl_text.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/components/text/text.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" @@ -11,7 +13,7 @@ namespace lvgl { class LVGLText : public text::Text { public: void set_control_lambda(std::function control_lambda) { - this->control_lambda_ = control_lambda; + this->control_lambda_ = std::move(control_lambda); if (this->initial_state_.has_value()) { this->control_lambda_(this->initial_state_.value()); this->initial_state_.reset(); From 5646ec7f9c601b5b3a0c1917edeae5b4487eff79 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 14 Aug 2024 23:41:29 +0200 Subject: [PATCH 112/160] [code-quality] fix clang-tidy prometheus (#7284) --- esphome/components/prometheus/prometheus_handler.cpp | 2 ++ esphome/components/prometheus/prometheus_handler.h | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 09913bd713..3e9cf81e6e 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -1,4 +1,5 @@ #include "prometheus_handler.h" +#ifdef USE_NETWORK #include "esphome/core/application.h" namespace esphome { @@ -350,3 +351,4 @@ void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) } // namespace prometheus } // namespace esphome +#endif diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index a9505a3572..f5e49a1419 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include #include @@ -117,3 +118,4 @@ class PrometheusHandler : public AsyncWebHandler, public Component { } // namespace prometheus } // namespace esphome +#endif From 1bc3ccd96907ff392d4e321e5cc1e5833e107ccd Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 15 Aug 2024 00:30:29 +0200 Subject: [PATCH 113/160] [code-quality] fix clang-tidy ota (#7282) --- esphome/components/esphome/ota/ota_esphome.cpp | 3 ++- esphome/components/esphome/ota/ota_esphome.h | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 9d5044aaeb..7e2ef42a97 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -1,5 +1,5 @@ #include "ota_esphome.h" - +#ifdef USE_OTA #include "esphome/components/md5/md5.h" #include "esphome/components/network/util.h" #include "esphome/components/ota/ota_backend.h" @@ -410,3 +410,4 @@ float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::A uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; } } // namespace esphome +#endif diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 42629b4346..e0d09ff37e 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/defines.h" +#ifdef USE_OTA #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #include "esphome/components/ota/ota_backend.h" @@ -41,3 +42,4 @@ class ESPHomeOTAComponent : public ota::OTAComponent { }; } // namespace esphome +#endif From ce7adbae995ecd4dbb3368678d6de5be9d1d4855 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 15 Aug 2024 00:31:19 +0200 Subject: [PATCH 114/160] [code-quality] fix clang-tidy e131 (#7281) --- esphome/components/e131/e131.cpp | 2 ++ esphome/components/e131/e131.h | 4 +++- esphome/components/e131/e131_addressable_light_effect.cpp | 2 ++ esphome/components/e131/e131_addressable_light_effect.h | 3 ++- esphome/components/e131/e131_packet.cpp | 2 ++ 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index c3ff21c1a0..a74fc9be4a 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -1,4 +1,5 @@ #include "e131.h" +#ifdef USE_NETWORK #include "e131_addressable_light_effect.h" #include "esphome/core/log.h" @@ -118,3 +119,4 @@ bool E131Component::process_(int universe, const E131Packet &packet) { } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 91b67f62eb..d0e38fa98c 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include "esphome/components/socket/socket.h" #include "esphome/core/component.h" @@ -53,3 +54,4 @@ class E131Component : public esphome::Component { } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index be3144f590..4d1f98ab6c 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -1,5 +1,6 @@ #include "e131_addressable_light_effect.h" #include "e131.h" +#ifdef USE_NETWORK #include "esphome/core/log.h" namespace esphome { @@ -90,3 +91,4 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h index 56df9cd80f..17d7bd2829 100644 --- a/esphome/components/e131/e131_addressable_light_effect.h +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/light/addressable_light_effect.h" - +#ifdef USE_NETWORK namespace esphome { namespace e131 { @@ -42,3 +42,4 @@ class E131AddressableLightEffect : public light::AddressableLightEffect { } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index e1ae41cbaf..b8fa73b707 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -1,5 +1,6 @@ #include #include "e131.h" +#ifdef USE_NETWORK #include "esphome/components/network/ip_address.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -137,3 +138,4 @@ bool E131Component::packet_(const std::vector &data, int &universe, E13 } // namespace e131 } // namespace esphome +#endif From ecd3d838c937d59bd9ee71d0ea714034033d8c4c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:35:03 +1000 Subject: [PATCH 115/160] [api] Bump noise-c library version (#7288) --- .github/workflows/ci.yml | 4 ++-- esphome/components/api/__init__.py | 2 +- esphome/components/host/__init__.py | 10 +++------- tests/components/api/common.yaml | 4 ---- tests/components/api/test.esp32-ard.yaml | 4 ++++ tests/components/api/test.esp32-c3-ard.yaml | 4 ++++ tests/components/api/test.esp32-c3-idf.yaml | 4 ++++ tests/components/api/test.esp32-idf.yaml | 4 ++++ tests/components/api/test.esp8266-ard.yaml | 4 ++++ tests/components/api/test.host.yaml | 3 +++ tests/components/api/test.rp2040-ard.yaml | 4 ++++ 11 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 tests/components/api/test.host.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 126a541b3d..2437dd5b8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -397,7 +397,7 @@ jobs: file: ${{ fromJson(needs.list-components.outputs.components) }} steps: - name: Install dependencies - run: sudo apt-get install libsodium-dev libsdl2-dev + run: sudo apt-get install libsdl2-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -451,7 +451,7 @@ jobs: run: echo ${{ matrix.components }} - name: Install dependencies - run: sudo apt-get install libsodium-dev libsdl2-dev + run: sudo apt-get install libsdl2-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.7 diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 38b50d4b9d..27de5c873b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -155,7 +155,7 @@ async def to_code(config): decoded = base64.b64decode(encryption_config[CONF_KEY]) cg.add(var.set_noise_psk(list(decoded))) cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.4") + cg.add_library("esphome/noise-c", "0.1.6") else: cg.add_define("USE_API_PLAINTEXT") diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index 39e418c9ea..e83bf2dba8 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -1,15 +1,14 @@ +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( + CONF_MAC_ADDRESS, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_HOST, - CONF_MAC_ADDRESS, ) from esphome.core import CORE -from esphome.helpers import IS_MACOS -import esphome.config_validation as cv -import esphome.codegen as cg from .const import KEY_HOST @@ -42,8 +41,5 @@ async def to_code(config): cg.add_build_flag("-DUSE_HOST") cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) cg.add_build_flag("-std=c++17") - cg.add_build_flag("-lsodium") - if IS_MACOS: - cg.add_build_flag("-L/opt/homebrew/lib") cg.add_define("ESPHOME_BOARD", "host") cg.add_platformio_option("platform", "platformio/native") diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index 6c2a333598..7ac11e4da6 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -11,10 +11,6 @@ esphome: message: Button was pressed - homeassistant.tag_scanned: pulse -wifi: - ssid: MySSID - password: password1 - api: port: 8000 password: pwd diff --git a/tests/components/api/test.esp32-ard.yaml b/tests/components/api/test.esp32-ard.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp32-ard.yaml +++ b/tests/components/api/test.esp32-ard.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp32-c3-ard.yaml b/tests/components/api/test.esp32-c3-ard.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp32-c3-ard.yaml +++ b/tests/components/api/test.esp32-c3-ard.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp32-c3-idf.yaml b/tests/components/api/test.esp32-c3-idf.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp32-c3-idf.yaml +++ b/tests/components/api/test.esp32-c3-idf.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp32-idf.yaml b/tests/components/api/test.esp32-idf.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp32-idf.yaml +++ b/tests/components/api/test.esp32-idf.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp8266-ard.yaml b/tests/components/api/test.esp8266-ard.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp8266-ard.yaml +++ b/tests/components/api/test.esp8266-ard.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.host.yaml b/tests/components/api/test.host.yaml new file mode 100644 index 0000000000..1ecafeab77 --- /dev/null +++ b/tests/components/api/test.host.yaml @@ -0,0 +1,3 @@ +<<: !include common.yaml + +network: diff --git a/tests/components/api/test.rp2040-ard.yaml b/tests/components/api/test.rp2040-ard.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.rp2040-ard.yaml +++ b/tests/components/api/test.rp2040-ard.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 From 965141fad75618afea619e9d4b7dfd8ac4007c89 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 15 Aug 2024 06:38:49 +0200 Subject: [PATCH 116/160] [code-quality] fix clang-tidy wireguard (#7287) --- esphome/components/wireguard/__init__.py | 15 +++++++++------ esphome/components/wireguard/wireguard.cpp | 3 ++- esphome/components/wireguard/wireguard.h | 4 +++- esphome/core/defines.h | 1 + 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index 16d0d0226e..5e34a8a19b 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -1,19 +1,20 @@ -import re import ipaddress +import re + +from esphome import automation import esphome.codegen as cg +from esphome.components import time +from esphome.components.esp32 import CORE, add_idf_sdkconfig_option import esphome.config_validation as cv from esphome.const import ( - CONF_ID, - CONF_TIME_ID, CONF_ADDRESS, + CONF_ID, CONF_REBOOT_TIMEOUT, + CONF_TIME_ID, KEY_CORE, KEY_FRAMEWORK_VERSION, ) -from esphome.components.esp32 import CORE, add_idf_sdkconfig_option -from esphome.components import time from esphome.core import TimePeriod -from esphome import automation CONF_NETMASK = "netmask" CONF_PRIVATE_KEY = "private_key" @@ -91,6 +92,8 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) + cg.add_define("USE_WIREGUARD") + cg.add(var.set_address(str(config[CONF_ADDRESS]))) cg.add(var.set_netmask(str(config[CONF_NETMASK]))) cg.add(var.set_private_key(config[CONF_PRIVATE_KEY])) diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp index 17ebc701e3..7b4011cb79 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -1,5 +1,5 @@ #include "wireguard.h" - +#ifdef USE_WIREGUARD #include #include #include @@ -289,3 +289,4 @@ std::string mask_key(const std::string &key) { return (key.substr(0, 5) + "[...] } // namespace wireguard } // namespace esphome +#endif diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index a0e9e27a1b..5db9a48c90 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_WIREGUARD #include #include #include @@ -170,3 +171,4 @@ template class WireguardDisableAction : public Action, pu } // namespace wireguard } // namespace esphome +#endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a4d473b76e..52cf7d4dd0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -75,6 +75,7 @@ #define USE_VALVE #define USE_WIFI #define USE_WIFI_AP +#define USE_WIREGUARD // Arduino-specific feature flags #ifdef USE_ARDUINO From 5c31ab40607a9418ada87ad19f59abb85eb5db83 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Thu, 15 Aug 2024 06:51:44 +0200 Subject: [PATCH 117/160] fix some small rtttl issues (#6817) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/rtttl/rtttl.cpp | 127 ++++++++++++++++++++++++----- esphome/components/rtttl/rtttl.h | 21 +++-- 2 files changed, 121 insertions(+), 27 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 0bdf65b7bd..a97120499d 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -29,6 +29,13 @@ inline double deg2rad(double degrees) { void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); } void Rtttl::play(std::string rtttl) { + if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { + int pos = this->rtttl_.find(':'); + auto name = this->rtttl_.substr(0, pos); + ESP_LOGW(TAG, "RTTL Component is already playing: %s", name.c_str()); + return; + } + this->rtttl_ = std::move(rtttl); this->default_duration_ = 4; @@ -98,13 +105,20 @@ void Rtttl::play(std::string rtttl) { this->note_duration_ = 1; #ifdef USE_SPEAKER - this->samples_sent_ = 0; - this->samples_count_ = 0; + if (this->speaker_ != nullptr) { + this->set_state_(State::STATE_INIT); + this->samples_sent_ = 0; + this->samples_count_ = 0; + } +#endif +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->set_state_(State::STATE_RUNNING); + } #endif } void Rtttl::stop() { - this->note_duration_ = 0; #ifdef USE_OUTPUT if (this->output_ != nullptr) { this->output_->set_level(0.0); @@ -117,16 +131,35 @@ void Rtttl::stop() { } } #endif + this->note_duration_ = 0; + this->set_state_(STATE_STOPPING); } void Rtttl::loop() { - if (this->note_duration_ == 0) + if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) return; #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { + if (this->state_ == State::STATE_STOPPING) { + if (this->speaker_->is_stopped()) { + this->set_state_(State::STATE_STOPPED); + } + } else if (this->state_ == State::STATE_INIT) { + if (this->speaker_->is_stopped()) { + this->speaker_->start(); + this->set_state_(State::STATE_STARTING); + } + } else if (this->state_ == State::STATE_STARTING) { + if (this->speaker_->is_running()) { + this->set_state_(State::STATE_RUNNING); + } + } + if (!this->speaker_->is_running()) { + return; + } if (this->samples_sent_ != this->samples_count_) { - SpeakerSample sample[SAMPLE_BUFFER_SIZE + 1]; + SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2]; int x = 0; double rem = 0.0; @@ -136,7 +169,7 @@ void Rtttl::loop() { if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note// rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_); - int16_t val = (49152 * this->gain_) * sin(deg2rad(rem)); + int16_t val = (127 * this->gain_) * sin(deg2rad(rem)); // 16bit = 49152 sample[x].left = val; sample[x].right = val; @@ -153,9 +186,9 @@ void Rtttl::loop() { x++; } if (x > 0) { - int send = this->speaker_->play((uint8_t *) (&sample), x * 4); + int send = this->speaker_->play((uint8_t *) (&sample), x * 2); if (send != x * 4) { - this->samples_sent_ -= (x - (send / 4)); + this->samples_sent_ -= (x - (send / 2)); } return; } @@ -167,14 +200,7 @@ void Rtttl::loop() { return; #endif if (!this->rtttl_[position_]) { - this->note_duration_ = 0; -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->output_->set_level(0.0); - } -#endif - ESP_LOGD(TAG, "Playback finished"); - this->on_finished_playback_callback_.call(); + this->finish_(); return; } @@ -213,6 +239,7 @@ void Rtttl::loop() { case 'a': note = 10; break; + case 'h': case 'b': note = 12; break; @@ -238,14 +265,21 @@ void Rtttl::loop() { uint8_t scale = get_integer_(); if (scale == 0) scale = this->default_octave_; + + if (scale < 4 || scale > 7) { + ESP_LOGE(TAG, "Octave out of valid range. Should be between 4 and 7. (Octave: %d)", scale); + this->finish_(); + return; + } bool need_note_gap = false; // Now play the note if (note) { auto note_index = (scale - 4) * 12 + note; if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { - ESP_LOGE(TAG, "Note out of valid range"); - this->note_duration_ = 0; + ESP_LOGE(TAG, "Note out of valid range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index, + (int) sizeof(NOTES)); + this->finish_(); return; } auto freq = NOTES[note_index]; @@ -285,14 +319,17 @@ void Rtttl::loop() { this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1600; //(ms); } if (this->output_freq_ != 0) { + // make sure there is enough samples to add a full last sinus. + + uint16_t samples_wish = this->samples_count_; this->samples_per_wave_ = (this->sample_rate_ << 10) / this->output_freq_; - // make sure there is enough samples to add a full last sinus. uint16_t division = ((this->samples_count_ << 10) / this->samples_per_wave_) + 1; - uint16_t x = this->samples_count_; + this->samples_count_ = (division * this->samples_per_wave_); - ESP_LOGD(TAG, "play time old: %d div: %d new: %d %d", x, division, this->samples_count_, this->samples_per_wave_); this->samples_count_ = this->samples_count_ >> 10; + ESP_LOGVV(TAG, "- Calc play time: wish: %d gets: %d (div: %d spw: %d)", samples_wish, this->samples_count_, + division, this->samples_per_wave_); } // Convert from frequency in Hz to high and low samples in fixed point } @@ -301,5 +338,53 @@ void Rtttl::loop() { this->last_note_ = millis(); } +void Rtttl::finish_() { +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + } +#endif +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + SpeakerSample sample[2]; + sample[0].left = 0; + sample[0].right = 0; + sample[1].left = 0; + sample[1].right = 0; + this->speaker_->play((uint8_t *) (&sample), 8); + + this->speaker_->finish(); + } +#endif + this->set_state_(State::STATE_STOPPING); + this->note_duration_ = 0; + this->on_finished_playback_callback_.call(); + ESP_LOGD(TAG, "Playback finished"); +} + +static const LogString *state_to_string(State state) { + switch (state) { + case STATE_STOPPED: + return LOG_STR("STATE_STOPPED"); + case STATE_STARTING: + return LOG_STR("STATE_STARTING"); + case STATE_RUNNING: + return LOG_STR("STATE_RUNNING"); + case STATE_STOPPING: + return LOG_STR("STATE_STOPPING"); + case STATE_INIT: + return LOG_STR("STATE_INIT"); + default: + return LOG_STR("UNKNOWN"); + } +}; + +void Rtttl::set_state_(State state) { + State old_state = this->state_; + this->state_ = state; + ESP_LOGD(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)), + LOG_STR_ARG(state_to_string(state))); +} + } // namespace rtttl } // namespace esphome diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index bf089ce980..3cb6e3f5fb 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -14,12 +14,20 @@ namespace esphome { namespace rtttl { +enum State : uint8_t { + STATE_STOPPED = 0, + STATE_INIT, + STATE_STARTING, + STATE_RUNNING, + STATE_STOPPING, +}; + #ifdef USE_SPEAKER -static const size_t SAMPLE_BUFFER_SIZE = 512; +static const size_t SAMPLE_BUFFER_SIZE = 2048; struct SpeakerSample { - int16_t left{0}; - int16_t right{0}; + int8_t left{0}; + int8_t right{0}; }; #endif @@ -42,7 +50,7 @@ class Rtttl : public Component { void stop(); void dump_config() override; - bool is_playing() { return this->note_duration_ != 0; } + bool is_playing() { return this->state_ != State::STATE_STOPPED; } void loop() override; void add_on_finished_playback_callback(std::function callback) { @@ -57,6 +65,8 @@ class Rtttl : public Component { } return ret; } + void finish_(); + void set_state_(State state); std::string rtttl_{""}; size_t position_{0}; @@ -68,13 +78,12 @@ class Rtttl : public Component { uint32_t output_freq_; float gain_{0.6f}; + State state_{State::STATE_STOPPED}; #ifdef USE_OUTPUT output::FloatOutput *output_; #endif - void play_output_(); - #ifdef USE_SPEAKER speaker::Speaker *speaker_{nullptr}; int sample_rate_{16000}; From 9713458368dfb9fd9aab8016cfe8c85d77b04887 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 15 Aug 2024 07:17:38 +0200 Subject: [PATCH 118/160] [code-quality] fix clang-tidy improv_serial (#7283) --- esphome/components/improv_serial/improv_serial_component.cpp | 3 ++- esphome/components/improv_serial/improv_serial_component.h | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 425a5c8576..c3a0f2eacc 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -1,5 +1,5 @@ #include "improv_serial_component.h" - +#ifdef USE_WIFI #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" @@ -313,3 +313,4 @@ ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidel } // namespace improv_serial } // namespace esphome +#endif diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index f737f93d86..5d2534c2fc 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -5,7 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" - +#ifdef USE_WIFI #include #include @@ -78,3 +78,4 @@ extern ImprovSerialComponent } // namespace improv_serial } // namespace esphome +#endif From abb2669f0fb94c72deb55781bb087c86fe2e382c Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 15 Aug 2024 23:16:06 +0200 Subject: [PATCH 119/160] [code-quality] fix clang-tidy captive_portal (#7280) --- esphome/components/captive_portal/captive_portal.cpp | 2 ++ esphome/components/captive_portal/captive_portal.h | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 630e00f0b7..d1960e9a93 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -1,4 +1,5 @@ #include "captive_portal.h" +#ifdef USE_CAPTIVE_PORTAL #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/components/wifi/wifi_component.h" @@ -91,3 +92,4 @@ CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avo } // namespace captive_portal } // namespace esphome +#endif diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index df45d40d12..24d1295e6a 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_CAPTIVE_PORTAL #include #ifdef USE_ARDUINO #include @@ -71,3 +72,4 @@ extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid- } // namespace captive_portal } // namespace esphome +#endif From 9001d1c0d46cf214f989baac9c1f05f4ed321804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kiss?= <70820303+g-kiss@users.noreply.github.com> Date: Fri, 16 Aug 2024 00:35:00 +0200 Subject: [PATCH 120/160] Fix overflow in ESPColorCorrection object (#7268) --- esphome/components/light/esp_color_correction.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index eedd71ab27..979a1acb07 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -41,29 +41,29 @@ class ESPColorCorrection { if (this->max_brightness_.red == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.green == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.white == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } protected: From c3668b9a4de8408c28089c8c0652e0c36119cdcd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 16 Aug 2024 11:05:26 +1200 Subject: [PATCH 121/160] [validation] Allow ``maybe_simple_value`` to not have default key in complex value (#7294) --- esphome/config_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index ef60d6e0d6..1c00e0699b 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1850,7 +1850,7 @@ def maybe_simple_value(*validators, **kwargs): if value == SCHEMA_EXTRACT: return (validator, key) - if isinstance(value, dict) and key in value: + if isinstance(value, dict): return validator(value) return validator({key: value}) From a0c54504cdc54521309364609883c002f88ae137 Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Fri, 16 Aug 2024 00:27:23 +0100 Subject: [PATCH 122/160] Add HMAC-MD5 support for authenticating OTA updates (#7200) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/hmac_md5/__init__.py | 2 + esphome/components/hmac_md5/hmac_md5.cpp | 56 ++++++++++++++++++++++++ esphome/components/hmac_md5/hmac_md5.h | 48 ++++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 esphome/components/hmac_md5/__init__.py create mode 100644 esphome/components/hmac_md5/hmac_md5.cpp create mode 100644 esphome/components/hmac_md5/hmac_md5.h diff --git a/CODEOWNERS b/CODEOWNERS index 1236c8d842..9159f5f843 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,6 +169,7 @@ esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode +esphome/components/hmac_md5/* @dwmw2 esphome/components/homeassistant/* @OttoWinter @esphome/core esphome/components/homeassistant/number/* @landonr esphome/components/homeassistant/switch/* @Links2004 diff --git a/esphome/components/hmac_md5/__init__.py b/esphome/components/hmac_md5/__init__.py new file mode 100644 index 0000000000..fe245c0cfd --- /dev/null +++ b/esphome/components/hmac_md5/__init__.py @@ -0,0 +1,2 @@ +AUTO_LOAD = ["md5"] +CODEOWNERS = ["@dwmw2"] diff --git a/esphome/components/hmac_md5/hmac_md5.cpp b/esphome/components/hmac_md5/hmac_md5.cpp new file mode 100644 index 0000000000..90bf91882f --- /dev/null +++ b/esphome/components/hmac_md5/hmac_md5.cpp @@ -0,0 +1,56 @@ +#include +#include +#include "hmac_md5.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace hmac_md5 { +void HmacMD5::init(const uint8_t *key, size_t len) { + uint8_t ipad[64], opad[64]; + + memset(ipad, 0, sizeof(ipad)); + if (len > 64) { + md5::MD5Digest keymd5; + keymd5.init(); + keymd5.add(key, len); + keymd5.calculate(); + keymd5.get_bytes(ipad); + } else { + memcpy(ipad, key, len); + } + memcpy(opad, ipad, sizeof(opad)); + + for (int i = 0; i < 64; i++) { + ipad[i] ^= 0x36; + opad[i] ^= 0x5c; + } + + this->ihash_.init(); + this->ihash_.add(ipad, sizeof(ipad)); + + this->ohash_.init(); + this->ohash_.add(opad, sizeof(opad)); +} + +void HmacMD5::add(const uint8_t *data, size_t len) { this->ihash_.add(data, len); } + +void HmacMD5::calculate() { + uint8_t ibytes[16]; + + this->ihash_.calculate(); + this->ihash_.get_bytes(ibytes); + + this->ohash_.add(ibytes, sizeof(ibytes)); + this->ohash_.calculate(); +} + +void HmacMD5::get_bytes(uint8_t *output) { this->ohash_.get_bytes(output); } + +void HmacMD5::get_hex(char *output) { this->ohash_.get_hex(output); } + +bool HmacMD5::equals_bytes(const uint8_t *expected) { return this->ohash_.equals_bytes(expected); } + +bool HmacMD5::equals_hex(const char *expected) { return this->ohash_.equals_hex(expected); } + +} // namespace hmac_md5 +} // namespace esphome diff --git a/esphome/components/hmac_md5/hmac_md5.h b/esphome/components/hmac_md5/hmac_md5.h new file mode 100644 index 0000000000..e6a97ad2e3 --- /dev/null +++ b/esphome/components/hmac_md5/hmac_md5.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/components/md5/md5.h" + +#include + +namespace esphome { +namespace hmac_md5 { + +class HmacMD5 { + public: + HmacMD5() = default; + ~HmacMD5() = default; + + /// Initialize a new MD5 digest computation. + void init(const uint8_t *key, size_t len); + void init(const char *key, size_t len) { this->init((const uint8_t *) key, len); } + void init(const std::string &key) { this->init(key.c_str(), key.length()); } + + /// Add bytes of data for the digest. + void add(const uint8_t *data, size_t len); + void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } + + /// Compute the digest, based on the provided data. + void calculate(); + + /// Retrieve the HMAC-MD5 digest as bytes. + /// The output must be able to hold 16 bytes or more. + void get_bytes(uint8_t *output); + + /// Retrieve the HMAC-MD5 digest as hex characters. + /// The output must be able to hold 32 bytes or more. + void get_hex(char *output); + + /// Compare the digest against a provided byte-encoded digest (16 bytes). + bool equals_bytes(const uint8_t *expected); + + /// Compare the digest against a provided hex-encoded digest (32 bytes). + bool equals_hex(const char *expected); + + protected: + md5::MD5Digest ihash_; + md5::MD5Digest ohash_; +}; + +} // namespace hmac_md5 +} // namespace esphome From a7167ec3bf1adaec467944586c71d442a93a68d2 Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Fri, 16 Aug 2024 02:32:00 +0100 Subject: [PATCH 123/160] [network] Always allow ``enable_ipv6: false`` (#7291) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/network/__init__.py | 6 +++++- esphome/config_validation.py | 14 ++++++++++++++ tests/components/network/test-ipv6.bk72xx-ard.yaml | 4 ++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/components/network/test-ipv6.bk72xx-ard.yaml diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 96db322bde..caa873a746 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -24,7 +24,11 @@ CONFIG_SCHEMA = cv.Schema( esp32=False, rp2040=False, ): cv.All( - cv.boolean, cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]) + cv.boolean, + cv.Any( + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + cv.boolean_false, + ), ), cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, } diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1c00e0699b..6d6cb451d6 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -370,6 +370,20 @@ def boolean(value): ) +def boolean_false(value): + """Validate the given config option to be a boolean, set to False. + + This option allows a bunch of different ways of expressing boolean values: + - instance of boolean + - 'true'/'false' + - 'yes'/'no' + - 'enable'/disable + """ + if boolean(value): + raise Invalid("Expected boolean value to be false") + return False + + @schema_extractor_list def ensure_list(*validators): """Validate this configuration option to be a list. diff --git a/tests/components/network/test-ipv6.bk72xx-ard.yaml b/tests/components/network/test-ipv6.bk72xx-ard.yaml new file mode 100644 index 0000000000..361ca09977 --- /dev/null +++ b/tests/components/network/test-ipv6.bk72xx-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "false" + +<<: !include common.yaml From bc20fd57fe3bdd52a5ed5000d29b90063f76dd0a Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 14 Aug 2024 00:55:23 -0700 Subject: [PATCH 124/160] remove extra number from pronto (#7263) --- esphome/components/remote_base/pronto_protocol.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 625af76235..35fd782248 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -201,9 +201,6 @@ std::string ProntoProtocol::compensate_and_dump_sequence_(const RawTimings &data out += dump_duration_(t_duration, timebase); } - // append minimum gap - out += dump_duration_(PRONTO_DEFAULT_GAP, timebase, true); - return out; } From e3bfbebb8fd3368ac3c351af33407cf77956910d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:35:03 +1000 Subject: [PATCH 125/160] [api] Bump noise-c library version (#7288) --- .github/workflows/ci.yml | 4 ++-- esphome/components/api/__init__.py | 2 +- esphome/components/host/__init__.py | 10 +++------- tests/components/api/common.yaml | 4 ---- tests/components/api/test.esp32-ard.yaml | 4 ++++ tests/components/api/test.esp32-c3-ard.yaml | 4 ++++ tests/components/api/test.esp32-c3-idf.yaml | 4 ++++ tests/components/api/test.esp32-idf.yaml | 4 ++++ tests/components/api/test.esp8266-ard.yaml | 4 ++++ tests/components/api/test.host.yaml | 3 +++ tests/components/api/test.rp2040-ard.yaml | 4 ++++ 11 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 tests/components/api/test.host.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 126a541b3d..2437dd5b8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -397,7 +397,7 @@ jobs: file: ${{ fromJson(needs.list-components.outputs.components) }} steps: - name: Install dependencies - run: sudo apt-get install libsodium-dev libsdl2-dev + run: sudo apt-get install libsdl2-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -451,7 +451,7 @@ jobs: run: echo ${{ matrix.components }} - name: Install dependencies - run: sudo apt-get install libsodium-dev libsdl2-dev + run: sudo apt-get install libsdl2-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.7 diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 38b50d4b9d..27de5c873b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -155,7 +155,7 @@ async def to_code(config): decoded = base64.b64decode(encryption_config[CONF_KEY]) cg.add(var.set_noise_psk(list(decoded))) cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.4") + cg.add_library("esphome/noise-c", "0.1.6") else: cg.add_define("USE_API_PLAINTEXT") diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index 39e418c9ea..e83bf2dba8 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -1,15 +1,14 @@ +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( + CONF_MAC_ADDRESS, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_HOST, - CONF_MAC_ADDRESS, ) from esphome.core import CORE -from esphome.helpers import IS_MACOS -import esphome.config_validation as cv -import esphome.codegen as cg from .const import KEY_HOST @@ -42,8 +41,5 @@ async def to_code(config): cg.add_build_flag("-DUSE_HOST") cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) cg.add_build_flag("-std=c++17") - cg.add_build_flag("-lsodium") - if IS_MACOS: - cg.add_build_flag("-L/opt/homebrew/lib") cg.add_define("ESPHOME_BOARD", "host") cg.add_platformio_option("platform", "platformio/native") diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index 6c2a333598..7ac11e4da6 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -11,10 +11,6 @@ esphome: message: Button was pressed - homeassistant.tag_scanned: pulse -wifi: - ssid: MySSID - password: password1 - api: port: 8000 password: pwd diff --git a/tests/components/api/test.esp32-ard.yaml b/tests/components/api/test.esp32-ard.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp32-ard.yaml +++ b/tests/components/api/test.esp32-ard.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp32-c3-ard.yaml b/tests/components/api/test.esp32-c3-ard.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp32-c3-ard.yaml +++ b/tests/components/api/test.esp32-c3-ard.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp32-c3-idf.yaml b/tests/components/api/test.esp32-c3-idf.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp32-c3-idf.yaml +++ b/tests/components/api/test.esp32-c3-idf.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp32-idf.yaml b/tests/components/api/test.esp32-idf.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp32-idf.yaml +++ b/tests/components/api/test.esp32-idf.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp8266-ard.yaml b/tests/components/api/test.esp8266-ard.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp8266-ard.yaml +++ b/tests/components/api/test.esp8266-ard.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.host.yaml b/tests/components/api/test.host.yaml new file mode 100644 index 0000000000..1ecafeab77 --- /dev/null +++ b/tests/components/api/test.host.yaml @@ -0,0 +1,3 @@ +<<: !include common.yaml + +network: diff --git a/tests/components/api/test.rp2040-ard.yaml b/tests/components/api/test.rp2040-ard.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.rp2040-ard.yaml +++ b/tests/components/api/test.rp2040-ard.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 From e17c7124f48ac3fc2fbc2c45bbe01b23a203f65a Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Thu, 15 Aug 2024 06:51:44 +0200 Subject: [PATCH 126/160] fix some small rtttl issues (#6817) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/rtttl/rtttl.cpp | 127 ++++++++++++++++++++++++----- esphome/components/rtttl/rtttl.h | 21 +++-- 2 files changed, 121 insertions(+), 27 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 0bdf65b7bd..a97120499d 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -29,6 +29,13 @@ inline double deg2rad(double degrees) { void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); } void Rtttl::play(std::string rtttl) { + if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { + int pos = this->rtttl_.find(':'); + auto name = this->rtttl_.substr(0, pos); + ESP_LOGW(TAG, "RTTL Component is already playing: %s", name.c_str()); + return; + } + this->rtttl_ = std::move(rtttl); this->default_duration_ = 4; @@ -98,13 +105,20 @@ void Rtttl::play(std::string rtttl) { this->note_duration_ = 1; #ifdef USE_SPEAKER - this->samples_sent_ = 0; - this->samples_count_ = 0; + if (this->speaker_ != nullptr) { + this->set_state_(State::STATE_INIT); + this->samples_sent_ = 0; + this->samples_count_ = 0; + } +#endif +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->set_state_(State::STATE_RUNNING); + } #endif } void Rtttl::stop() { - this->note_duration_ = 0; #ifdef USE_OUTPUT if (this->output_ != nullptr) { this->output_->set_level(0.0); @@ -117,16 +131,35 @@ void Rtttl::stop() { } } #endif + this->note_duration_ = 0; + this->set_state_(STATE_STOPPING); } void Rtttl::loop() { - if (this->note_duration_ == 0) + if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) return; #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { + if (this->state_ == State::STATE_STOPPING) { + if (this->speaker_->is_stopped()) { + this->set_state_(State::STATE_STOPPED); + } + } else if (this->state_ == State::STATE_INIT) { + if (this->speaker_->is_stopped()) { + this->speaker_->start(); + this->set_state_(State::STATE_STARTING); + } + } else if (this->state_ == State::STATE_STARTING) { + if (this->speaker_->is_running()) { + this->set_state_(State::STATE_RUNNING); + } + } + if (!this->speaker_->is_running()) { + return; + } if (this->samples_sent_ != this->samples_count_) { - SpeakerSample sample[SAMPLE_BUFFER_SIZE + 1]; + SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2]; int x = 0; double rem = 0.0; @@ -136,7 +169,7 @@ void Rtttl::loop() { if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note// rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_); - int16_t val = (49152 * this->gain_) * sin(deg2rad(rem)); + int16_t val = (127 * this->gain_) * sin(deg2rad(rem)); // 16bit = 49152 sample[x].left = val; sample[x].right = val; @@ -153,9 +186,9 @@ void Rtttl::loop() { x++; } if (x > 0) { - int send = this->speaker_->play((uint8_t *) (&sample), x * 4); + int send = this->speaker_->play((uint8_t *) (&sample), x * 2); if (send != x * 4) { - this->samples_sent_ -= (x - (send / 4)); + this->samples_sent_ -= (x - (send / 2)); } return; } @@ -167,14 +200,7 @@ void Rtttl::loop() { return; #endif if (!this->rtttl_[position_]) { - this->note_duration_ = 0; -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->output_->set_level(0.0); - } -#endif - ESP_LOGD(TAG, "Playback finished"); - this->on_finished_playback_callback_.call(); + this->finish_(); return; } @@ -213,6 +239,7 @@ void Rtttl::loop() { case 'a': note = 10; break; + case 'h': case 'b': note = 12; break; @@ -238,14 +265,21 @@ void Rtttl::loop() { uint8_t scale = get_integer_(); if (scale == 0) scale = this->default_octave_; + + if (scale < 4 || scale > 7) { + ESP_LOGE(TAG, "Octave out of valid range. Should be between 4 and 7. (Octave: %d)", scale); + this->finish_(); + return; + } bool need_note_gap = false; // Now play the note if (note) { auto note_index = (scale - 4) * 12 + note; if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { - ESP_LOGE(TAG, "Note out of valid range"); - this->note_duration_ = 0; + ESP_LOGE(TAG, "Note out of valid range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index, + (int) sizeof(NOTES)); + this->finish_(); return; } auto freq = NOTES[note_index]; @@ -285,14 +319,17 @@ void Rtttl::loop() { this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1600; //(ms); } if (this->output_freq_ != 0) { + // make sure there is enough samples to add a full last sinus. + + uint16_t samples_wish = this->samples_count_; this->samples_per_wave_ = (this->sample_rate_ << 10) / this->output_freq_; - // make sure there is enough samples to add a full last sinus. uint16_t division = ((this->samples_count_ << 10) / this->samples_per_wave_) + 1; - uint16_t x = this->samples_count_; + this->samples_count_ = (division * this->samples_per_wave_); - ESP_LOGD(TAG, "play time old: %d div: %d new: %d %d", x, division, this->samples_count_, this->samples_per_wave_); this->samples_count_ = this->samples_count_ >> 10; + ESP_LOGVV(TAG, "- Calc play time: wish: %d gets: %d (div: %d spw: %d)", samples_wish, this->samples_count_, + division, this->samples_per_wave_); } // Convert from frequency in Hz to high and low samples in fixed point } @@ -301,5 +338,53 @@ void Rtttl::loop() { this->last_note_ = millis(); } +void Rtttl::finish_() { +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + } +#endif +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + SpeakerSample sample[2]; + sample[0].left = 0; + sample[0].right = 0; + sample[1].left = 0; + sample[1].right = 0; + this->speaker_->play((uint8_t *) (&sample), 8); + + this->speaker_->finish(); + } +#endif + this->set_state_(State::STATE_STOPPING); + this->note_duration_ = 0; + this->on_finished_playback_callback_.call(); + ESP_LOGD(TAG, "Playback finished"); +} + +static const LogString *state_to_string(State state) { + switch (state) { + case STATE_STOPPED: + return LOG_STR("STATE_STOPPED"); + case STATE_STARTING: + return LOG_STR("STATE_STARTING"); + case STATE_RUNNING: + return LOG_STR("STATE_RUNNING"); + case STATE_STOPPING: + return LOG_STR("STATE_STOPPING"); + case STATE_INIT: + return LOG_STR("STATE_INIT"); + default: + return LOG_STR("UNKNOWN"); + } +}; + +void Rtttl::set_state_(State state) { + State old_state = this->state_; + this->state_ = state; + ESP_LOGD(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)), + LOG_STR_ARG(state_to_string(state))); +} + } // namespace rtttl } // namespace esphome diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index bf089ce980..3cb6e3f5fb 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -14,12 +14,20 @@ namespace esphome { namespace rtttl { +enum State : uint8_t { + STATE_STOPPED = 0, + STATE_INIT, + STATE_STARTING, + STATE_RUNNING, + STATE_STOPPING, +}; + #ifdef USE_SPEAKER -static const size_t SAMPLE_BUFFER_SIZE = 512; +static const size_t SAMPLE_BUFFER_SIZE = 2048; struct SpeakerSample { - int16_t left{0}; - int16_t right{0}; + int8_t left{0}; + int8_t right{0}; }; #endif @@ -42,7 +50,7 @@ class Rtttl : public Component { void stop(); void dump_config() override; - bool is_playing() { return this->note_duration_ != 0; } + bool is_playing() { return this->state_ != State::STATE_STOPPED; } void loop() override; void add_on_finished_playback_callback(std::function callback) { @@ -57,6 +65,8 @@ class Rtttl : public Component { } return ret; } + void finish_(); + void set_state_(State state); std::string rtttl_{""}; size_t position_{0}; @@ -68,13 +78,12 @@ class Rtttl : public Component { uint32_t output_freq_; float gain_{0.6f}; + State state_{State::STATE_STOPPED}; #ifdef USE_OUTPUT output::FloatOutput *output_; #endif - void play_output_(); - #ifdef USE_SPEAKER speaker::Speaker *speaker_{nullptr}; int sample_rate_{16000}; From 033ab552068374f4ad06dca122a250f18ba2a979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kiss?= <70820303+g-kiss@users.noreply.github.com> Date: Fri, 16 Aug 2024 00:35:00 +0200 Subject: [PATCH 127/160] Fix overflow in ESPColorCorrection object (#7268) --- esphome/components/light/esp_color_correction.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index eedd71ab27..979a1acb07 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -41,29 +41,29 @@ class ESPColorCorrection { if (this->max_brightness_.red == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.green == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.white == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } protected: From 2c47eb62a7f1060be9fc727c8a5fc70ed92c1fd8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 16 Aug 2024 11:05:26 +1200 Subject: [PATCH 128/160] [validation] Allow ``maybe_simple_value`` to not have default key in complex value (#7294) --- esphome/config_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index ef60d6e0d6..1c00e0699b 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1850,7 +1850,7 @@ def maybe_simple_value(*validators, **kwargs): if value == SCHEMA_EXTRACT: return (validator, key) - if isinstance(value, dict) and key in value: + if isinstance(value, dict): return validator(value) return validator({key: value}) From 343650e37d29058a5f2ef7caf8c7f868c9b1746c Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Fri, 16 Aug 2024 02:32:00 +0100 Subject: [PATCH 129/160] [network] Always allow ``enable_ipv6: false`` (#7291) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/network/__init__.py | 6 +++++- esphome/config_validation.py | 14 ++++++++++++++ tests/components/network/test-ipv6.bk72xx-ard.yaml | 4 ++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/components/network/test-ipv6.bk72xx-ard.yaml diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 96db322bde..caa873a746 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -24,7 +24,11 @@ CONFIG_SCHEMA = cv.Schema( esp32=False, rp2040=False, ): cv.All( - cv.boolean, cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]) + cv.boolean, + cv.Any( + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + cv.boolean_false, + ), ), cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, } diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1c00e0699b..6d6cb451d6 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -370,6 +370,20 @@ def boolean(value): ) +def boolean_false(value): + """Validate the given config option to be a boolean, set to False. + + This option allows a bunch of different ways of expressing boolean values: + - instance of boolean + - 'true'/'false' + - 'yes'/'no' + - 'enable'/disable + """ + if boolean(value): + raise Invalid("Expected boolean value to be false") + return False + + @schema_extractor_list def ensure_list(*validators): """Validate this configuration option to be a list. diff --git a/tests/components/network/test-ipv6.bk72xx-ard.yaml b/tests/components/network/test-ipv6.bk72xx-ard.yaml new file mode 100644 index 0000000000..361ca09977 --- /dev/null +++ b/tests/components/network/test-ipv6.bk72xx-ard.yaml @@ -0,0 +1,4 @@ +substitutions: + network_enable_ipv6: "false" + +<<: !include common.yaml From e779a09586e16f03f4544dff36044e6e1318e4a3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:38:06 +1200 Subject: [PATCH 130/160] Bump version to 2024.8.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 47aacd6452..39d2ee74a1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.8.0b1" +__version__ = "2024.8.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 56aa58780da5fe73637ed9f583fafbd0b3a26db7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:27:03 +1200 Subject: [PATCH 131/160] Revert "[validation] Allow ``maybe_simple_value`` to not have default key in complex value" (#7305) --- esphome/config_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 6d6cb451d6..719cc43b31 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1864,7 +1864,7 @@ def maybe_simple_value(*validators, **kwargs): if value == SCHEMA_EXTRACT: return (validator, key) - if isinstance(value, dict): + if isinstance(value, dict) and key in value: return validator(value) return validator({key: value}) From ac9417d4694a69d843457ce3fa40f6e9c959c64a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 19 Aug 2024 08:43:23 +1000 Subject: [PATCH 132/160] [lvgl] Bug fixes (#7300) --- esphome/components/lvgl/defines.py | 32 ++++---- esphome/components/lvgl/lv_validation.py | 86 +++++++++++++-------- esphome/components/lvgl/schemas.py | 32 +++++--- esphome/components/lvgl/widgets/__init__.py | 23 +++++- tests/components/lvgl/common.yaml | 8 ++ tests/components/lvgl/lvgl-package.yaml | 58 ++++++++++++-- 6 files changed, 173 insertions(+), 66 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 8f7a973722..6a8b20b505 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -6,8 +6,8 @@ Constants already defined in esphome.const are not duplicated here and must be i from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS -from esphome.core import ID, Lambda -from esphome.cpp_generator import MockObj +from esphome.core import Lambda +from esphome.cpp_generator import LambdaExpression, MockObj from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor @@ -22,19 +22,22 @@ def literal(arg): return arg +def call_lambda(lamb: LambdaExpression): + expr = lamb.content.strip() + if expr.startswith("return") and expr.endswith(";"): + return expr[7:][:-1] + return f"{lamb}()" + + class LValidator: """ A validator for a particular type used in LVGL. Usable in configs as a validator, also has `process()` to convert a value during code generation """ - def __init__( - self, validator, rtype, idtype=None, idexpr=None, retmapper=None, requires=None - ): + def __init__(self, validator, rtype, retmapper=None, requires=None): self.validator = validator self.rtype = rtype - self.idtype = idtype - self.idexpr = idexpr self.retmapper = retmapper self.requires = requires @@ -43,8 +46,6 @@ class LValidator: value = requires_component(self.requires)(value) if isinstance(value, cv.Lambda): return cv.returning_lambda(value) - if self.idtype is not None and isinstance(value, ID): - return cv.use_id(self.idtype)(value) return self.validator(value) async def process(self, value, args=()): @@ -52,10 +53,10 @@ class LValidator: return None if isinstance(value, Lambda): return cg.RawExpression( - f"{await cg.process_lambda(value, args, return_type=self.rtype)}()" + call_lambda( + await cg.process_lambda(value, args, return_type=self.rtype) + ) ) - if self.idtype is not None and isinstance(value, ID): - return cg.RawExpression(f"{value}->{self.idexpr}") if self.retmapper is not None: return self.retmapper(value) return cg.safe_exp(value) @@ -89,7 +90,7 @@ class LvConstant(LValidator): cv.ensure_list(self.one_of), uint32, retmapper=self.mapper ) - def mapper(self, value, args=()): + def mapper(self, value): if not isinstance(value, list): value = [value] return literal( @@ -103,7 +104,7 @@ class LvConstant(LValidator): def extend(self, *choices): """ - Extend an LVCconstant with additional choices. + Extend an LVconstant with additional choices. :param choices: The extra choices :return: A new LVConstant instance """ @@ -431,6 +432,8 @@ CONF_ONE_LINE = "one_line" CONF_ON_SELECT = "on_select" CONF_ONE_CHECKED = "one_checked" CONF_NEXT = "next" +CONF_PAD_ROW = "pad_row" +CONF_PAD_COLUMN = "pad_column" CONF_PAGE = "page" CONF_PAGE_WRAP = "page_wrap" CONF_PASSWORD_MODE = "password_mode" @@ -462,6 +465,7 @@ CONF_SKIP = "skip" CONF_SYMBOL = "symbol" CONF_TAB_ID = "tab_id" CONF_TABS = "tabs" +CONF_TIME_FORMAT = "time_format" CONF_TILE = "tile" CONF_TILE_ID = "tile_id" CONF_TILES = "tiles" diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index b351b84af6..a2be4a2abe 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,17 +1,14 @@ from typing import Union import esphome.codegen as cg -from esphome.components.binary_sensor import BinarySensor from esphome.components.color import ColorStruct from esphome.components.font import Font from esphome.components.image import Image_ -from esphome.components.sensor import Sensor -from esphome.components.text_sensor import TextSensor import esphome.config_validation as cv -from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_VALUE -from esphome.core import HexInt +from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_TIME, CONF_VALUE +from esphome.core import HexInt, Lambda from esphome.cpp_generator import MockObj -from esphome.cpp_types import uint32 +from esphome.cpp_types import ESPTime, uint32 from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor @@ -19,9 +16,11 @@ from . import types as ty from .defines import ( CONF_END_VALUE, CONF_START_VALUE, + CONF_TIME_FORMAT, LV_FONTS, LValidator, LvConstant, + call_lambda, literal, ) from .helpers import ( @@ -110,13 +109,13 @@ def angle(value): def size_validator(value): """A size in one axis - one of "size_content", a number (pixels) or a percentage""" if value == SCHEMA_EXTRACT: - return ["size_content", "pixels", "..%"] + return ["SIZE_CONTENT", "number of pixels", "percentage"] if isinstance(value, str) and value.lower().endswith("px"): value = cv.int_(value[:-2]) if isinstance(value, str) and not value.endswith("%"): if value.upper() == "SIZE_CONTENT": return "LV_SIZE_CONTENT" - raise cv.Invalid("must be 'size_content', a pixel position or a percentage") + raise cv.Invalid("must be 'size_content', a percentage or an integer (pixels)") if isinstance(value, int): return cv.int_(value) # Will throw an exception if not a percentage. @@ -125,6 +124,15 @@ def size_validator(value): size = LValidator(size_validator, uint32, retmapper=literal) + +def pixels_validator(value): + if isinstance(value, str) and value.lower().endswith("px"): + return cv.int_(value[:-2]) + return cv.int_(value) + + +pixels = LValidator(pixels_validator, uint32, retmapper=literal) + radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") @@ -167,9 +175,7 @@ lv_image = LValidator( retmapper=lambda x: lv_expr.img_from(MockObj(x)), requires="image", ) -lv_bool = LValidator( - cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal -) +lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal) def lv_pct(value: Union[int, float]): @@ -185,42 +191,60 @@ def lvms_validator_(value): lv_milliseconds = LValidator( - lvms_validator_, - cg.int32, - retmapper=lambda x: x.total_milliseconds, + lvms_validator_, cg.int32, retmapper=lambda x: x.total_milliseconds ) class TextValidator(LValidator): def __init__(self): - super().__init__( - cv.string, - cg.const_char_ptr, - TextSensor, - "get_state().c_str()", - lambda s: cg.safe_exp(f"{s}"), - ) + super().__init__(cv.string, cg.std_string, lambda s: cg.safe_exp(f"{s}")) def __call__(self, value): - if isinstance(value, dict): + if isinstance(value, dict) and CONF_FORMAT in value: return value return super().__call__(value) async def process(self, value, args=()): if isinstance(value, dict): - args = [str(x) for x in value[CONF_ARGS]] - arg_expr = cg.RawExpression(",".join(args)) - format_str = cpp_string_escape(value[CONF_FORMAT]) - return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") + if format_str := value.get(CONF_FORMAT): + args = [str(x) for x in value[CONF_ARGS]] + arg_expr = cg.RawExpression(",".join(args)) + format_str = cpp_string_escape(format_str) + return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") + if time_format := value.get(CONF_TIME_FORMAT): + source = value[CONF_TIME] + if isinstance(source, Lambda): + time_format = cpp_string_escape(time_format) + return cg.RawExpression( + call_lambda( + await cg.process_lambda(source, args, return_type=ESPTime) + ) + + f".strftime({time_format}).c_str()" + ) + # must be an ID + source = await cg.get_variable(source) + return source.now().strftime(time_format).c_str() + if isinstance(value, Lambda): + value = call_lambda( + await cg.process_lambda(value, args, return_type=self.rtype) + ) + + # Was the lambda call reduced to a string? + if value.endswith("c_str()") or ( + value.endswith('"') and value.startswith('"') + ): + pass + else: + # Either a std::string or a lambda call returning that. We need const char* + value = f"({value}).c_str()" + return cg.RawExpression(value) return await super().process(value, args) lv_text = TextValidator() -lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") -lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()") -lv_brightness = LValidator( - cv.percentage, cg.float_, Sensor, "get_state()", retmapper=lambda x: int(x * 255) -) +lv_float = LValidator(cv.float_, cg.float_) +lv_int = LValidator(cv.int_, cg.int_) +lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255)) def is_lv_font(font): diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index f1c7ff4df6..e9714e3b1a 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,5 +1,6 @@ from esphome import config_validation as cv from esphome.automation import Trigger, validate_automation +from esphome.components.time import RealTimeClock from esphome.const import ( CONF_ARGS, CONF_FORMAT, @@ -8,6 +9,7 @@ from esphome.const import ( CONF_ON_VALUE, CONF_STATE, CONF_TEXT, + CONF_TIME, CONF_TRIGGER_ID, CONF_TYPE, ) @@ -15,6 +17,7 @@ from esphome.core import TimePeriod from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid +from .defines import CONF_TIME_FORMAT from .helpers import add_lv_use, requires_component, validate_printf from .lv_validation import lv_color, lv_font, lv_image from .lvcode import LvglComponent @@ -46,7 +49,13 @@ TEXT_SCHEMA = cv.Schema( ), validate_printf, ), - lvalid.lv_text, + cv.Schema( + { + cv.Required(CONF_TIME_FORMAT): cv.string, + cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)), + } + ), + cv.templatable(cv.string), ) } ) @@ -116,15 +125,13 @@ STYLE_PROPS = { "opa_layered": lvalid.opacity, "outline_color": lvalid.lv_color, "outline_opa": lvalid.opacity, - "outline_pad": lvalid.size, - "outline_width": lvalid.size, - "pad_all": lvalid.size, - "pad_bottom": lvalid.size, - "pad_column": lvalid.size, - "pad_left": lvalid.size, - "pad_right": lvalid.size, - "pad_row": lvalid.size, - "pad_top": lvalid.size, + "outline_pad": lvalid.pixels, + "outline_width": lvalid.pixels, + "pad_all": lvalid.pixels, + "pad_bottom": lvalid.pixels, + "pad_left": lvalid.pixels, + "pad_right": lvalid.pixels, + "pad_top": lvalid.pixels, "shadow_color": lvalid.lv_color, "shadow_ofs_x": cv.int_, "shadow_ofs_y": cv.int_, @@ -304,6 +311,8 @@ LAYOUT_SCHEMA = { cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, }, df.TYPE_FLEX: { cv.Optional( @@ -312,6 +321,8 @@ LAYOUT_SCHEMA = { cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, }, }, lower=True, @@ -338,7 +349,6 @@ DISP_BG_SCHEMA = cv.Schema( } ) - # A style schema that can include text STYLED_TEXT_SCHEMA = cv.maybe_simple_value( STYLE_SCHEMA.extend(TEXT_SCHEMA), key=CONF_TEXT diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 603de6aa3e..4abb25c61d 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -20,6 +20,8 @@ from ..defines import ( CONF_GRID_ROWS, CONF_LAYOUT, CONF_MAIN, + CONF_PAD_COLUMN, + CONF_PAD_ROW, CONF_SCROLLBAR_MODE, CONF_STYLES, CONF_WIDGETS, @@ -29,6 +31,7 @@ from ..defines import ( TYPE_FLEX, TYPE_GRID, LValidator, + call_lambda, join_enums, literal, ) @@ -273,6 +276,10 @@ async def set_obj_properties(w: Widget, config): layout_type: str = layout[CONF_TYPE] add_lv_use(layout_type) lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) + if (pad_row := layout.get(CONF_PAD_ROW)) is not None: + w.set_style(CONF_PAD_ROW, pad_row, 0) + if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None: + w.set_style(CONF_PAD_COLUMN, pad_column, 0) if layout_type == TYPE_GRID: wid = config[CONF_ID] rows = [str(x) for x in layout[CONF_GRID_ROWS]] @@ -316,8 +323,13 @@ async def set_obj_properties(w: Widget, config): flag_clr = set() flag_set = set() props = parts[CONF_MAIN][CONF_DEFAULT] + lambs = {} + flag_set = set() + flag_clr = set() for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items(): - if value: + if isinstance(value, cv.Lambda): + lambs[prop] = value + elif value: flag_set.add(prop) else: flag_clr.add(prop) @@ -327,6 +339,13 @@ async def set_obj_properties(w: Widget, config): if flag_clr: clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") w.clear_flag(clrs) + for key, value in lambs.items(): + lamb = await cg.process_lambda(value, [], return_type=cg.bool_) + flag = f"LV_OBJ_FLAG_{key.upper()}" + with LvConditional(call_lambda(lamb)) as cond: + w.add_flag(flag) + cond.else_() + w.clear_flag(flag) if states := config.get(CONF_STATE): adds = set() @@ -348,7 +367,7 @@ async def set_obj_properties(w: Widget, config): for key, value in lambs.items(): lamb = await cg.process_lambda(value, [], return_type=cg.bool_) state = f"LV_STATE_{key.upper()}" - with LvConditional(f"{lamb}()") as cond: + with LvConditional(call_lambda(lamb)) as cond: w.add_state(state) cond.else_() w.clear_state(state) diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 002c7a118d..7ef7772ac9 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -127,3 +127,11 @@ binary_sensor: - platform: lvgl name: LVGL checkbox widget: checkbox_id + +wifi: + ssid: SSID + password: PASSWORD123 + +time: + platform: sntp + id: time_id diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 54022354f5..800d6eff27 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -16,8 +16,6 @@ lvgl: border_width: 0 radius: 0 pad_all: 0 - pad_row: 0 - pad_column: 0 border_color: 0x0077b3 text_color: 0xFFFFFF width: 100% @@ -55,6 +53,13 @@ lvgl: pages: - id: page1 skip: true + layout: + type: flex + pad_row: 4 + pad_column: 4px + flex_align_main: center + flex_align_cross: start + flex_align_track: end widgets: - animimg: height: 60 @@ -118,10 +123,8 @@ lvgl: outline_width: 10px pad_all: 10px pad_bottom: 10px - pad_column: 10px pad_left: 10px pad_right: 10px - pad_row: 10px pad_top: 10px shadow_color: light_blue shadow_ofs_x: 5 @@ -221,10 +224,47 @@ lvgl: - label: text: Button on_click: - lvgl.label.update: - id: hello_label - bg_color: 0x123456 - text: clicked + - lvgl.label.update: + id: hello_label + bg_color: 0x123456 + text: clicked + - lvgl.label.update: + id: hello_label + text: !lambda return "hello world"; + - lvgl.label.update: + id: hello_label + text: !lambda |- + ESP_LOGD("label", "multi-line lambda"); + return "hello world"; + - lvgl.label.update: + id: hello_label + text: !lambda 'return str_sprintf("Hello space");' + - lvgl.label.update: + id: hello_label + text: + format: "sprintf format %s" + args: ['x ? "checked" : "unchecked"'] + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: time_id + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: !lambda return id(time_id).now(); + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: !lambda |- + ESP_LOGD("label", "multi-line lambda"); + return id(time_id).now(); on_value: logger.log: format: "state now %d" @@ -396,6 +436,8 @@ lvgl: grid_row_align: end grid_rows: [25px, fr(1), content] grid_columns: [40, fr(1), fr(1)] + pad_row: 6px + pad_column: 0 widgets: - image: grid_cell_row_pos: 0 From 8b6d6fe6616f0c83c72c9ff395f74134a8956dd4 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Mon, 19 Aug 2024 00:45:10 +0200 Subject: [PATCH 133/160] [speaker] Fix header includes (#7304) --- esphome/components/speaker/speaker.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 142231881c..193049402d 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -1,5 +1,9 @@ #pragma once +#include +#include +#include + namespace esphome { namespace speaker { From baedd74c7a5f54a871eb413a629d6dd94c15510a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:45:22 +1200 Subject: [PATCH 134/160] [microphone] Fix header includes (#7310) --- esphome/components/microphone/microphone.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/microphone/microphone.h b/esphome/components/microphone/microphone.h index e01a10e15c..883ca97710 100644 --- a/esphome/components/microphone/microphone.h +++ b/esphome/components/microphone/microphone.h @@ -1,6 +1,9 @@ #pragma once -#include "esphome/core/entity_base.h" +#include +#include +#include +#include #include "esphome/core/helpers.h" namespace esphome { From 7464b440c078794c0dec88c3a991e2c081695855 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:27:03 +1200 Subject: [PATCH 135/160] Revert "[validation] Allow ``maybe_simple_value`` to not have default key in complex value" (#7305) --- esphome/config_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 6d6cb451d6..719cc43b31 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1864,7 +1864,7 @@ def maybe_simple_value(*validators, **kwargs): if value == SCHEMA_EXTRACT: return (validator, key) - if isinstance(value, dict): + if isinstance(value, dict) and key in value: return validator(value) return validator({key: value}) From 5c7d070307c7a04e452cd641cdc3fa4baa477e18 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 19 Aug 2024 08:43:23 +1000 Subject: [PATCH 136/160] [lvgl] Bug fixes (#7300) --- esphome/components/lvgl/defines.py | 32 ++++---- esphome/components/lvgl/lv_validation.py | 86 +++++++++++++-------- esphome/components/lvgl/schemas.py | 32 +++++--- esphome/components/lvgl/widgets/__init__.py | 23 +++++- tests/components/lvgl/common.yaml | 8 ++ tests/components/lvgl/lvgl-package.yaml | 58 ++++++++++++-- 6 files changed, 173 insertions(+), 66 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 8f7a973722..6a8b20b505 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -6,8 +6,8 @@ Constants already defined in esphome.const are not duplicated here and must be i from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS -from esphome.core import ID, Lambda -from esphome.cpp_generator import MockObj +from esphome.core import Lambda +from esphome.cpp_generator import LambdaExpression, MockObj from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor @@ -22,19 +22,22 @@ def literal(arg): return arg +def call_lambda(lamb: LambdaExpression): + expr = lamb.content.strip() + if expr.startswith("return") and expr.endswith(";"): + return expr[7:][:-1] + return f"{lamb}()" + + class LValidator: """ A validator for a particular type used in LVGL. Usable in configs as a validator, also has `process()` to convert a value during code generation """ - def __init__( - self, validator, rtype, idtype=None, idexpr=None, retmapper=None, requires=None - ): + def __init__(self, validator, rtype, retmapper=None, requires=None): self.validator = validator self.rtype = rtype - self.idtype = idtype - self.idexpr = idexpr self.retmapper = retmapper self.requires = requires @@ -43,8 +46,6 @@ class LValidator: value = requires_component(self.requires)(value) if isinstance(value, cv.Lambda): return cv.returning_lambda(value) - if self.idtype is not None and isinstance(value, ID): - return cv.use_id(self.idtype)(value) return self.validator(value) async def process(self, value, args=()): @@ -52,10 +53,10 @@ class LValidator: return None if isinstance(value, Lambda): return cg.RawExpression( - f"{await cg.process_lambda(value, args, return_type=self.rtype)}()" + call_lambda( + await cg.process_lambda(value, args, return_type=self.rtype) + ) ) - if self.idtype is not None and isinstance(value, ID): - return cg.RawExpression(f"{value}->{self.idexpr}") if self.retmapper is not None: return self.retmapper(value) return cg.safe_exp(value) @@ -89,7 +90,7 @@ class LvConstant(LValidator): cv.ensure_list(self.one_of), uint32, retmapper=self.mapper ) - def mapper(self, value, args=()): + def mapper(self, value): if not isinstance(value, list): value = [value] return literal( @@ -103,7 +104,7 @@ class LvConstant(LValidator): def extend(self, *choices): """ - Extend an LVCconstant with additional choices. + Extend an LVconstant with additional choices. :param choices: The extra choices :return: A new LVConstant instance """ @@ -431,6 +432,8 @@ CONF_ONE_LINE = "one_line" CONF_ON_SELECT = "on_select" CONF_ONE_CHECKED = "one_checked" CONF_NEXT = "next" +CONF_PAD_ROW = "pad_row" +CONF_PAD_COLUMN = "pad_column" CONF_PAGE = "page" CONF_PAGE_WRAP = "page_wrap" CONF_PASSWORD_MODE = "password_mode" @@ -462,6 +465,7 @@ CONF_SKIP = "skip" CONF_SYMBOL = "symbol" CONF_TAB_ID = "tab_id" CONF_TABS = "tabs" +CONF_TIME_FORMAT = "time_format" CONF_TILE = "tile" CONF_TILE_ID = "tile_id" CONF_TILES = "tiles" diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index b351b84af6..a2be4a2abe 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,17 +1,14 @@ from typing import Union import esphome.codegen as cg -from esphome.components.binary_sensor import BinarySensor from esphome.components.color import ColorStruct from esphome.components.font import Font from esphome.components.image import Image_ -from esphome.components.sensor import Sensor -from esphome.components.text_sensor import TextSensor import esphome.config_validation as cv -from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_VALUE -from esphome.core import HexInt +from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_TIME, CONF_VALUE +from esphome.core import HexInt, Lambda from esphome.cpp_generator import MockObj -from esphome.cpp_types import uint32 +from esphome.cpp_types import ESPTime, uint32 from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor @@ -19,9 +16,11 @@ from . import types as ty from .defines import ( CONF_END_VALUE, CONF_START_VALUE, + CONF_TIME_FORMAT, LV_FONTS, LValidator, LvConstant, + call_lambda, literal, ) from .helpers import ( @@ -110,13 +109,13 @@ def angle(value): def size_validator(value): """A size in one axis - one of "size_content", a number (pixels) or a percentage""" if value == SCHEMA_EXTRACT: - return ["size_content", "pixels", "..%"] + return ["SIZE_CONTENT", "number of pixels", "percentage"] if isinstance(value, str) and value.lower().endswith("px"): value = cv.int_(value[:-2]) if isinstance(value, str) and not value.endswith("%"): if value.upper() == "SIZE_CONTENT": return "LV_SIZE_CONTENT" - raise cv.Invalid("must be 'size_content', a pixel position or a percentage") + raise cv.Invalid("must be 'size_content', a percentage or an integer (pixels)") if isinstance(value, int): return cv.int_(value) # Will throw an exception if not a percentage. @@ -125,6 +124,15 @@ def size_validator(value): size = LValidator(size_validator, uint32, retmapper=literal) + +def pixels_validator(value): + if isinstance(value, str) and value.lower().endswith("px"): + return cv.int_(value[:-2]) + return cv.int_(value) + + +pixels = LValidator(pixels_validator, uint32, retmapper=literal) + radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") @@ -167,9 +175,7 @@ lv_image = LValidator( retmapper=lambda x: lv_expr.img_from(MockObj(x)), requires="image", ) -lv_bool = LValidator( - cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal -) +lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal) def lv_pct(value: Union[int, float]): @@ -185,42 +191,60 @@ def lvms_validator_(value): lv_milliseconds = LValidator( - lvms_validator_, - cg.int32, - retmapper=lambda x: x.total_milliseconds, + lvms_validator_, cg.int32, retmapper=lambda x: x.total_milliseconds ) class TextValidator(LValidator): def __init__(self): - super().__init__( - cv.string, - cg.const_char_ptr, - TextSensor, - "get_state().c_str()", - lambda s: cg.safe_exp(f"{s}"), - ) + super().__init__(cv.string, cg.std_string, lambda s: cg.safe_exp(f"{s}")) def __call__(self, value): - if isinstance(value, dict): + if isinstance(value, dict) and CONF_FORMAT in value: return value return super().__call__(value) async def process(self, value, args=()): if isinstance(value, dict): - args = [str(x) for x in value[CONF_ARGS]] - arg_expr = cg.RawExpression(",".join(args)) - format_str = cpp_string_escape(value[CONF_FORMAT]) - return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") + if format_str := value.get(CONF_FORMAT): + args = [str(x) for x in value[CONF_ARGS]] + arg_expr = cg.RawExpression(",".join(args)) + format_str = cpp_string_escape(format_str) + return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") + if time_format := value.get(CONF_TIME_FORMAT): + source = value[CONF_TIME] + if isinstance(source, Lambda): + time_format = cpp_string_escape(time_format) + return cg.RawExpression( + call_lambda( + await cg.process_lambda(source, args, return_type=ESPTime) + ) + + f".strftime({time_format}).c_str()" + ) + # must be an ID + source = await cg.get_variable(source) + return source.now().strftime(time_format).c_str() + if isinstance(value, Lambda): + value = call_lambda( + await cg.process_lambda(value, args, return_type=self.rtype) + ) + + # Was the lambda call reduced to a string? + if value.endswith("c_str()") or ( + value.endswith('"') and value.startswith('"') + ): + pass + else: + # Either a std::string or a lambda call returning that. We need const char* + value = f"({value}).c_str()" + return cg.RawExpression(value) return await super().process(value, args) lv_text = TextValidator() -lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") -lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()") -lv_brightness = LValidator( - cv.percentage, cg.float_, Sensor, "get_state()", retmapper=lambda x: int(x * 255) -) +lv_float = LValidator(cv.float_, cg.float_) +lv_int = LValidator(cv.int_, cg.int_) +lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255)) def is_lv_font(font): diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index f1c7ff4df6..e9714e3b1a 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,5 +1,6 @@ from esphome import config_validation as cv from esphome.automation import Trigger, validate_automation +from esphome.components.time import RealTimeClock from esphome.const import ( CONF_ARGS, CONF_FORMAT, @@ -8,6 +9,7 @@ from esphome.const import ( CONF_ON_VALUE, CONF_STATE, CONF_TEXT, + CONF_TIME, CONF_TRIGGER_ID, CONF_TYPE, ) @@ -15,6 +17,7 @@ from esphome.core import TimePeriod from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid +from .defines import CONF_TIME_FORMAT from .helpers import add_lv_use, requires_component, validate_printf from .lv_validation import lv_color, lv_font, lv_image from .lvcode import LvglComponent @@ -46,7 +49,13 @@ TEXT_SCHEMA = cv.Schema( ), validate_printf, ), - lvalid.lv_text, + cv.Schema( + { + cv.Required(CONF_TIME_FORMAT): cv.string, + cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)), + } + ), + cv.templatable(cv.string), ) } ) @@ -116,15 +125,13 @@ STYLE_PROPS = { "opa_layered": lvalid.opacity, "outline_color": lvalid.lv_color, "outline_opa": lvalid.opacity, - "outline_pad": lvalid.size, - "outline_width": lvalid.size, - "pad_all": lvalid.size, - "pad_bottom": lvalid.size, - "pad_column": lvalid.size, - "pad_left": lvalid.size, - "pad_right": lvalid.size, - "pad_row": lvalid.size, - "pad_top": lvalid.size, + "outline_pad": lvalid.pixels, + "outline_width": lvalid.pixels, + "pad_all": lvalid.pixels, + "pad_bottom": lvalid.pixels, + "pad_left": lvalid.pixels, + "pad_right": lvalid.pixels, + "pad_top": lvalid.pixels, "shadow_color": lvalid.lv_color, "shadow_ofs_x": cv.int_, "shadow_ofs_y": cv.int_, @@ -304,6 +311,8 @@ LAYOUT_SCHEMA = { cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, }, df.TYPE_FLEX: { cv.Optional( @@ -312,6 +321,8 @@ LAYOUT_SCHEMA = { cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, }, }, lower=True, @@ -338,7 +349,6 @@ DISP_BG_SCHEMA = cv.Schema( } ) - # A style schema that can include text STYLED_TEXT_SCHEMA = cv.maybe_simple_value( STYLE_SCHEMA.extend(TEXT_SCHEMA), key=CONF_TEXT diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 603de6aa3e..4abb25c61d 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -20,6 +20,8 @@ from ..defines import ( CONF_GRID_ROWS, CONF_LAYOUT, CONF_MAIN, + CONF_PAD_COLUMN, + CONF_PAD_ROW, CONF_SCROLLBAR_MODE, CONF_STYLES, CONF_WIDGETS, @@ -29,6 +31,7 @@ from ..defines import ( TYPE_FLEX, TYPE_GRID, LValidator, + call_lambda, join_enums, literal, ) @@ -273,6 +276,10 @@ async def set_obj_properties(w: Widget, config): layout_type: str = layout[CONF_TYPE] add_lv_use(layout_type) lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) + if (pad_row := layout.get(CONF_PAD_ROW)) is not None: + w.set_style(CONF_PAD_ROW, pad_row, 0) + if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None: + w.set_style(CONF_PAD_COLUMN, pad_column, 0) if layout_type == TYPE_GRID: wid = config[CONF_ID] rows = [str(x) for x in layout[CONF_GRID_ROWS]] @@ -316,8 +323,13 @@ async def set_obj_properties(w: Widget, config): flag_clr = set() flag_set = set() props = parts[CONF_MAIN][CONF_DEFAULT] + lambs = {} + flag_set = set() + flag_clr = set() for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items(): - if value: + if isinstance(value, cv.Lambda): + lambs[prop] = value + elif value: flag_set.add(prop) else: flag_clr.add(prop) @@ -327,6 +339,13 @@ async def set_obj_properties(w: Widget, config): if flag_clr: clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") w.clear_flag(clrs) + for key, value in lambs.items(): + lamb = await cg.process_lambda(value, [], return_type=cg.bool_) + flag = f"LV_OBJ_FLAG_{key.upper()}" + with LvConditional(call_lambda(lamb)) as cond: + w.add_flag(flag) + cond.else_() + w.clear_flag(flag) if states := config.get(CONF_STATE): adds = set() @@ -348,7 +367,7 @@ async def set_obj_properties(w: Widget, config): for key, value in lambs.items(): lamb = await cg.process_lambda(value, [], return_type=cg.bool_) state = f"LV_STATE_{key.upper()}" - with LvConditional(f"{lamb}()") as cond: + with LvConditional(call_lambda(lamb)) as cond: w.add_state(state) cond.else_() w.clear_state(state) diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 002c7a118d..7ef7772ac9 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -127,3 +127,11 @@ binary_sensor: - platform: lvgl name: LVGL checkbox widget: checkbox_id + +wifi: + ssid: SSID + password: PASSWORD123 + +time: + platform: sntp + id: time_id diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 54022354f5..800d6eff27 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -16,8 +16,6 @@ lvgl: border_width: 0 radius: 0 pad_all: 0 - pad_row: 0 - pad_column: 0 border_color: 0x0077b3 text_color: 0xFFFFFF width: 100% @@ -55,6 +53,13 @@ lvgl: pages: - id: page1 skip: true + layout: + type: flex + pad_row: 4 + pad_column: 4px + flex_align_main: center + flex_align_cross: start + flex_align_track: end widgets: - animimg: height: 60 @@ -118,10 +123,8 @@ lvgl: outline_width: 10px pad_all: 10px pad_bottom: 10px - pad_column: 10px pad_left: 10px pad_right: 10px - pad_row: 10px pad_top: 10px shadow_color: light_blue shadow_ofs_x: 5 @@ -221,10 +224,47 @@ lvgl: - label: text: Button on_click: - lvgl.label.update: - id: hello_label - bg_color: 0x123456 - text: clicked + - lvgl.label.update: + id: hello_label + bg_color: 0x123456 + text: clicked + - lvgl.label.update: + id: hello_label + text: !lambda return "hello world"; + - lvgl.label.update: + id: hello_label + text: !lambda |- + ESP_LOGD("label", "multi-line lambda"); + return "hello world"; + - lvgl.label.update: + id: hello_label + text: !lambda 'return str_sprintf("Hello space");' + - lvgl.label.update: + id: hello_label + text: + format: "sprintf format %s" + args: ['x ? "checked" : "unchecked"'] + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: time_id + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: !lambda return id(time_id).now(); + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: !lambda |- + ESP_LOGD("label", "multi-line lambda"); + return id(time_id).now(); on_value: logger.log: format: "state now %d" @@ -396,6 +436,8 @@ lvgl: grid_row_align: end grid_rows: [25px, fr(1), content] grid_columns: [40, fr(1), fr(1)] + pad_row: 6px + pad_column: 0 widgets: - image: grid_cell_row_pos: 0 From 0f82114e64f50a167c85374ec299c43fd2cd84ff Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Mon, 19 Aug 2024 00:45:10 +0200 Subject: [PATCH 137/160] [speaker] Fix header includes (#7304) --- esphome/components/speaker/speaker.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 142231881c..193049402d 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -1,5 +1,9 @@ #pragma once +#include +#include +#include + namespace esphome { namespace speaker { From c96784f59108b476597a6f09a83de358d18ef3b2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:45:22 +1200 Subject: [PATCH 138/160] [microphone] Fix header includes (#7310) --- esphome/components/microphone/microphone.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/microphone/microphone.h b/esphome/components/microphone/microphone.h index e01a10e15c..883ca97710 100644 --- a/esphome/components/microphone/microphone.h +++ b/esphome/components/microphone/microphone.h @@ -1,6 +1,9 @@ #pragma once -#include "esphome/core/entity_base.h" +#include +#include +#include +#include #include "esphome/core/helpers.h" namespace esphome { From 409e84090eff4d3c8aa536e2077f036cd51cda54 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 19 Aug 2024 13:09:59 +1200 Subject: [PATCH 139/160] Bump version to 2024.8.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 39d2ee74a1..a321ddd19f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.8.0b2" +__version__ = "2024.8.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From b425912a80aea1ed71ebf5e3515963f09f559832 Mon Sep 17 00:00:00 2001 From: Roving Ronin <108674933+Roving-Ronin@users.noreply.github.com> Date: Tue, 20 Aug 2024 07:18:06 +1000 Subject: [PATCH 140/160] Update const.py - Add missing UNIT_LITRE (#7317) --- esphome/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/const.py b/esphome/const.py index 6157ce32f7..b9c37a53a8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1042,6 +1042,7 @@ UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR" UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" +UNIT_LITRE = "L" UNIT_LUX = "lx" UNIT_METER = "m" UNIT_METER_PER_SECOND_SQUARED = "m/s²" From 1ffee9c4d2d89823346b48585a987e9ab7233a93 Mon Sep 17 00:00:00 2001 From: Ali Jafri Date: Tue, 20 Aug 2024 03:12:41 +0530 Subject: [PATCH 141/160] Fix RP2040 Neopixel flickering issue (#7307) --- .../rp2040_pio_led_strip/led_strip.cpp | 36 ++++++++++++++++--- .../rp2040_pio_led_strip/led_strip.h | 5 +++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index 3e5e82898d..2aaa2ceb19 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -7,8 +7,10 @@ #include #include +#include #include #include +#include namespace esphome { namespace rp2040_pio_led_strip { @@ -23,6 +25,19 @@ static std::map conf_count_ = { {CHIPSET_WS2812, false}, {CHIPSET_WS2812B, false}, {CHIPSET_SK6812, false}, {CHIPSET_SM16703, false}, {CHIPSET_CUSTOM, false}, }; +static bool dma_chan_active_[12]; +static struct semaphore dma_write_complete_sem_[12]; + +// DMA interrupt service routine +void RP2040PIOLEDStripLightOutput::dma_write_complete_handler_() { + uint32_t channel = dma_hw->ints0; + for (uint dma_chan = 0; dma_chan < 12; ++dma_chan) { + if (RP2040PIOLEDStripLightOutput::dma_chan_active_[dma_chan] && (channel & (1u << dma_chan))) { + dma_hw->ints0 = (1u << dma_chan); // Clear the interrupt + sem_release(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[dma_chan]); // Handle the interrupt + } + } +} void RP2040PIOLEDStripLightOutput::setup() { ESP_LOGCONFIG(TAG, "Setting up RP2040 LED Strip..."); @@ -57,22 +72,22 @@ void RP2040PIOLEDStripLightOutput::setup() { // but there are only 4 state machines on each PIO so we can only have 4 strips per PIO uint offset = 0; - if (num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { + if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { ESP_LOGE(TAG, "Too many instances of PIO program"); this->mark_failed(); return; } // keep track of how many instances of the PIO program are running on each PIO - num_instance_[this->pio_ == pio0 ? 0 : 1]++; + RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1]++; // if there are multiple strips of the same chipset, we can reuse the same PIO program and save space if (this->conf_count_[this->chipset_]) { - offset = chipset_offsets_[this->chipset_]; + offset = RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_]; } else { // Load the assembled program into the PIO and get its location in the PIO's instruction memory and save it offset = pio_add_program(this->pio_, this->program_); - chipset_offsets_[this->chipset_] = offset; - conf_count_[this->chipset_] = true; + RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_] = offset; + RP2040PIOLEDStripLightOutput::conf_count_[this->chipset_] = true; } // Configure the state machine's PIO, and start it @@ -93,6 +108,9 @@ void RP2040PIOLEDStripLightOutput::setup() { return; } + // Mark the DMA channel as active + RP2040PIOLEDStripLightOutput::dma_chan_active_[this->dma_chan_] = true; + this->dma_config_ = dma_channel_get_default_config(this->dma_chan_); channel_config_set_transfer_data_size( &this->dma_config_, @@ -109,6 +127,13 @@ void RP2040PIOLEDStripLightOutput::setup() { false // don't start yet ); + // Initialize the semaphore for this DMA channel + sem_init(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_], 1, 1); + + irq_set_exclusive_handler(DMA_IRQ_0, dma_write_complete_handler_); // after DMA all data, raise an interrupt + dma_channel_set_irq0_enabled(this->dma_chan_, true); // map DMA channel to interrupt + irq_set_enabled(DMA_IRQ_0, true); // enable interrupt + this->init_(this->pio_, this->sm_, offset, this->pin_, this->max_refresh_rate_); } @@ -126,6 +151,7 @@ void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) { } // the bits are already in the correct order for the pio program so we can just copy the buffer using DMA + sem_acquire_blocking(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_]); dma_channel_transfer_from_buffer_now(this->dma_chan_, this->buf_, this->get_buffer_size_()); } diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.h b/esphome/components/rp2040_pio_led_strip/led_strip.h index 9976842f02..7b62648974 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.h +++ b/esphome/components/rp2040_pio_led_strip/led_strip.h @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace esphome { @@ -95,6 +96,8 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight { size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } + static void dma_write_complete_handler_(); + uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; @@ -120,6 +123,8 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight { inline static int num_instance_[2]; inline static std::map conf_count_; inline static std::map chipset_offsets_; + inline static bool dma_chan_active_[12]; + inline static struct semaphore dma_write_complete_sem_[12]; }; } // namespace rp2040_pio_led_strip From 30414667d023c18767852d12f3da7bc119c6ae1e Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Tue, 20 Aug 2024 00:22:19 +0200 Subject: [PATCH 142/160] add the ability to add more idf components to an existing setup (#7302) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0a5dd46478..b630c7638e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -172,6 +172,19 @@ def add_idf_component( KEY_COMPONENTS: components, KEY_SUBMODULES: submodules, } + else: + component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name] + if components is not None: + component_config[KEY_COMPONENTS] = list( + set(component_config[KEY_COMPONENTS] + components) + ) + if submodules is not None: + if component_config[KEY_SUBMODULES] is None: + component_config[KEY_SUBMODULES] = submodules + else: + component_config[KEY_SUBMODULES] = list( + set(component_config[KEY_SUBMODULES] + submodules) + ) def add_extra_script(stage: str, filename: str, path: str): From 3cbdf63f567621bf559f7cf82b05d078b56a8e28 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 20 Aug 2024 00:53:15 +0200 Subject: [PATCH 143/160] [code-quality] fix clang-tidy socket (#7285) --- esphome/components/socket/socket.cpp | 2 ++ esphome/components/socket/socket.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 5d3528dad8..e260fce05e 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -1,4 +1,5 @@ #include "socket.h" +#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) #include #include #include @@ -74,3 +75,4 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po } } // namespace socket } // namespace esphome +#endif diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 5c12210d15..cefdb51e0d 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -5,6 +5,7 @@ #include "esphome/core/optional.h" #include "headers.h" +#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) namespace esphome { namespace socket { @@ -57,3 +58,4 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po } // namespace socket } // namespace esphome +#endif From fa497d06b047334de87267ade785c9394b0074f5 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 21 Aug 2024 00:01:50 +0200 Subject: [PATCH 144/160] [code-quality] fix clang-tidy cstddef (#7324) --- esphome/components/microphone/microphone.h | 2 +- esphome/components/speaker/speaker.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/microphone/microphone.h b/esphome/components/microphone/microphone.h index 883ca97710..914ad80bea 100644 --- a/esphome/components/microphone/microphone.h +++ b/esphome/components/microphone/microphone.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 193049402d..375ccc4e8c 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include From bd3d065a23693340fffdc887178e76ca93830be2 Mon Sep 17 00:00:00 2001 From: Sung-jin Brian Hong Date: Wed, 21 Aug 2024 08:44:21 +0900 Subject: [PATCH 145/160] Fix waveshare 2.13" epaper stride calculation error (#7303) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/waveshare_epaper/waveshare_epaper.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 24df428e6f..7c1d436673 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -480,7 +480,7 @@ void HOT WaveshareEPaperTypeA::display() { this->start_data_(); switch (this->model_) { case TTGO_EPAPER_2_13_IN_B1: { // block needed because of variable initializations - int16_t wb = ((this->get_width_internal()) >> 3); + int16_t wb = ((this->get_width_controller()) >> 3); for (int i = 0; i < this->get_height_internal(); i++) { for (int j = 0; j < wb; j++) { int idx = j + (this->get_height_internal() - 1 - i) * wb; @@ -766,7 +766,7 @@ void WaveshareEPaper2P7InV2::initialize() { // XRAM_START_AND_END_POSITION this->command(0x44); this->data(0x00); - this->data(((get_width_internal() - 1) >> 3) & 0xFF); + this->data(((this->get_width_controller() - 1) >> 3) & 0xFF); // YRAM_START_AND_END_POSITION this->command(0x45); this->data(0x00); @@ -928,8 +928,8 @@ void HOT WaveshareEPaper2P7InB::display() { // TCON_RESOLUTION this->command(0x61); - this->data(this->get_width_internal() >> 8); - this->data(this->get_width_internal() & 0xff); // 176 + this->data(this->get_width_controller() >> 8); + this->data(this->get_width_controller() & 0xff); // 176 this->data(this->get_height_internal() >> 8); this->data(this->get_height_internal() & 0xff); // 264 @@ -994,7 +994,7 @@ void WaveshareEPaper2P7InBV2::initialize() { // self.SetWindows(0, 0, self.width-1, self.height-1) // SetWindows(self, Xstart, Ystart, Xend, Yend): - uint32_t xend = this->get_width_internal() - 1; + uint32_t xend = this->get_width_controller() - 1; uint32_t yend = this->get_height_internal() - 1; this->command(0x44); this->data(0x00); From 848fd0442d67dece75bf8eddf6a5242ead5b6dc4 Mon Sep 17 00:00:00 2001 From: NewoPL <27411874+NewoPL@users.noreply.github.com> Date: Wed, 21 Aug 2024 01:46:15 +0200 Subject: [PATCH 146/160] [rtttl] fix STOPPED state (#7323) --- esphome/components/rtttl/rtttl.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index a97120499d..495b5c1c8a 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -32,7 +32,7 @@ void Rtttl::play(std::string rtttl) { if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { int pos = this->rtttl_.find(':'); auto name = this->rtttl_.substr(0, pos); - ESP_LOGW(TAG, "RTTL Component is already playing: %s", name.c_str()); + ESP_LOGW(TAG, "RTTTL Component is already playing: %s", name.c_str()); return; } @@ -122,6 +122,7 @@ void Rtttl::stop() { #ifdef USE_OUTPUT if (this->output_ != nullptr) { this->output_->set_level(0.0); + this->set_state_(STATE_STOPPED); } #endif #ifdef USE_SPEAKER @@ -129,10 +130,10 @@ void Rtttl::stop() { if (this->speaker_->is_running()) { this->speaker_->stop(); } + this->set_state_(STATE_STOPPING); } #endif this->note_duration_ = 0; - this->set_state_(STATE_STOPPING); } void Rtttl::loop() { @@ -342,6 +343,7 @@ void Rtttl::finish_() { #ifdef USE_OUTPUT if (this->output_ != nullptr) { this->output_->set_level(0.0); + this->set_state_(State::STATE_STOPPED); } #endif #ifdef USE_SPEAKER @@ -354,9 +356,9 @@ void Rtttl::finish_() { this->speaker_->play((uint8_t *) (&sample), 8); this->speaker_->finish(); + this->set_state_(State::STATE_STOPPING); } #endif - this->set_state_(State::STATE_STOPPING); this->note_duration_ = 0; this->on_finished_playback_callback_.call(); ESP_LOGD(TAG, "Playback finished"); From 8fae60931622771feb31af6ee788e4cf8ac96f9c Mon Sep 17 00:00:00 2001 From: Ali Jafri Date: Tue, 20 Aug 2024 03:12:41 +0530 Subject: [PATCH 147/160] Fix RP2040 Neopixel flickering issue (#7307) --- .../rp2040_pio_led_strip/led_strip.cpp | 36 ++++++++++++++++--- .../rp2040_pio_led_strip/led_strip.h | 5 +++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index 3e5e82898d..2aaa2ceb19 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -7,8 +7,10 @@ #include #include +#include #include #include +#include namespace esphome { namespace rp2040_pio_led_strip { @@ -23,6 +25,19 @@ static std::map conf_count_ = { {CHIPSET_WS2812, false}, {CHIPSET_WS2812B, false}, {CHIPSET_SK6812, false}, {CHIPSET_SM16703, false}, {CHIPSET_CUSTOM, false}, }; +static bool dma_chan_active_[12]; +static struct semaphore dma_write_complete_sem_[12]; + +// DMA interrupt service routine +void RP2040PIOLEDStripLightOutput::dma_write_complete_handler_() { + uint32_t channel = dma_hw->ints0; + for (uint dma_chan = 0; dma_chan < 12; ++dma_chan) { + if (RP2040PIOLEDStripLightOutput::dma_chan_active_[dma_chan] && (channel & (1u << dma_chan))) { + dma_hw->ints0 = (1u << dma_chan); // Clear the interrupt + sem_release(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[dma_chan]); // Handle the interrupt + } + } +} void RP2040PIOLEDStripLightOutput::setup() { ESP_LOGCONFIG(TAG, "Setting up RP2040 LED Strip..."); @@ -57,22 +72,22 @@ void RP2040PIOLEDStripLightOutput::setup() { // but there are only 4 state machines on each PIO so we can only have 4 strips per PIO uint offset = 0; - if (num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { + if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { ESP_LOGE(TAG, "Too many instances of PIO program"); this->mark_failed(); return; } // keep track of how many instances of the PIO program are running on each PIO - num_instance_[this->pio_ == pio0 ? 0 : 1]++; + RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1]++; // if there are multiple strips of the same chipset, we can reuse the same PIO program and save space if (this->conf_count_[this->chipset_]) { - offset = chipset_offsets_[this->chipset_]; + offset = RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_]; } else { // Load the assembled program into the PIO and get its location in the PIO's instruction memory and save it offset = pio_add_program(this->pio_, this->program_); - chipset_offsets_[this->chipset_] = offset; - conf_count_[this->chipset_] = true; + RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_] = offset; + RP2040PIOLEDStripLightOutput::conf_count_[this->chipset_] = true; } // Configure the state machine's PIO, and start it @@ -93,6 +108,9 @@ void RP2040PIOLEDStripLightOutput::setup() { return; } + // Mark the DMA channel as active + RP2040PIOLEDStripLightOutput::dma_chan_active_[this->dma_chan_] = true; + this->dma_config_ = dma_channel_get_default_config(this->dma_chan_); channel_config_set_transfer_data_size( &this->dma_config_, @@ -109,6 +127,13 @@ void RP2040PIOLEDStripLightOutput::setup() { false // don't start yet ); + // Initialize the semaphore for this DMA channel + sem_init(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_], 1, 1); + + irq_set_exclusive_handler(DMA_IRQ_0, dma_write_complete_handler_); // after DMA all data, raise an interrupt + dma_channel_set_irq0_enabled(this->dma_chan_, true); // map DMA channel to interrupt + irq_set_enabled(DMA_IRQ_0, true); // enable interrupt + this->init_(this->pio_, this->sm_, offset, this->pin_, this->max_refresh_rate_); } @@ -126,6 +151,7 @@ void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) { } // the bits are already in the correct order for the pio program so we can just copy the buffer using DMA + sem_acquire_blocking(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_]); dma_channel_transfer_from_buffer_now(this->dma_chan_, this->buf_, this->get_buffer_size_()); } diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.h b/esphome/components/rp2040_pio_led_strip/led_strip.h index 9976842f02..7b62648974 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.h +++ b/esphome/components/rp2040_pio_led_strip/led_strip.h @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace esphome { @@ -95,6 +96,8 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight { size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } + static void dma_write_complete_handler_(); + uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; @@ -120,6 +123,8 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight { inline static int num_instance_[2]; inline static std::map conf_count_; inline static std::map chipset_offsets_; + inline static bool dma_chan_active_[12]; + inline static struct semaphore dma_write_complete_sem_[12]; }; } // namespace rp2040_pio_led_strip From c043bbe598f7b3d92c2a12969b1f2fbecc1f4fb6 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Tue, 20 Aug 2024 00:22:19 +0200 Subject: [PATCH 148/160] add the ability to add more idf components to an existing setup (#7302) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0a5dd46478..b630c7638e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -172,6 +172,19 @@ def add_idf_component( KEY_COMPONENTS: components, KEY_SUBMODULES: submodules, } + else: + component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name] + if components is not None: + component_config[KEY_COMPONENTS] = list( + set(component_config[KEY_COMPONENTS] + components) + ) + if submodules is not None: + if component_config[KEY_SUBMODULES] is None: + component_config[KEY_SUBMODULES] = submodules + else: + component_config[KEY_SUBMODULES] = list( + set(component_config[KEY_SUBMODULES] + submodules) + ) def add_extra_script(stage: str, filename: str, path: str): From 436c6282da1a1c784ab7365f99e59fc00f88cf0f Mon Sep 17 00:00:00 2001 From: Sung-jin Brian Hong Date: Wed, 21 Aug 2024 08:44:21 +0900 Subject: [PATCH 149/160] Fix waveshare 2.13" epaper stride calculation error (#7303) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/waveshare_epaper/waveshare_epaper.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 24df428e6f..7c1d436673 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -480,7 +480,7 @@ void HOT WaveshareEPaperTypeA::display() { this->start_data_(); switch (this->model_) { case TTGO_EPAPER_2_13_IN_B1: { // block needed because of variable initializations - int16_t wb = ((this->get_width_internal()) >> 3); + int16_t wb = ((this->get_width_controller()) >> 3); for (int i = 0; i < this->get_height_internal(); i++) { for (int j = 0; j < wb; j++) { int idx = j + (this->get_height_internal() - 1 - i) * wb; @@ -766,7 +766,7 @@ void WaveshareEPaper2P7InV2::initialize() { // XRAM_START_AND_END_POSITION this->command(0x44); this->data(0x00); - this->data(((get_width_internal() - 1) >> 3) & 0xFF); + this->data(((this->get_width_controller() - 1) >> 3) & 0xFF); // YRAM_START_AND_END_POSITION this->command(0x45); this->data(0x00); @@ -928,8 +928,8 @@ void HOT WaveshareEPaper2P7InB::display() { // TCON_RESOLUTION this->command(0x61); - this->data(this->get_width_internal() >> 8); - this->data(this->get_width_internal() & 0xff); // 176 + this->data(this->get_width_controller() >> 8); + this->data(this->get_width_controller() & 0xff); // 176 this->data(this->get_height_internal() >> 8); this->data(this->get_height_internal() & 0xff); // 264 @@ -994,7 +994,7 @@ void WaveshareEPaper2P7InBV2::initialize() { // self.SetWindows(0, 0, self.width-1, self.height-1) // SetWindows(self, Xstart, Ystart, Xend, Yend): - uint32_t xend = this->get_width_internal() - 1; + uint32_t xend = this->get_width_controller() - 1; uint32_t yend = this->get_height_internal() - 1; this->command(0x44); this->data(0x00); From aaae8f4a87d2ed38c35e16705cea0186120e876e Mon Sep 17 00:00:00 2001 From: NewoPL <27411874+NewoPL@users.noreply.github.com> Date: Wed, 21 Aug 2024 01:46:15 +0200 Subject: [PATCH 150/160] [rtttl] fix STOPPED state (#7323) --- esphome/components/rtttl/rtttl.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index a97120499d..495b5c1c8a 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -32,7 +32,7 @@ void Rtttl::play(std::string rtttl) { if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { int pos = this->rtttl_.find(':'); auto name = this->rtttl_.substr(0, pos); - ESP_LOGW(TAG, "RTTL Component is already playing: %s", name.c_str()); + ESP_LOGW(TAG, "RTTTL Component is already playing: %s", name.c_str()); return; } @@ -122,6 +122,7 @@ void Rtttl::stop() { #ifdef USE_OUTPUT if (this->output_ != nullptr) { this->output_->set_level(0.0); + this->set_state_(STATE_STOPPED); } #endif #ifdef USE_SPEAKER @@ -129,10 +130,10 @@ void Rtttl::stop() { if (this->speaker_->is_running()) { this->speaker_->stop(); } + this->set_state_(STATE_STOPPING); } #endif this->note_duration_ = 0; - this->set_state_(STATE_STOPPING); } void Rtttl::loop() { @@ -342,6 +343,7 @@ void Rtttl::finish_() { #ifdef USE_OUTPUT if (this->output_ != nullptr) { this->output_->set_level(0.0); + this->set_state_(State::STATE_STOPPED); } #endif #ifdef USE_SPEAKER @@ -354,9 +356,9 @@ void Rtttl::finish_() { this->speaker_->play((uint8_t *) (&sample), 8); this->speaker_->finish(); + this->set_state_(State::STATE_STOPPING); } #endif - this->set_state_(State::STATE_STOPPING); this->note_duration_ = 0; this->on_finished_playback_callback_.call(); ESP_LOGD(TAG, "Playback finished"); From 4ed6a648699c47c135ac992171757d49c75fbf74 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:46:56 +1200 Subject: [PATCH 151/160] Bump version to 2024.8.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index a321ddd19f..788eca10a1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.8.0b3" +__version__ = "2024.8.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 5d4bf5f8e5431cb3e08ccfddba9ce4bc269ab263 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 21 Aug 2024 14:20:29 +1200 Subject: [PATCH 152/160] Bump version to 2024.8.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 788eca10a1..f99d442be3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.8.0b4" +__version__ = "2024.8.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 68272c39c0176ce33c13646ac29ccb3d3d0ca981 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 22 Aug 2024 02:58:11 +0200 Subject: [PATCH 153/160] Add output source priority "hybrid" (#7322) --- esphome/components/pipsolar/pipsolar.cpp | 3 +++ esphome/components/pipsolar/pipsolar.h | 1 + esphome/components/pipsolar/switch/__init__.py | 2 ++ tests/components/pipsolar/test.esp32-ard.yaml | 2 ++ tests/components/pipsolar/test.esp32-c3-ard.yaml | 2 ++ tests/components/pipsolar/test.esp32-c3-idf.yaml | 2 ++ tests/components/pipsolar/test.esp32-idf.yaml | 2 ++ tests/components/pipsolar/test.esp8266-ard.yaml | 2 ++ tests/components/pipsolar/test.rp2040-ard.yaml | 2 ++ 9 files changed, 18 insertions(+) diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index 2cd1aeba44..c4bc018b75 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -136,6 +136,9 @@ void Pipsolar::loop() { if (this->output_source_priority_battery_switch_) { this->output_source_priority_battery_switch_->publish_state(value_output_source_priority_ == 2); } + if (this->output_source_priority_hybrid_switch_) { + this->output_source_priority_hybrid_switch_->publish_state(value_output_source_priority_ == 3); + } if (this->charger_source_priority_) { this->charger_source_priority_->publish_state(value_charger_source_priority_); } diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h index f20f44f095..373911b2d7 100644 --- a/esphome/components/pipsolar/pipsolar.h +++ b/esphome/components/pipsolar/pipsolar.h @@ -174,6 +174,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { PIPSOLAR_SWITCH(output_source_priority_utility_switch, QPIRI) PIPSOLAR_SWITCH(output_source_priority_solar_switch, QPIRI) PIPSOLAR_SWITCH(output_source_priority_battery_switch, QPIRI) + PIPSOLAR_SWITCH(output_source_priority_hybrid_switch, QPIRI) PIPSOLAR_SWITCH(input_voltage_range_switch, QPIRI) PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI) PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI) diff --git a/esphome/components/pipsolar/switch/__init__.py b/esphome/components/pipsolar/switch/__init__.py index 7658c7d4f8..80bcdad62e 100644 --- a/esphome/components/pipsolar/switch/__init__.py +++ b/esphome/components/pipsolar/switch/__init__.py @@ -9,6 +9,7 @@ DEPENDENCIES = ["uart"] CONF_OUTPUT_SOURCE_PRIORITY_UTILITY = "output_source_priority_utility" CONF_OUTPUT_SOURCE_PRIORITY_SOLAR = "output_source_priority_solar" CONF_OUTPUT_SOURCE_PRIORITY_BATTERY = "output_source_priority_battery" +CONF_OUTPUT_SOURCE_PRIORITY_HYBRID = "output_source_priority_hybrid" CONF_INPUT_VOLTAGE_RANGE = "input_voltage_range" CONF_PV_OK_CONDITION_FOR_PARALLEL = "pv_ok_condition_for_parallel" CONF_PV_POWER_BALANCE = "pv_power_balance" @@ -17,6 +18,7 @@ TYPES = { CONF_OUTPUT_SOURCE_PRIORITY_UTILITY: ("POP00", None), CONF_OUTPUT_SOURCE_PRIORITY_SOLAR: ("POP01", None), CONF_OUTPUT_SOURCE_PRIORITY_BATTERY: ("POP02", None), + CONF_OUTPUT_SOURCE_PRIORITY_HYBRID: ("POP03", None), CONF_INPUT_VOLTAGE_RANGE: ("PGR01", "PGR00"), CONF_PV_OK_CONDITION_FOR_PARALLEL: ("PPVOKC1", "PPVOKC0"), CONF_PV_POWER_BALANCE: ("PSPB1", "PSPB0"), diff --git a/tests/components/pipsolar/test.esp32-ard.yaml b/tests/components/pipsolar/test.esp32-ard.yaml index fcd4575739..b7a7e0cbd9 100644 --- a/tests/components/pipsolar/test.esp32-ard.yaml +++ b/tests/components/pipsolar/test.esp32-ard.yaml @@ -220,6 +220,8 @@ switch: name: inverter0_output_source_priority_solar output_source_priority_battery: name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid input_voltage_range: name: inverter0_input_voltage_range pv_ok_condition_for_parallel: diff --git a/tests/components/pipsolar/test.esp32-c3-ard.yaml b/tests/components/pipsolar/test.esp32-c3-ard.yaml index 12e9266343..83d7070669 100644 --- a/tests/components/pipsolar/test.esp32-c3-ard.yaml +++ b/tests/components/pipsolar/test.esp32-c3-ard.yaml @@ -220,6 +220,8 @@ switch: name: inverter0_output_source_priority_solar output_source_priority_battery: name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid input_voltage_range: name: inverter0_input_voltage_range pv_ok_condition_for_parallel: diff --git a/tests/components/pipsolar/test.esp32-c3-idf.yaml b/tests/components/pipsolar/test.esp32-c3-idf.yaml index 12e9266343..83d7070669 100644 --- a/tests/components/pipsolar/test.esp32-c3-idf.yaml +++ b/tests/components/pipsolar/test.esp32-c3-idf.yaml @@ -220,6 +220,8 @@ switch: name: inverter0_output_source_priority_solar output_source_priority_battery: name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid input_voltage_range: name: inverter0_input_voltage_range pv_ok_condition_for_parallel: diff --git a/tests/components/pipsolar/test.esp32-idf.yaml b/tests/components/pipsolar/test.esp32-idf.yaml index fcd4575739..b7a7e0cbd9 100644 --- a/tests/components/pipsolar/test.esp32-idf.yaml +++ b/tests/components/pipsolar/test.esp32-idf.yaml @@ -220,6 +220,8 @@ switch: name: inverter0_output_source_priority_solar output_source_priority_battery: name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid input_voltage_range: name: inverter0_input_voltage_range pv_ok_condition_for_parallel: diff --git a/tests/components/pipsolar/test.esp8266-ard.yaml b/tests/components/pipsolar/test.esp8266-ard.yaml index 12e9266343..83d7070669 100644 --- a/tests/components/pipsolar/test.esp8266-ard.yaml +++ b/tests/components/pipsolar/test.esp8266-ard.yaml @@ -220,6 +220,8 @@ switch: name: inverter0_output_source_priority_solar output_source_priority_battery: name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid input_voltage_range: name: inverter0_input_voltage_range pv_ok_condition_for_parallel: diff --git a/tests/components/pipsolar/test.rp2040-ard.yaml b/tests/components/pipsolar/test.rp2040-ard.yaml index 12e9266343..83d7070669 100644 --- a/tests/components/pipsolar/test.rp2040-ard.yaml +++ b/tests/components/pipsolar/test.rp2040-ard.yaml @@ -220,6 +220,8 @@ switch: name: inverter0_output_source_priority_solar output_source_priority_battery: name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid input_voltage_range: name: inverter0_input_voltage_range pv_ok_condition_for_parallel: From 11e155d86657c24014694cca23200d5a80b75cb2 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 21 Aug 2024 17:58:43 -0700 Subject: [PATCH 154/160] Enable verbose mode from env ESPHOME_VERBOSE or --verbose (#6987) --- esphome/__main__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 5c197ff486..cf2741dbdb 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -38,7 +38,7 @@ from esphome.const import ( SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine -from esphome.helpers import indent, is_ip_address +from esphome.helpers import indent, is_ip_address, get_bool_env from esphome.log import Fore, color, setup_log from esphome.util import ( get_serial_ports, @@ -731,7 +731,11 @@ POST_CONFIG_ACTIONS = { def parse_args(argv): options_parser = argparse.ArgumentParser(add_help=False) options_parser.add_argument( - "-v", "--verbose", help="Enable verbose ESPHome logs.", action="store_true" + "-v", + "--verbose", + help="Enable verbose ESPHome logs.", + action="store_true", + default=get_bool_env("ESPHOME_VERBOSE"), ) options_parser.add_argument( "-q", "--quiet", help="Disable all ESPHome logs.", action="store_true" From ab620acd4f086242dd6ecbe6f5763962982dc303 Mon Sep 17 00:00:00 2001 From: Piotr Szulc Date: Thu, 22 Aug 2024 02:59:31 +0200 Subject: [PATCH 155/160] Tuya Number: allow to set hidden datapoints (#7024) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/tuya/__init__.py | 1 + esphome/components/tuya/number/__init__.py | 38 ++++++++++++++++++- .../components/tuya/number/tuya_number.cpp | 19 ++++++++++ esphome/components/tuya/number/tuya_number.h | 6 ++- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py index 2eaaa2a625..0738f9b6a4 100644 --- a/esphome/components/tuya/__init__.py +++ b/esphome/components/tuya/__init__.py @@ -15,6 +15,7 @@ CONF_DATAPOINT_TYPE = "datapoint_type" CONF_STATUS_PIN = "status_pin" tuya_ns = cg.esphome_ns.namespace("tuya") +TuyaDatapointType = tuya_ns.enum("TuyaDatapointType", is_class=True) Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice) DPTYPE_ANY = "any" diff --git a/esphome/components/tuya/number/__init__.py b/esphome/components/tuya/number/__init__.py index 4dae6d8d60..25be6329ab 100644 --- a/esphome/components/tuya/number/__init__.py +++ b/esphome/components/tuya/number/__init__.py @@ -8,18 +8,36 @@ from esphome.const import ( CONF_MIN_VALUE, CONF_MULTIPLY, CONF_STEP, + CONF_INITIAL_VALUE, ) -from .. import tuya_ns, CONF_TUYA_ID, Tuya +from .. import tuya_ns, CONF_TUYA_ID, Tuya, TuyaDatapointType DEPENDENCIES = ["tuya"] CODEOWNERS = ["@frankiboy1"] +CONF_DATAPOINT_HIDDEN = "datapoint_hidden" +CONF_DATAPOINT_TYPE = "datapoint_type" + TuyaNumber = tuya_ns.class_("TuyaNumber", number.Number, cg.Component) +DATAPOINT_TYPES = { + "int": TuyaDatapointType.INTEGER, + "uint": TuyaDatapointType.INTEGER, + "enum": TuyaDatapointType.ENUM, +} + def validate_min_max(config): - if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + max_value = config[CONF_MAX_VALUE] + min_value = config[CONF_MIN_VALUE] + if max_value <= min_value: raise cv.Invalid("max_value must be greater than min_value") + if hidden_config := config.get(CONF_DATAPOINT_HIDDEN): + if (initial_value := hidden_config.get(CONF_INITIAL_VALUE, None)) is not None: + if (initial_value > max_value) or (initial_value < min_value): + raise cv.Invalid( + f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}" + ) return config @@ -33,6 +51,16 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_STEP): cv.positive_float, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + cv.Optional(CONF_DATAPOINT_HIDDEN): cv.All( + cv.Schema( + { + cv.Required(CONF_DATAPOINT_TYPE): cv.enum( + DATAPOINT_TYPES, lower=True + ), + cv.Optional(CONF_INITIAL_VALUE): cv.float_, + } + ) + ), } ) .extend(cv.COMPONENT_SCHEMA), @@ -56,3 +84,9 @@ async def to_code(config): cg.add(var.set_tuya_parent(parent)) cg.add(var.set_number_id(config[CONF_NUMBER_DATAPOINT])) + if hidden_config := config.get(CONF_DATAPOINT_HIDDEN): + cg.add(var.set_datapoint_type(hidden_config[CONF_DATAPOINT_TYPE])) + if ( + hidden_init_value := hidden_config.get(CONF_INITIAL_VALUE, None) + ) is not None: + cg.add(var.set_datapoint_initial_value(hidden_init_value)) diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp index e883c72d3d..7eeb08fde2 100644 --- a/esphome/components/tuya/number/tuya_number.cpp +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -15,8 +15,18 @@ void TuyaNumber::setup() { ESP_LOGV(TAG, "MCU reported number %u is: %u", datapoint.id, datapoint.value_enum); this->publish_state(datapoint.value_enum); } + if ((this->type_) && (this->type_ != datapoint.type)) { + ESP_LOGW(TAG, "Reported type (%d) different than previously set (%d)!", static_cast(datapoint.type), + static_cast(*this->type_)); + } this->type_ = datapoint.type; }); + + this->parent_->add_on_initialized_callback([this] { + if ((this->initial_value_) && (this->type_)) { + this->control(*this->initial_value_); + } + }); } void TuyaNumber::control(float value) { @@ -33,6 +43,15 @@ void TuyaNumber::control(float value) { void TuyaNumber::dump_config() { LOG_NUMBER("", "Tuya Number", this); ESP_LOGCONFIG(TAG, " Number has datapoint ID %u", this->number_id_); + if (this->type_) { + ESP_LOGCONFIG(TAG, " Datapoint type is %d", static_cast(*this->type_)); + } else { + ESP_LOGCONFIG(TAG, " Datapoint type is unknown"); + } + + if (this->initial_value_) { + ESP_LOGCONFIG(TAG, " Initial Value: %f", *this->initial_value_); + } } } // namespace tuya diff --git a/esphome/components/tuya/number/tuya_number.h b/esphome/components/tuya/number/tuya_number.h index f64dac8957..545584128e 100644 --- a/esphome/components/tuya/number/tuya_number.h +++ b/esphome/components/tuya/number/tuya_number.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/tuya/tuya.h" #include "esphome/components/number/number.h" +#include "esphome/core/optional.h" namespace esphome { namespace tuya { @@ -13,6 +14,8 @@ class TuyaNumber : public number::Number, public Component { void dump_config() override; void set_number_id(uint8_t number_id) { this->number_id_ = number_id; } void set_write_multiply(float factor) { multiply_by_ = factor; } + void set_datapoint_type(TuyaDatapointType type) { type_ = type; } + void set_datapoint_initial_value(float value) { this->initial_value_ = value; } void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } @@ -22,7 +25,8 @@ class TuyaNumber : public number::Number, public Component { Tuya *parent_; uint8_t number_id_{0}; float multiply_by_{1.0}; - TuyaDatapointType type_{}; + optional type_{}; + optional initial_value_{}; }; } // namespace tuya From 5cc8dbace41f3f5863473d60367f0d32c876ac7c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 23 Aug 2024 04:56:53 +1000 Subject: [PATCH 156/160] [lvgl] Bug fixes (#7338) --- esphome/components/lvgl/automation.py | 16 +++++++++++++--- esphome/components/lvgl/lvgl_esphome.cpp | 7 +++++++ esphome/components/lvgl/lvgl_esphome.h | 1 + esphome/components/lvgl/widgets/__init__.py | 6 ++++++ esphome/components/lvgl/widgets/line.py | 12 +++++++----- esphome/components/lvgl/widgets/msgbox.py | 3 ++- tests/components/lvgl/lvgl-package.yaml | 6 +++++- 7 files changed, 41 insertions(+), 10 deletions(-) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index a39f589136..efcac977ab 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -5,6 +5,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TIMEOUT +from esphome.cpp_generator import RawExpression from esphome.cpp_types import nullptr from .defines import ( @@ -26,6 +27,7 @@ from .lvcode import ( add_line_marks, lv, lv_add, + lv_expr, lv_obj, lvgl_comp, ) @@ -38,7 +40,13 @@ from .types import ( lv_disp_t, lv_obj_t, ) -from .widgets import Widget, get_widgets, lv_scr_act, set_obj_properties +from .widgets import ( + Widget, + get_widgets, + lv_scr_act, + set_obj_properties, + wait_for_widgets, +) async def action_to_code( @@ -48,10 +56,12 @@ async def action_to_code( template_arg, args, ): + await wait_for_widgets() async with LambdaContext(parameters=args, where=action_id) as context: + with LvConditional(lv_expr.is_pre_initialise()): + context.add(RawExpression("return")) for widget in widgets: - with LvConditional(widget.obj != nullptr): - await action(widget) + await action(widget) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) return var diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 92f7a880c3..6882986e7c 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -294,6 +294,13 @@ void LvglComponent::loop() { } lv_timer_handler_run_in_period(5); } +bool lv_is_pre_initialise() { + if (!lv_is_initialized()) { + ESP_LOGE(TAG, "LVGL call before component is initialised"); + return true; + } + return false; +} #ifdef USE_LVGL_IMAGE lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) { diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 3a3d1aa6c5..df3d4aa68c 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -40,6 +40,7 @@ namespace lvgl { extern lv_event_code_t lv_api_event; // NOLINT extern lv_event_code_t lv_update_event; // NOLINT +extern bool lv_is_pre_initialise(); #ifdef USE_LVGL_COLOR inline lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); } #endif // USE_LVGL_COLOR diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 4abb25c61d..50da6e131d 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -1,3 +1,4 @@ +import asyncio import sys from typing import Any, Union @@ -223,6 +224,11 @@ async def get_widget_(wid: Widget): return await FakeAwaitable(get_widget_generator(wid)) +async def wait_for_widgets(): + while not Widget.widgets_completed: + await asyncio.sleep(0) + + async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]: if not config: return [] diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py index 8ce4b1965f..4c6439fde4 100644 --- a/esphome/components/lvgl/widgets/line.py +++ b/esphome/components/lvgl/widgets/line.py @@ -3,7 +3,7 @@ import functools import esphome.codegen as cg import esphome.config_validation as cv -from ..defines import CONF_MAIN, literal +from ..defines import CONF_MAIN from ..lvcode import lv from ..types import LvType from . import Widget, WidgetType @@ -38,13 +38,15 @@ LINE_SCHEMA = { class LineType(WidgetType): def __init__(self): - super().__init__(CONF_LINE, LvType("lv_line_t"), (CONF_MAIN,), LINE_SCHEMA) + super().__init__( + CONF_LINE, LvType("lv_line_t"), (CONF_MAIN,), LINE_SCHEMA, modify_schema={} + ) async def to_code(self, w: Widget, config): """For a line object, create and add the points""" - data = literal(config[CONF_POINTS]) - points = cg.static_const_array(config[CONF_POINT_LIST_ID], data) - lv.line_set_points(w.obj, points, len(data)) + if data := config.get(CONF_POINTS): + points = cg.static_const_array(config[CONF_POINT_LIST_ID], data) + lv.line_set_points(w.obj, points, len(data)) line_spec = LineType() diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py index 63c4326c7c..c377af6bde 100644 --- a/esphome/components/lvgl/widgets/msgbox.py +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -13,7 +13,7 @@ from ..defines import ( TYPE_FLEX, literal, ) -from ..helpers import add_lv_use +from ..helpers import add_lv_use, lvgl_components_required from ..lv_validation import lv_bool, lv_pct, lv_text from ..lvcode import ( EVENT_ARG, @@ -72,6 +72,7 @@ async def msgbox_to_code(conf): *buttonmatrix_spec.get_uses(), *button_spec.get_uses(), ) + lvgl_components_required.add("BUTTONMATRIX") messagebox_id = conf[CONF_ID] outer = lv_Pvariable(lv_obj_t, messagebox_id.id) buttonmatrix = new_Pvariable( diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 800d6eff27..1479ce7358 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -379,6 +379,7 @@ lvgl: format: "bar value %f" args: [x] - line: + id: lv_line_id align: center points: - 5, 5 @@ -387,7 +388,10 @@ lvgl: - 180, 60 - 240, 10 on_click: - lvgl.page.next: + - lvgl.widget.update: + id: lv_line_id + line_color: 0xFFFF + - lvgl.page.next: - switch: align: right_mid - checkbox: From 3c65cabe1dc7a1d94cb889617427f2239e6fb734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Thu, 22 Aug 2024 23:30:22 +0200 Subject: [PATCH 157/160] feat: Expand ByteBuffer (#7316) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/core/bytebuffer.cpp | 225 +++++++++++++++++++++--------------- esphome/core/bytebuffer.h | 90 +++++++++++---- 2 files changed, 201 insertions(+), 114 deletions(-) diff --git a/esphome/core/bytebuffer.cpp b/esphome/core/bytebuffer.cpp index fb2ade3166..65525ecfcf 100644 --- a/esphome/core/bytebuffer.cpp +++ b/esphome/core/bytebuffer.cpp @@ -1,19 +1,64 @@ #include "bytebuffer.h" #include +#include namespace esphome { -ByteBuffer ByteBuffer::create(size_t capacity) { - std::vector data(capacity); - return {data}; -} - -ByteBuffer ByteBuffer::wrap(uint8_t *ptr, size_t len) { +ByteBuffer ByteBuffer::wrap(const uint8_t *ptr, size_t len, Endian endianness) { + // there is a double copy happening here, could be optimized but at cost of clarity. std::vector data(ptr, ptr + len); - return {data}; + ByteBuffer buffer = {data}; + buffer.endianness_ = endianness; + return buffer; } -ByteBuffer ByteBuffer::wrap(std::vector data) { return {std::move(data)}; } +ByteBuffer ByteBuffer::wrap(std::vector const &data, Endian endianness) { + ByteBuffer buffer = {data}; + buffer.endianness_ = endianness; + return buffer; +} + +ByteBuffer ByteBuffer::wrap(uint8_t value) { + ByteBuffer buffer = ByteBuffer(1); + buffer.put_uint8(value); + buffer.flip(); + return buffer; +} + +ByteBuffer ByteBuffer::wrap(uint16_t value, Endian endianness) { + ByteBuffer buffer = ByteBuffer(2, endianness); + buffer.put_uint16(value); + buffer.flip(); + return buffer; +} + +ByteBuffer ByteBuffer::wrap(uint32_t value, Endian endianness) { + ByteBuffer buffer = ByteBuffer(4, endianness); + buffer.put_uint32(value); + buffer.flip(); + return buffer; +} + +ByteBuffer ByteBuffer::wrap(uint64_t value, Endian endianness) { + ByteBuffer buffer = ByteBuffer(8, endianness); + buffer.put_uint64(value); + buffer.flip(); + return buffer; +} + +ByteBuffer ByteBuffer::wrap(float value, Endian endianness) { + ByteBuffer buffer = ByteBuffer(sizeof(float), endianness); + buffer.put_float(value); + buffer.flip(); + return buffer; +} + +ByteBuffer ByteBuffer::wrap(double value, Endian endianness) { + ByteBuffer buffer = ByteBuffer(sizeof(double), endianness); + buffer.put_double(value); + buffer.flip(); + return buffer; +} void ByteBuffer::set_limit(size_t limit) { assert(limit <= this->get_capacity()); @@ -27,108 +72,102 @@ void ByteBuffer::clear() { this->limit_ = this->get_capacity(); this->position_ = 0; } -uint16_t ByteBuffer::get_uint16() { - assert(this->get_remaining() >= 2); - uint16_t value; - if (endianness_ == LITTLE) { - value = this->data_[this->position_++]; - value |= this->data_[this->position_++] << 8; - } else { - value = this->data_[this->position_++] << 8; - value |= this->data_[this->position_++]; - } - return value; +void ByteBuffer::flip() { + this->limit_ = this->position_; + this->position_ = 0; } -uint32_t ByteBuffer::get_uint32() { - assert(this->get_remaining() >= 4); - uint32_t value; - if (endianness_ == LITTLE) { - value = this->data_[this->position_++]; - value |= this->data_[this->position_++] << 8; - value |= this->data_[this->position_++] << 16; - value |= this->data_[this->position_++] << 24; - } else { - value = this->data_[this->position_++] << 24; - value |= this->data_[this->position_++] << 16; - value |= this->data_[this->position_++] << 8; - value |= this->data_[this->position_++]; - } - return value; -} -uint32_t ByteBuffer::get_uint24() { - assert(this->get_remaining() >= 3); - uint32_t value; - if (endianness_ == LITTLE) { - value = this->data_[this->position_++]; - value |= this->data_[this->position_++] << 8; - value |= this->data_[this->position_++] << 16; - } else { - value = this->data_[this->position_++] << 16; - value |= this->data_[this->position_++] << 8; - value |= this->data_[this->position_++]; - } - return value; -} -uint32_t ByteBuffer::get_int24() { - auto value = this->get_uint24(); - uint32_t mask = (~(uint32_t) 0) << 23; - if ((value & mask) != 0) - value |= mask; - return value; -} +/// Getters uint8_t ByteBuffer::get_uint8() { assert(this->get_remaining() >= 1); return this->data_[this->position_++]; } -float ByteBuffer::get_float() { - auto value = this->get_uint32(); - return *(float *) &value; +uint64_t ByteBuffer::get_uint(size_t length) { + assert(this->get_remaining() >= length); + uint64_t value = 0; + if (this->endianness_ == LITTLE) { + this->position_ += length; + auto index = this->position_; + while (length-- != 0) { + value <<= 8; + value |= this->data_[--index]; + } + } else { + while (length-- != 0) { + value <<= 8; + value |= this->data_[this->position_++]; + } + } + return value; } + +uint32_t ByteBuffer::get_int24() { + auto value = this->get_uint24(); + uint32_t mask = (~static_cast(0)) << 23; + if ((value & mask) != 0) + value |= mask; + return value; +} +float ByteBuffer::get_float() { + assert(this->get_remaining() >= sizeof(float)); + auto ui_value = this->get_uint32(); + float value; + memcpy(&value, &ui_value, sizeof(float)); + return value; +} +double ByteBuffer::get_double() { + assert(this->get_remaining() >= sizeof(double)); + auto ui_value = this->get_uint64(); + double value; + memcpy(&value, &ui_value, sizeof(double)); + return value; +} +std::vector ByteBuffer::get_vector(size_t length) { + assert(this->get_remaining() >= length); + auto start = this->data_.begin() + this->position_; + this->position_ += length; + return {start, start + length}; +} + +/// Putters void ByteBuffer::put_uint8(uint8_t value) { assert(this->get_remaining() >= 1); this->data_[this->position_++] = value; } -void ByteBuffer::put_uint16(uint16_t value) { - assert(this->get_remaining() >= 2); +void ByteBuffer::put_uint(uint64_t value, size_t length) { + assert(this->get_remaining() >= length); if (this->endianness_ == LITTLE) { - this->data_[this->position_++] = (uint8_t) value; - this->data_[this->position_++] = (uint8_t) (value >> 8); + while (length-- != 0) { + this->data_[this->position_++] = static_cast(value); + value >>= 8; + } } else { - this->data_[this->position_++] = (uint8_t) (value >> 8); - this->data_[this->position_++] = (uint8_t) value; + this->position_ += length; + auto index = this->position_; + while (length-- != 0) { + this->data_[--index] = static_cast(value); + value >>= 8; + } } } -void ByteBuffer::put_uint24(uint32_t value) { - assert(this->get_remaining() >= 3); - if (this->endianness_ == LITTLE) { - this->data_[this->position_++] = (uint8_t) value; - this->data_[this->position_++] = (uint8_t) (value >> 8); - this->data_[this->position_++] = (uint8_t) (value >> 16); - } else { - this->data_[this->position_++] = (uint8_t) (value >> 16); - this->data_[this->position_++] = (uint8_t) (value >> 8); - this->data_[this->position_++] = (uint8_t) value; - } +void ByteBuffer::put_float(float value) { + static_assert(sizeof(float) == sizeof(uint32_t), "Float sizes other than 32 bit not supported"); + assert(this->get_remaining() >= sizeof(float)); + uint32_t ui_value; + memcpy(&ui_value, &value, sizeof(float)); // this work-around required to silence compiler warnings + this->put_uint32(ui_value); } -void ByteBuffer::put_uint32(uint32_t value) { - assert(this->get_remaining() >= 4); - if (this->endianness_ == LITTLE) { - this->data_[this->position_++] = (uint8_t) value; - this->data_[this->position_++] = (uint8_t) (value >> 8); - this->data_[this->position_++] = (uint8_t) (value >> 16); - this->data_[this->position_++] = (uint8_t) (value >> 24); - } else { - this->data_[this->position_++] = (uint8_t) (value >> 24); - this->data_[this->position_++] = (uint8_t) (value >> 16); - this->data_[this->position_++] = (uint8_t) (value >> 8); - this->data_[this->position_++] = (uint8_t) value; - } +void ByteBuffer::put_double(double value) { + static_assert(sizeof(double) == sizeof(uint64_t), "Double sizes other than 64 bit not supported"); + assert(this->get_remaining() >= sizeof(double)); + uint64_t ui_value; + memcpy(&ui_value, &value, sizeof(double)); + this->put_uint64(ui_value); } -void ByteBuffer::put_float(float value) { this->put_uint32(*(uint32_t *) &value); } -void ByteBuffer::flip() { - this->limit_ = this->position_; - this->position_ = 0; +void ByteBuffer::put_vector(const std::vector &value) { + assert(this->get_remaining() >= value.size()); + std::copy(value.begin(), value.end(), this->data_.begin() + this->position_); + this->position_ += value.size(); } } // namespace esphome diff --git a/esphome/core/bytebuffer.h b/esphome/core/bytebuffer.h index f242e5e333..d44d01f275 100644 --- a/esphome/core/bytebuffer.h +++ b/esphome/core/bytebuffer.h @@ -15,55 +15,103 @@ enum Endian { LITTLE, BIG }; * * There are three variables maintained pointing into the buffer: * - * 0 <= position <= limit <= capacity - * - * capacity: the maximum amount of data that can be stored + * capacity: the maximum amount of data that can be stored - set on construction and cannot be changed * limit: the limit of the data currently available to get or put * position: the current insert or extract position * + * 0 <= position <= limit <= capacity + * * In addition a mark can be set to the current position with mark(). A subsequent call to reset() will restore * the position to the mark. * * The buffer can be marked to be little-endian (default) or big-endian. All subsequent operations will use that order. * + * The flip() operation will reset the position to 0 and limit to the current position. This is useful for reading + * data from a buffer after it has been written. + * */ class ByteBuffer { public: + // Default constructor (compatibility with TEMPLATABLE_VALUE) + ByteBuffer() : ByteBuffer(std::vector()) {} /** * Create a new Bytebuffer with the given capacity */ - static ByteBuffer create(size_t capacity); + ByteBuffer(size_t capacity, Endian endianness = LITTLE) + : data_(std::vector(capacity)), endianness_(endianness), limit_(capacity){}; /** - * Wrap an existing vector in a Bytebufffer + * Wrap an existing vector in a ByteBufffer */ - static ByteBuffer wrap(std::vector data); + static ByteBuffer wrap(std::vector const &data, Endian endianness = LITTLE); /** - * Wrap an existing array in a Bytebufffer + * Wrap an existing array in a ByteBuffer. Note that this will create a copy of the data. */ - static ByteBuffer wrap(uint8_t *ptr, size_t len); + static ByteBuffer wrap(const uint8_t *ptr, size_t len, Endian endianness = LITTLE); + // Convenience functions to create a ByteBuffer from a value + static ByteBuffer wrap(uint8_t value); + static ByteBuffer wrap(uint16_t value, Endian endianness = LITTLE); + static ByteBuffer wrap(uint32_t value, Endian endianness = LITTLE); + static ByteBuffer wrap(uint64_t value, Endian endianness = LITTLE); + static ByteBuffer wrap(int8_t value) { return wrap(static_cast(value)); } + static ByteBuffer wrap(int16_t value, Endian endianness = LITTLE) { + return wrap(static_cast(value), endianness); + } + static ByteBuffer wrap(int32_t value, Endian endianness = LITTLE) { + return wrap(static_cast(value), endianness); + } + static ByteBuffer wrap(int64_t value, Endian endianness = LITTLE) { + return wrap(static_cast(value), endianness); + } + static ByteBuffer wrap(float value, Endian endianness = LITTLE); + static ByteBuffer wrap(double value, Endian endianness = LITTLE); + static ByteBuffer wrap(bool value) { return wrap(static_cast(value)); } + // Get an integral value from the buffer, increment position by length + uint64_t get_uint(size_t length); // Get one byte from the buffer, increment position by 1 uint8_t get_uint8(); // Get a 16 bit unsigned value, increment by 2 - uint16_t get_uint16(); + uint16_t get_uint16() { return static_cast(this->get_uint(sizeof(uint16_t))); }; // Get a 24 bit unsigned value, increment by 3 - uint32_t get_uint24(); + uint32_t get_uint24() { return static_cast(this->get_uint(3)); }; // Get a 32 bit unsigned value, increment by 4 - uint32_t get_uint32(); - // signed versions of the get functions - uint8_t get_int8() { return (int8_t) this->get_uint8(); }; - int16_t get_int16() { return (int16_t) this->get_uint16(); } + uint32_t get_uint32() { return static_cast(this->get_uint(sizeof(uint32_t))); }; + // Get a 64 bit unsigned value, increment by 8 + uint64_t get_uint64() { return this->get_uint(sizeof(uint64_t)); }; + // Signed versions of the get functions + uint8_t get_int8() { return static_cast(this->get_uint8()); }; + int16_t get_int16() { return static_cast(this->get_uint(sizeof(int16_t))); } uint32_t get_int24(); - int32_t get_int32() { return (int32_t) this->get_uint32(); } + int32_t get_int32() { return static_cast(this->get_uint(sizeof(int32_t))); } + int64_t get_int64() { return static_cast(this->get_uint(sizeof(int64_t))); } // Get a float value, increment by 4 float get_float(); + // Get a double value, increment by 8 + double get_double(); + // Get a bool value, increment by 1 + bool get_bool() { return this->get_uint8(); } + // Get vector of bytes, increment by length + std::vector get_vector(size_t length); - // put values into the buffer, increment the position accordingly + // Put values into the buffer, increment the position accordingly + // put any integral value, length represents the number of bytes + void put_uint(uint64_t value, size_t length); void put_uint8(uint8_t value); - void put_uint16(uint16_t value); - void put_uint24(uint32_t value); - void put_uint32(uint32_t value); + void put_uint16(uint16_t value) { this->put_uint(value, sizeof(uint16_t)); } + void put_uint24(uint32_t value) { this->put_uint(value, 3); } + void put_uint32(uint32_t value) { this->put_uint(value, sizeof(uint32_t)); } + void put_uint64(uint64_t value) { this->put_uint(value, sizeof(uint64_t)); } + // Signed versions of the put functions + void put_int8(int8_t value) { this->put_uint8(static_cast(value)); } + void put_int16(int32_t value) { this->put_uint(static_cast(value), sizeof(uint16_t)); } + void put_int24(int32_t value) { this->put_uint(static_cast(value), 3); } + void put_int32(int32_t value) { this->put_uint(static_cast(value), sizeof(uint32_t)); } + void put_int64(int64_t value) { this->put_uint(static_cast(value), sizeof(uint64_t)); } + // Extra put functions void put_float(float value); + void put_double(double value); + void put_bool(bool value) { this->put_uint8(value); } + void put_vector(const std::vector &value); inline size_t get_capacity() const { return this->data_.size(); } inline size_t get_position() const { return this->position_; } @@ -80,12 +128,12 @@ class ByteBuffer { // set limit to current position, postition to zero. Used when swapping from write to read operations. void flip(); // retrieve a pointer to the underlying data. - uint8_t *array() { return this->data_.data(); }; + std::vector get_data() { return this->data_; }; void rewind() { this->position_ = 0; } void reset() { this->position_ = this->mark_; } protected: - ByteBuffer(std::vector data) : data_(std::move(data)) { this->limit_ = this->get_capacity(); } + ByteBuffer(std::vector const &data) : data_(data), limit_(data.size()) {} std::vector data_; Endian endianness_{LITTLE}; size_t position_{0}; From 43f8f2fd2ef30802025e078c4be0f0afcc08308f Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:09:40 +1000 Subject: [PATCH 158/160] [core] Clean build if the loaded integrations changed (#7344) --- esphome/writer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/writer.py b/esphome/writer.py index c6111cbe3f..57435d3463 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -106,6 +106,8 @@ def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool: return True if old.build_path != new.build_path: return True + if old.loaded_integrations != new.loaded_integrations: + return True return False @@ -117,7 +119,9 @@ def update_storage_json(): return if storage_should_clean(old, new): - _LOGGER.info("Core config or version changed, cleaning build files...") + _LOGGER.info( + "Core config, version or integrations changed, cleaning build files..." + ) clean_build() new.save(path) From a01fea54a03e26ce265cd44780c6166d73973a28 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 24 Aug 2024 02:32:08 -0500 Subject: [PATCH 159/160] [ledc] Tweak fix in #6997 (#7336) --- esphome/components/ledc/ledc_output.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 90e11fe4ad..7f91eb2d7a 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -8,6 +8,8 @@ #endif #include +#include + #define CLOCK_FREQUENCY 80e6f #ifdef USE_ARDUINO @@ -120,13 +122,17 @@ void LEDCOutput::write_state(float state) { ledcWrite(this->channel_, duty); #endif #ifdef USE_ESP_IDF +#if !defined(USE_ESP32_VARIANT_ESP32C3) || (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 1, 0)) // ensure that 100% on is not 99.975% on + // note: on the C3, this tweak will result in the outputs turning off at 100%, so it has been omitted if ((duty == max_duty) && (max_duty != 1)) { duty = max_duty + 1; } +#endif auto speed_mode = get_speed_mode(channel_); auto chan_num = static_cast(channel_ % 8); int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); + ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_); ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); ledc_update_duty(speed_mode, chan_num); #endif From caaae59ea9db397bc80e6e51504bd698ece059f3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 24 Aug 2024 19:56:13 +1000 Subject: [PATCH 160/160] [ledc] Fix maximum brightness on ESP-IDF 5.1 (#7342) Co-authored-by: Keith Burzinski --- esphome/components/ledc/ledc_output.cpp | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 7f91eb2d7a..4ced4b8f76 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -117,24 +117,22 @@ void LEDCOutput::write_state(float state) { const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1; const float duty_rounded = roundf(state * max_duty); auto duty = static_cast(duty_rounded); + ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_); #ifdef USE_ARDUINO - ESP_LOGV(TAG, "Setting duty: %u on channel %u", duty, this->channel_); ledcWrite(this->channel_, duty); #endif #ifdef USE_ESP_IDF -#if !defined(USE_ESP32_VARIANT_ESP32C3) || (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 1, 0)) - // ensure that 100% on is not 99.975% on - // note: on the C3, this tweak will result in the outputs turning off at 100%, so it has been omitted - if ((duty == max_duty) && (max_duty != 1)) { - duty = max_duty + 1; - } -#endif auto speed_mode = get_speed_mode(channel_); auto chan_num = static_cast(channel_ % 8); int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); - ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_); - ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); - ledc_update_duty(speed_mode, chan_num); + if (duty == max_duty) { + ledc_stop(speed_mode, chan_num, 1); + } else if (duty == 0) { + ledc_stop(speed_mode, chan_num, 0); + } else { + ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); + ledc_update_duty(speed_mode, chan_num); + } #endif }